899cdb0e1d
* Skip unwritten tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Tidy jest fake timers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unnecessary sessionStorage mock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve async assertions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve error assertions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve object assertions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove assertion testing unclear mock This test failed when ran individually, same as after the clearAllMocks call Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Avoid awaiting non-thenables Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Pass nop function when stubbing out console, vitest won't accept it any other way Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix mistaken assertions not testing all values in array Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix hidden non-running tests in room.spec.ts Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update fetch-mock-jest to @fetch-mock/jest Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Make knip happier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Make knip happier 2.0 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from Jest to Vitest Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix CI Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unnecessary fake timers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update vite Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Revert irrelevant changes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix coverage spec paths Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix slow test reporter Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix bad merge conflict resolution Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix babel config Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
253 lines
10 KiB
TypeScript
253 lines
10 KiB
TypeScript
/*
|
|
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
|
|
|
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 { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
|
|
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
|
|
|
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
|
|
|
vi.useFakeTimers();
|
|
|
|
describe("MatrixHttpApi", () => {
|
|
const baseUrl = "http://baseUrl";
|
|
const prefix = ClientPrefix.V3;
|
|
|
|
let upload: Promise<UploadResponse>;
|
|
|
|
const DONE = 0;
|
|
|
|
function getRequest(): Writeable<XMLHttpRequest> | undefined {
|
|
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
// We stub out XHR here as it is not available in the test environment
|
|
// @ts-ignore
|
|
globalThis.XMLHttpRequest = vi.fn().mockImplementation(function (this: XMLHttpRequest) {
|
|
// @ts-ignore
|
|
this.upload = {} as XMLHttpRequestUpload;
|
|
this.open = vi.fn();
|
|
this.send = vi.fn();
|
|
this.abort = vi.fn();
|
|
this.setRequestHeader = vi.fn();
|
|
// @ts-ignore
|
|
this.onreadystatechange = undefined;
|
|
this.getResponseHeader = vi.fn();
|
|
this.getAllResponseHeaders = vi.fn();
|
|
});
|
|
// @ts-ignore
|
|
globalThis.XMLHttpRequest.DONE = DONE;
|
|
});
|
|
|
|
afterEach(() => {
|
|
upload?.catch(() => {});
|
|
const xhr = getRequest();
|
|
if (xhr) {
|
|
// Abort any remaining requests
|
|
xhr.readyState = DONE;
|
|
xhr.status = 0;
|
|
// @ts-ignore
|
|
xhr.onreadystatechange?.(new Event("test"));
|
|
}
|
|
});
|
|
|
|
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
|
globalThis.XMLHttpRequest = undefined!;
|
|
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
await upload;
|
|
expect(fetchFn).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should prefer xhr where available", () => {
|
|
const fetchFn = vi.fn().mockResolvedValue({ ok: true });
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
expect(fetchFn).not.toHaveBeenCalled();
|
|
expect(getRequest()!.open).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should send access token in query params if header disabled", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
|
baseUrl,
|
|
prefix,
|
|
accessToken: "token",
|
|
useAuthorizationHeader: false,
|
|
onlyData: true,
|
|
});
|
|
upload = api.uploadContent({} as File);
|
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
|
Method.Post,
|
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
|
);
|
|
expect(getRequest()!.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
|
});
|
|
|
|
it("should send access token in header by default", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
|
baseUrl,
|
|
prefix,
|
|
accessToken: "token",
|
|
onlyData: true,
|
|
});
|
|
upload = api.uploadContent({} as File);
|
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
|
Method.Post,
|
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
|
);
|
|
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
|
});
|
|
|
|
it("should include filename by default", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File, { name: "name" });
|
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
|
Method.Post,
|
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
|
);
|
|
});
|
|
|
|
it("should allow not sending the filename", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
|
expect(getRequest()!.open).toHaveBeenCalledWith(
|
|
Method.Post,
|
|
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
|
);
|
|
});
|
|
|
|
it("should abort xhr when the upload is aborted", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
api.cancelUpload(upload);
|
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
|
return expect(upload).rejects.toThrow("Aborted");
|
|
});
|
|
|
|
it("should timeout if no progress in 30s", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
vi.advanceTimersByTime(25000);
|
|
// @ts-ignore
|
|
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
|
vi.advanceTimersByTime(25000);
|
|
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(5000);
|
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should call progressHandler", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
const progressHandler = vi.fn();
|
|
upload = api.uploadContent({} as File, { progressHandler });
|
|
const progressEvent = new Event("progress") as ProgressEvent;
|
|
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
|
// @ts-ignore
|
|
getRequest()!.upload.onprogress(progressEvent);
|
|
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
|
|
|
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
|
// @ts-ignore
|
|
getRequest()!.upload.onprogress(progressEvent);
|
|
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
|
});
|
|
|
|
it("should error when no response body", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
|
|
getRequest()!.readyState = DONE;
|
|
getRequest()!.responseText = "";
|
|
getRequest()!.status = 200;
|
|
// @ts-ignore
|
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
|
|
|
return expect(upload).rejects.toThrow("No response body.");
|
|
});
|
|
|
|
it("should error on a 400-code", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
|
|
getRequest()!.readyState = DONE;
|
|
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
|
getRequest()!.status = 404;
|
|
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
|
|
name.toLowerCase() === "content-type" ? "application/json" : null,
|
|
);
|
|
vi.mocked(getRequest()!.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
|
// @ts-ignore
|
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
|
|
|
return expect(upload).rejects.toThrow("Not found");
|
|
});
|
|
|
|
it("should return response on successful upload", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
|
|
getRequest()!.readyState = DONE;
|
|
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
|
|
getRequest()!.status = 200;
|
|
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
|
// @ts-ignore
|
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
|
|
|
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
|
});
|
|
|
|
it("should abort xhr when calling `cancelUpload`", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
expect(api.cancelUpload(upload)).toBeTruthy();
|
|
expect(getRequest()!.abort).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
|
|
getRequest()!.readyState = DONE;
|
|
getRequest()!.status = 500;
|
|
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
|
// @ts-ignore
|
|
getRequest()!.onreadystatechange?.(new Event("test"));
|
|
await upload.catch(() => {});
|
|
|
|
expect(api.cancelUpload(upload)).toBeFalsy();
|
|
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should return active uploads in `getCurrentUploads`", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
|
upload = api.uploadContent({} as File);
|
|
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
|
|
api.cancelUpload(upload);
|
|
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeFalsy();
|
|
});
|
|
|
|
it("should return expected object from `getContentUri`", () => {
|
|
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
|
baseUrl,
|
|
prefix,
|
|
accessToken: "token",
|
|
onlyData: true,
|
|
});
|
|
expect(api.getContentUri()).toMatchSnapshot();
|
|
});
|
|
});
|