Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impr: use tsrest for configurations endpoint (@fehmer) #5796

Merged
merged 12 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/__tests__/__testData__/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Configuration } from "@monkeytype/shared-types";
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
import { randomBytes } from "crypto";
import { hash } from "bcrypt";
import { ObjectId } from "mongodb";
Expand Down
194 changes: 194 additions & 0 deletions backend/__tests__/api/controllers/configuration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import request from "supertest";
import app from "../../../src/app";
import {
BASE_CONFIGURATION,
CONFIGURATION_FORM_SCHEMA,
} from "../../../src/constants/base-configuration";
import * as Configuration from "../../../src/init/configuration";
import type { Configuration as ConfigurationType } from "@monkeytype/contracts/schemas/configuration";
import { ObjectId } from "mongodb";
import * as Misc from "../../../src/utils/misc";
import { DecodedIdToken } from "firebase-admin/auth";
import * as AuthUtils from "../../../src/utils/auth";
import * as AdminUuids from "../../../src/dal/admin-uids";

const mockApp = request(app);
const uid = new ObjectId().toHexString();
const mockDecodedToken = {
uid,
email: "[email protected]",
iat: 0,
} as DecodedIdToken;

describe("Configuration Controller", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
const verifyIdTokenMock = vi.spyOn(AuthUtils, "verifyIdToken");
const isAdminMock = vi.spyOn(AdminUuids, "isAdmin");

beforeEach(() => {
isAdminMock.mockReset();
verifyIdTokenMock.mockReset();
isDevEnvironmentMock.mockReset();

isDevEnvironmentMock.mockReturnValue(true);
isAdminMock.mockResolvedValue(true);
});

describe("getConfiguration", () => {
it("should get without authentication", async () => {
//GIVEN

//WHEN
const { body } = await mockApp.get("/configuration").expect(200);

//THEN
expect(body).toEqual({
message: "Configuration retrieved",
data: BASE_CONFIGURATION,
});
});
});

describe("getConfigurationSchema", () => {
it("should get without authentication on dev", async () => {
//GIVEN

//WHEN
const { body } = await mockApp.get("/configuration/schema").expect(200);

//THEN
expect(body).toEqual({
message: "Configuration schema retrieved",
data: CONFIGURATION_FORM_SCHEMA,
});
});

it("should fail without authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);

//WHEN
await mockApp.get("/configuration/schema").expect(401);
});
it("should get with authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);

//WHEN
const { body } = await mockApp
.get("/configuration/schema")
.set("Authorization", "Bearer 123456789")
.expect(200);

//THEN
expect(body).toEqual({
message: "Configuration schema retrieved",
data: CONFIGURATION_FORM_SCHEMA,
});

expect(verifyIdTokenMock).toHaveBeenCalled();
});
it("should fail with non-admin user on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
isAdminMock.mockResolvedValue(false);

//WHEN
const { body } = await mockApp
.get("/configuration/schema")
.set("Authorization", "Bearer 123456789")
.expect(403);

//THEN
expect(body.message).toEqual("You don't have permission to do this.");
expect(verifyIdTokenMock).toHaveBeenCalled();
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});

describe("updateConfiguration", () => {
const patchConfigurationMock = vi.spyOn(
Configuration,
"patchConfiguration"
);
beforeEach(() => {
patchConfigurationMock.mockReset();
patchConfigurationMock.mockResolvedValue(true);
});

it("should update without authentication on dev", async () => {
//GIVEN
const patch = {
users: {
premium: {
enabled: true,
},
},
} as Partial<ConfigurationType>;

//WHEN
const { body } = await mockApp
.patch("/configuration")
.send({ configuration: patch })
.expect(200);

//THEN
expect(body).toEqual({
message: "Configuration updated",
data: null,
});

expect(patchConfigurationMock).toHaveBeenCalledWith(patch);
});

it("should fail update without authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);

//WHEN
await request(app)
.patch("/configuration")
.send({ configuration: {} })
.expect(401);

//THEN
expect(patchConfigurationMock).not.toHaveBeenCalled();
});
it("should update with authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);

//WHEN
await mockApp
.patch("/configuration")
.set("Authorization", "Bearer 123456789")
.send({ configuration: {} })
.expect(200);

//THEN
expect(patchConfigurationMock).toHaveBeenCalled();
expect(verifyIdTokenMock).toHaveBeenCalled();
});

it("should fail for non admin users on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
isAdminMock.mockResolvedValue(false);
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);

//WHEN
await mockApp
.patch("/configuration")
.set("Authorization", "Bearer 123456789")
.send({ configuration: {} })
.expect(403);

//THEN
expect(patchConfigurationMock).not.toHaveBeenCalled();
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});
});
139 changes: 127 additions & 12 deletions backend/__tests__/middlewares/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,15 @@ describe("middlewares/auth", () => {
requireFreshToken: true,
});

let result;

try {
result = await authenticateRequest(
expect(() =>
authenticateRequest(
mockRequest as Request,
mockResponse as Response,
nextFunction
);
} catch (e) {
result = e;
}

expect(result.message).toBe(
)
).rejects.toThrowError(
"Unauthorized\nStack: This endpoint requires a fresh token"
);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow the request if token is fresh", async () => {
Date.now = vi.fn(() => 10000);
Expand Down Expand Up @@ -321,7 +314,7 @@ describe("middlewares/auth", () => {
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail wit apeKey if apeKey is not supported", async () => {
it("should fail with apeKey if apeKey is not supported", async () => {
//WHEN
await expect(() =>
authenticate(
Expand All @@ -332,6 +325,22 @@ describe("middlewares/auth", () => {

//THEN
});
it("should fail with apeKey if apeKeys are disabled", async () => {
//GIVEN

//@ts-expect-error
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;

//WHEN
await expect(() =>
authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: false }
)
).rejects.toThrowError("ApeKeys are not being accepted at this time");

//THEN
});
it("should allow the request with authentation on public endpoint", async () => {
//WHEN
const result = await authenticate({}, { isPublic: true });
Expand Down Expand Up @@ -489,6 +498,112 @@ describe("middlewares/auth", () => {
expect.anything()
);
});
it("should allow the request with authentation on dev public endpoint", async () => {
//WHEN
const result = await authenticate({}, { isPublicOnDev: true });

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow the request without authentication on dev public endpoint", async () => {
//WHEN
const result = await authenticate(
{ headers: {} },
{ isPublicOnDev: true }
);

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("None");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("");
expect(nextFunction).toHaveBeenCalledTimes(1);

expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("None");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request with apeKey on dev public endpoint", async () => {
//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true }
);

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);

expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow with apeKey if apeKeys are disabled on dev public endpoint", async () => {
//GIVEN

//@ts-expect-error
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;

//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true }
);

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);

expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request with authentation on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);
const result = await authenticate({}, { isPublicOnDev: true });

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail without authentication on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);

//THEN
await expect(() =>
authenticate({ headers: {} }, { isPublicOnDev: true })
).rejects.toThrowError("Unauthorized");
});
it("should allow with apeKey on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true }
);

//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);

expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
});
});

Expand Down
10 changes: 8 additions & 2 deletions backend/scripts/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export function getOpenApi(): OpenAPIObject {
{
name: "configs",
description:
"User specific configurations like test settings, theme or tags.",
"x-displayName": "User configuration",
"User specific configs like test settings, theme or tags.",
"x-displayName": "User configs",
"x-public": "no",
},
{
Expand Down Expand Up @@ -99,6 +99,12 @@ export function getOpenApi(): OpenAPIObject {
"x-displayName": "Admin",
"x-public": "no",
},
{
name: "configuration",
description: "Server configuration",
"x-displayName": "Server configuration",
"x-public": "yes",
},
],
},

Expand Down
Loading
Loading