diff --git a/.env.sample b/.env.sample index 949acb1f..1a639931 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,6 @@ # ################# REQUIRED ENV VARS START ################# PORT=8080 +MONGO_MEMORY_SERVER_PORT=10000 # mongodb port for e2e testing MONGODB_URI=mongodb://mongodb:27017 # `mongodb://localhost:27017` in case using local mongodb NODE_ENV=development # changing this will avoid stack traces in the error response EXPRESS_SESSION_SECRET=7fdOMCFRSLD9cv1k-5n3Dz5n3DmVmVHVIg9GG_OGTUkBfLNdgZAwKDNtoCJ0X0cyqaM0ogR80-zh9kx0Mkx # ok to change diff --git a/e2e/db.js b/e2e/db.js new file mode 100644 index 00000000..fd83b7e7 --- /dev/null +++ b/e2e/db.js @@ -0,0 +1,40 @@ +import mongoose, { mongo } from "mongoose"; +import { MongoMemoryServer } from "mongodb-memory-server"; + +const MONGO_MEMORY_SERVER_PORT = process.env.MONGO_MEMORY_SERVER_PORT || 10000; +const MONGODB_URL = `mongodb://127.0.0.1:${MONGO_MEMORY_SERVER_PORT}/`; + +let mongoServer = null; +let dbInstance = undefined; + +const connectDB = async () => { + try { + await mongoose.disconnect(); + mongoServer = await MongoMemoryServer.create({ + instance: { + port: +MONGO_MEMORY_SERVER_PORT, + }, + }); + dbInstance = await mongoose.connect(MONGODB_URL); + } catch (error) { + console.error("Mongo db connect error: ", error); + process.exit(1); + } +}; +export const clearDB = async (collectionName = null) => { + if (!dbInstance) { + dbInstance = await mongoose.connect(MONGODB_URL); + } + const connection = mongoose.connection; + if (collectionName) { + await connection.db.collection(collectionName).deleteMany({}); + } else { + const collections = await connection.db.listCollections().toArray(); + const collectionNames = collections.map((col) => col.name); + for (let name of collectionNames) { + await connection.db.collection(name).deleteMany({}); + } + } +}; + +export default connectDB; diff --git a/e2e/routes/apps/todo.test.js b/e2e/routes/apps/todo.test.js index 2f660c02..37e383ce 100644 --- a/e2e/routes/apps/todo.test.js +++ b/e2e/routes/apps/todo.test.js @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; +import { clearDB } from "../../db.js"; let apiContext; let todoId = null; @@ -7,6 +8,7 @@ let todoId = null; test.describe("Todo App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); + await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); diff --git a/e2e/routes/seeds/chat-app.test.js b/e2e/routes/seeds/chat-app.test.js new file mode 100644 index 00000000..75e67186 --- /dev/null +++ b/e2e/routes/seeds/chat-app.test.js @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { getApiContext } from "../../common.js"; +import { clearDB } from "../../db.js"; + +let apiContext; + +test.describe("Seed Chat App", () => { + test.beforeAll(async ({ playwright }) => { + apiContext = await getApiContext(playwright); + await clearDB(); + }); + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.describe("POST:/api/v1/seed/chat-app - Seed Chat", async () => { + test("should seed Chat App DB", async ({ page }) => { + const res = await apiContext.post("/api/v1/seed/chat-app"); + expect(res.status()).toEqual(201); + }); + }); +}); diff --git a/e2e/routes/seeds/ecommerce.test.js b/e2e/routes/seeds/ecommerce.test.js new file mode 100644 index 00000000..d3d81643 --- /dev/null +++ b/e2e/routes/seeds/ecommerce.test.js @@ -0,0 +1,62 @@ +import { test, expect } from "@playwright/test"; +import { getApiContext } from "../../common.js"; +import { clearDB } from "../../db.js"; +import { + CATEGORIES_COUNT, + PRODUCTS_COUNT, +} from "../../../src/seeds/_constants.js"; + +let apiContext; + +test.describe("Seed Ecommerce App", () => { + test.beforeAll(async ({ playwright }) => { + apiContext = await getApiContext(playwright); + await clearDB(); + }); + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.describe("POST:/api/v1/seed/ecommerce - Seed Ecommerce", async () => { + test("should return 0 products before seed", async ({ page }) => { + const res = await apiContext.get( + "/api/v1/ecommerce/products?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalProducts).toEqual(0); + }); + test("should return 0 categories before seed", async ({ page }) => { + const res = await apiContext.get( + "/api/v1/ecommerce/categories?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalCategories).toEqual(0); + }); + test("should seed ecommerce DB", async ({ page }) => { + const res = await apiContext.post("/api/v1/seed/ecommerce"); + expect(res.status()).toEqual(201); + }); + test(`should return ${PRODUCTS_COUNT} products after seed`, async ({ + page, + }) => { + const res = await apiContext.get( + "/api/v1/ecommerce/products?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalProducts).toEqual(PRODUCTS_COUNT); + }); + test(`should return ${CATEGORIES_COUNT} categories after seed`, async ({ + page, + }) => { + const res = await apiContext.get( + "/api/v1/ecommerce/categories?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalCategories).toEqual(CATEGORIES_COUNT); + }); + }); +}); diff --git a/e2e/routes/seeds/generated-credentials.test.js b/e2e/routes/seeds/generated-credentials.test.js new file mode 100644 index 00000000..988287ac --- /dev/null +++ b/e2e/routes/seeds/generated-credentials.test.js @@ -0,0 +1,30 @@ +import fs from "fs"; +import { test, expect } from "@playwright/test"; +import { getApiContext } from "../../common.js"; + +let apiContext; + +test.describe("Get credentials", () => { + test.beforeAll(async ({ playwright }) => { + apiContext = await getApiContext(playwright); + }); + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.describe("GET:/api/v1/seed/generated-credentials - Get credentials", async () => { + test("should return public/temp/seed-credentials.json content", async ({ + page, + }) => { + const seedCredentialsText = fs.readFileSync( + "./public/temp/seed-credentials.json", + "utf8" + ); + const seedCredentials = JSON.parse(seedCredentialsText); + const res = await apiContext.get("/api/v1/seed/generated-credentials"); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data).toMatchObject(seedCredentials); + }); + }); +}); diff --git a/e2e/routes/seeds/social-media.test.js b/e2e/routes/seeds/social-media.test.js new file mode 100644 index 00000000..97bcb449 --- /dev/null +++ b/e2e/routes/seeds/social-media.test.js @@ -0,0 +1,41 @@ +import { test, expect } from "@playwright/test"; +import { getApiContext } from "../../common.js"; +import { clearDB } from "../../db.js"; +import { SOCIAL_POSTS_COUNT } from "../../../src/seeds/_constants.js"; + +let apiContext; + +test.describe("Seed social-media App", () => { + test.beforeAll(async ({ playwright }) => { + apiContext = await getApiContext(playwright); + await clearDB(); + }); + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.describe("POST:/api/v1/seed/social-media - Seed social-media", async () => { + test("should return 0 posts before seed", async ({ page }) => { + const res = await apiContext.get( + "/api/v1/social-media/posts?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalPosts).toEqual(0); + }); + test("should seed social-media DB", async ({ page }) => { + const res = await apiContext.post("/api/v1/seed/social-media"); + expect(res.status()).toEqual(201); + }); + test(`should return ${SOCIAL_POSTS_COUNT} post after seed`, async ({ + page, + }) => { + const res = await apiContext.get( + "/api/v1/social-media/posts?page=1&limit=1" + ); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.totalPosts).toEqual(SOCIAL_POSTS_COUNT); + }); + }); +}); diff --git a/e2e/routes/seeds/todo.test.js b/e2e/routes/seeds/todo.test.js new file mode 100644 index 00000000..6be1a3ca --- /dev/null +++ b/e2e/routes/seeds/todo.test.js @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import { getApiContext } from "../../common.js"; +import { clearDB } from "../../db.js"; +import { TODOS_COUNT } from "../../../src/seeds/_constants.js"; + +let apiContext; + +test.describe("Seed Todo App", () => { + test.beforeAll(async ({ playwright }) => { + apiContext = await getApiContext(playwright); + await clearDB(); + }); + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.describe("POST:/api/v1/seed/todos - Seed Todos", async () => { + test("should return 0 todos before seed", async ({ page }) => { + const res = await apiContext.get("/api/v1/todos"); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.length).toEqual(0); + }); + + test("should seed todo DB", async ({ page }) => { + const res = await apiContext.post("/api/v1/seed/todos"); + expect(res.status()).toEqual(201); + }); + + test(`should return ${TODOS_COUNT} todos after seed`, async ({ page }) => { + const res = await apiContext.get("/api/v1/todos"); + const json = await res.json(); + expect(res.status()).toEqual(200); + expect(json.data.length).toEqual(TODOS_COUNT); + }); + }); +}); diff --git a/e2e/test-server.js b/e2e/test-server.js index c6609b5a..02f8e8da 100644 --- a/e2e/test-server.js +++ b/e2e/test-server.js @@ -1,36 +1,12 @@ import dotenv from "dotenv"; -import mongoose from "mongoose"; -import { MongoMemoryServer } from "mongodb-memory-server"; -import { httpServer } from "../src/app.js"; - dotenv.config({ path: "../.env", }); +import { httpServer } from "../src/app.js"; +import connectDB from "./db.js"; -let mongoServer = null; -let dbInstance = undefined; const PORT = process.env.PORT || 8080; -const connectDB = async () => { - try { - await mongoose.disconnect(); - mongoServer = await MongoMemoryServer.create(); - dbInstance = await mongoose.connect(`${mongoServer.getUri()}`); - await clearDB(); - } catch (error) { - console.log("Mongo db connect error: ", error); - process.exit(1); - } -}; -export const clearDB = async () => { - const collections = mongoose.connection.collections; - - for (const key in collections) { - const collection = collections[key]; - await collection.deleteMany({}); - } -}; - /** * Starting from Node.js v14 top-level await is available and it is only available in ES modules. * This means you can not use it with common js modules or Node version < 14. diff --git a/package.json b/package.json index 529dd2ea..8cb2ba37 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "pre-commit": "lint-staged", "prepare": "node prepare.js", "start:test-server": "node -r dotenv/config --experimental-json-modules e2e/test-server.js", - "test:playwright": "set NODE_OPTIONS=--experimental-vm-modules && npx playwright test" + "test:playwright": "set NODE_OPTIONS=--experimental-vm-modules -r dotenv/config --experimental-json-modules && npx playwright test" }, "repository": { "type": "git", diff --git a/playwright.config.js b/playwright.config.js index 1e5f667d..89f0297d 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -19,7 +19,8 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + // using multiple workers will create DB overwrite issue because we use one DB to test everything + workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */