summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-06-01 01:03:40 +0200
committerRory& <root@rory.gay>2025-06-01 01:03:40 +0200
commit99220d73469210f94493ef92a9edc64ab50eb0d9 (patch)
tree765781a300db5c1e1a15afaa8bd09f48712a5313 /src
parentInit test frontend (diff)
downloadnodejs-final-assignment-99220d73469210f94493ef92a9edc64ab50eb0d9.tar.xz
Add some tests
Diffstat (limited to 'src')
-rw-r--r--src/db/dbAccess/index.js1
-rw-r--r--src/db/dbAccess/user.js41
-rw-r--r--src/db/dbAccess/user.test.js44
-rw-r--r--src/db/index.js1
-rw-r--r--src/db/schemas/user.js1
-rw-r--r--src/util/error.js36
-rw-r--r--src/util/jwtUtils.js34
-rw-r--r--src/util/jwtUtils.test.js35
-rw-r--r--src/util/secretUtils.js2
-rw-r--r--src/util/secretUtils.test.js23
10 files changed, 217 insertions, 1 deletions
diff --git a/src/db/dbAccess/index.js b/src/db/dbAccess/index.js
new file mode 100644

index 0000000..ee1c337 --- /dev/null +++ b/src/db/dbAccess/index.js
@@ -0,0 +1 @@ +export * from './user.js'; diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js new file mode 100644
index 0000000..413b1cf --- /dev/null +++ b/src/db/dbAccess/user.js
@@ -0,0 +1,41 @@ +import { hash, compare } from 'bcrypt'; +import { DbUser } from '#db/schemas/index.js'; + +export async function registerUser(username, password, email, type = 'user') { + if (!username || !password || !email) { + throw new Error( + 'Username, password, and email are required to register a user.' + ); + } + + const passwordHash = await hash(password, 10); + if (!passwordHash) { + throw new Error('Failed to hash password.'); + } + + return DbUser.create({ + username, + passwordHash, + email, + type + }); +} + +export async function deleteUser(id, password) { + const user = await DbUser.findById(id); + DbUser.exists({ _id: id }).then(exists => { + if (!exists) { + throw new Error('User does not exist.'); + } + }); + if (!user) { + throw new Error('User not found.'); + } + + const isPasswordValid = await compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new Error('Invalid password.'); + } + + await DbUser.findByIdAndDelete(id); +} diff --git a/src/db/dbAccess/user.test.js b/src/db/dbAccess/user.test.js new file mode 100644
index 0000000..e8e1af5 --- /dev/null +++ b/src/db/dbAccess/user.test.js
@@ -0,0 +1,44 @@ +import * as dotenv from 'dotenv'; +import { it } from 'node:test'; +import { deleteUser, registerUser } from '#db/index.js'; +import * as assert from 'node:assert'; +import { initDb } from '#db/db.js'; +import { disconnect } from 'mongoose'; + +dotenv.config(); + +await initDb(); + +async function createTestUser() { + const username = (Math.random() * 1000000).toString(); + const password = (Math.random() * 1000000).toString(); + const email = (Math.random() * 1000000).toString() + '@example.com'; + return { + username, + password, + email, + user: await registerUser(username, password, email) + }; +} + +await it('Can create user', async () => { + await assert.doesNotReject(createTestUser()); +}); + +await it('Can delete user', async () => { + const { username, password, email, user } = await createTestUser(); + + const deletePromise = deleteUser(user._id, password); + await assert.doesNotReject(deletePromise); +}); + +await it('Cant delete nonexistant user', async () => { + await assert.rejects(deleteUser('abc', '')); +}); + +await it('Cant delete user with invalid password', async () => { + const user = await createTestUser(); + await assert.rejects(deleteUser(user.user._id, 'wrongpassword')); +}); + +await disconnect(); diff --git a/src/db/index.js b/src/db/index.js
index cd306b7..0680880 100644 --- a/src/db/index.js +++ b/src/db/index.js
@@ -1,2 +1,3 @@ export * from './db.js'; export * from './schemas/index.js'; +export * from './dbAccess/index.js'; diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 1a7e048..9d08680 100644 --- a/src/db/schemas/user.js +++ b/src/db/schemas/user.js
@@ -1,4 +1,5 @@ import { model, Schema } from 'mongoose'; +import { hash, compare } from 'bcrypt'; /** * User schema for MongoDB. diff --git a/src/util/error.js b/src/util/error.js new file mode 100644
index 0000000..96f1deb --- /dev/null +++ b/src/util/error.js
@@ -0,0 +1,36 @@ +export class SafeNSoundError extends Error { + constructor(options) { + super(); + if (typeof options === 'string') { + this.errCode = options; + super.message = this.getDefaultMessage(); + } else if (typeof options === 'object') { + this.errCode = options.errCode || 'UNKNOWN_ERROR'; + super.message = options.message || this.getDefaultMessage(); + } else { + this.errCode = 'UNKNOWN_ERROR'; + this.message = + 'An unknown error occurred (invalid SafeNSoundError constructor options)'; + } + } + + getDefaultMessage() { + switch (this.type) { + case 'USER_NOT_FOUND': + return 'User not found'; + case 'INVALID_CREDENTIALS': + return 'Invalid credentials'; + case 'EMAIL_ALREADY_EXISTS': + return 'Email already exists'; + case 'PASSWORD_TOO_WEAK': + return 'Password is too weak'; + case 'TOKEN_EXPIRED': + return 'Token has expired'; + case 'UNAUTHORIZED': + return 'Unauthorized access'; + case 'UNKNOWN_ERROR': + default: + return 'An unknown error occurred'; + } + } +} diff --git a/src/util/jwtUtils.js b/src/util/jwtUtils.js
index 115c9c5..9031631 100644 --- a/src/util/jwtUtils.js +++ b/src/util/jwtUtils.js
@@ -1,6 +1,7 @@ import {existsSync} from 'fs'; import {readFile, writeFile} from "node:fs/promises"; import {generateKeyPairSync, createHash, createPublicKey, createPrivateKey} from 'node:crypto'; +import jwt from "jsonwebtoken"; let privateKey, publicKey, fingerprint; @@ -10,6 +11,8 @@ export async function initJwt() { throw new Error('JWT secret path is not defined in environment variables, or the directory does not exist.'); } + console.log(`[JWT] Initializing JWT with secret path: ${secretPath}`); + const privateKeyPath = `${secretPath}/jwt.key`; const publicKeyPath = `${secretPath}/jwt.key.pub`; @@ -55,9 +58,40 @@ const jwtOptions = { } export async function generateJwtToken(user) { + if (!privateKey) { + throw new Error('JWT private key is not initialized. Please call initJwt() first.'); + } + + const payload = { + sub: user._id.toString(), + username: user.username, + type: user.type, + iat: Math.floor(Date.now() / 1000) + }; + return new Promise((resolve, reject) => { + jwt.sign(payload, privateKey, jwtOptions, (err, token) => { + if (err) { + console.error('[JWT] Error generating token:', err); + return reject(err); + } + resolve(token); + }); + }); } export async function validateJwtToken(token) { + if (!publicKey) { + throw new Error('JWT public key is not initialized. Please call initJwt() first.'); + } + return new Promise((resolve, reject) => { + jwt.verify(token, publicKey, jwtOptions, (err, decoded) => { + if (err) { + console.error('[JWT] Token validation failed:', err); + return reject(err); + } + resolve(decoded); + }); + }); } \ No newline at end of file diff --git a/src/util/jwtUtils.test.js b/src/util/jwtUtils.test.js new file mode 100644
index 0000000..94a7d99 --- /dev/null +++ b/src/util/jwtUtils.test.js
@@ -0,0 +1,35 @@ +import { it } from 'node:test'; +import { initJwt, generateJwtToken, validateJwtToken } from './jwtUtils.js'; +import * as dotenv from 'dotenv'; +import { tmpdir } from 'node:os'; +import { mkdtemp } from 'node:fs/promises'; +import * as assert from 'node:assert'; +dotenv.config(); + +const user = { + _id: 'meow', + username: 'testuser' +}; + +await it('Should be able to generate new secrets', async () => { + process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir()); + await initJwt(); +}); + +await it('Should be able to load the JWT utils', async () => { + process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir()); + await initJwt(); + await initJwt(); +}); + +it('Should be able to create a JWT token', async () => { + await initJwt(); + await generateJwtToken(user); +}); + +it('Should be able to validate a JWT token', async () => { + await initJwt(); + const token = await generateJwtToken(user); + const result = await validateJwtToken(token); + console.log(result); +}); diff --git a/src/util/secretUtils.js b/src/util/secretUtils.js
index 92e1b1c..7394395 100644 --- a/src/util/secretUtils.js +++ b/src/util/secretUtils.js
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises'; export async function readSecret(name, path) { console.log(`[SECRET] Reading secret "${name}" from path: ${path}`); if (!path) { - throw new Error('Path to secret file is required'); + throw new Error(`Path to secret file is required: ${name}`); } const content = await fs.readFile(path, 'utf8'); return content.trim(); diff --git a/src/util/secretUtils.test.js b/src/util/secretUtils.test.js new file mode 100644
index 0000000..d95d56f --- /dev/null +++ b/src/util/secretUtils.test.js
@@ -0,0 +1,23 @@ +import { it } from 'node:test'; +import * as dotenv from 'dotenv'; +import { tmpdir } from 'node:os'; +import * as assert from 'node:assert'; +import { readSecret } from '#util/secretUtils.js'; +import { writeFile, mkdtemp } from 'node:fs/promises'; + +dotenv.config(); + +await it('Should fail on nonexistant secret', async () => { + const secretPath = (await mkdtemp(tmpdir())) + '/secret'; + await assert.rejects(readSecret('Nonexistant secret', secretPath)); +}); + +await it('Should read secret from file', async () => { + const secretPath = (await mkdtemp(tmpdir())) + '/secret'; + await writeFile(secretPath, 'testsecret'); + await assert.doesNotReject(readSecret('Test secret', secretPath)); + await assert.equal( + await readSecret('Test secret', secretPath), + 'testsecret' + ); +});