diff --git a/package.json b/package.json index 3428525..c26100b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "db:models:generate": "npx prisma generate", "db:migration:create": "npx prisma migrate dev --create-only", "db:migration:migrate": "npx prisma migrate deploy", - "test": "npm run build && NODE_ENV=test node ./build/Test/**/*.test.js" + "test": "npm run build && NODE_ENV=test find . -name \"*.test.js\" | xargs -n 1 node" }, "dependencies": { "@prisma/client": "^5.20.0", diff --git a/src/Db/Repository/UserRepository.ts b/src/Db/Repository/UserRepository.ts index ac6a2b3..39cec62 100644 --- a/src/Db/Repository/UserRepository.ts +++ b/src/Db/Repository/UserRepository.ts @@ -44,4 +44,14 @@ export class UserRepository { } }) as Promise; } + + public deleteUsers(ids: UuidV7[]) { + return this.dbClient.deleteMany({ + where: { + id: { + in: ids.map(id => id.toString()) + } + } + }) + } } \ No newline at end of file diff --git a/src/Http/Controller/AuthenticateController.ts b/src/Http/Controller/AuthenticateController.ts index fb4176c..c9cdb15 100644 --- a/src/Http/Controller/AuthenticateController.ts +++ b/src/Http/Controller/AuthenticateController.ts @@ -33,10 +33,10 @@ export class AuthenticateController extends BaseHttpController { const user = (await this.userRepository.getUserByEmails([body.email])).pop(); if (!user) - return this.json({code: "wrong_credentials"}, 404); + return this.json({code: "wrong_credentials"}, 401); if (!bcrypt.compareSync(body.password, user.password)) - return this.json({code: "wrong_credentials"}, 404); + return this.json({code: "wrong_credentials"}, 401); return this.json({ token: await this.userAuthentication.createToken({id: user.id, email: user.email}), diff --git a/src/Test/Api/AccountController.test.ts b/src/Test/Api/AccountController.test.ts new file mode 100644 index 0000000..b2d8a5a --- /dev/null +++ b/src/Test/Api/AccountController.test.ts @@ -0,0 +1,82 @@ +import "reflect-metadata"; +import {afterEach, beforeEach, describe, it} from "node:test"; +import {strictEqual} from "node:assert"; +import {container} from "../../Kernel/Container"; +import {Http} from "../../Kernel/Http"; +import axios from "axios"; +import {Environment, EnvironmentKeys} from "../../Kernel/Environment"; +import AxiosXHR = Axios.AxiosXHR; +import {ErrorResponse} from "./Types"; +import {faker} from '@faker-js/faker'; +import {hashSync} from "bcryptjs"; +import {sleep} from "../../Common/Misc"; +import {UserAuthentication} from "../../Service/Authentication/UserAuthentication"; +import {UserRepository} from "../../Db/Repository/UserRepository"; +import {UuidV7} from "../../Common/UuidV7"; + +const environment: Environment = container.get(Environment); +const userRepository: UserRepository = container.get(UserRepository); +const http = new Http(container); + +const requestClient: Axios.AxiosInstance = axios.create({ + baseURL: `http://localhost:${environment.get(EnvironmentKeys.APP_HTTP_PORT)}/account`, + validateStatus: status => true +}); + +const userId = UuidV7.new(); +const userEmail = faker.internet.email().toLowerCase(); +const userPassword = faker.internet.password({length: 8}); +const userPasswordHashed = hashSync(userPassword, 13); + +beforeEach(async () => { + await sleep(100); + await http.start(); + await userRepository.createUser(userId, userEmail, userPasswordHashed); +}); + +afterEach(async () => { + await http.stop(); + await userRepository.deleteUsers([userId]); +}); + +describe("AccountController", () => { + describe("GET /", () => { + it("should not authorize with empty header", async () => { + const response = await requestClient.get("/") as AxiosXHR; + + strictEqual(response.status, 401); + strictEqual(response.data.code, "not_authenticated"); + strictEqual(response.data.meta, undefined); + }); + + it("should not authorize with wrong jwt", async () => { + const response = await requestClient.get("/", { + headers: { + "x-access-token": "false-jwt" + } + }) as AxiosXHR; + + strictEqual(response.status, 401); + strictEqual(response.data.code, "not_authenticated"); + strictEqual(response.data.meta, undefined); + }); + + it("should get user data with jwt", async () => { + const userJwt = await container.get(UserAuthentication).createToken({ + id: userId.toString(), + email: userEmail + }); + + const response = await requestClient.get("/", {headers: {"x-access-token": userJwt}}) as AxiosXHR<{ + id: string, + email: string, + password: string + }>; + + strictEqual(response.status, 200); + strictEqual(userId.toString(), response.data.id); + strictEqual(userEmail, response.data.email); + strictEqual(userPasswordHashed, response.data.password); + }); + }); +}); \ No newline at end of file diff --git a/src/Test/Api/AuthenticateController.test.ts b/src/Test/Api/AuthenticateController.test.ts new file mode 100644 index 0000000..0d21927 --- /dev/null +++ b/src/Test/Api/AuthenticateController.test.ts @@ -0,0 +1,101 @@ +import "reflect-metadata"; +import {afterEach, beforeEach, describe, it} from "node:test"; +import {deepStrictEqual, strictEqual} from "node:assert"; +import {container} from "../../Kernel/Container"; +import {Http} from "../../Kernel/Http"; +import axios from "axios"; +import {Environment, EnvironmentKeys} from "../../Kernel/Environment"; +import AxiosXHR = Axios.AxiosXHR; +import {ErrorResponse} from "./Types"; +import {faker} from '@faker-js/faker'; +import {hashSync} from "bcryptjs"; +import {sleep} from "../../Common/Misc"; +import {UserAuthentication} from "../../Service/Authentication/UserAuthentication"; +import {UserRepository} from "../../Db/Repository/UserRepository"; +import {UuidV7} from "../../Common/UuidV7"; + +const environment: Environment = container.get(Environment); +const userRepository: UserRepository = container.get(UserRepository); +const http = new Http(container); + +const requestClient: Axios.AxiosInstance = axios.create({ + baseURL: `http://localhost:${environment.get(EnvironmentKeys.APP_HTTP_PORT)}/authenticate`, + validateStatus: status => true +}); + +const userId = UuidV7.new(); +const userEmail = faker.internet.email().toLowerCase(); +const userPassword = faker.internet.password({length: 8}); +const userPasswordHashed = hashSync(userPassword, 13); + +beforeEach(async () => { + await sleep(100); + await http.start(); + await userRepository.createUser(userId, userEmail, userPasswordHashed); +}); + +afterEach(async () => { + await http.stop(); + await userRepository.deleteUsers([userId]); +}); + +describe("AuthenticateController", () => { + describe("POST /email-and-password", () => { + it("should not authenticate with empty body", async () => { + const response = await requestClient.post("/email-and-password") as AxiosXHR; + + strictEqual(response.status, 400); + strictEqual(response.data.code, "bad_request"); + deepStrictEqual(response.data.meta, [ + { + location: "body", + msg: "email must be valid", + path: "email", + type: "field" + }, + { + location: "body", + msg: "password must be string", + path: "password", + type: "field" + }, + { + location: "body", + msg: "password must not be empty", + path: "password", + type: "field" + } + ]); + }); + + it("should not authenticate with wrong email and password", async () => { + const response = await requestClient.post("/email-and-password", { + email: userEmail, + password: userPassword + "-false" + }) as AxiosXHR; + + strictEqual(response.status, 401); + strictEqual(response.data.code, "wrong_credentials"); + strictEqual(response.data.meta, undefined); + }); + + it("should authenticate with email and password", async () => { + const response = await requestClient.post("/email-and-password", {email: userEmail, password: userPassword}) as AxiosXHR<{ + token: string, + user: { + id: string, + email: string, + password: string + } + }>; + + const responseToken = container.get(UserAuthentication).verifyToken(response.data.token); + + strictEqual(response.status, 200); + strictEqual(true, responseToken !== null); + strictEqual(userId.toString(), response.data.user.id); + strictEqual(userEmail, response.data.user.email); + strictEqual(userPasswordHashed, response.data.user.password); + }); + }); +}); \ No newline at end of file diff --git a/src/Test/Api/SignUpController.test.ts b/src/Test/Api/SignUpController.test.ts index 19fe661..ed81149 100644 --- a/src/Test/Api/SignUpController.test.ts +++ b/src/Test/Api/SignUpController.test.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; import {afterEach, beforeEach, describe, it} from "node:test"; -import * as assert from "assert" +import {deepStrictEqual, strictEqual} from "node:assert"; import {container} from "../../Kernel/Container"; import {Http} from "../../Kernel/Http"; import axios from "axios"; @@ -35,9 +35,9 @@ describe("SignUpController", () => { it("should return bad request on non-existing data", async () => { const response = await requestClient.post("/") as AxiosXHR; - assert.strictEqual(response.status, 400); - assert.strictEqual(response.data.code, "bad_request"); - assert.deepStrictEqual(response.data.meta, [ + strictEqual(response.status, 400); + strictEqual(response.data.code, "bad_request"); + deepStrictEqual(response.data.meta, [ { location: "body", msg: "email must be valid", @@ -65,9 +65,9 @@ describe("SignUpController", () => { password: "bar" }) as AxiosXHR; - assert.strictEqual(response.status, 400); - assert.strictEqual(response.data.code, "bad_request"); - assert.deepStrictEqual(response.data.meta, [ + strictEqual(response.status, 400); + strictEqual(response.data.code, "bad_request"); + deepStrictEqual(response.data.meta, [ { location: "body", msg: "email must be valid", @@ -91,13 +91,13 @@ describe("SignUpController", () => { const responseWithOk = await requestClient.post("/", {email, password}) as AxiosXHR; - assert.strictEqual(responseWithOk.status, 200); + strictEqual(responseWithOk.status, 200); const responseWithError = await requestClient.post("/", {email, password}) as AxiosXHR; - assert.strictEqual(responseWithError.status, 422); - assert.strictEqual(responseWithError.data.code, "user_exists"); - assert.strictEqual(responseWithError.data.meta, undefined); + strictEqual(responseWithError.status, 422); + strictEqual(responseWithError.data.code, "user_exists"); + strictEqual(responseWithError.data.meta, undefined); }); it('should return user data on sign-up', async () => { @@ -115,12 +115,12 @@ describe("SignUpController", () => { const responseToken = container.get(UserAuthentication).verifyToken(responseWithOk.data.token); - assert.strictEqual(responseWithOk.status, 200); - assert.strictEqual(email.toLowerCase(), responseToken?.email); - assert.strictEqual(true, validate(responseToken?.id ?? "")); - assert.strictEqual(true, validate(responseWithOk.data.user.id)); - assert.strictEqual(email.toLowerCase(), responseWithOk.data.user.email); - assert.strictEqual(true, compareSync(password, responseWithOk.data.user.password)); + strictEqual(responseWithOk.status, 200); + strictEqual(email.toLowerCase(), responseToken?.email); + strictEqual(true, validate(responseToken?.id ?? "")); + strictEqual(true, validate(responseWithOk.data.user.id)); + strictEqual(email.toLowerCase(), responseWithOk.data.user.email); + strictEqual(true, compareSync(password, responseWithOk.data.user.password)); }); }); }); \ No newline at end of file