From bca04c7e8cc3f8ae647f871f4b5fffc8c1b36e81 Mon Sep 17 00:00:00 2001 From: Ilia Liubinskii Date: Sat, 22 Jun 2024 16:13:37 +0300 Subject: [PATCH 1/3] Auto-run playwright on push to develop --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 458a747..6d3d8f5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,7 +1,7 @@ name: Playwright Tests on: push: - branches: [ main, master ] + branches: [ develop ] pull_request: branches: [ main, master ] workflow_dispatch: From 7fa73b85c0e5d340aa85a1470562deb4110e59bf Mon Sep 17 00:00:00 2001 From: Ilia Liubinskii Date: Sat, 22 Jun 2024 17:31:53 +0300 Subject: [PATCH 2/3] Added e2e test for POST /categories/ --- .dockerignore | 7 +++++++ .env.e2e.example | 5 +++++ .gitignore | 3 ++- .vercelignore | 8 ++++++-- package.json | 1 + playwright.config.ts | 12 ++---------- src/config/index.ts | 2 -- tests/categories.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ tests/config.ts | 14 ++++++++++++++ tests/index.ts | 1 + tests/root.spec.ts | 2 +- 11 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 .env.e2e.example create mode 100644 tests/categories.spec.ts create mode 100644 tests/config.ts create mode 100644 tests/index.ts diff --git a/.dockerignore b/.dockerignore index 76906be..ff25d84 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,14 @@ .DS_Store .env +.env.e2e +.next .vercel +blob-report certificates coverage dist +docker node_modules +playwright/.cache +playwright-report +test-results diff --git a/.env.e2e.example b/.env.e2e.example new file mode 100644 index 0000000..720345c --- /dev/null +++ b/.env.e2e.example @@ -0,0 +1,5 @@ +CI=false +BASE_URL=https://preview-api.zero-company.app +JWT_ADMIN_EMAIL=... +JWT_EMAIL=... +JWT_SECRET=... diff --git a/.gitignore b/.gitignore index 5596557..ff25d84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ .DS_Store .env +.env.e2e .next .vercel blob-report certificates coverage -docker dist +docker node_modules playwright/.cache playwright-report diff --git a/.vercelignore b/.vercelignore index 6ee95e7..d4fc8f6 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1,13 +1,17 @@ .DS_Store .env +.env.e2e .next .vercel +blob-report certificates coverage -docker dist +docker node_modules -.DS_Store +playwright/.cache +playwright-report +test-results *.test.js *.test.jsx *.test.ts diff --git a/package.json b/package.json index 62705f8..5ebbf35 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "prepare": "husky", "start": "node dist/index.js", "test": "jest --coverage", + "test-e2e": "playwright test", "type-check": "tsc --incremental false --noEmit" }, "lint-staged": { diff --git a/playwright.config.ts b/playwright.config.ts index 54c2a0a..a719061 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,15 +1,7 @@ /* eslint-disable node/no-unpublished-import -- Ok */ -import { bool, cleanEnv } from "envalid"; +import { BASE_URL, CI } from "./tests"; import { defineConfig, devices } from "@playwright/test"; -import { config } from "dotenv"; - -config(); - -// eslint-disable-next-line no-process-env -- Ok -const { CI } = cleanEnv(process.env, { - CI: bool({ default: false }) -}); export default defineConfig({ forbidOnly: CI, @@ -24,7 +16,7 @@ export default defineConfig({ retries: CI ? 2 : 0, testDir: "./tests", use: { - baseURL: "https://preview-api.zero-company.app/", + baseURL: BASE_URL, trace: "on-first-retry" }, workers: CI ? 1 : 2 diff --git a/src/config/index.ts b/src/config/index.ts index 6a03dbd..780fdc4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -28,7 +28,6 @@ const env = cleanEnv(process.env, { AUTH0_CLIENT_SECRET: str(), AUTH0_DOMAIN: str(), AUTH0_RETURN_URL: str(), - CI: bool({ default: false }), CLOUDINARY_API_KEY: str(), CLOUDINARY_API_SECRET: str(), CLOUDINARY_BASE_FOLDER: str(), @@ -61,7 +60,6 @@ export const { AUTH0_CLIENT_SECRET, AUTH0_DOMAIN, AUTH0_RETURN_URL, - CI, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, CLOUDINARY_BASE_FOLDER, diff --git a/tests/categories.spec.ts b/tests/categories.spec.ts new file mode 100644 index 0000000..45c5235 --- /dev/null +++ b/tests/categories.spec.ts @@ -0,0 +1,37 @@ +import { BASE_URL, JWT_ADMIN_EMAIL, JWT_SECRET } from "./config"; +import { expect, test } from "@playwright/test"; +import { StatusCodes } from "http-status-codes"; +import jwt from "jsonwebtoken"; + +test.describe.parallel("Categories", () => { + const token = jwt.sign({ email: JWT_ADMIN_EMAIL }, JWT_SECRET); + + test.describe("POST /", () => { + const newCategoryData = { + description: "This is a new category for testing.", + name: "New", + pinned: false, + tagline: "Testing new category creation." + }; + + test("should create a new category", async ({ request }) => { + const createResponse = await request.post(`${BASE_URL}/categories`, { + data: newCategoryData, + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + } + }); + + expect(createResponse.status()).toBe(StatusCodes.CREATED); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Ok + const responseBody = await createResponse.json(); + + // eslint-disable-next-line no-warning-comments -- Ok + // TODO: Use expect to check response body + // eslint-disable-next-line no-console -- Temp + console.log(responseBody); + }); + }); +}); diff --git a/tests/config.ts b/tests/config.ts new file mode 100644 index 0000000..a19e9d9 --- /dev/null +++ b/tests/config.ts @@ -0,0 +1,14 @@ +import { bool, cleanEnv, str } from "envalid"; +import { config } from "dotenv"; + +config({ path: ".env.e2e" }); + +export const { BASE_URL, CI, JWT_ADMIN_EMAIL, JWT_EMAIL, JWT_SECRET } = + // eslint-disable-next-line no-process-env -- Ok + cleanEnv(process.env, { + BASE_URL: str(), + CI: bool({ default: false }), + JWT_ADMIN_EMAIL: str(), + JWT_EMAIL: str(), + JWT_SECRET: str() + }); diff --git a/tests/index.ts b/tests/index.ts new file mode 100644 index 0000000..5c62e04 --- /dev/null +++ b/tests/index.ts @@ -0,0 +1 @@ +export * from "./config"; diff --git a/tests/root.spec.ts b/tests/root.spec.ts index 317032d..81ede2b 100644 --- a/tests/root.spec.ts +++ b/tests/root.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { StatusCodes } from "http-status-codes"; -test.describe.parallel("API Testing", () => { +test.describe.parallel("Root", () => { // eslint-disable-next-line no-warning-comments -- Ok // TODO: Take it from configuration const baseURL = "https://preview-api.zero-company.app"; From 8f08179ff62289caf3817d2202697e88e6ad0fbc Mon Sep 17 00:00:00 2001 From: Ilia Liubinskii Date: Sun, 23 Jun 2024 16:27:24 +0300 Subject: [PATCH 3/3] Do not use jest globals to avoid conflict with playwright --- .eslintrc.cjs | 7 +- api/index.ts | 3 + jest.config.mjs | 3 +- jest.setup-after-env.ts | 5 +- package-lock.json | 2 + package.json | 5 +- src/global.d.ts | 16 ++- src/index.ts | 1 + src/langs/index.ts | 2 + src/providers/redis.ts | 2 +- src/routes/categories/controllers.ts | 12 +- src/routes/companies/controllers.ts | 12 +- src/routes/companies/images/controllers.ts | 8 +- src/routes/documents/controllers.ts | 10 +- src/routes/users/contollers.test.ts | 122 +++++++++++++-------- src/routes/users/controllers.ts | 16 +-- src/routes/users/service.test.ts | 46 ++++---- src/schema-mongodb/models.ts | 29 ++++- src/utils/assertions.ts | 12 -- src/utils/index.ts | 1 + src/utils/json.ts | 21 ++++ tsconfig.json | 2 +- utils/mongodb-memory-server.ts | 9 ++ 23 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 src/utils/json.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1e5ada9..c513dc9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,10 +2,7 @@ * @type {import("eslint").Linter.Config } */ const config = { - env: { - es2020: true, - jest: true - }, + env: { es2020: true }, extends: ["./.eslintrc.base.cjs"], globals: { Express: true }, ignorePatterns: ["!.*", "coverage/**", "dist/**", "node_modules/**"], @@ -104,6 +101,7 @@ const config = { "lcfirst", "localhost", "logform", + "matchers", "mjs", "mongodb", "multer", @@ -140,6 +138,7 @@ const config = { "ttl", "txt", "ucfirst", + "undef", "unlink", "uploader", "upsert", diff --git a/api/index.ts b/api/index.ts index 8050c03..25a0db7 100644 --- a/api/index.ts +++ b/api/index.ts @@ -5,6 +5,7 @@ import { lang, logServerInfo, logger, + modelsExist, mongodbConnectionExists, redisClientExists } from "../src"; @@ -30,6 +31,8 @@ export default async function handler( : lang.MongodbConnectionCacheMiss ); + logger.info(modelsExist() ? lang.ModelsCacheHit : lang.ModelsCacheMiss); + if (SESSION_STORE_PROVIDER === "redis") logger.info( redisClientExists() ? lang.RedisClientCacheHit : lang.RedisClientCacheMiss diff --git a/jest.config.mjs b/jest.config.mjs index d6443e0..c9c65ce 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -12,7 +12,8 @@ const config = { globalTeardown: "./jest.global-teardown.ts", preset: "ts-jest", setupFilesAfterEnv: ["jest-extended/all", "./jest.setup-after-env.ts"], - testEnvironment: "node" + testEnvironment: "node", + testPathIgnorePatterns: ["/dist/", "/node_modules/", "/tests/"] }; export default config; diff --git a/jest.setup-after-env.ts b/jest.setup-after-env.ts index 2f5c448..0a09902 100644 --- a/jest.setup-after-env.ts +++ b/jest.setup-after-env.ts @@ -3,8 +3,7 @@ /* eslint-disable import/no-namespace -- Ok */ import * as config from "./src/config"; - -jest.mock("./src/config"); +import { getMongodbMemoryServerUri } from "./utils"; // @ts-expect-error -config.MONGODB_URI = `mongodb://127.0.0.1:${config.TEST_MONGODB_PORT}/`; +config.MONGODB_URI = getMongodbMemoryServerUri(); diff --git a/package-lock.json b/package-lock.json index 40ccca9..c6391cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.44.1", "@types/eslint": "^8.56.10", @@ -2074,6 +2075,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", diff --git a/package.json b/package.json index 5ebbf35..0be41fc 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,7 @@ "version": "0.1.0", "private": true, "description": "Zero Company API", - "keywords": [ - "crowdworking" - ], + "keywords": [], "repository": { "type": "git", "url": "https://github.com/iliubinskii/zero-company-api" @@ -103,6 +101,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.44.1", "@types/eslint": "^8.56.10", diff --git a/src/global.d.ts b/src/global.d.ts index c311d36..5c0d008 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -3,11 +3,6 @@ import type { CSSProperties, DetailedHTMLProps, HTMLAttributes } from "react"; import type { Jwt } from "./schema"; -export type IntrinsicElement = DetailedHTMLProps< - HTMLAttributes, - HTMLElement -> & { name: string; role: string; style?: CSSProperties }; - declare global { namespace JSX { interface IntrinsicElements { @@ -18,6 +13,12 @@ declare global { } } +declare module "@jest/expect" { + export interface Matchers + // eslint-disable-next-line no-undef -- Ok + extends CustomMatchers {} +} + declare module "express-serve-static-core" { interface Request { idParam?: string | undefined; @@ -43,3 +44,8 @@ declare module "express-session" { successReturnUrl?: string | undefined; } } + +export type IntrinsicElement = DetailedHTMLProps< + HTMLAttributes, + HTMLElement +> & { name: string; role: string; style?: CSSProperties }; diff --git a/src/index.ts b/src/index.ts index 7c002c5..464b1e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from "./middleware"; export * from "./providers"; export * from "./routes"; export * from "./schema"; +export * from "./schema-mongodb"; export * from "./services"; export * from "./types"; export * from "./utils"; diff --git a/src/langs/index.ts b/src/langs/index.ts index 82cb386..e678a1f 100644 --- a/src/langs/index.ts +++ b/src/langs/index.ts @@ -26,6 +26,8 @@ export const lang = { JwtVerificationFailed: "JWT verification failed", LogLevel: "Log level", MethodNotAllowed: "Method not allowed. Https is required", + ModelsCacheHit: "Models cache hit", + ModelsCacheMiss: "Models cache miss", MongodbAll: "MongoDB all", MongodbClose: "MongoDB close", MongodbConnected: "MongoDB connected", diff --git a/src/providers/redis.ts b/src/providers/redis.ts index 7c37f37..c4f18fc 100644 --- a/src/providers/redis.ts +++ b/src/providers/redis.ts @@ -3,7 +3,7 @@ import { createClient } from "redis"; import { lang } from "../langs"; import { logger } from "../services"; -// Cache the connection in serverless environments +// Cache the client in serverless environments let cachedClient: ReturnType | undefined; /** diff --git a/src/routes/categories/controllers.ts b/src/routes/categories/controllers.ts index 09e133e..bba7e51 100644 --- a/src/routes/categories/controllers.ts +++ b/src/routes/categories/controllers.ts @@ -12,8 +12,8 @@ import { } from "../../schema"; import { assertDefined, - assertValidForJsonStringify, buildErrorResponse, + dangerouslyAssumeJsonTransform, sendResponse, wrapAsyncHandler } from "../../utils"; @@ -40,7 +40,7 @@ export function createCategoryControllers( sendResponse( res, StatusCodes.CREATED, - assertValidForJsonStringify(category) + dangerouslyAssumeJsonTransform(category) ); } else sendResponse( @@ -67,7 +67,7 @@ export function createCategoryControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(categories) + dangerouslyAssumeJsonTransform(categories) ); } else sendResponse( @@ -85,7 +85,7 @@ export function createCategoryControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(category) + dangerouslyAssumeJsonTransform(category) ); else sendResponse( @@ -108,7 +108,7 @@ export function createCategoryControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(companies) + dangerouslyAssumeJsonTransform(companies) ); } else sendResponse( @@ -129,7 +129,7 @@ export function createCategoryControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(category) + dangerouslyAssumeJsonTransform(category) ); else sendResponse( diff --git a/src/routes/companies/controllers.ts b/src/routes/companies/controllers.ts index 83a398a..0d12d55 100644 --- a/src/routes/companies/controllers.ts +++ b/src/routes/companies/controllers.ts @@ -8,8 +8,8 @@ import { } from "../../schema"; import { assertDefined, - assertValidForJsonStringify, buildErrorResponse, + dangerouslyAssumeJsonTransform, sendResponse, wrapAsyncHandler } from "../../utils"; @@ -42,7 +42,7 @@ export function createCompanyControllers( sendResponse( res, StatusCodes.CREATED, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); } else sendResponse( @@ -76,7 +76,7 @@ export function createCompanyControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( @@ -94,7 +94,7 @@ export function createCompanyControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(companies) + dangerouslyAssumeJsonTransform(companies) ); } else sendResponse( @@ -112,7 +112,7 @@ export function createCompanyControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( @@ -133,7 +133,7 @@ export function createCompanyControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( diff --git a/src/routes/companies/images/controllers.ts b/src/routes/companies/images/controllers.ts index 62de8b9..29f31ea 100644 --- a/src/routes/companies/images/controllers.ts +++ b/src/routes/companies/images/controllers.ts @@ -5,8 +5,8 @@ import type { import { CompanyImageCreateValidationSchema, ErrorCode } from "../../../schema"; import { assertDefined, - assertValidForJsonStringify, buildErrorResponse, + dangerouslyAssumeJsonTransform, sendResponse, wrapAsyncHandler } from "../../../utils"; @@ -34,7 +34,7 @@ export function createCompanyImageControllers( sendResponse( res, StatusCodes.CREATED, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( @@ -60,7 +60,7 @@ export function createCompanyImageControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( @@ -87,7 +87,7 @@ export function createCompanyImageControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(company) + dangerouslyAssumeJsonTransform(company) ); else sendResponse( diff --git a/src/routes/documents/controllers.ts b/src/routes/documents/controllers.ts index 0397d9b..007227f 100644 --- a/src/routes/documents/controllers.ts +++ b/src/routes/documents/controllers.ts @@ -7,8 +7,8 @@ import { } from "../../schema"; import { assertDefined, - assertValidForJsonStringify, buildErrorResponse, + dangerouslyAssumeJsonTransform, sendResponse, wrapAsyncHandler } from "../../utils"; @@ -36,7 +36,7 @@ export function createDocumentControllers( sendResponse( res, StatusCodes.CREATED, - assertValidForJsonStringify(document) + dangerouslyAssumeJsonTransform(document) ); } else sendResponse( @@ -63,7 +63,7 @@ export function createDocumentControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(document) + dangerouslyAssumeJsonTransform(document) ); else sendResponse( @@ -81,7 +81,7 @@ export function createDocumentControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(documents) + dangerouslyAssumeJsonTransform(documents) ); } else sendResponse( @@ -102,7 +102,7 @@ export function createDocumentControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(document) + dangerouslyAssumeJsonTransform(document) ); else sendResponse( diff --git a/src/routes/users/contollers.test.ts b/src/routes/users/contollers.test.ts index 2bce128..be3d49c 100644 --- a/src/routes/users/contollers.test.ts +++ b/src/routes/users/contollers.test.ts @@ -1,12 +1,18 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return -- Ok */ - -import { ErrorCode } from "../../schema"; +import type { + CompaniesService, + DocumentsService, + UserRef, + UsersService +} from "../../types"; +import { CompanyStatus, ErrorCode } from "../../schema"; +import { describe, expect, it, jest } from "@jest/globals"; import type { Jwt } from "../../schema"; import { StatusCodes } from "http-status-codes"; -import type { UserRef } from "../../types"; import { createUserControllers } from "./controllers"; import express from "express"; import { faker } from "@faker-js/faker"; +import { jsonTransform } from "../../utils"; +import mongoose from "mongoose"; import request from "supertest"; describe("createUserControllers", () => { @@ -18,29 +24,30 @@ describe("createUserControllers", () => { }; const mockUsersService = { - addUser: jest.fn(), - deleteUser: jest.fn(), - getUser: jest.fn(), - getUsers: jest.fn(), - updateUser: jest.fn() - }; + addUser: jest.fn(), + deleteUser: jest.fn(), + getUser: jest.fn(), + getUsers: jest.fn(), + updateUser: jest.fn() + } as const; const mockCompaniesService = { - addCompany: jest.fn(), - deleteCompany: jest.fn(), - generateFoundingAgreement: jest.fn(), - getCompanies: jest.fn(), - getCompany: jest.fn(), - updateCompany: jest.fn() - }; + addCompany: jest.fn(), + deleteCompany: jest.fn(), + generateFoundingAgreement: + jest.fn(), + getCompanies: jest.fn(), + getCompany: jest.fn(), + updateCompany: jest.fn() + } as const; const mockDocumentsService = { - addDocument: jest.fn(), - deleteDocument: jest.fn(), - getDocument: jest.fn(), - getDocuments: jest.fn(), - updateDocument: jest.fn() - }; + addDocument: jest.fn(), + deleteDocument: jest.fn(), + getDocument: jest.fn(), + getDocuments: jest.fn(), + updateDocument: jest.fn() + } as const; const controllers = createUserControllers( mockUsersService, @@ -68,23 +75,28 @@ describe("createUserControllers", () => { describe("addUser", () => { it("should add a user and return 201", async () => { - const user = { + const data = { firstName: faker.person.firstName(), lastName: faker.person.lastName() - }; + } as const; - mockUsersService.addUser.mockImplementationOnce(u => u); + const user = { + _id: new mongoose.Types.ObjectId(), + email: jwt.email, + favoriteCompanies: [], + ...data + } as const; - const response = await request(app).post("/users").send(user); + mockUsersService.addUser.mockResolvedValueOnce(user); + + const response = await request(app).post("/users").send(data); expect(response.status).toBe(StatusCodes.CREATED); - expect(response.body).toEqual({ ...user, email: jwt.email }); + expect(response.body).toEqual(jsonTransform(user)); }); it("should return 400 for invalid data", async () => { - const response = await request(app) - .post("/users") - .send({ invalid: "data" }); + const response = await request(app).post("/users").send({ firstName: 1 }); expect(response.status).toBe(StatusCodes.BAD_REQUEST); expect(response.body).toHaveProperty("error", ErrorCode.InvalidData); @@ -97,7 +109,7 @@ describe("createUserControllers", () => { lastName: faker.person.lastName() }; - mockUsersService.addUser.mockResolvedValueOnce(undefined); + mockUsersService.addUser.mockResolvedValueOnce(null); const response = await request(app).post("/users").send(user); @@ -125,7 +137,16 @@ describe("createUserControllers", () => { const companies = { count: 1, docs: [ - { _id: faker.database.mongodbObjectId(), name: faker.company.name() } + { + _id: new mongoose.Types.ObjectId(), + categories: [], + country: "us", + createdAt: faker.date.past(), + founders: [], + images: [], + name: faker.company.name(), + status: CompanyStatus.founded + } ], total: 1 }; @@ -137,13 +158,13 @@ describe("createUserControllers", () => { ); expect(response.status).toBe(StatusCodes.OK); - expect(response.body).toEqual(companies); + expect(response.body).toEqual(jsonTransform(companies)); }); it("should return 400 for invalid query", async () => { const response = await request(app) .get(`/users/${faker.database.mongodbObjectId()}/companies`) - .query({ invalid: "query" }); + .query({ limit: "x" }); expect(response.status).toBe(StatusCodes.BAD_REQUEST); expect(response.body).toHaveProperty("error", ErrorCode.InvalidQuery); @@ -154,7 +175,9 @@ describe("createUserControllers", () => { describe("getUser", () => { it("should get a user and return 200", async () => { const user = { - _id: faker.database.mongodbObjectId(), + _id: new mongoose.Types.ObjectId(), + email: faker.internet.email(), + favoriteCompanies: [], firstName: faker.person.firstName(), lastName: faker.person.lastName() }; @@ -166,11 +189,11 @@ describe("createUserControllers", () => { ); expect(response.status).toBe(StatusCodes.OK); - expect(response.body).toEqual(user); + expect(response.body).toEqual(jsonTransform(user)); }); - it("should return 404 if user not found", async () => { - mockUsersService.getUser.mockResolvedValueOnce(undefined); + it("should return 404 if user was not found", async () => { + mockUsersService.getUser.mockResolvedValueOnce(null); const response = await request(app).get( `/users/${faker.database.mongodbObjectId()}` @@ -188,7 +211,9 @@ describe("createUserControllers", () => { count: 1, docs: [ { - _id: faker.database.mongodbObjectId(), + _id: new mongoose.Types.ObjectId(), + email: faker.internet.email(), + favoriteCompanies: [], firstName: faker.person.firstName(), lastName: faker.person.lastName() } @@ -201,13 +226,11 @@ describe("createUserControllers", () => { const response = await request(app).get("/users"); expect(response.status).toBe(StatusCodes.OK); - expect(response.body).toEqual(users); + expect(response.body).toEqual(jsonTransform(users)); }); it("should return 400 for invalid query", async () => { - const response = await request(app) - .get("/users") - .query({ invalid: "query" }); + const response = await request(app).get("/users").query({ limit: "x" }); expect(response.status).toBe(StatusCodes.BAD_REQUEST); expect(response.body).toHaveProperty("error", ErrorCode.InvalidQuery); @@ -218,6 +241,9 @@ describe("createUserControllers", () => { describe("updateUser", () => { it("should update a user and return 200", async () => { const user = { + _id: new mongoose.Types.ObjectId(), + email: faker.internet.email(), + favoriteCompanies: [], firstName: faker.person.firstName(), lastName: faker.person.lastName() }; @@ -229,21 +255,21 @@ describe("createUserControllers", () => { .send(user); expect(response.status).toBe(StatusCodes.OK); - expect(response.body).toEqual(user); + expect(response.body).toEqual(jsonTransform(user)); }); it("should return 400 for invalid data", async () => { const response = await request(app) .put(`/users/${faker.database.mongodbObjectId()}`) - .send({ invalid: "data" }); + .send({ firstName: 1 }); expect(response.status).toBe(StatusCodes.BAD_REQUEST); expect(response.body).toHaveProperty("error", ErrorCode.InvalidData); expect(response.body).toHaveProperty("errorMessage"); }); - it("should return 404 if user not found", async () => { - mockUsersService.updateUser.mockResolvedValueOnce(undefined); + it("should return 404 if user was not found", async () => { + mockUsersService.updateUser.mockResolvedValueOnce(null); const response = await request(app).put( `/users/${faker.database.mongodbObjectId()}` diff --git a/src/routes/users/controllers.ts b/src/routes/users/controllers.ts index 00ef7d2..06fe7e5 100644 --- a/src/routes/users/controllers.ts +++ b/src/routes/users/controllers.ts @@ -16,8 +16,8 @@ import { } from "../../schema"; import { assertDefined, - assertValidForJsonStringify, buildErrorResponse, + dangerouslyAssumeJsonTransform, sendResponse, wrapAsyncHandler } from "../../utils"; @@ -53,7 +53,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.CREATED, - assertValidForJsonStringify(user) + dangerouslyAssumeJsonTransform(user) ); else sendResponse( @@ -109,7 +109,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(companies) + dangerouslyAssumeJsonTransform(companies) ); } else sendResponse( @@ -150,7 +150,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(documents) + dangerouslyAssumeJsonTransform(documents) ); } else sendResponse( @@ -191,7 +191,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(companies) + dangerouslyAssumeJsonTransform(companies) ); } else sendResponse( @@ -209,7 +209,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(user) + dangerouslyAssumeJsonTransform(user) ); else sendResponse( @@ -227,7 +227,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(users) + dangerouslyAssumeJsonTransform(users) ); } else sendResponse( @@ -248,7 +248,7 @@ export function createUserControllers( sendResponse( res, StatusCodes.OK, - assertValidForJsonStringify(user) + dangerouslyAssumeJsonTransform(user) ); else sendResponse( diff --git a/src/routes/users/service.test.ts b/src/routes/users/service.test.ts index 12aab24..e527e6d 100644 --- a/src/routes/users/service.test.ts +++ b/src/routes/users/service.test.ts @@ -1,5 +1,6 @@ import type { ExistingUser, UserUpdate } from "../../schema"; -import { assertDefined, assertNotNull } from "../../utils"; +import { assertDefined, assertNotNull, jsonTransform } from "../../utils"; +import { beforeAll, describe, expect, it } from "@jest/globals"; import { createUsersService } from "./service"; import { faker } from "@faker-js/faker"; import { getModels } from "../../schema-mongodb"; @@ -23,23 +24,19 @@ describe("createUsersService", () => { }; }; - const toObject = (obj: unknown): unknown => - // eslint-disable-next-line unicorn/prefer-structured-clone -- Ok - JSON.parse(JSON.stringify(obj)); - describe("addUser", () => { const data = getData(); it("should add a user", async () => { const user = await usersService.addUser(data); - expect(toObject(user)).toStrictEqual({ + expect(jsonTransform(user)).toStrictEqual({ ...data, _id: assertNotNull(user)._id.toString() }); }); - it("should return undefined for duplicate email", async () => { + it("should return null for duplicate email", async () => { const user = await usersService.addUser(data); expect(user).toBeNull(); @@ -106,31 +103,32 @@ describe("createUsersService", () => { describe("getUser", () => { describe("By email", () => { - const data = getData(); + it("should get a user", async () => { + const data = getData(); - beforeAll(async () => { await usersService.addUser(data); - }); - it("should get a user", async () => { const user = await usersService.getUser({ email: data.email, type: "email" }); - expect(toObject(user)).toEqual({ + expect(jsonTransform(user)).toEqual({ ...data, _id: assertNotNull(user)._id.toString() }); }); - it("should return undefined for missing user", async () => { - const user = await usersService.getUser({ - email: faker.internet.email(), - type: "email" - }); + it("should create and return a user", async () => { + const { email } = getData(); - expect(user).toBeNull(); + const user = await usersService.getUser({ email, type: "email" }); + + expect(jsonTransform(user)).toEqual({ + _id: assertNotNull(user)._id.toString(), + email, + favoriteCompanies: [] + }); }); }); @@ -151,13 +149,13 @@ describe("createUsersService", () => { type: "id" }); - expect(toObject(user)).toEqual({ + expect(jsonTransform(user)).toEqual({ ...data, _id: assertNotNull(user)._id.toString() }); }); - it("should return undefined for missing user", async () => { + it("should return null for missing user", async () => { const user = await usersService.getUser({ id: faker.database.mongodbObjectId(), type: "id" @@ -252,14 +250,14 @@ describe("createUsersService", () => { update ); - expect(toObject(user)).toEqual({ + expect(jsonTransform(user)).toEqual({ ...data, ...update, _id: assertNotNull(user)._id.toString() }); }); - it("should return undefined for missing user", async () => { + it("should return null for missing user", async () => { const update = getUpdate(); const user = await usersService.updateUser( @@ -297,14 +295,14 @@ describe("createUsersService", () => { update ); - expect(toObject(user)).toEqual({ + expect(jsonTransform(user)).toEqual({ ...data, ...update, _id: assertNotNull(user)._id.toString() }); }); - it("should return undefined for missing user", async () => { + it("should return null for missing user", async () => { const update = getUpdate(); const user = await usersService.updateUser( diff --git a/src/schema-mongodb/models.ts b/src/schema-mongodb/models.ts index cce0d83..4a9965b 100644 --- a/src/schema-mongodb/models.ts +++ b/src/schema-mongodb/models.ts @@ -4,18 +4,41 @@ import { getDocumentModel } from "./documents"; import { getMongodbConnection } from "../providers"; import { getUserModel } from "./users"; +// Cache the models in serverless environments +let cachedModels: Models | undefined; + /** * Gets the models. * @returns The models. */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Ok -export async function getModels() { +export async function getModels(): Promise { + if (cachedModels) return cachedModels; + const connection = await getMongodbConnection(); - return { + cachedModels = { CategoryModel: getCategoryModel(connection), CompanyModel: getCompanyModel(connection), DocumentModel: getDocumentModel(connection), UserModel: getUserModel(connection) } as const; + + await connection.syncIndexes(); + + return cachedModels; +} + +/** + * Check if models exist. + * @returns Whether models exist. + */ +export function modelsExist(): boolean { + return Boolean(cachedModels); +} + +export interface Models { + readonly CategoryModel: ReturnType; + readonly CompanyModel: ReturnType; + readonly DocumentModel: ReturnType; + readonly UserModel: ReturnType; } diff --git a/src/utils/assertions.ts b/src/utils/assertions.ts index 1cefea2..29c559b 100644 --- a/src/utils/assertions.ts +++ b/src/utils/assertions.ts @@ -1,5 +1,3 @@ -import type { JsonTransform } from "../schema"; - /** * Asserts that a value is defined. * @param value - The value to check. @@ -48,16 +46,6 @@ export function assertString(value: unknown): string { throw new Error("Value is not a string"); } -/** - * Asserts that the value is valid for JSON.stringify. - * @param value - The value to assert. - * @returns The value. - */ -export function assertValidForJsonStringify(value: T): JsonTransform { - // eslint-disable-next-line no-type-assertion/no-type-assertion -- Ok - return value as JsonTransform; -} - /** * Requires a value to be of a certain type. * @param value - The value to require. diff --git a/src/utils/index.ts b/src/utils/index.ts index bf2da07..08e1a55 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ export * from "./assertions"; export * from "./async"; export * from "./express"; +export * from "./json"; export * from "./objects"; export * from "./query"; export * from "./strings"; diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 0000000..41d8fda --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,21 @@ +import type { JsonTransform } from "../schema"; + +/** + * Asserts that the value is valid for JSON.stringify. + * @param value - The value to assert. + * @returns The value. + */ +export function dangerouslyAssumeJsonTransform(value: T): JsonTransform { + // eslint-disable-next-line no-type-assertion/no-type-assertion -- Ok + return value as JsonTransform; +} + +/** + * Clones an object using JSON. + * @param obj - The object to clone. + * @returns The cloned object. + */ +export function jsonTransform(obj: T): JsonTransform { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, unicorn/prefer-structured-clone -- Ok + return JSON.parse(JSON.stringify(obj)); +} diff --git a/tsconfig.json b/tsconfig.json index 77d02d0..e5d2e64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,7 @@ "sourceMap": true, "strict": true, "target": "ES2020", - "types": ["jest"], + "types": [], "useDefineForClassFields": true, "useUnknownInCatchVariables": true }, diff --git a/utils/mongodb-memory-server.ts b/utils/mongodb-memory-server.ts index ac47a33..3805c30 100644 --- a/utils/mongodb-memory-server.ts +++ b/utils/mongodb-memory-server.ts @@ -1,4 +1,5 @@ import { MongoMemoryServer } from "mongodb-memory-server"; +import { TEST_MONGODB_PORT } from "../src"; let server: MongoMemoryServer | undefined; @@ -15,3 +16,11 @@ export async function createMongodbMemoryServer(): Promise { export async function stopMongodbMemoryServer(): Promise { if (server) await server.stop(); } + +/** + * Get the URI for the in-memory MongoDB server. + * @returns The URI for the in-memory MongoDB server. + */ +export function getMongodbMemoryServerUri(): string { + return `mongodb://127.0.0.1:${TEST_MONGODB_PORT}/`; +}