Skip to content

Commit

Permalink
Merge branch 'master' into tapeRTL
Browse files Browse the repository at this point in the history
  • Loading branch information
NadAlaba committed Aug 26, 2024
2 parents fd5238b + 30d440a commit e053c56
Show file tree
Hide file tree
Showing 233 changed files with 9,054 additions and 6,166 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/monkey-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ concurrency:

jobs:
pre-ci:
if: github.event.pull_request.draft == false
if: github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'force-ci') || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
name: pre-ci
runs-on: ubuntu-latest
outputs:
Expand Down Expand Up @@ -72,7 +72,7 @@ jobs:
name: prime-cache
runs-on: ubuntu-latest
needs: [pre-ci]
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true'
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:

- name: Checkout pnpm-lock
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
name: check-pretty
needs: [pre-ci, prime-cache]
runs-on: ubuntu-latest
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true'
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -169,7 +169,7 @@ jobs:
name: ci-be
needs: [pre-ci, prime-cache, check-pretty]
runs-on: ubuntu-latest
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true'
if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -217,7 +217,7 @@ jobs:
name: ci-fe
needs: [pre-ci, prime-cache, check-pretty]
runs-on: ubuntu-latest
if: needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true'
if: needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:

- uses: actions/checkout@v4
Expand Down Expand Up @@ -270,7 +270,7 @@ jobs:
name: ci-assets
needs: [pre-ci, prime-cache, check-pretty]
runs-on: ubuntu-latest
if: needs.pre-ci.outputs.assets-json == 'true'
if: needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -339,7 +339,7 @@ jobs:
name: ci-pkg
needs: [pre-ci, prime-cache,check-pretty]
runs-on: ubuntu-latest
if: needs.pre-ci.outputs.should-build-pkg == 'true'
if: needs.pre-ci.outputs.should-build-pkg == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci')
steps:

- uses: actions/checkout@v4
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/semantic-pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
main:
name: check
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
steps:
- name: Lint and verify PR title
uses: amannn/action-semantic-pull-request@v5
Expand Down Expand Up @@ -49,7 +50,7 @@ jobs:
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Expand Down
5 changes: 4 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"recommendations": [
"esbenp.prettier-vscode",
"vitest.explorer",
"huntertran.auto-markdown-toc"
"huntertran.auto-markdown-toc",
"ms-vscode.vscode-typescript-next",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
37 changes: 37 additions & 0 deletions backend/__tests__/__testData__/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
import { randomBytes } from "crypto";
import { hash } from "bcrypt";
import { ObjectId } from "mongodb";
import { base64UrlEncode } from "../../src/utils/misc";
import * as ApeKeyDal from "../../src/dal/ape-keys";

export async function mockAuthenticateWithApeKey(
uid: string,
config: Configuration
): Promise<string> {
if (!config.apeKeys.acceptKeys)
throw Error("config.apeKeys.acceptedKeys needs to be set to true");
const { apeKeyBytes, apeKeySaltRounds } = config.apeKeys;

const apiKey = randomBytes(apeKeyBytes).toString("base64url");
const saltyHash = await hash(apiKey, apeKeySaltRounds);

const apeKey: MonkeyTypes.ApeKeyDB = {
_id: new ObjectId(),
name: "bob",
enabled: true,
uid,
hash: saltyHash,
createdOn: Date.now(),
modifiedOn: Date.now(),
lastUsedOn: -1,
useCount: 0,
};

const apeKeyId = new ObjectId().toHexString();

vi.spyOn(ApeKeyDal, "getApeKey").mockResolvedValue(apeKey);
vi.spyOn(ApeKeyDal, "updateLastUsedOn").mockResolvedValue();

return base64UrlEncode(`${apeKeyId}.${apiKey}`);
}
4 changes: 2 additions & 2 deletions backend/__tests__/api/controllers/ape-key.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ const configuration = Configuration.getCachedConfiguration();
const uid = new ObjectId().toHexString();

describe("ApeKeyController", () => {
const getUserMock = vi.spyOn(UserDal, "getUser");
const getUserMock = vi.spyOn(UserDal, "getPartialUser");

beforeEach(async () => {
await enableApeKeysEndpoints(true);
getUserMock.mockResolvedValue(user(uid, { canManageApeKeys: true }));
getUserMock.mockResolvedValue(user(uid, {}));
vi.useFakeTimers();
vi.setSystemTime(1000);
});
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);
});
});
});
59 changes: 59 additions & 0 deletions backend/__tests__/api/controllers/dev.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import request from "supertest";
import app from "../../../src/app";

import { ObjectId } from "mongodb";
import * as Misc from "../../../src/utils/misc";

const uid = new ObjectId().toHexString();
const mockApp = request(app);

describe("DevController", () => {
describe("generate testData", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");

beforeEach(() => {
isDevEnvironmentMock.mockReset();
isDevEnvironmentMock.mockReturnValue(true);
});

it("should fail on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({ username: "test" })
.expect(503);
//THEN
expect(body.message).toEqual(
"Development endpoints are only available in DEV mode."
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({})
.expect(422);

//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [`"username" Required`],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({ username: "Bob", extra: "value" })
.expect(422);

//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
});
Loading

0 comments on commit e053c56

Please sign in to comment.