From 99220d73469210f94493ef92a9edc64ab50eb0d9 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 1 Jun 2025 01:03:40 +0200 Subject: Add some tests --- src/util/error.js | 36 ++++++++++++++++++++++++++++++++++++ src/util/jwtUtils.js | 34 ++++++++++++++++++++++++++++++++++ src/util/jwtUtils.test.js | 35 +++++++++++++++++++++++++++++++++++ src/util/secretUtils.js | 2 +- src/util/secretUtils.test.js | 23 +++++++++++++++++++++++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/util/error.js create mode 100644 src/util/jwtUtils.test.js create mode 100644 src/util/secretUtils.test.js (limited to 'src/util') 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' + ); +}); -- cgit 1.5.1