1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-06-13 00:41:55 +03:00
Files
matrix-js-sdk/spec/unit/http-api/fetch.spec.ts

234 lines
10 KiB
TypeScript

/*
Copyright 2022 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 { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
const idBaseUrl = "http://idBaseUrl";
const prefix = ClientPrefix.V3;
it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
});
describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
.toThrow("No identity server base URL set");
});
it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
});
});
it("should return the Response object if onlyData=false", async () => {
const res = { ok: true };
const fetchFn = jest.fn().mockResolvedValue(res);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
});
it("should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
json: false,
})).resolves.toBe(text);
});
it("should send token via query params if useAuthorizationHeader=false", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
})),
});
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent),
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
describe("authedRequest", () => {
it("should not include token if unset", () => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
});
});