From 3da6837aae6291a8fc57ae7fbf02cbca7502aef9 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 24 Jul 2023 20:12:08 +0200 Subject: [PATCH 1/2] Add an action to upload media files according to MSC4039 Signed-off-by: Dominik Henneke --- src/ClientWidgetApi.ts | 57 ++++++++ src/WidgetApi.ts | 48 ++++++ src/driver/WidgetDriver.ts | 23 +++ src/index.ts | 2 + src/interfaces/ApiVersion.ts | 2 + src/interfaces/Capabilities.ts | 4 + src/interfaces/GetMediaConfigAction.ts | 38 +++++ src/interfaces/UploadFileAction.ts | 40 +++++ src/interfaces/WidgetApiAction.ts | 10 ++ src/templating/url-template.ts | 4 + test/ClientWidgetApi-test.ts | 194 +++++++++++++++++++++++++ test/WidgetApi-test.ts | 94 ++++++++++++ test/url-template-test.ts | 19 +++ 13 files changed, 535 insertions(+) create mode 100644 src/interfaces/GetMediaConfigAction.ts create mode 100644 src/interfaces/UploadFileAction.ts diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index fd8b049..92cc095 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -86,6 +86,14 @@ import { IReadRoomAccountDataFromWidgetActionRequest, IReadRoomAccountDataFromWidgetResponseData, } from "./interfaces/ReadRoomAccountDataAction"; +import { + IGetMediaConfigActionFromWidgetActionRequest, + IGetMediaConfigActionFromWidgetResponseData, +} from "./interfaces/GetMediaConfigAction"; +import { + IUploadFileActionFromWidgetActionRequest, + IUploadFileActionFromWidgetResponseData, +} from "./interfaces/UploadFileAction"; /** * API handler for the client side of widgets. This raises events @@ -701,6 +709,50 @@ export class ClientWidgetApi extends EventEmitter { } } + private async handleGetMediaConfig(request: IGetMediaConfigActionFromWidgetActionRequest) { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); + } + + try { + const result = await this.driver.getMediaConfig() + + return this.transport.reply( + request, + result, + ); + } catch (e) { + console.error("error while getting the media configuration", e); + await this.transport.reply(request, { + error: { message: "Unexpected error while getting the media configuration" }, + }); + } + } + + private async handleUploadFile(request: IUploadFileActionFromWidgetActionRequest) { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); + } + + try { + const result = await this.driver.uploadFile(request.data.file); + + return this.transport.reply( + request, + { content_uri: result.contentUri }, + ); + } catch (e) { + console.error("error while uploading a file", e); + await this.transport.reply(request, { + error: { message: "Unexpected error while uploading a file" }, + }); + } + } + private handleMessage(ev: CustomEvent) { if (this.isStopped) return; const actionEv = new CustomEvent(`action:${ev.detail.action}`, { @@ -736,6 +788,11 @@ export class ClientWidgetApi extends EventEmitter { return this.handleUserDirectorySearch(ev.detail) case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: return this.handleReadRoomAccountData(ev.detail); + case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: + return this.handleGetMediaConfig(ev.detail); + case WidgetApiFromWidgetAction.MSC4039UploadFileAction: + return this.handleUploadFile(ev.detail); + default: return this.transport.reply(ev.detail, { error: { diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 97980ca..1dc17ee 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -77,6 +77,14 @@ import { IUserDirectorySearchFromWidgetRequestData, IUserDirectorySearchFromWidgetResponseData, } from "./interfaces/UserDirectorySearchAction"; +import { + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData, +} from "./interfaces/GetMediaConfigAction"; +import { + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData, +} from "./interfaces/UploadFileAction"; /** * API handler for widgets. This raises events for each action @@ -662,6 +670,46 @@ export class WidgetApi extends EventEmitter { >(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data); } + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public async getMediaConfig(): Promise { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error("The get_media_config action is not supported by the client.") + } + + const data: IGetMediaConfigActionFromWidgetRequestData = {}; + + return this.transport.send< + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data); + } + + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public async uploadFile(file: XMLHttpRequestBodyInit): Promise { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error("The upload_file action is not supported by the client.") + } + + const data: IUploadFileActionFromWidgetRequestData = { + file, + }; + + return this.transport.send< + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data); + } + /** * Starts the communication channel. This should be done early to ensure * that messages are not missed. Communication can only be stopped by the client. diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index 9570c49..95d9410 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -274,4 +274,27 @@ export abstract class WidgetDriver { ): Promise { return Promise.resolve({ limited: false, results: [] }); } + + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public getMediaConfig(): Promise<{ + [key: string]: unknown; + "m.upload.size"?: number; + }> { + throw new Error("Get media config is not implemented"); + } + + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public uploadFile( + file: XMLHttpRequestBodyInit, + ): Promise<{ contentUri: string }> { + throw new Error("Upload file is not implemented"); + } } diff --git a/src/index.ts b/src/index.ts index f34689a..1a75a84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,8 @@ export * from "./interfaces/IRoomAccountData"; export * from "./interfaces/NavigateAction"; export * from "./interfaces/TurnServerActions"; export * from "./interfaces/ReadRelationsAction"; +export * from "./interfaces/GetMediaConfigAction"; +export * from "./interfaces/UploadFileAction"; // Complex models export * from "./models/WidgetEventCapability"; diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index 6586c14..af4921d 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -30,6 +30,7 @@ export enum UnstableApiVersion { MSC3846 = "town.robin.msc3846", MSC3869 = "org.matrix.msc3869", MSC3973 = "org.matrix.msc3973", + MSC4039 = "org.matrix.msc4039", } export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string; @@ -47,4 +48,5 @@ export const CurrentApiVersions: ApiVersion[] = [ UnstableApiVersion.MSC3846, UnstableApiVersion.MSC3869, UnstableApiVersion.MSC3973, + UnstableApiVersion.MSC4039, ]; diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index 1572105..ab49341 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -34,6 +34,10 @@ export enum MatrixCapabilities { * @deprecated It is not recommended to rely on this existing - it can be removed without notice. */ MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFile = "org.matrix.msc4039.upload_file", } export type Capability = MatrixCapabilities | string; diff --git a/src/interfaces/GetMediaConfigAction.ts b/src/interfaces/GetMediaConfigAction.ts new file mode 100644 index 0000000..f67c2c8 --- /dev/null +++ b/src/interfaces/GetMediaConfigAction.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Nordeck IT + Consulting GmbH. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; + +export interface IGetMediaConfigActionFromWidgetRequestData + extends IWidgetApiRequestData {} + +export interface IGetMediaConfigActionFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; + data: IGetMediaConfigActionFromWidgetRequestData; +} + +export interface IGetMediaConfigActionFromWidgetResponseData + extends IWidgetApiResponseData { + "m.upload.size"?: number; +} + +export interface IGetMediaConfigActionFromWidgetActionResponse + extends IGetMediaConfigActionFromWidgetActionRequest { + response: IGetMediaConfigActionFromWidgetResponseData; +} diff --git a/src/interfaces/UploadFileAction.ts b/src/interfaces/UploadFileAction.ts new file mode 100644 index 0000000..86d529f --- /dev/null +++ b/src/interfaces/UploadFileAction.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Nordeck IT + Consulting GmbH. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; + +export interface IUploadFileActionFromWidgetRequestData + extends IWidgetApiRequestData { + file: XMLHttpRequestBodyInit; +} + +export interface IUploadFileActionFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; + data: IUploadFileActionFromWidgetRequestData; +} + +export interface IUploadFileActionFromWidgetResponseData + extends IWidgetApiResponseData { + content_uri: string; // eslint-disable-line camelcase +} + +export interface IUploadFileActionFromWidgetActionResponse + extends IUploadFileActionFromWidgetActionRequest { + response: IUploadFileActionFromWidgetResponseData; +} diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index b341b64..de78e52 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -69,6 +69,16 @@ export enum WidgetApiFromWidgetAction { * @deprecated It is not recommended to rely on this existing - it can be removed without notice. */ MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", + + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", } export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string; diff --git a/src/templating/url-template.ts b/src/templating/url-template.ts index 34c8f0f..207729d 100644 --- a/src/templating/url-template.ts +++ b/src/templating/url-template.ts @@ -25,6 +25,7 @@ export interface ITemplateParams { clientTheme?: string; clientLanguage?: string; deviceId?: string; + baseUrl?: string; } export function runTemplate(url: string, widget: IWidget, params: ITemplateParams): string { @@ -43,6 +44,9 @@ export function runTemplate(url: string, widget: IWidget, params: ITemplateParam // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) 'org.matrix.msc3819.matrix_device_id': params.deviceId || "", + + // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) + 'org.matrix.msc4039.matrix_base_url': params.baseUrl || "", }); let result = url; for (const key of Object.keys(variables)) { diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index e9654cf..9d7ff80 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -29,6 +29,8 @@ import { WidgetApiDirection } from '../src/interfaces/WidgetApiDirection'; import { Widget } from '../src/models/Widget'; import { PostmessageTransport } from '../src/transport/PostmessageTransport'; import { IReadEventFromWidgetActionRequest } from '../src'; +import { IGetMediaConfigActionFromWidgetActionRequest } from '../src/interfaces/GetMediaConfigAction'; +import { IUploadFileActionFromWidgetActionRequest } from '../src/interfaces/UploadFileAction'; jest.mock('../src/transport/PostmessageTransport') @@ -79,6 +81,8 @@ describe('ClientWidgetApi', () => { readEventRelations: jest.fn(), validateCapabilities: jest.fn(), searchUserDirectory: jest.fn(), + getMediaConfig: jest.fn(), + uploadFile: jest.fn(), } as Partial as jest.Mocked; clientWidgetApi = new ClientWidgetApi( @@ -672,4 +676,194 @@ describe('ClientWidgetApi', () => { }); }); }); + + describe('org.matrix.msc4039.get_media_config action', () => { + it('should present as supported api version', () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent('', { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }); + }); + + it('should handle and process the request', async () => { + driver.getMediaConfig.mockResolvedValue({ + 'm.upload.size': 1000, + }); + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; + + await loadIframe([ + 'org.matrix.msc4039.upload_file', + ]); + + emitEvent(new CustomEvent('', { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + 'm.upload.size': 1000, + }); + }); + + expect(driver.getMediaConfig).toBeCalled(); + }); + + it('should reject requests when the capability was not requested', async () => { + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; + + emitEvent(new CustomEvent('', { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + error: { message: 'Missing capability' }, + }); + + expect(driver.getMediaConfig).not.toBeCalled(); + }); + + it('should reject requests when the driver throws an exception', async () => { + driver.getMediaConfig.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; + + await loadIframe([ + 'org.matrix.msc4039.upload_file', + ]); + + emitEvent(new CustomEvent('', { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: 'Unexpected error while getting the media configuration' }, + }); + }); + }); + }); + + describe('org.matrix.msc4039.upload_file action', () => { + it('should present as supported api version', () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent('', { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }); + }); + + it('should handle and process the request', async () => { + driver.uploadFile.mockResolvedValue({ + contentUri: 'mxc://...', + }); + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: 'data', + }, + }; + + await loadIframe([ + 'org.matrix.msc4039.upload_file', + ]); + + emitEvent(new CustomEvent('', { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + content_uri: 'mxc://...', + }); + }); + + expect(driver.uploadFile).toBeCalled(); + }); + + it('should reject requests when the capability was not requested', async () => { + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: 'data', + }, + }; + + emitEvent(new CustomEvent('', { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + error: { message: 'Missing capability' }, + }); + + expect(driver.uploadFile).not.toBeCalled(); + }); + + it('should reject requests when the driver throws an exception', async () => { + driver.getMediaConfig.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: 'test', + requestId: '0', + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: 'data', + }, + }; + + await loadIframe([ + 'org.matrix.msc4039.upload_file', + ]); + + emitEvent(new CustomEvent('', { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: 'Unexpected error while uploading a file' }, + }); + }); + }); + }); }); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index f8d42f5..38c1ece 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -15,8 +15,10 @@ */ import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; +import { IGetMediaConfigActionFromWidgetResponseData } from '../src/interfaces/GetMediaConfigAction'; import { IReadRelationsFromWidgetResponseData } from '../src/interfaces/ReadRelationsAction'; import { ISupportedVersionsActionResponseData } from '../src/interfaces/SupportedVersionsAction'; +import { IUploadFileActionFromWidgetResponseData } from '../src/interfaces/UploadFileAction'; import { IUserDirectorySearchFromWidgetResponseData } from '../src/interfaces/UserDirectorySearchAction'; import { WidgetApiFromWidgetAction } from '../src/interfaces/WidgetApiAction'; import { PostmessageTransport } from '../src/transport/PostmessageTransport'; @@ -178,4 +180,96 @@ describe('WidgetApi', () => { )).rejects.toThrow('An error occurred'); }); }); + + describe('getMediaConfig', () => { + it('should forward the request to the ClientWidgetApi', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, + ); + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValue( + { 'm.upload.size': 1000 } as IGetMediaConfigActionFromWidgetResponseData, + ); + + await expect(widgetApi.getMediaConfig()).resolves.toEqual({ + 'm.upload.size': 1000, + }); + + expect(PostmessageTransport.prototype.send).toBeCalledWith( + WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + {}, + ); + }); + + it('should reject the request if the api is not supported', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [] } as ISupportedVersionsActionResponseData, + ); + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + "The get_media_config action is not supported by the client.", + ); + + expect(PostmessageTransport.prototype.send) + .not.toBeCalledWith(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, expect.anything()); + }); + + it('should handle an error', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, + ); + jest.mocked(PostmessageTransport.prototype.send).mockRejectedValue( + new Error('An error occurred'), + ); + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + 'An error occurred', + ); + }); + }); + + describe('uploadFile', () => { + it('should forward the request to the ClientWidgetApi', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, + ); + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValue( + { content_uri: 'mxc://...' } as IUploadFileActionFromWidgetResponseData, + ); + + await expect(widgetApi.uploadFile("data")).resolves.toEqual({ + content_uri: 'mxc://...', + }); + + expect(PostmessageTransport.prototype.send).toBeCalledWith( + WidgetApiFromWidgetAction.MSC4039UploadFileAction, + { file: "data" }, + ); + }); + + it('should reject the request if the api is not supported', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [] } as ISupportedVersionsActionResponseData, + ); + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "The upload_file action is not supported by the client.", + ); + + expect(PostmessageTransport.prototype.send) + .not.toBeCalledWith(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, expect.anything()); + }); + + it('should handle an error', async () => { + jest.mocked(PostmessageTransport.prototype.send).mockResolvedValueOnce( + { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, + ); + jest.mocked(PostmessageTransport.prototype.send).mockRejectedValue( + new Error('An error occurred'), + ); + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + 'An error occurred', + ); + }); + }); }); diff --git a/test/url-template-test.ts b/test/url-template-test.ts index b426fa4..b1db0fe 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -35,4 +35,23 @@ describe("runTemplate", () => { expect(replacedUrl).toBe("https://localhost/?my-query#device_id=my-device-id"); }); + + it("should replace base url template in url", () => { + const url = "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; + const replacedUrl = runTemplate( + url, + { + id: "widget-id", + creatorUserId: '@user-id', + type: 'type', + url, + }, + { + currentUserId: '@user-id', + baseUrl: 'https://localhost/api', + }, + ); + + expect(replacedUrl).toBe("https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi"); + }); }); From 013db8c60abf4eb588a393e139ac1eae9ffd5262 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 1 Aug 2023 09:31:58 +0200 Subject: [PATCH 2/2] Extract interface in the WidgetDriver Signed-off-by: Dominik Henneke --- src/driver/WidgetDriver.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index 95d9410..c98722a 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -49,6 +49,11 @@ export interface ISearchUserDirectoryResult { }>; } +export interface IGetMediaConfigResult { + [key: string]: unknown; + "m.upload.size"?: number; +} + /** * Represents the functions and behaviour the widget-api is unable to * do, such as prompting the user for information or interacting with @@ -279,10 +284,7 @@ export abstract class WidgetDriver { * Get the config for the media repository. * @returns Promise which resolves with an object containing the config. */ - public getMediaConfig(): Promise<{ - [key: string]: unknown; - "m.upload.size"?: number; - }> { + public getMediaConfig(): Promise { throw new Error("Get media config is not implemented"); }