From 4bc3be87b47cdc0027d25000b48ccb3f99ec555b75b98cf47a5c929c2e870b48 Mon Sep 17 00:00:00 2001 From: aslan Date: Sat, 27 Dec 2025 13:02:02 +0100 Subject: [PATCH] Add user and auth tests --- package-lock.json | 20 ++++- package.json | 6 +- src/config.json | 3 +- src/controllers/auth/auth.ts | 5 +- src/controllers/auth/types.ts | 6 +- src/controllers/channel/types.ts | 4 +- src/controllers/community/types.ts | 14 +-- src/controllers/errors.ts | 1 + src/controllers/role/types.ts | 4 +- src/controllers/session/types.ts | 6 +- src/controllers/user/types.ts | 8 +- src/services/auth/auth.ts | 12 ++- tests/api.js | 56 ++++++++++++ tests/community.test.js | 33 +++++++ tests/user.test.js | 136 +++++++++++++++++++++++++++++ 15 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 tests/api.js create mode 100644 tests/community.test.js create mode 100644 tests/user.test.js diff --git a/package-lock.json b/package-lock.json index 12bd555..4e8d2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tether", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tether", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-only", "dependencies": { "@prisma/adapter-pg": "^7.2.0", @@ -14,7 +14,8 @@ "argon2": "^0.44.0", "fastify": "^5.6.2", "jsonwebtoken": "^9.0.3", - "pg": "^8.16.3" + "pg": "^8.16.3", + "uuid": "^13.0.0" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.10", @@ -2230,6 +2231,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 9eda4cf..968c641 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "main": "index.js", "scripts": { "build": "npx tsc", - "start": "npx tsc && node --env-file=.env dist/index.js" + "start": "npx tsc && node --env-file=.env dist/index.js", + "test": "node --test tests/**/*.test.js" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.10", @@ -29,6 +30,7 @@ "argon2": "^0.44.0", "fastify": "^5.6.2", "jsonwebtoken": "^9.0.3", - "pg": "^8.16.3" + "pg": "^8.16.3", + "uuid": "^13.0.0" } } diff --git a/src/config.json b/src/config.json index a00ce72..46adfbd 100644 --- a/src/config.json +++ b/src/config.json @@ -1,4 +1,3 @@ { - "port": 3012, - "db": "db.json" + "port": 3012 } diff --git a/src/controllers/auth/auth.ts b/src/controllers/auth/auth.ts index 7a3b921..24dcb0d 100644 --- a/src/controllers/auth/auth.ts +++ b/src/controllers/auth/auth.ts @@ -8,6 +8,7 @@ import type { ILoginResponseSuccess, } from "./types.js"; import { loginUser, registerUser } from "../../services/auth/auth.js"; +import { API_ERROR } from "../errors.js"; const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => { const { username, password, email } = request.body as IRegisterRequest; @@ -20,7 +21,7 @@ const postRegister = async (request: FastifyRequest, _reply: FastifyReply) => { if (!newUser) { return { - error: "user already exists", + error: API_ERROR.USER_ALREADY_EXISTS, } as IRegisterResponseError; } @@ -42,7 +43,7 @@ const postLogin = async (request: FastifyRequest, _reply: FastifyReply) => { if (!session) { return { username: username, - error: "incorrect credentials", + error: API_ERROR.ACCESS_DENIED, } as ILoginResponseError; } diff --git a/src/controllers/auth/types.ts b/src/controllers/auth/types.ts index b812f2b..3bc267f 100644 --- a/src/controllers/auth/types.ts +++ b/src/controllers/auth/types.ts @@ -1,3 +1,5 @@ +import type { API_ERROR } from "../errors.js"; + interface IRegisterRequest { username: string; password: string; @@ -11,7 +13,7 @@ interface IRegisterResponseSuccess { } interface IRegisterResponseError { - error: string; + error: API_ERROR; } interface ILoginRequest { @@ -27,7 +29,7 @@ interface ILoginResponseSuccess { interface ILoginResponseError { username: string; - error: string; + error: API_ERROR; } export { diff --git a/src/controllers/channel/types.ts b/src/controllers/channel/types.ts index 2ed4e27..ff8c166 100644 --- a/src/controllers/channel/types.ts +++ b/src/controllers/channel/types.ts @@ -1,10 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + interface IGetChannelParams { id: string; } interface IGetChannelResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetChannelResponseSuccess { diff --git a/src/controllers/community/types.ts b/src/controllers/community/types.ts index 9d7703a..c9c8a84 100644 --- a/src/controllers/community/types.ts +++ b/src/controllers/community/types.ts @@ -1,10 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + interface IGetCommunityParams { id: string; } interface IGetCommunityResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetCommunityResponseSuccess { @@ -25,7 +27,7 @@ interface IPatchCommunityRequest { interface IPatchCommunityResponseError { id: string; - error: string; + error: API_ERROR; } interface IPatchCommunityResponseSuccess { @@ -40,7 +42,7 @@ interface IGetMembersParams { interface IGetMembersResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetMembersResponseSuccess { @@ -60,7 +62,7 @@ interface IGetChannelsParams { interface IGetChannelsResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetChannelsResponseSuccess { @@ -80,7 +82,7 @@ interface IGetRolesParams { interface IGetRolesResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetRolesResponseSuccess { @@ -106,7 +108,7 @@ interface IPostCreateInviteRequest { interface IPostCreateInviteResponseError { id: string; - error: string; + error: API_ERROR; } interface IPostCreateInviteResponseSuccess { diff --git a/src/controllers/errors.ts b/src/controllers/errors.ts index 389792b..82b2f90 100644 --- a/src/controllers/errors.ts +++ b/src/controllers/errors.ts @@ -1,4 +1,5 @@ enum API_ERROR { + USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS", NOT_FOUND = "NOT_FOUND", ACCESS_DENIED = "ACCESS_DENIED", } diff --git a/src/controllers/role/types.ts b/src/controllers/role/types.ts index 00fefc7..8d8ffed 100644 --- a/src/controllers/role/types.ts +++ b/src/controllers/role/types.ts @@ -1,10 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + interface IGetRoleParams { id: string; } interface IGetRoleResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetRoleResponseSuccess { diff --git a/src/controllers/session/types.ts b/src/controllers/session/types.ts index 3d1aa5f..689ff4c 100644 --- a/src/controllers/session/types.ts +++ b/src/controllers/session/types.ts @@ -1,10 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + interface IGetSessionParams { id: string; } interface IGetSessionResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetSessionResponseSuccess { @@ -19,7 +21,7 @@ interface IDeleteSessionParams { interface IDeleteSessionResponseError { id: string; - error: string; + error: API_ERROR; } interface IDeleteSessionResponseSuccess { diff --git a/src/controllers/user/types.ts b/src/controllers/user/types.ts index 44f1246..dca63f0 100644 --- a/src/controllers/user/types.ts +++ b/src/controllers/user/types.ts @@ -1,10 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + interface IGetUserParams { id: string; } interface IGetUserResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetUserResponseSuccess { @@ -28,7 +30,7 @@ interface IPatchUserRequest { interface IPatchUserResponseError { id: string; - error: string; + error: API_ERROR; } interface IPatchUserResponseSuccess { @@ -43,7 +45,7 @@ interface IGetSessionsParams { interface IGetSessionsResponseError { id: string; - error: string; + error: API_ERROR; } interface IGetSessionsResponseSuccess { diff --git a/src/services/auth/auth.ts b/src/services/auth/auth.ts index 68e64d6..0de2d7e 100644 --- a/src/services/auth/auth.ts +++ b/src/services/auth/auth.ts @@ -9,12 +9,20 @@ import { getJwtSecret } from "./helpers.js"; const registerUser = async ( registration: IUserRegistration, ): Promise => { - const existingUser = await getDB().user.findUnique({ + const existingUserUsername = await getDB().user.findUnique({ where: { username: registration.username }, }); - if (existingUser) { + if (existingUserUsername) { return null; } + if (registration.email) { + const existingUserEmail = await getDB().user.findUnique({ + where: { email: registration.email }, + }); + if (existingUserEmail) { + return null; + } + } const passwordHash = await hashPassword(registration.password); diff --git a/tests/api.js b/tests/api.js new file mode 100644 index 0000000..2cfb511 --- /dev/null +++ b/tests/api.js @@ -0,0 +1,56 @@ +import config from "../src/config.json" with { type: "json" }; + +const url = `http://localhost:${config.port}/api/v1`; + +const apiGet = async (endpoint, token) => { + const response = await fetch(`${url}/${endpoint}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + return await response.json(); +}; + +const apiPost = async (endpoint, request, token) => { + const response = await fetch(`${url}/${endpoint}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(request), + }); + + return await response.json(); +}; + +const apiPatch = async (endpoint, request, token) => { + const response = await fetch(`${url}/${endpoint}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(request), + }); + + return await response.json(); +}; + +const apiDelete = async (endpoint, request, token) => { + const response = await fetch(`${url}/${endpoint}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(request), + }); + + return await response.json(); +}; + +export { apiGet, apiPost, apiPatch, apiDelete }; diff --git a/tests/community.test.js b/tests/community.test.js new file mode 100644 index 0000000..306b1d0 --- /dev/null +++ b/tests/community.test.js @@ -0,0 +1,33 @@ +import assert from "node:assert"; +import { test } from "node:test"; +import { validate } from "uuid"; +import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js"; + +const state = {}; + +test("can create community", async () => { + state.communityName = "testCommunity"; + + state.username1 = "testuser1"; + state.password1 = "8556"; + state.email1 = "testuser1@test.test"; + state.username2 = "testuser2"; + state.password2 = "8556"; + state.email2 = "testuser2@test.test"; + + const response1 = await apiPost(`auth/login`, { + username: state.username1, + password: state.password1, + }); + state.sessionId1 = response1.id; + state.token1 = response1.token; + + const response2 = await apiPost(`auth/login`, { + username: state.username2, + password: state.password2, + }); + state.sessionId2 = response2.id; + state.token2 = response2.token; +}); + +// TO-DO: Create community test and code diff --git a/tests/user.test.js b/tests/user.test.js new file mode 100644 index 0000000..f9221f3 --- /dev/null +++ b/tests/user.test.js @@ -0,0 +1,136 @@ +import assert from "node:assert"; +import { test } from "node:test"; +import { validate } from "uuid"; +import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js"; + +const state = {}; + +test("can register", async () => { + state.username = "testuser"; + state.password = "8556"; + state.email = "testuser@test.test"; + + const response = await apiPost(`auth/register`, { + username: state.username, + password: state.password, + email: state.email, + }); + + assert.equal(validate(response.id), true); + assert.equal(response.username, state.username); + assert.equal(response.registerDate > 0, true); + + state.userId = response.id; +}); + +test("shouldn't be able to login", async () => { + const response = await apiPost(`auth/login`, { + username: state.username, + password: "wrong password", + }); + + assert.equal(response.username, state.username); + assert.equal(response.error, "ACCESS_DENIED"); +}); + +test("can login", async () => { + const response = await apiPost(`auth/login`, { + username: state.username, + password: state.password, + }); + + assert.equal(validate(response.id), true); + assert.equal(validate(response.ownerId), true); + assert.equal(response.token.length > 0, true); + assert.equal(response.ownerId, state.userId); + + state.sessionId = response.id; + state.token = response.token; +}); + +test("shouldn't be authorized to get user", async () => { + const response1 = await apiGet(`user/${state.userId}`); + assert.equal(response1.error, "ACCESS_DENIED"); + const response2 = await apiGet(`user/ac5b5aa7-3bee-4038-90c5-1007e83de1a8`); + assert.equal(response2.error, "ACCESS_DENIED"); +}); + +test("can get user", async () => { + const response = await apiGet(`user/${state.userId}`, state.token); + + assert.equal(response.id, state.userId); + assert.equal(response.username, state.username); + assert.equal(response.email, state.email); +}); + +test("can modify user", async () => { + state.email = "testusermod@test.test"; + state.description = "this is a test user"; + + const responsePatch = await apiPatch( + `user/${state.userId}`, + { + email: state.email, + description: state.description, + }, + state.token, + ); + assert.equal(responsePatch.id, state.userId); + assert.equal(responsePatch.email, state.email); + assert.equal(responsePatch.description, state.description); + + const responseGet = await apiGet(`user/${state.userId}`, state.token); + assert.equal(responseGet.id, state.userId); + assert.equal(responseGet.email, state.email); + assert.equal(responseGet.description, state.description); +}); + +test("can get user sessions", async () => { + const response = await apiGet(`user/${state.userId}/sessions`, state.token); + + assert.equal(response.id, state.userId); + assert.equal(response.sessions.length, 1); + assert.equal(response.sessions[0].id, state.sessionId); + assert.equal(response.sessions[0].userId, state.userId); +}); + +test("can get session", async () => { + const response = await apiGet(`session/${state.sessionId}`, state.token); + + assert.equal(response.id, state.sessionId); + assert.equal(response.userId, state.userId); + assert.equal(response.creationDate > 0, true); +}); + +test("can delete session", async () => { + const responseLogin = await apiPost(`auth/login`, { + username: state.username, + password: state.password, + }); + const sessionToDelete = responseLogin.id; + + const responseSessions1 = await apiGet( + `user/${state.userId}/sessions`, + state.token, + ); + assert.equal(responseSessions1.id, state.userId); + assert.equal(responseSessions1.sessions.length, 2); + + const responseDelete = await apiDelete( + `session/${sessionToDelete}`, + {}, + state.token, + ); + assert.equal(responseDelete.id, sessionToDelete); + assert.equal(responseDelete.userId, state.userId); + + const responseGet = await apiGet(`session/${sessionToDelete}`, state.token); + assert.equal(responseGet.error, "ACCESS_DENIED"); + + const responseSessions2 = await apiGet( + `user/${state.userId}/sessions`, + state.token, + ); + assert.equal(responseSessions2.id, state.userId); + assert.equal(responseSessions2.sessions.length, 1); +});