Skip to content

Commit

Permalink
feat: tests (unit, e2e) (#7)
Browse files Browse the repository at this point in the history
* feat: configure jest for TypeScript and test database setup

* feat: expand e2e tests for auth.controller and add new migration file

* feat: update password change functionality and schema

* feat: initialize test context and services

* feat: create unit test setup function

* feat: initialize test context and services for e2e tests

* feat: create e2e test setup function

* refactor: improve user and credential factories, add userWithCredentialFactory

* feat: update dependencies and add JWT_REFRESH_SECRET

* refactor: refactor user factory and add hashPassword function

* fix: update password handling in user controller e2e tests

* feat: refactor userFactory and authService.register

* chore: update assertions in auth and users service tests
  • Loading branch information
typeWolffo committed Jul 18, 2024
1 parent 26173dc commit 03fcabb
Show file tree
Hide file tree
Showing 32 changed files with 1,599 additions and 100 deletions.
2 changes: 1 addition & 1 deletion examples/common_nestjs_remix/apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DATABASE_URL="postgres://postgres:guidebook@localhost:5432/guidebook"
JWT_SECRET=
REFRESH_SECRET=
JWT_REFRESH_SECRET=
20 changes: 20 additions & 0 deletions examples/common_nestjs_remix/apps/api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Config } from "jest";

const config: Config = {
moduleFileExtensions: ["js", "json", "ts"],
rootDir: ".",
testRegex: ".*\\.spec\\.ts$",
transform: {
"^.+\\.(t|j)s$": "ts-jest",
},
collectCoverageFrom: ["**/*.(t|j)s"],
coverageDirectory: "./coverage",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/test/jest-setup.ts"],
moduleNameMapper: {
"^src/(.*)$": "<rootDir>/src/$1",
},
modulePaths: ["."],
};

export default config;
24 changes: 7 additions & 17 deletions examples/common_nestjs_remix/apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:watch": "jest --config ./test/jest-e2e.json --watch",
"db:migrate": "drizzle-kit migrate",
"db:generate": "drizzle-kit generate"
},
Expand All @@ -35,6 +36,7 @@
"@sinclair/typebox": "^0.32.34",
"add": "^2.0.6",
"bcrypt": "^5.1.1",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.6",
"drizzle-kit": "^0.22.8",
"drizzle-orm": "^0.31.2",
Expand All @@ -50,10 +52,12 @@
"uuid": "^10.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie": "^0.6.0",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
Expand All @@ -68,31 +72,17 @@
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"faker": "link:@types/@faker-js/faker",
"fishery": "^2.2.2",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"testcontainers": "^10.10.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
3 changes: 1 addition & 2 deletions examples/common_nestjs_remix/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { JwtModule } from "@nestjs/jwt";
import jwtConfig from "./common/configuration/jwt";
import authConfig from "./common/configuration/auth";
import { APP_GUARD } from "@nestjs/core";
import { JwtAuthGuard } from "./common/guards/jwt-auth-guard";

@Module({
imports: [
ConfigModule.forRoot({
load: [database, jwtConfig, authConfig],
load: [database, jwtConfig],
isGlobal: true,
}),
DrizzlePostgresModule.registerAsync({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { DatabasePg } from "../../common/index";
import { INestApplication } from "@nestjs/common";
import { isArray } from "lodash";
import request from "supertest";
import { createUserFactory } from "../../../test/factory/user.factory";
import { createE2ETest } from "../../../test/create-e2e-test";
import { AuthService } from "../auth.service";
import * as cookie from "cookie";

describe("AuthController (e2e)", () => {
let app: INestApplication;
let authService: AuthService;
let db: DatabasePg;
let userFactory: ReturnType<typeof createUserFactory>;

beforeAll(async () => {
const { app: testApp } = await createE2ETest();
app = testApp;
authService = app.get(AuthService);
db = app.get("DB");
userFactory = createUserFactory(db);
});

describe("POST /auth/register", () => {
it("should register a new user", async () => {
const user = await userFactory
.withCredentials({ password: "password123" })
.build();

const response = await request(app.getHttpServer())
.post("/auth/register")
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.send({
email: user.email,
password: user.credentials?.password,
});

expect(response.status).toEqual(201);
expect(response.body.data).toHaveProperty("id");
expect(response.body.data.email).toBe(user.email);
});

it("should return 409 if user already exists", async () => {
const existingUser = {
email: "[email protected]",
password: "password123",
};

await authService.register(existingUser.email, existingUser.password);

await request(app.getHttpServer())
.post("/auth/register")
.send(existingUser)
.expect(409);
});
});

describe("POST /auth/login", () => {
it("should login and return user data with cookies", async () => {
const user = await userFactory
.withCredentials({
password: "password123",
})
.create({
email: "[email protected]",
});

const response = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: user.credentials?.password,
});

expect(response.status).toEqual(201);
expect(response.body.data).toHaveProperty("id");
expect(response.body.data.email).toBe(user.email);
expect(response.headers["set-cookie"]).toBeDefined();
expect(response.headers["set-cookie"].length).toBe(2);
});

it("should return 401 for invalid credentials", async () => {
await request(app.getHttpServer())
.post("/auth/login")
.send({
email: "[email protected]",
password: "wrongpassword",
})
.expect(401);
});
});

describe("POST /auth/logout", () => {
it("should clear token cookies for a logged-in user", async () => {
let accessToken = "";

const user = userFactory.build();
const password = "password123";
await authService.register(user.email, password);

const loginResponse = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: password,
});

const cookies = loginResponse.headers["set-cookie"];

if (Array.isArray(cookies)) {
cookies.forEach((cookieString) => {
const parsedCookie = cookie.parse(cookieString);
if ("access_token" in parsedCookie) {
accessToken = parsedCookie.access_token;
}
});
}

const logoutResponse = await request(app.getHttpServer())
.post("/auth/logout")
.set("Cookie", `access_token=${accessToken};`);

const logoutCookies = logoutResponse.headers["set-cookie"];

expect(loginResponse.status).toBe(201);
expect(logoutResponse.status).toBe(201);
expect(logoutResponse.headers["set-cookie"]).toBeDefined();
expect(logoutCookies.length).toBe(2);
expect(logoutCookies[0]).toContain("access_token=;");
expect(logoutCookies[1]).toContain("refresh_token=;");
});
});

describe("POST /auth/refresh", () => {
it("should refresh tokens", async () => {
const user = await userFactory.build();
const password = "password123";
await authService.register(user.email, password);

const loginResponse = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: password,
})
.expect(201);

const cookies = loginResponse.headers["set-cookie"];

let refreshToken = "";

if (isArray(cookies)) {
cookies.forEach((cookie) => {
if (cookie.startsWith("refresh_token=")) {
refreshToken = cookie;
}
});
}

const response = await request(app.getHttpServer())
.post("/auth/refresh")
.set("Cookie", [refreshToken])
.expect(201);

expect(response.headers["set-cookie"]).toBeDefined();
expect(response.headers["set-cookie"].length).toBe(2);
});

it("should return 401 for invalid refresh token", async () => {
await request(app.getHttpServer())
.post("/auth/refresh")
.set("Cookie", ["refreshToken=invalid_token"])
.expect(401);
});
});
});
Loading

0 comments on commit 03fcabb

Please sign in to comment.