229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
|
|
/**
|
||
|
|
* User model unit tests
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
|
||
|
|
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
|
||
|
|
import { setupTestDb, teardownTestDb, cleanupTestDb } from '../../helpers/index.ts';
|
||
|
|
import { User } from '../../../ts/models/user.ts';
|
||
|
|
|
||
|
|
describe('User Model', () => {
|
||
|
|
beforeAll(async () => {
|
||
|
|
await setupTestDb();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await teardownTestDb();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
await cleanupTestDb();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('createUser', () => {
|
||
|
|
it('should create a user with valid data', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'test@example.com',
|
||
|
|
username: 'testuser',
|
||
|
|
passwordHash,
|
||
|
|
displayName: 'Test User',
|
||
|
|
});
|
||
|
|
|
||
|
|
assertExists(user.id);
|
||
|
|
assertEquals(user.email, 'test@example.com');
|
||
|
|
assertEquals(user.username, 'testuser');
|
||
|
|
assertEquals(user.displayName, 'Test User');
|
||
|
|
assertEquals(user.status, 'pending_verification');
|
||
|
|
assertEquals(user.emailVerified, false);
|
||
|
|
assertEquals(user.isPlatformAdmin, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should lowercase email and username', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'TEST@EXAMPLE.COM',
|
||
|
|
username: 'TestUser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
assertEquals(user.email, 'test@example.com');
|
||
|
|
assertEquals(user.username, 'testuser');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should use username as displayName if not provided', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'test2@example.com',
|
||
|
|
username: 'testuser2',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
assertEquals(user.displayName, 'testuser2');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findByEmail', () => {
|
||
|
|
it('should find user by email (case-insensitive)', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
await User.createUser({
|
||
|
|
email: 'findme@example.com',
|
||
|
|
username: 'findme',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const found = await User.findByEmail('FINDME@example.com');
|
||
|
|
assertExists(found);
|
||
|
|
assertEquals(found.email, 'findme@example.com');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return null for non-existent email', async () => {
|
||
|
|
const found = await User.findByEmail('nonexistent@example.com');
|
||
|
|
assertEquals(found, null);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findByUsername', () => {
|
||
|
|
it('should find user by username (case-insensitive)', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
await User.createUser({
|
||
|
|
email: 'user@example.com',
|
||
|
|
username: 'findbyname',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const found = await User.findByUsername('FINDBYNAME');
|
||
|
|
assertExists(found);
|
||
|
|
assertEquals(found.username, 'findbyname');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findById', () => {
|
||
|
|
it('should find user by ID', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('testpassword');
|
||
|
|
const created = await User.createUser({
|
||
|
|
email: 'byid@example.com',
|
||
|
|
username: 'byid',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const found = await User.findById(created.id);
|
||
|
|
assertExists(found);
|
||
|
|
assertEquals(found.id, created.id);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('password hashing', () => {
|
||
|
|
it('should hash password with salt', async () => {
|
||
|
|
const hash = await User.hashPassword('mypassword');
|
||
|
|
assertExists(hash);
|
||
|
|
assertEquals(hash.includes(':'), true);
|
||
|
|
|
||
|
|
const [salt, _hashPart] = hash.split(':');
|
||
|
|
assertEquals(salt.length, 32); // 16 bytes = 32 hex chars
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should produce different hashes for same password', async () => {
|
||
|
|
const hash1 = await User.hashPassword('samepassword');
|
||
|
|
const hash2 = await User.hashPassword('samepassword');
|
||
|
|
|
||
|
|
// Different salts should produce different hashes
|
||
|
|
assertEquals(hash1 !== hash2, true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('verifyPassword', () => {
|
||
|
|
it('should verify correct password', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('correctpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'verify@example.com',
|
||
|
|
username: 'verifyuser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const isValid = await user.verifyPassword('correctpassword');
|
||
|
|
assertEquals(isValid, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject incorrect password', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('correctpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'reject@example.com',
|
||
|
|
username: 'rejectuser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const isValid = await user.verifyPassword('wrongpassword');
|
||
|
|
assertEquals(isValid, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject empty password', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('correctpassword');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'empty@example.com',
|
||
|
|
username: 'emptyuser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
const isValid = await user.verifyPassword('');
|
||
|
|
assertEquals(isValid, false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isActive', () => {
|
||
|
|
it('should return true for active status', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('test');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'active@example.com',
|
||
|
|
username: 'activeuser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
user.status = 'active';
|
||
|
|
await user.save();
|
||
|
|
|
||
|
|
assertEquals(user.isActive, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false for suspended status', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('test');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'suspended@example.com',
|
||
|
|
username: 'suspendeduser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
user.status = 'suspended';
|
||
|
|
|
||
|
|
assertEquals(user.isActive, false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isPlatformAdmin', () => {
|
||
|
|
it('should default to false', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('test');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'notadmin@example.com',
|
||
|
|
username: 'notadmin',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
|
||
|
|
assertEquals(user.isPlatformAdmin, false);
|
||
|
|
assertEquals(user.isSystemAdmin, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should be settable to true', async () => {
|
||
|
|
const passwordHash = await User.hashPassword('test');
|
||
|
|
const user = await User.createUser({
|
||
|
|
email: 'admin@example.com',
|
||
|
|
username: 'adminuser',
|
||
|
|
passwordHash,
|
||
|
|
});
|
||
|
|
user.isPlatformAdmin = true;
|
||
|
|
await user.save();
|
||
|
|
|
||
|
|
const found = await User.findById(user.id);
|
||
|
|
assertEquals(found!.isPlatformAdmin, true);
|
||
|
|
assertEquals(found!.isSystemAdmin, true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|