|
|
@ -1,13 +1,18 @@
|
|
|
|
import { Database } from 'bun:sqlite';
|
|
|
|
import { Database } from 'bun:sqlite';
|
|
|
|
import { initializeDatabase } from '../../src/db';
|
|
|
|
import { initializeDatabase, ensureUserExists } from '../../src/db'; // Import ensureUserExists
|
|
|
|
import { describe, test, expect, beforeEach, afterAll, beforeAll } from 'bun:test';
|
|
|
|
import { describe, test, expect, beforeEach, afterAll, beforeAll } from 'bun:test';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Helper function to introduce slight delay for timestamp checks
|
|
|
|
|
|
|
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
|
|
|
|
|
|
|
|
describe('Database', () => {
|
|
|
|
describe('Database', () => {
|
|
|
|
let testDb: Database;
|
|
|
|
let testDb: Database;
|
|
|
|
|
|
|
|
|
|
|
|
// Create an in-memory database before any tests run
|
|
|
|
// Create an in-memory database before any tests run
|
|
|
|
beforeAll(() => {
|
|
|
|
beforeAll(() => {
|
|
|
|
testDb = new Database(':memory:');
|
|
|
|
testDb = new Database(':memory:');
|
|
|
|
|
|
|
|
// Enable foreign keys for the test database instance
|
|
|
|
|
|
|
|
testDb.exec('PRAGMA foreign_keys = ON;');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Close the database connection after all tests have run
|
|
|
|
// Close the database connection after all tests have run
|
|
|
@ -17,8 +22,8 @@ describe('Database', () => {
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
beforeEach(() => {
|
|
|
|
// Reset database schema between tests by dropping tables and re-initializing
|
|
|
|
// Reset database schema between tests by dropping tables and re-initializing
|
|
|
|
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS task_assignments'); // Drop dependent tables first
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS tasks');
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS tasks');
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS task_assignments');
|
|
|
|
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS users');
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS users');
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS groups');
|
|
|
|
testDb.exec('DROP TABLE IF EXISTS groups');
|
|
|
|
// Initialize schema on the test database instance
|
|
|
|
// Initialize schema on the test database instance
|
|
|
@ -26,15 +31,18 @@ describe('Database', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('Table Creation', () => {
|
|
|
|
describe('Table Creation', () => {
|
|
|
|
test('should create all required tables', () => {
|
|
|
|
test('should create all required tables in correct order', () => {
|
|
|
|
const tables = testDb
|
|
|
|
const tables = testDb
|
|
|
|
.query("SELECT name FROM sqlite_master WHERE type='table'")
|
|
|
|
.query("SELECT name FROM sqlite_master WHERE type='table'")
|
|
|
|
.all()
|
|
|
|
.all()
|
|
|
|
.map((t: any) => t.name);
|
|
|
|
.map((t: any) => t.name);
|
|
|
|
|
|
|
|
|
|
|
|
const expectedTables = ['groups', 'task_assignments', 'tasks', 'users'];
|
|
|
|
// Order matters if foreign keys are involved during creation, though SQLite is flexible
|
|
|
|
|
|
|
|
const expectedTables = ['users', 'groups', 'tasks', 'task_assignments'];
|
|
|
|
const userTables = tables.filter(t => !t.startsWith('sqlite_'));
|
|
|
|
const userTables = tables.filter(t => !t.startsWith('sqlite_'));
|
|
|
|
expect(userTables.sort()).toEqual(expectedTables.sort());
|
|
|
|
// Check if all expected tables exist, order might vary slightly depending on execution
|
|
|
|
|
|
|
|
expect(userTables).toHaveLength(expectedTables.length);
|
|
|
|
|
|
|
|
expectedTables.forEach(table => expect(userTables).toContain(table));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
@ -58,6 +66,14 @@ describe('Database', () => {
|
|
|
|
expect(columns).toContain('created_by');
|
|
|
|
expect(columns).toContain('created_by');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('task_assignments table should have correct columns', () => {
|
|
|
|
|
|
|
|
const columns = testDb
|
|
|
|
|
|
|
|
.query("PRAGMA table_info(task_assignments)")
|
|
|
|
|
|
|
|
.all()
|
|
|
|
|
|
|
|
.map((c: any) => c.name);
|
|
|
|
|
|
|
|
expect(columns).toEqual(['task_id', 'user_id', 'assigned_by', 'assigned_at']);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('groups table should have active flag default to true', () => {
|
|
|
|
test('groups table should have active flag default to true', () => {
|
|
|
|
testDb.exec(`
|
|
|
|
testDb.exec(`
|
|
|
|
INSERT INTO groups (id, community_id, name)
|
|
|
|
INSERT INTO groups (id, community_id, name)
|
|
|
@ -69,73 +85,172 @@ describe('Database', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('Foreign Keys', () => {
|
|
|
|
describe('Foreign Keys', () => {
|
|
|
|
test('task_assignments should reference tasks', () => {
|
|
|
|
test('tasks should reference users via created_by', () => {
|
|
|
|
const fkInfo = testDb
|
|
|
|
const fkInfo = testDb.query("PRAGMA foreign_key_list(tasks)").all();
|
|
|
|
.query("PRAGMA foreign_key_list(task_assignments)")
|
|
|
|
const createdByFk = fkInfo.find(fk => fk.from === 'created_by');
|
|
|
|
.all();
|
|
|
|
expect(createdByFk).toBeDefined();
|
|
|
|
expect(fkInfo.length).toBe(1);
|
|
|
|
expect(createdByFk.table).toBe('users');
|
|
|
|
expect(fkInfo[0].from).toBe('task_id');
|
|
|
|
expect(createdByFk.to).toBe('id');
|
|
|
|
expect(fkInfo[0].to).toBe('id');
|
|
|
|
|
|
|
|
expect(fkInfo[0].table).toBe('tasks');
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('tasks should reference users via created_by', () => {
|
|
|
|
test('tasks should reference groups via group_id', () => {
|
|
|
|
const fkInfo = testDb
|
|
|
|
const fkInfo = testDb.query("PRAGMA foreign_key_list(tasks)").all();
|
|
|
|
.query("PRAGMA foreign_key_list(tasks)")
|
|
|
|
const groupIdFk = fkInfo.find(fk => fk.from === 'group_id');
|
|
|
|
.all();
|
|
|
|
expect(groupIdFk).toBeDefined();
|
|
|
|
expect(fkInfo.length).toBe(1);
|
|
|
|
expect(groupIdFk.table).toBe('groups');
|
|
|
|
expect(fkInfo[0].from).toBe('created_by');
|
|
|
|
expect(groupIdFk.to).toBe('id');
|
|
|
|
expect(fkInfo[0].to).toBe('id');
|
|
|
|
expect(groupIdFk.on_delete).toBe('SET NULL');
|
|
|
|
expect(fkInfo[0].table).toBe('users');
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('task_assignments should reference tasks via task_id', () => {
|
|
|
|
|
|
|
|
const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all();
|
|
|
|
|
|
|
|
const taskIdFk = fkInfo.find(fk => fk.from === 'task_id');
|
|
|
|
|
|
|
|
expect(taskIdFk).toBeDefined();
|
|
|
|
|
|
|
|
expect(taskIdFk.table).toBe('tasks');
|
|
|
|
|
|
|
|
expect(taskIdFk.to).toBe('id');
|
|
|
|
|
|
|
|
expect(taskIdFk.on_delete).toBe('CASCADE');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('task_assignments should reference users via user_id', () => {
|
|
|
|
|
|
|
|
const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all();
|
|
|
|
|
|
|
|
const userIdFk = fkInfo.find(fk => fk.from === 'user_id');
|
|
|
|
|
|
|
|
expect(userIdFk).toBeDefined();
|
|
|
|
|
|
|
|
expect(userIdFk.table).toBe('users');
|
|
|
|
|
|
|
|
expect(userIdFk.to).toBe('id');
|
|
|
|
|
|
|
|
expect(userIdFk.on_delete).toBe('CASCADE');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('task_assignments should reference users via assigned_by', () => {
|
|
|
|
|
|
|
|
const fkInfo = testDb.query("PRAGMA foreign_key_list(task_assignments)").all();
|
|
|
|
|
|
|
|
const assignedByFk = fkInfo.find(fk => fk.from === 'assigned_by');
|
|
|
|
|
|
|
|
expect(assignedByFk).toBeDefined();
|
|
|
|
|
|
|
|
expect(assignedByFk.table).toBe('users');
|
|
|
|
|
|
|
|
expect(assignedByFk.to).toBe('id');
|
|
|
|
|
|
|
|
expect(assignedByFk.on_delete).toBe('CASCADE');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should prevent inserting task with non-existent user', () => {
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
|
|
|
testDb.prepare(`
|
|
|
|
|
|
|
|
INSERT INTO tasks (description, created_by)
|
|
|
|
|
|
|
|
VALUES ('Task for non-existent user', 'nonexistentuser')
|
|
|
|
|
|
|
|
`).run();
|
|
|
|
|
|
|
|
}).toThrow(); // Foreign key constraint failure
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should prevent inserting assignment with non-existent user', () => {
|
|
|
|
|
|
|
|
// Need a valid user and task first
|
|
|
|
|
|
|
|
testDb.exec(`INSERT INTO users (id) VALUES ('user1')`);
|
|
|
|
|
|
|
|
const taskResult = testDb.prepare(`INSERT INTO tasks (description, created_by) VALUES ('Test Task', 'user1') RETURNING id`).get();
|
|
|
|
|
|
|
|
const taskId = taskResult.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
|
|
|
testDb.prepare(`
|
|
|
|
|
|
|
|
INSERT INTO task_assignments (task_id, user_id, assigned_by)
|
|
|
|
|
|
|
|
VALUES (?, 'nonexistentuser', 'user1')
|
|
|
|
|
|
|
|
`).run(taskId);
|
|
|
|
|
|
|
|
}).toThrow(); // Foreign key constraint failure on user_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
|
|
|
testDb.prepare(`
|
|
|
|
|
|
|
|
INSERT INTO task_assignments (task_id, user_id, assigned_by)
|
|
|
|
|
|
|
|
VALUES (?, 'user1', 'nonexistentassigner')
|
|
|
|
|
|
|
|
`).run(taskId);
|
|
|
|
|
|
|
|
}).toThrow(); // Foreign key constraint failure on assigned_by
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('User Operations', () => {
|
|
|
|
describe('ensureUserExists', () => {
|
|
|
|
test('should reject duplicate user IDs', () => {
|
|
|
|
const rawUserId = '1234567890@s.whatsapp.net';
|
|
|
|
// First insert should succeed
|
|
|
|
const normalizedId = '1234567890';
|
|
|
|
const firstInsert = testDb.prepare(`
|
|
|
|
|
|
|
|
INSERT INTO users (id) VALUES ('34650112233')
|
|
|
|
test('should create a new user if they do not exist', () => {
|
|
|
|
`).run();
|
|
|
|
const resultId = ensureUserExists(rawUserId, testDb);
|
|
|
|
expect(firstInsert.changes).toBe(1);
|
|
|
|
expect(resultId).toBe(normalizedId);
|
|
|
|
|
|
|
|
|
|
|
|
// Second insert with same ID should fail due to PRIMARY KEY constraint
|
|
|
|
const user = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId);
|
|
|
|
expect(() => {
|
|
|
|
expect(user).toBeDefined();
|
|
|
|
testDb.prepare(`
|
|
|
|
expect(user.id).toBe(normalizedId);
|
|
|
|
INSERT INTO users (id) VALUES ('34650112233')
|
|
|
|
expect(user.first_seen).toBeDefined();
|
|
|
|
`).run();
|
|
|
|
expect(user.last_seen).toBe(user.first_seen); // Initially they are the same
|
|
|
|
}).toThrow(); // Bun's SQLite driver throws an error on constraint violation
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Verify only one record exists
|
|
|
|
test('should update last_seen for an existing user', async () => {
|
|
|
|
const countResult = testDb.prepare(`
|
|
|
|
// First call creates the user
|
|
|
|
SELECT COUNT(*) as count FROM users WHERE id = '34650112233'
|
|
|
|
ensureUserExists(rawUserId, testDb);
|
|
|
|
`).get();
|
|
|
|
const initialUser = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId);
|
|
|
|
expect(countResult.count).toBe(1);
|
|
|
|
const initialLastSeen = initialUser.last_seen;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Wait a bit to ensure timestamp changes
|
|
|
|
|
|
|
|
await sleep(50);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Second call should update last_seen
|
|
|
|
|
|
|
|
const resultId = ensureUserExists(rawUserId, testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBe(normalizedId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const updatedUser = testDb.query("SELECT * FROM users WHERE id = ?").get(normalizedId);
|
|
|
|
|
|
|
|
expect(updatedUser.last_seen).toBeDefined();
|
|
|
|
|
|
|
|
expect(updatedUser.last_seen).not.toBe(initialLastSeen); // last_seen should be updated
|
|
|
|
|
|
|
|
expect(updatedUser.first_seen).toBe(initialUser.first_seen); // first_seen should NOT be updated
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should return the normalized ID on success', () => {
|
|
|
|
|
|
|
|
const resultId = ensureUserExists('9876543210@s.whatsapp.net', testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBe('9876543210');
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should return null for invalid raw user ID', () => {
|
|
|
|
|
|
|
|
const resultId = ensureUserExists('invalid-id!', testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBeNull();
|
|
|
|
|
|
|
|
const userCount = testDb.query("SELECT COUNT(*) as count FROM users").get();
|
|
|
|
|
|
|
|
expect(userCount.count).toBe(0); // No user should be created
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should return null for null input', () => {
|
|
|
|
|
|
|
|
const resultId = ensureUserExists(null, testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBeNull();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should return null for undefined input', () => {
|
|
|
|
|
|
|
|
const resultId = ensureUserExists(undefined, testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBeNull();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should handle user ID with participant correctly', () => {
|
|
|
|
|
|
|
|
const resultId = ensureUserExists('1122334455:12@s.whatsapp.net', testDb);
|
|
|
|
|
|
|
|
expect(resultId).toBe('1122334455');
|
|
|
|
|
|
|
|
const user = testDb.query("SELECT * FROM users WHERE id = ?").get('1122334455');
|
|
|
|
|
|
|
|
expect(user).toBeDefined();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('Data Operations', () => {
|
|
|
|
// Keep existing Data Operations tests, but ensure users/groups are created first
|
|
|
|
|
|
|
|
describe('Data Operations (with user checks)', () => {
|
|
|
|
|
|
|
|
const user1 = '34650112233';
|
|
|
|
|
|
|
|
const group1 = 'test-group-123';
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
beforeEach(() => {
|
|
|
|
// Seed necessary data for these tests into the test database
|
|
|
|
// Ensure users and groups exist before task operations
|
|
|
|
|
|
|
|
ensureUserExists(`${user1}@s.whatsapp.net`, testDb);
|
|
|
|
testDb.exec(`
|
|
|
|
testDb.exec(`
|
|
|
|
INSERT INTO users (id) VALUES ('34650112233');
|
|
|
|
INSERT OR IGNORE INTO groups (id, community_id, name)
|
|
|
|
INSERT INTO groups (id, community_id, name)
|
|
|
|
VALUES (?, 'test-community', 'Test Group')
|
|
|
|
VALUES ('test-group', 'test-community', 'Test Group')
|
|
|
|
`, [group1]);
|
|
|
|
`);
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('should allow inserting group tasks', () => {
|
|
|
|
test('should allow inserting group tasks with existing user and group', () => {
|
|
|
|
const result = testDb.prepare(`
|
|
|
|
const result = testDb.prepare(`
|
|
|
|
INSERT INTO tasks (description, created_by, group_id)
|
|
|
|
INSERT INTO tasks (description, created_by, group_id)
|
|
|
|
VALUES ('Test task', '34650112233', 'test-group')
|
|
|
|
VALUES ('Test task', ?, ?)
|
|
|
|
`).run();
|
|
|
|
`).run(user1, group1);
|
|
|
|
expect(result.changes).toBe(1);
|
|
|
|
expect(result.changes).toBe(1);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('should allow inserting private tasks', () => {
|
|
|
|
test('should allow inserting private tasks with existing user', () => {
|
|
|
|
const result = testDb.prepare(`
|
|
|
|
const result = testDb.prepare(`
|
|
|
|
INSERT INTO tasks (description, created_by)
|
|
|
|
INSERT INTO tasks (description, created_by)
|
|
|
|
VALUES ('Private task', '34650112233')
|
|
|
|
VALUES ('Private task', ?)
|
|
|
|
`).run();
|
|
|
|
`).run(user1);
|
|
|
|
expect(result.changes).toBe(1);
|
|
|
|
expect(result.changes).toBe(1);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|