import { describe, test, expect, beforeAll, afterAll } from 'bun:test' import { Database } from 'bun:sqlite' describe('Authentication', () => { const db = new Database(':memory:') beforeAll(() => { // Create tables db.run(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin', is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) `) db.run(` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, role TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `) // Seed test user (password: admin123) db.run( 'INSERT INTO users (id, email, password_hash, name, role, is_active) VALUES (?, ?, ?, ?, ?, ?)', ['user-001', 'admin@test.com', '$2b$10$wlW1hhV6tgq8gKFtnmTBXOO8yNEv3d2UyUvwbnbX84iW3JbB3h07O', 'Test Admin', 'admin', 1] ) }) afterAll(() => { db.close() }) test('should create user correctly', () => { const user = db.query('SELECT * FROM users WHERE email = ?').get('admin@test.com') as any expect(user).toBeDefined() expect(user.email).toBe('admin@test.com') expect(user.role).toBe('admin') expect(user.is_active).toBe(1) }) test('should verify password hash', async () => { const user = db.query('SELECT * FROM users WHERE email = ?').get('admin@test.com') as any const isValid = await Bun.password.verify('admin123', user.password_hash) expect(isValid).toBe(true) }) test('should reject invalid password', async () => { const user = db.query('SELECT * FROM users WHERE email = ?').get('admin@test.com') as any const isValid = await Bun.password.verify('wrongpassword', user.password_hash) expect(isValid).toBe(false) }) test('should create and query session', () => { const sessionId = crypto.randomUUID() const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() db.run( 'INSERT INTO sessions (id, user_id, role, expires_at) VALUES (?, ?, ?, ?)', [sessionId, 'user-001', 'admin', expiresAt] ) const session = db.query('SELECT * FROM sessions WHERE id = ?').get(sessionId) as any expect(session).toBeDefined() expect(session.user_id).toBe('user-001') expect(session.role).toBe('admin') }) test('should validate session expiry', () => { const sessionId = crypto.randomUUID() const expiredDate = new Date(Date.now() - 1000).toISOString() db.run( 'INSERT INTO sessions (id, user_id, role, expires_at) VALUES (?, ?, ?, ?)', [sessionId, 'user-001', 'admin', expiredDate] ) const session = db.query('SELECT * FROM sessions WHERE id = ?').get(sessionId) as any const isExpired = new Date(session.expires_at) < new Date() expect(isExpired).toBe(true) }) }) describe('Admin API', () => { test('should have CSRF protection', async () => { const res = await fetch('http://localhost:8080/api/csrf-token') expect(res.status).toBe(200) const data = await res.json() expect(data.success).toBe(true) expect(data.token).toBeDefined() }) test('should reject unauthenticated admin requests', async () => { const res = await fetch('http://localhost:8080/api/admin/stats') expect(res.status).toBe(401) }) test('should reject invalid login credentials', async () => { const res = await fetch('http://localhost:8080/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'nonexistent@test.com', password: 'wrong' }) }) expect(res.status).toBe(401) }) test('should validate email format on login', async () => { const res = await fetch('http://localhost:8080/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'invalid-email', password: 'test123' }) }) expect(res.status).toBe(400) }) test('should require password on login', async () => { const res = await fetch('http://localhost:8080/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'admin@test.com' }) }) expect(res.status).toBe(400) }) }) describe('Property CRUD', () => { const db = new Database(':memory:') beforeAll(() => { db.run(` CREATE TABLE IF NOT EXISTS properties ( id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, reference TEXT UNIQUE NOT NULL, type TEXT NOT NULL, status TEXT NOT NULL, title_es TEXT NOT NULL, title_ru TEXT NOT NULL, description_es TEXT NOT NULL, description_ru TEXT NOT NULL, city TEXT NOT NULL, area INTEGER NOT NULL, price INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) `) }) afterAll(() => { db.close() }) test('should create property', () => { const id = crypto.randomUUID() db.run( 'INSERT INTO properties (id, slug, reference, type, status, title_es, title_ru, description_es, description_ru, city, area, price) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, 'test-property', 'TP-TEST', 'urban', 'active', 'Test Property', 'Тест', 'Desc', 'Описание', 'Test City', 1000, 100000] ) const prop = db.query('SELECT * FROM properties WHERE slug = ?').get('test-property') as any expect(prop).toBeDefined() expect(prop.type).toBe('urban') expect(prop.price).toBe(100000) }) test('should update property', () => { const id = crypto.randomUUID() db.run( 'INSERT INTO properties (id, slug, reference, type, status, title_es, title_ru, description_es, description_ru, city, area, price) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, 'test-prop-2', 'TP-TEST2', 'urban', 'active', 'Test', 'Тест', 'Desc', 'Описание', 'City', 500, 50000] ) db.run('UPDATE properties SET price = ? WHERE id = ?', [60000, id]) const prop = db.query('SELECT * FROM properties WHERE id = ?').get(id) as any expect(prop.price).toBe(60000) }) test('should delete property', () => { const id = crypto.randomUUID() db.run( 'INSERT INTO properties (id, slug, reference, type, status, title_es, title_ru, description_es, description_ru, city, area, price) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, 'test-prop-3', 'TP-TEST3', 'urban', 'active', 'Test', 'Тест', 'Desc', 'Описание', 'City', 500, 50000] ) db.run('DELETE FROM properties WHERE id = ?', [id]) const prop = db.query('SELECT * FROM properties WHERE id = ?').get(id) expect(prop).toBeNull() }) test('should filter properties by type', () => { const types = ['urban', 'agricultural', 'house', 'apartment'] types.forEach((type, i) => { db.run( 'INSERT INTO properties (id, slug, reference, type, status, title_es, title_ru, description_es, description_ru, city, area, price) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [crypto.randomUUID(), `type-test-${i}`, `TP-TYPE-${i}`, type, 'active', 'Test', 'Тест', 'Desc', 'Описание', 'City', 500, 50000] ) }) const urbanProps = db.query('SELECT * FROM properties WHERE type = ?').all('urban') as any[] expect(urbanProps.length).toBeGreaterThan(0) expect(urbanProps.every(p => p.type === 'urban')).toBe(true) }) }) describe('Input Validation', () => { test('should sanitize XSS in lead form', () => { const sanitize = (str: string) => str ? String(str).replace(/[<>]/g, '') : '' const xssInput = '' const sanitized = sanitize(xssInput) expect(sanitized).not.toContain('') }) test('should validate email format', () => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const validEmails = ['test@example.com', 'user@domain.es', 'admin@tenerifeprop.com'] const invalidEmails = ['invalid', 'no@domain', '@nodomain.com', 'spaces in@email.com'] validEmails.forEach(email => { expect(emailRegex.test(email)).toBe(true) }) invalidEmails.forEach(email => { expect(emailRegex.test(email)).toBe(false) }) }) test('should validate phone format', () => { const phoneRegex = /^[\d\s\+\-\(\)]{7,20}$/ const validPhones = ['+34600123456', '600 123 456', '+34 600 123 456'] const invalidPhones = ['123', 'abc', ''] validPhones.forEach(phone => { expect(phoneRegex.test(phone)).toBe(true) }) invalidPhones.forEach(phone => { expect(phoneRegex.test(phone)).toBe(false) }) }) }) describe('Rate Limiting', () => { test('should have rate limit configuration', async () => { const res = await fetch('http://localhost:8080/api/properties') expect(res.status).toBe(200) // Check rate limit headers exist // In production, these would be set by the rate limiter }) })