Skip to content

Commit

Permalink
Merge branch 'monkeytypegame:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcosatc147 authored Aug 11, 2024
2 parents 3d9eff4 + 092d513 commit eae2fc0
Show file tree
Hide file tree
Showing 78 changed files with 1,232 additions and 562 deletions.
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"
]
}
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
[![](https://github.com/monkeytypegame/monkeytype/blob/master/frontend/static/images/githubbanner2.png?raw=true)](https://monkeytype.com/)
<br />

![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)
![ChartJs](https://img.shields.io/badge/Chart.js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white)
![Eslint](https://img.shields.io/badge/eslint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white)
![Express](https://img.shields.io/badge/-Express-373737?style=for-the-badge&logo=Express&logoColor=white)
![Firebase](https://img.shields.io/badge/firebase-ffca28?style=for-the-badge&logo=firebase&logoColor=black)
![Fontawesome](https://img.shields.io/badge/fontawesome-538DD7?style=for-the-badge&logo=fontawesome&logoColor=white)
![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white)
![JQuery](https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white)
![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?style=for-the-badge&logo=mongodb&logoColor=white)
![PNPM](https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=white)
![Redis](https://img.shields.io/badge/Redis-DC382D?style=for-the-badge&logo=redis&logoColor=white)
![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)
![TsRest](https://img.shields.io/badge/-TSREST-9333ea?style=for-the-badge&logoColor=white&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMjAuMzA2Nzc4bW0iCiAgIGhlaWdodD0iMTIuMDgzMjMzbW0iCiAgIHZpZXdCb3g9IjAgMCAyMC4zMDY3NzggMTIuMDgzMjMzIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmcxIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3MSIKICAgICBwYWdlY29sb3I9IiM1MDUwNTAiCiAgICAgYm9yZGVyY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMSIKICAgICBpbmtzY2FwZTpzaG93cGFnZXNoYWRvdz0iMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIxIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iIC8+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiIC8+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMuODE5ODA1NCwtMi4yMTQ3MTkzKSI+CiAgICA8cGF0aAogICAgICAgZD0ibSAxNS40NTgwMzUsOC45NzMzOTUzIDguNjMzMjUsMC4wNDQ4NyAwLjAwOSwtMS42NjgxOTggLTguNjMzMjIsLTAuMDQ0ODUgeiBtIDAuMDI2MywtNS4wNTYxMDggOC42MzMyNSwwLjA0NDg1IDAuMDA5LC0xLjcwMjU2OCAtOC42MzMyNSwtMC4wNDQ4NSB6IG0gLTAuMDQ0OCw4LjYzMzI0NzcgOC42MzMyMywwLjA0NDg1IC0wLjAwOSwxLjcwMjU2NyAtOC42MzMyNSwtMC4wNDQ4NSB6IgogICAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICAgIGlkPSJwYXRoMSIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSAxMS4xMTE3MjUsMTAuMjg2NjI4IGMgMS42NTEsLTAuNjE5MTI0NyAyLjU5Njg4LC0xLjk2MDU2MjcgMi41OTY4OCwtMy44MDA3Mzk3IDAsLTIuNjQ4NDc5IC0xLjkyNjE2LC00LjI0Nzg4NSAtNS4wNzMzNzk2LC00LjI0Nzg4NSBoIC00LjgxNTQyIHYgMS43MDI1OTQgaCA0Ljc0NjYzIGMgMi4wODA5Mzk2LDAgMy4xNjQ0MDk2LDAuOTI4Njg3IDMuMTY0NDA5NiwyLjU0NTI5MSAwLDEuNTk5NDA2IC0xLjA4MzQ3LDIuNTQ1MjkyIC0zLjE2NDQwOTYsMi41NDUyOTIgaCAtNC43NDY2MyB2IDUuMjQ1MzYzNyBoIDEuOTYwNTYgdiAtMy41NzcxNjYgaCAyLjg1NDg2IGMgMC4yMDYzNywwIDAuNDI5OTUsMCAwLjYxOTEyLC0wLjAxNzIgbCAyLjUyODA5OTYsMy41OTQzNjQgaCAyLjEzMjU0IHoiCiAgICAgICBmaWxsPSIjZmZmZmZmIgogICAgICAgaWQ9InBhdGgyIgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICA8L2c+Cjwvc3ZnPgo=)
![Turborepo](https://img.shields.io/badge/-Turborepo-EF4444?style=for-the-badge&logo=turborepo&logoColor=white)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=Vite&logoColor=white)
![Vitest](https://img.shields.io/badge/vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white)
![Zod](https://img.shields.io/badge/-Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white)

# About

Expand Down
2 changes: 1 addition & 1 deletion backend/__tests__/api/controllers/ape-key.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("ApeKeyController", () => {

beforeEach(async () => {
await enableApeKeysEndpoints(true);
getUserMock.mockResolvedValue(user(uid, { canManageApeKeys: true }));
getUserMock.mockResolvedValue(user(uid, {}));
vi.useFakeTimers();
vi.setSystemTime(1000);
});
Expand Down
80 changes: 80 additions & 0 deletions backend/__tests__/api/controllers/psa.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import request from "supertest";
import app from "../../../src/app";
import * as PsaDal from "../../../src/dal/psa";
import * as Prometheus from "../../../src/utils/prometheus";
import { ObjectId } from "mongodb";
const mockApp = request(app);

describe("Psa Controller", () => {
describe("get psa", () => {
const getPsaMock = vi.spyOn(PsaDal, "get");
const recordClientVersionMock = vi.spyOn(Prometheus, "recordClientVersion");

afterEach(() => {
getPsaMock.mockReset();
recordClientVersionMock.mockReset();
});

it("get psas without authorization", async () => {
//GIVEN
const psaOne: PsaDal.DBPSA = {
_id: new ObjectId(),
message: "test2",
date: 1000,
level: 1,
sticky: true,
};
const psaTwo: PsaDal.DBPSA = {
_id: new ObjectId(),
message: "test2",
date: 2000,
level: 2,
sticky: false,
};
getPsaMock.mockResolvedValue([psaOne, psaTwo]);

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

//THEN
expect(body).toEqual({
message: "PSAs retrieved",
data: [
{
_id: psaOne._id.toHexString(),
date: 1000,
level: 1,
message: "test2",
sticky: true,
},
{
_id: psaTwo._id.toHexString(),
date: 2000,
level: 2,
message: "test2",
sticky: false,
},
],
});

expect(recordClientVersionMock).toHaveBeenCalledWith("unknown");
});
it("get psas with authorization", async () => {
await mockApp
.get("/psas")
.set("authorization", `Uid 123456789`)
.expect(200);
});

it("get psas records x-client-version", async () => {
await mockApp.get("/psas").set("x-client-version", "1.0").expect(200);

expect(recordClientVersionMock).toHaveBeenCalledWith("1.0");
});
it("get psas records client-version", async () => {
await mockApp.get("/psas").set("client-version", "2.0").expect(200);

expect(recordClientVersionMock).toHaveBeenCalledWith("2.0");
});
});
});
144 changes: 144 additions & 0 deletions backend/__tests__/api/controllers/public.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import request from "supertest";
import app from "../../../src/app";
import * as PublicDal from "../../../src/dal/public";
const mockApp = request(app);

describe("PublicController", () => {
describe("get speed histogram", () => {
const getSpeedHistogramMock = vi.spyOn(PublicDal, "getSpeedHistogram");

afterEach(() => {
getSpeedHistogramMock.mockReset();
});

it("gets for english time 60", async () => {
//GIVEN
getSpeedHistogramMock.mockResolvedValue({ "0": 1, "10": 2 });

//WHEN
const { body } = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode: "time", mode2: "60" });
//.expect(200);
console.log(body);

//THEN
expect(body).toEqual({
message: "Public speed histogram retrieved",
data: { "0": 1, "10": 2 },
});

expect(getSpeedHistogramMock).toHaveBeenCalledWith(
"english",
"time",
"60"
);
});

it("gets for mode", async () => {
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
const response = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode, mode2: "custom" });
expect(response.status, "for mode " + mode).toEqual(200);
}
});

it("gets for mode2", async () => {
for (const mode2 of [
"10",
"25",
"50",
"100",
"15",
"30",
"60",
"120",
"zen",
"custom",
]) {
const response = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode: "words", mode2 });

expect(response.status, "for mode2 " + mode2).toEqual(200);
}
});
it("fails for missing query", async () => {
const { body } = await mockApp.get("/public/speedHistogram").expect(422);

expect(body).toEqual({
message: "Invalid query schema",
validationErrors: [
'"language" Required',
'"mode" Required',
'"mode2" Needs to be either a number, "zen" or "custom."',
],
});
});
it("fails for invalid query", async () => {
const { body } = await mockApp
.get("/public/speedHistogram")
.query({
language: "en?gli.sh",
mode: "unknownMode",
mode2: "unknownMode2",
})
.expect(422);

expect(body).toEqual({
message: "Invalid query schema",
validationErrors: [
'"language" Invalid',
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
'"mode2" Needs to be either a number, "zen" or "custom."',
],
});
});
it("fails for unknown query", async () => {
const { body } = await mockApp
.get("/public/speedHistogram")
.query({
language: "english",
mode: "time",
mode2: "60",
extra: "value",
})
.expect(422);

expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
describe("get typing stats", () => {
const getTypingStatsMock = vi.spyOn(PublicDal, "getTypingStats");

afterEach(() => {
getTypingStatsMock.mockReset();
});

it("gets without authentication", async () => {
//GIVEN
getTypingStatsMock.mockResolvedValue({
testsCompleted: 23,
testsStarted: 42,
timeTyping: 1000,
} as any);

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

//THEN
expect(body).toEqual({
message: "Public typing stats retrieved",
data: {
testsCompleted: 23,
testsStarted: 42,
timeTyping: 1000,
},
});
});
});
});
4 changes: 2 additions & 2 deletions backend/__tests__/dal/admin-uids.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("AdminUidsDal", () => {
it("should return true for existing admin user", async () => {
//GIVEN
const uid = new ObjectId().toHexString();
AdminUidsDal.getCollection().insertOne({
await AdminUidsDal.getCollection().insertOne({
_id: new ObjectId(),
uid: uid,
});
Expand All @@ -17,7 +17,7 @@ describe("AdminUidsDal", () => {

it("should return false for non-existing admin user", async () => {
//GIVEN
AdminUidsDal.getCollection().insertOne({
await AdminUidsDal.getCollection().insertOne({
_id: new ObjectId(),
uid: "admin",
});
Expand Down
3 changes: 3 additions & 0 deletions backend/__tests__/utils/misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,5 +649,8 @@ describe("Misc Utils", () => {
},
]);
});
it("handles undefined", () => {
expect(misc.replaceObjectIds(undefined as any)).toBeUndefined();
});
});
});
5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
"start": "node ./dist/server.js",
"test": "vitest run",
"test-coverage": "vitest run --coverage",
"dev": "concurrently \"tsx watch --clear-screen=false --inspect ./src/server.ts\" \"tsc --preserveWatchOutput --noEmit --watch\" \"npx eslint-watch \"./src/**/*.ts\"\"",
"dev": "concurrently -p none \"tsx watch --clear-screen=false --inspect ./src/server.ts\" \"tsc --preserveWatchOutput --noEmit --watch\" \"esw src/ -w --ext .ts --cache --color\"",
"knip": "knip",
"docker-db-only": "docker compose -f docker/compose.db-only.yml up",
"docker": "docker compose -f docker/compose.yml up",
"gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2"
"gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && openapi-recursive-tagging dist/static/api/openapi.json dist/static/api/openapi-tagged.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2"
},
"engines": {
"node": "20.16.0"
Expand Down Expand Up @@ -90,6 +90,7 @@
"eslint": "8.57.0",
"eslint-watch": "8.0.0",
"ioredis-mock": "7.4.0",
"openapi-recursive-tagging": "0.0.6",
"readline-sync": "1.4.10",
"supertest": "6.2.3",
"tsx": "4.16.2",
Expand Down
2 changes: 1 addition & 1 deletion backend/redocly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ apis:
internal@v2:
root: dist/static/api/openapi.json
public-filter:
root: dist/static/api/openapi.json
root: dist/static/api/openapi-tagged.json
decorators:
filter-in:
property: x-public
Expand Down
10 changes: 10 additions & 0 deletions backend/scripts/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ export function getOpenApi(): OpenAPIObject {
description: "Ape keys provide access to certain API endpoints.",
"x-displayName": "Ape Keys",
},
{
name: "public",
description: "Public endpoints such as typing stats.",
"x-displayName": "public",
},
{
name: "psas",
description: "Public service announcements.",
"x-displayName": "PSAs",
},
{
name: "admin",
description:
Expand Down
10 changes: 7 additions & 3 deletions backend/src/api/controllers/psa.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { GetPsaResponse } from "@monkeytype/contracts/psas";
import * as PsaDAL from "../../dal/psa";
import { MonkeyResponse } from "../../utils/monkey-response";
import { MonkeyResponse2 } from "../../utils/monkey-response";
import { replaceObjectIds } from "../../utils/misc";

export async function getPsas(): Promise<MonkeyResponse> {
export async function getPsas(
_req: MonkeyTypes.Request2
): Promise<GetPsaResponse> {
const data = await PsaDAL.get();
return new MonkeyResponse("PSAs retrieved", data);
return new MonkeyResponse2("PSAs retrieved", replaceObjectIds(data));
}
29 changes: 15 additions & 14 deletions backend/src/api/controllers/public.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import {
GetSpeedHistogramQuery,
GetSpeedHistogramResponse,
GetTypingStatsResponse,
} from "@monkeytype/contracts/public";
import * as PublicDAL from "../../dal/public";
import { MonkeyResponse } from "../../utils/monkey-response";
import { MonkeyResponse2 } from "../../utils/monkey-response";

export async function getPublicSpeedHistogram(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
export async function getSpeedHistogram(
req: MonkeyTypes.Request2<GetSpeedHistogramQuery>
): Promise<GetSpeedHistogramResponse> {
const { language, mode, mode2 } = req.query;
const data = await PublicDAL.getSpeedHistogram(
language as string,
mode as string,
mode2 as string
);
return new MonkeyResponse("Public speed histogram retrieved", data);
const data = await PublicDAL.getSpeedHistogram(language, mode, mode2);
return new MonkeyResponse2("Public speed histogram retrieved", data);
}

export async function getPublicTypingStats(
_req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
export async function getTypingStats(
_req: MonkeyTypes.Request2
): Promise<GetTypingStatsResponse> {
const data = await PublicDAL.getTypingStats();
return new MonkeyResponse("Public typing stats retrieved", data);
return new MonkeyResponse2("Public typing stats retrieved", data);
}
Loading

0 comments on commit eae2fc0

Please sign in to comment.