/** * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ import routerInjectable, { routeInjectionToken } from "./router.injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { Router } from "./router"; import type { Cluster } from "../../common/cluster/cluster"; import { Request } from "mock-http"; import { getInjectable } from "@ogre-tools/injectable"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import parseRequestInjectable from "./parse-request.injectable"; import { contentTypes } from "./router-content-types"; import mockFs from "mock-fs"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import type { Route } from "./route"; import type { SetRequired } from "type-fest"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import fsInjectable from "../../common/fs/fs.injectable"; import { runInAction } from "mobx"; describe("router", () => { let router: Router; let routeHandlerMock: AsyncFnMock<() => any>; beforeEach(async () => { routeHandlerMock = asyncFn(); const di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); di.permitSideEffects(fsInjectable); di.override(parseRequestInjectable, () => () => Promise.resolve({ payload: "some-payload", mime: "some-mime", })); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); const injectable = getInjectable({ id: "some-route", instantiate: () => ({ method: "get", path: "/some-path", handler: routeHandlerMock, } as Route), injectionToken: routeInjectionToken, }); runInAction(() => { di.register(injectable); }); router = di.inject(routerInjectable); }); afterEach(() => { mockFs.restore(); }); describe("when navigating to the route", () => { let actualPromise: Promise; let clusterStub: Cluster; let requestStub: SetRequired; let responseStub: any; beforeEach(() => { requestStub = new Request({ url: "/some-path", method: "get", headers: { "content-type": "application/json", }, }) as SetRequired; responseStub = { end: jest.fn(), setHeader: jest.fn(), write: jest.fn(), statusCode: undefined }; clusterStub = {} as Cluster; actualPromise = router.route(clusterStub, requestStub, responseStub); }); it("calls handler with the request", () => { expect(routeHandlerMock).toHaveBeenCalledWith({ cluster: clusterStub, params: {}, path: "/some-path", payload: "some-payload", query: expect.any(URLSearchParams), raw: { req: requestStub, res: responseStub }, }); }); it("given no content-type is specified, when handler resolves, resolves with JSON", async () => { await routeHandlerMock.resolve({ response: "some-response-from-route-handler" }); await actualPromise; expect(responseStub.setHeader.mock.calls).toEqual([ ["Content-Type", "application/json"], ]); }); it("given JSON content-type is specified, when handler resolves with object, resolves with JSON", async () => { await routeHandlerMock.resolve({ response: { some: "object" }}); await actualPromise; expect(responseStub.end).toHaveBeenCalledWith('{"some":"object"}'); }); describe("when handler resolves without any result", () => { beforeEach(async () => { await routeHandlerMock.resolve(undefined); await actualPromise; }); it("resolves as plain text", () => { expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]); }); it("resolves with status code for no content", async () => { expect(responseStub.statusCode).toBe(204); }); it("resolves without content", async () => { expect(responseStub.end.mock.calls).toEqual([[]]); }); }); describe("when handler rejects", () => { beforeEach(async () => { await routeHandlerMock.reject(new Error("some-error")); await actualPromise; }); it("resolves as plain text", () => { expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]); }); it('resolves with "500" status code', () => { expect(responseStub.statusCode).toBe(500); }); it("resolves with error", () => { expect(responseStub.end).toHaveBeenCalledWith("Error: some-error"); }); }); [ { contentType: "text/plain", contentTypeObject: contentTypes.txt }, { contentType: "application/json", contentTypeObject: contentTypes.json }, { contentType: "text/html", contentTypeObject: contentTypes.html }, { contentType: "text/css", contentTypeObject: contentTypes.css }, { contentType: "image/gif", contentTypeObject: contentTypes.gif }, { contentType: "image/jpeg", contentTypeObject: contentTypes.jpg }, { contentType: "image/png", contentTypeObject: contentTypes.png }, { contentType: "image/svg+xml", contentTypeObject: contentTypes.svg }, { contentType: "application/javascript", contentTypeObject: contentTypes.js }, { contentType: "font/woff2", contentTypeObject: contentTypes.woff2 }, { contentType: "font/ttf", contentTypeObject: contentTypes.ttf }, ].forEach(scenario => { describe(`given content type is "${scenario.contentType}", when handler resolves with response`, () => { beforeEach(async () => { await routeHandlerMock.resolve({ response: "some-response", contentType: scenario.contentTypeObject }); await actualPromise; }); it("has content type specific headers", () => { expect(responseStub.setHeader.mock.calls).toEqual([ ["Content-Type", scenario.contentType], ]); }); it("defaults to successful status code", () => { expect(responseStub.statusCode).toBe(200); }); it("has response as body", () => { expect(responseStub.end).toHaveBeenCalledWith("some-response"); }); }); it(`given content type is "${scenario.contentType}", when handler resolves with success and custom status code, defaults to "200" as status code`, async () => { await routeHandlerMock.resolve({ response: "some-response", statusCode: 204, contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.statusCode).toBe(204); }); it(`given content type is "${scenario.contentType}", when handler resolves with success but without status code, defaults to "200" as status code`, async () => { await routeHandlerMock.resolve({ response: "some-response", contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.statusCode).toBe(200); }); it(`given content type is "${scenario.contentType}", when handler resolves without response, has no body`, async () => { await routeHandlerMock.resolve({ response: undefined, contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.end.mock.calls).toEqual([[]]); }); it(`given content type is "${scenario.contentType}", when handler resolves with error, has error as body`, async () => { await routeHandlerMock.resolve({ error: "some-error", contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.end).toHaveBeenCalledWith("some-error"); }); it(`given content type is "${scenario.contentType}", when handler resolves with error and status code, has custom status code`, async () => { await routeHandlerMock.resolve({ error: "some-error", statusCode: 414, contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.statusCode).toBe(414); }); it(`given content type is "${scenario.contentType}", when handler resolves with error but without status code, defaults to "400" as status code`, async () => { await routeHandlerMock.resolve({ error: "some-error", contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.statusCode).toBe(400); }); it(`given content type is "${scenario.contentType}", when handler resolves custom headers, resolves with content type specific headers and custom headers`, async () => { await routeHandlerMock.resolve({ response: "irrelevant", headers: { "Content-Type": "some-content-type-to-be-overridden", "Some-Header": "some-header-value", }, contentType: scenario.contentTypeObject, }); await actualPromise; expect(responseStub.setHeader.mock.calls).toEqual([ ["Content-Type", scenario.contentType], ["Some-Header", "some-header-value"], ]); }); describe(`given content type is "${scenario.contentType}", when handler resolves with binary content`, () => { let responseBufferStub: Buffer; beforeEach(async () => { responseBufferStub = Buffer.from("some-binary-content"); await routeHandlerMock.resolve({ response: responseBufferStub, contentType: scenario.contentTypeObject, }); await actualPromise; }); it("writes binary content to response", () => { expect(responseStub.write).toHaveBeenCalledWith(responseBufferStub); }); it("does not end with the response", () => { expect(responseStub.end.mock.calls[0]).toEqual([]); }); }); }); }); });