mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-25 07:17:23 +08:00
parent
8863fb70d1
commit
30d440a70e
14 changed files with 176 additions and 93 deletions
59
backend/__tests__/api/controllers/dev.spec.ts
Normal file
59
backend/__tests__/api/controllers/dev.spec.ts
Normal file
|
|
@ -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'"],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -105,6 +105,13 @@ export function getOpenApi(): OpenAPIObject {
|
|||
"x-displayName": "Server configuration",
|
||||
"x-public": "yes",
|
||||
},
|
||||
{
|
||||
name: "dev",
|
||||
description:
|
||||
"Development related endpoints. Only available on dev environment",
|
||||
"x-displayName": "Development",
|
||||
"x-public": "no",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import { MonkeyResponse2 } from "../../utils/monkey-response";
|
||||
import * as UserDal from "../../dal/user";
|
||||
import FirebaseAdmin from "../../init/firebase-admin";
|
||||
import Logger from "../../utils/logger";
|
||||
|
|
@ -9,30 +9,27 @@ import { roundTo2 } from "../../utils/misc";
|
|||
import { ObjectId } from "mongodb";
|
||||
import * as LeaderboardDal from "../../dal/leaderboards";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import isNumber from "lodash/isNumber";
|
||||
|
||||
import {
|
||||
Mode,
|
||||
PersonalBest,
|
||||
PersonalBests,
|
||||
} from "@monkeytype/contracts/schemas/shared";
|
||||
import {
|
||||
GenerateDataRequest,
|
||||
GenerateDataResponse,
|
||||
} from "@monkeytype/contracts/dev";
|
||||
|
||||
type GenerateDataOptions = {
|
||||
firstTestTimestamp: Date;
|
||||
lastTestTimestamp: Date;
|
||||
minTestsPerDay: number;
|
||||
maxTestsPerDay: number;
|
||||
};
|
||||
|
||||
const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = {
|
||||
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())),
|
||||
lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())),
|
||||
const CREATE_RESULT_DEFAULT_OPTIONS = {
|
||||
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())).valueOf(),
|
||||
lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())).valueOf(),
|
||||
minTestsPerDay: 0,
|
||||
maxTestsPerDay: 50,
|
||||
};
|
||||
|
||||
export async function createTestData(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2<undefined, GenerateDataRequest>
|
||||
): Promise<GenerateDataResponse> {
|
||||
const { username, createUser } = req.body;
|
||||
const user = await getOrCreateUser(username, "password", createUser);
|
||||
|
||||
|
|
@ -42,7 +39,7 @@ export async function createTestData(
|
|||
await updateUser(uid);
|
||||
await updateLeaderboard();
|
||||
|
||||
return new MonkeyResponse("test data created", { uid, email }, 200);
|
||||
return new MonkeyResponse2("test data created", { uid, email });
|
||||
}
|
||||
|
||||
async function getOrCreateUser(
|
||||
|
|
@ -73,20 +70,18 @@ async function getOrCreateUser(
|
|||
|
||||
async function createTestResults(
|
||||
user: MonkeyTypes.DBUser,
|
||||
configOptions: Partial<GenerateDataOptions>
|
||||
configOptions: GenerateDataRequest
|
||||
): Promise<void> {
|
||||
const config = {
|
||||
...CREATE_RESULT_DEFAULT_OPTIONS,
|
||||
...configOptions,
|
||||
};
|
||||
if (isNumber(config.firstTestTimestamp))
|
||||
config.firstTestTimestamp = toDate(config.firstTestTimestamp);
|
||||
if (isNumber(config.lastTestTimestamp))
|
||||
config.lastTestTimestamp = toDate(config.lastTestTimestamp);
|
||||
const start = toDate(config.firstTestTimestamp);
|
||||
const end = toDate(config.lastTestTimestamp);
|
||||
|
||||
const days = DateUtils.eachDayOfInterval({
|
||||
start: config.firstTestTimestamp,
|
||||
end: config.lastTestTimestamp,
|
||||
start,
|
||||
end,
|
||||
}).map((day) => ({
|
||||
timestamp: DateUtils.startOfDay(day),
|
||||
amount: Math.round(random(config.minTestsPerDay, config.maxTestsPerDay)),
|
||||
|
|
|
|||
|
|
@ -1,35 +1,15 @@
|
|||
import { Router } from "express";
|
||||
import joi from "joi";
|
||||
import { createTestData } from "../controllers/dev";
|
||||
import { isDevEnvironment } from "../../utils/misc";
|
||||
import { validate } from "../../middlewares/configuration";
|
||||
import { validateRequest } from "../../middlewares/validation";
|
||||
import { asyncHandler } from "../../middlewares/utility";
|
||||
import { devContract } from "@monkeytype/contracts/dev";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
|
||||
const router = Router();
|
||||
import * as DevController from "../controllers/dev";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
import { onlyAvailableOnDev } from "../../middlewares/utility";
|
||||
|
||||
router.use(
|
||||
validate({
|
||||
criteria: () => {
|
||||
return isDevEnvironment();
|
||||
},
|
||||
invalidMessage: "Development endpoints are only available in DEV mode.",
|
||||
})
|
||||
);
|
||||
const s = initServer();
|
||||
|
||||
router.post(
|
||||
"/generateData",
|
||||
validateRequest({
|
||||
body: {
|
||||
username: joi.string().required(),
|
||||
createUser: joi.boolean().optional(),
|
||||
firstTestTimestamp: joi.number().optional(),
|
||||
lastTestTimestamp: joi.number().optional(),
|
||||
minTestsPerDay: joi.number().optional(),
|
||||
maxTestsPerDay: joi.number().optional(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(createTestData)
|
||||
);
|
||||
|
||||
export default router;
|
||||
export default s.router(devContract, {
|
||||
generateData: {
|
||||
middleware: [onlyAvailableOnDev()],
|
||||
handler: async (r) => callController(DevController.createTestData)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const router = s.router(contract, {
|
|||
leaderboards,
|
||||
results,
|
||||
configuration,
|
||||
dev,
|
||||
});
|
||||
|
||||
export function addApiRoutes(app: Application): void {
|
||||
|
|
@ -139,9 +140,6 @@ function applyDevApiRoutes(app: Application): void {
|
|||
}
|
||||
next();
|
||||
});
|
||||
|
||||
//enable dev edpoints
|
||||
app.use("/dev", dev);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import _ from "lodash";
|
|||
import type { Request, Response, NextFunction, RequestHandler } from "express";
|
||||
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
|
||||
import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus";
|
||||
import { validate } from "./configuration";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
|
||||
export const emptyMiddleware = (
|
||||
_req: MonkeyTypes.Request,
|
||||
|
|
@ -49,3 +51,12 @@ export function recordClientVersion(): RequestHandler {
|
|||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export function onlyAvailableOnDev(): RequestHandler {
|
||||
return validate({
|
||||
criteria: () => {
|
||||
return isDevEnvironment();
|
||||
},
|
||||
invalidMessage: "Development endpoints are only available in DEV mode.",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
const BASE_PATH = "/dev";
|
||||
|
||||
export default class Dev {
|
||||
constructor(private httpClient: Ape.HttpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
async generateData(
|
||||
params: Ape.Dev.GenerateData
|
||||
): Ape.EndpointResponse<Ape.Dev.GenerateDataResponse> {
|
||||
return await this.httpClient.post(BASE_PATH + "/generateData", {
|
||||
payload: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import Quotes from "./quotes";
|
||||
import Users from "./users";
|
||||
import Dev from "./dev";
|
||||
|
||||
export default {
|
||||
Quotes,
|
||||
Users,
|
||||
Dev,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { buildHttpClient } from "./adapters/axios-adapter";
|
|||
import { envConfig } from "../constants/env-config";
|
||||
import { buildClient } from "./adapters/ts-rest-adapter";
|
||||
import { contract } from "@monkeytype/contracts";
|
||||
import { devContract } from "@monkeytype/contracts/dev";
|
||||
|
||||
const API_PATH = "";
|
||||
const BASE_URL = envConfig.backendUrl;
|
||||
|
|
@ -10,13 +11,14 @@ const API_URL = `${BASE_URL}${API_PATH}`;
|
|||
|
||||
const httpClient = buildHttpClient(API_URL, 10_000);
|
||||
const tsRestClient = buildClient(contract, BASE_URL, 10_000);
|
||||
const devClient = buildClient(devContract, BASE_URL, 240_000);
|
||||
|
||||
// API Endpoints
|
||||
const Ape = {
|
||||
...tsRestClient,
|
||||
users: new endpoints.Users(httpClient),
|
||||
quotes: new endpoints.Quotes(httpClient),
|
||||
dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)),
|
||||
dev: devClient,
|
||||
};
|
||||
|
||||
export default Ape;
|
||||
|
|
|
|||
14
frontend/src/ts/ape/types/dev.d.ts
vendored
14
frontend/src/ts/ape/types/dev.d.ts
vendored
|
|
@ -1,14 +0,0 @@
|
|||
declare namespace Ape.Dev {
|
||||
type GenerateData = {
|
||||
username: string;
|
||||
createUser?: boolean;
|
||||
firstTestTimestamp?: number;
|
||||
lastTestTimestamp?: number;
|
||||
minTestsPerDay?: number;
|
||||
maxTestsPerDay?: number;
|
||||
};
|
||||
type GenerateDataResponse = {
|
||||
uid: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import {
|
|||
TextInput,
|
||||
} from "../utils/simple-modal";
|
||||
import { ShowOptions } from "../utils/animated-modal";
|
||||
import { GenerateDataRequest } from "@monkeytype/contracts/dev";
|
||||
|
||||
type PopupKey =
|
||||
| "updateEmail"
|
||||
|
|
@ -1308,7 +1309,7 @@ list.devGenerateData = new SimpleModal({
|
|||
minTestsPerDay,
|
||||
maxTestsPerDay
|
||||
): Promise<ExecReturn> => {
|
||||
const request: Ape.Dev.GenerateData = {
|
||||
const request: GenerateDataRequest = {
|
||||
username,
|
||||
createUser: createUser === "true",
|
||||
};
|
||||
|
|
@ -1321,11 +1322,11 @@ list.devGenerateData = new SimpleModal({
|
|||
if (maxTestsPerDay !== undefined && maxTestsPerDay.length > 0)
|
||||
request.maxTestsPerDay = Number.parseInt(maxTestsPerDay);
|
||||
|
||||
const result = await Ape.dev.generateData(request);
|
||||
const result = await Ape.dev.generateData({ body: request });
|
||||
|
||||
return {
|
||||
status: result.status === 200 ? 1 : -1,
|
||||
message: result.message,
|
||||
message: result.body.message,
|
||||
hideOptions: {
|
||||
clearModalChain: true,
|
||||
},
|
||||
|
|
|
|||
58
packages/contracts/src/dev.ts
Normal file
58
packages/contracts/src/dev.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
import { IdSchema } from "./schemas/util";
|
||||
|
||||
export const GenerateDataRequestSchema = z.object({
|
||||
username: z.string(),
|
||||
createUser: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"If `true` create user with <username>@example.com and password `password`. If false user has to exist."
|
||||
),
|
||||
firstTestTimestamp: z.number().int().nonnegative().optional(),
|
||||
lastTestTimestamp: z.number().int().nonnegative().optional(),
|
||||
minTestsPerDay: z.number().int().nonnegative().optional(),
|
||||
maxTestsPerDay: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export type GenerateDataRequest = z.infer<typeof GenerateDataRequestSchema>;
|
||||
|
||||
export const GenerateDataResponseSchema = responseWithData(
|
||||
z.object({
|
||||
uid: IdSchema,
|
||||
email: z.string().email(),
|
||||
})
|
||||
);
|
||||
export type GenerateDataResponse = z.infer<typeof GenerateDataResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const devContract = c.router(
|
||||
{
|
||||
generateData: {
|
||||
summary: "generate data",
|
||||
description: "Generate test results for the given user.",
|
||||
method: "POST",
|
||||
path: "/generateData",
|
||||
body: GenerateDataRequestSchema.strict(),
|
||||
responses: {
|
||||
200: GenerateDataResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/dev",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
openApiTags: "dev",
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
@ -8,6 +8,7 @@ import { publicContract } from "./public";
|
|||
import { leaderboardsContract } from "./leaderboards";
|
||||
import { resultsContract } from "./results";
|
||||
import { configurationContract } from "./configuration";
|
||||
import { devContract } from "./dev";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
|
|
@ -21,4 +22,5 @@ export const contract = c.router({
|
|||
leaderboards: leaderboardsContract,
|
||||
results: resultsContract,
|
||||
configuration: configurationContract,
|
||||
dev: devContract,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export type OpenApiTag =
|
|||
| "public"
|
||||
| "leaderboards"
|
||||
| "results"
|
||||
| "configuration";
|
||||
| "configuration"
|
||||
| "dev";
|
||||
|
||||
export type EndpointMetadata = {
|
||||
/** Authentication options, by default a bearer token is required. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue