-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Selleo/jw/user-auth
feat: user auth
- Loading branch information
Showing
61 changed files
with
9,343 additions
and
7,674 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
DATABASE_URL="postgres://postgres:guidebook@localhost:5432/guidebook" | ||
JWT_SECRET= | ||
JWT_REFRESH_SECRET= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,29 @@ | ||
module.exports = { | ||
parser: '@typescript-eslint/parser', | ||
parser: "@typescript-eslint/parser", | ||
parserOptions: { | ||
project: 'tsconfig.json', | ||
project: "tsconfig.json", | ||
tsconfigRootDir: __dirname, | ||
sourceType: 'module', | ||
sourceType: "module", | ||
}, | ||
plugins: ['@typescript-eslint/eslint-plugin'], | ||
plugins: ["@typescript-eslint/eslint-plugin"], | ||
extends: [ | ||
'plugin:@typescript-eslint/recommended', | ||
'plugin:prettier/recommended', | ||
"plugin:@typescript-eslint/recommended", | ||
"plugin:prettier/recommended", | ||
], | ||
root: true, | ||
env: { | ||
node: true, | ||
jest: true, | ||
}, | ||
ignorePatterns: ['.eslintrc.js'], | ||
ignorePatterns: [".eslintrc.js"], | ||
rules: { | ||
'@typescript-eslint/interface-name-prefix': 'off', | ||
'@typescript-eslint/explicit-function-return-type': 'off', | ||
'@typescript-eslint/explicit-module-boundary-types': 'off', | ||
'@typescript-eslint/no-explicit-any': 'off', | ||
"@typescript-eslint/interface-name-prefix": "off", | ||
"@typescript-eslint/explicit-function-return-type": "off", | ||
"@typescript-eslint/explicit-module-boundary-types": "off", | ||
"@typescript-eslint/no-explicit-any": "off", | ||
"@typescript-eslint/no-unused-vars": [ | ||
"error", | ||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, | ||
], | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
examples/common_nestjs_remix/apps/api/src/auth/__tests__/auth.controller.e2e-spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: "existing@example.com", | ||
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: "test@example.com", | ||
}); | ||
|
||
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: "wrong@example.com", | ||
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.