test(02-02): add repository adapter tests
- createTestDatabase helper with in-memory SQLite schema - initiative.test.ts: CRUD, defaults, not-found errors - phase.test.ts: CRUD, FK constraint, findByInitiativeId ordering - plan.test.ts: CRUD, FK constraint, findByPhaseId ordering - task.test.ts: CRUD, FK constraint, findByPlanId ordering - Fixed adapters to apply defaults and fetch inserted records - All 42 tests passing
This commit is contained in:
124
src/db/repositories/drizzle/initiative.test.ts
Normal file
124
src/db/repositories/drizzle/initiative.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* DrizzleInitiativeRepository Tests
|
||||
*
|
||||
* Tests for the Initiative repository adapter.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DrizzleInitiativeRepository } from './initiative.js';
|
||||
import { createTestDatabase } from './test-helpers.js';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
|
||||
describe('DrizzleInitiativeRepository', () => {
|
||||
let db: DrizzleDatabase;
|
||||
let repo: DrizzleInitiativeRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createTestDatabase();
|
||||
repo = new DrizzleInitiativeRepository(db);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create an initiative with generated id and timestamps', async () => {
|
||||
const initiative = await repo.create({
|
||||
name: 'Test Initiative',
|
||||
description: 'A test initiative',
|
||||
});
|
||||
|
||||
expect(initiative.id).toBeDefined();
|
||||
expect(initiative.id.length).toBeGreaterThan(0);
|
||||
expect(initiative.name).toBe('Test Initiative');
|
||||
expect(initiative.description).toBe('A test initiative');
|
||||
expect(initiative.status).toBe('active');
|
||||
expect(initiative.createdAt).toBeInstanceOf(Date);
|
||||
expect(initiative.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should use provided status', async () => {
|
||||
const initiative = await repo.create({
|
||||
name: 'Completed Initiative',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
expect(initiative.status).toBe('completed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null for non-existent initiative', async () => {
|
||||
const result = await repo.findById('non-existent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should find an existing initiative', async () => {
|
||||
const created = await repo.create({
|
||||
name: 'Find Me',
|
||||
});
|
||||
|
||||
const found = await repo.findById(created.id);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.id).toBe(created.id);
|
||||
expect(found!.name).toBe('Find Me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return empty array initially', async () => {
|
||||
const all = await repo.findAll();
|
||||
expect(all).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all initiatives', async () => {
|
||||
await repo.create({ name: 'Initiative 1' });
|
||||
await repo.create({ name: 'Initiative 2' });
|
||||
await repo.create({ name: 'Initiative 3' });
|
||||
|
||||
const all = await repo.findAll();
|
||||
expect(all.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update fields and updatedAt', async () => {
|
||||
const created = await repo.create({
|
||||
name: 'Original Name',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Small delay to ensure updatedAt differs
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updated = await repo.update(created.id, {
|
||||
name: 'Updated Name',
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.status).toBe('completed');
|
||||
expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime());
|
||||
});
|
||||
|
||||
it('should throw for non-existent initiative', async () => {
|
||||
await expect(
|
||||
repo.update('non-existent-id', { name: 'New Name' })
|
||||
).rejects.toThrow('Initiative not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an existing initiative', async () => {
|
||||
const created = await repo.create({ name: 'To Delete' });
|
||||
|
||||
await repo.delete(created.id);
|
||||
|
||||
const found = await repo.findById(created.id);
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw for non-existent initiative', async () => {
|
||||
await expect(repo.delete('non-existent-id')).rejects.toThrow(
|
||||
'Initiative not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,17 +24,20 @@ export class DrizzleInitiativeRepository implements InitiativeRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreateInitiativeData): Promise<Initiative> {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const initiative = {
|
||||
id: nanoid(),
|
||||
|
||||
await this.db.insert(initiatives).values({
|
||||
id,
|
||||
...data,
|
||||
status: data.status ?? 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
await this.db.insert(initiatives).values(initiative);
|
||||
|
||||
return initiative as Initiative;
|
||||
// Fetch to get the complete record with all defaults applied
|
||||
const created = await this.findById(id);
|
||||
return created!;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Initiative | null> {
|
||||
|
||||
171
src/db/repositories/drizzle/phase.test.ts
Normal file
171
src/db/repositories/drizzle/phase.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* DrizzlePhaseRepository Tests
|
||||
*
|
||||
* Tests for the Phase repository adapter.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DrizzlePhaseRepository } from './phase.js';
|
||||
import { DrizzleInitiativeRepository } from './initiative.js';
|
||||
import { createTestDatabase } from './test-helpers.js';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
|
||||
describe('DrizzlePhaseRepository', () => {
|
||||
let db: DrizzleDatabase;
|
||||
let phaseRepo: DrizzlePhaseRepository;
|
||||
let initiativeRepo: DrizzleInitiativeRepository;
|
||||
let testInitiativeId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = createTestDatabase();
|
||||
phaseRepo = new DrizzlePhaseRepository(db);
|
||||
initiativeRepo = new DrizzleInitiativeRepository(db);
|
||||
|
||||
// Create a test initiative for FK constraint
|
||||
const initiative = await initiativeRepo.create({
|
||||
name: 'Test Initiative',
|
||||
});
|
||||
testInitiativeId = initiative.id;
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a phase with generated id and timestamps', async () => {
|
||||
const phase = await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 1,
|
||||
name: 'Test Phase',
|
||||
description: 'A test phase',
|
||||
});
|
||||
|
||||
expect(phase.id).toBeDefined();
|
||||
expect(phase.id.length).toBeGreaterThan(0);
|
||||
expect(phase.initiativeId).toBe(testInitiativeId);
|
||||
expect(phase.number).toBe(1);
|
||||
expect(phase.name).toBe('Test Phase');
|
||||
expect(phase.status).toBe('pending');
|
||||
expect(phase.createdAt).toBeInstanceOf(Date);
|
||||
expect(phase.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw for invalid initiativeId (FK constraint)', async () => {
|
||||
await expect(
|
||||
phaseRepo.create({
|
||||
initiativeId: 'invalid-initiative-id',
|
||||
number: 1,
|
||||
name: 'Invalid Phase',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null for non-existent phase', async () => {
|
||||
const result = await phaseRepo.findById('non-existent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should find an existing phase', async () => {
|
||||
const created = await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 1,
|
||||
name: 'Find Me',
|
||||
});
|
||||
|
||||
const found = await phaseRepo.findById(created.id);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.id).toBe(created.id);
|
||||
expect(found!.name).toBe('Find Me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByInitiativeId', () => {
|
||||
it('should return empty array for initiative with no phases', async () => {
|
||||
const phases = await phaseRepo.findByInitiativeId(testInitiativeId);
|
||||
expect(phases).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only matching phases ordered by number', async () => {
|
||||
// Create phases out of order
|
||||
await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 3,
|
||||
name: 'Phase 3',
|
||||
});
|
||||
await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 1,
|
||||
name: 'Phase 1',
|
||||
});
|
||||
await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 2,
|
||||
name: 'Phase 2',
|
||||
});
|
||||
|
||||
// Create another initiative with phases
|
||||
const otherInitiative = await initiativeRepo.create({
|
||||
name: 'Other Initiative',
|
||||
});
|
||||
await phaseRepo.create({
|
||||
initiativeId: otherInitiative.id,
|
||||
number: 1,
|
||||
name: 'Other Phase',
|
||||
});
|
||||
|
||||
const phases = await phaseRepo.findByInitiativeId(testInitiativeId);
|
||||
expect(phases.length).toBe(3);
|
||||
expect(phases[0].name).toBe('Phase 1');
|
||||
expect(phases[1].name).toBe('Phase 2');
|
||||
expect(phases[2].name).toBe('Phase 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update fields and updatedAt', async () => {
|
||||
const created = await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 1,
|
||||
name: 'Original Name',
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updated = await phaseRepo.update(created.id, {
|
||||
name: 'Updated Name',
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.status).toBe('in_progress');
|
||||
expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime());
|
||||
});
|
||||
|
||||
it('should throw for non-existent phase', async () => {
|
||||
await expect(
|
||||
phaseRepo.update('non-existent-id', { name: 'New Name' })
|
||||
).rejects.toThrow('Phase not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an existing phase', async () => {
|
||||
const created = await phaseRepo.create({
|
||||
initiativeId: testInitiativeId,
|
||||
number: 1,
|
||||
name: 'To Delete',
|
||||
});
|
||||
|
||||
await phaseRepo.delete(created.id);
|
||||
|
||||
const found = await phaseRepo.findById(created.id);
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw for non-existent phase', async () => {
|
||||
await expect(phaseRepo.delete('non-existent-id')).rejects.toThrow(
|
||||
'Phase not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,17 +24,20 @@ export class DrizzlePhaseRepository implements PhaseRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreatePhaseData): Promise<Phase> {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const phase = {
|
||||
id: nanoid(),
|
||||
|
||||
await this.db.insert(phases).values({
|
||||
id,
|
||||
...data,
|
||||
status: data.status ?? 'pending',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
await this.db.insert(phases).values(phase);
|
||||
|
||||
return phase as Phase;
|
||||
// Fetch to get the complete record with all defaults applied
|
||||
const created = await this.findById(id);
|
||||
return created!;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Phase | null> {
|
||||
|
||||
169
src/db/repositories/drizzle/plan.test.ts
Normal file
169
src/db/repositories/drizzle/plan.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* DrizzlePlanRepository Tests
|
||||
*
|
||||
* Tests for the Plan repository adapter.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DrizzlePlanRepository } from './plan.js';
|
||||
import { DrizzlePhaseRepository } from './phase.js';
|
||||
import { DrizzleInitiativeRepository } from './initiative.js';
|
||||
import { createTestDatabase } from './test-helpers.js';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
|
||||
describe('DrizzlePlanRepository', () => {
|
||||
let db: DrizzleDatabase;
|
||||
let planRepo: DrizzlePlanRepository;
|
||||
let phaseRepo: DrizzlePhaseRepository;
|
||||
let initiativeRepo: DrizzleInitiativeRepository;
|
||||
let testPhaseId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = createTestDatabase();
|
||||
planRepo = new DrizzlePlanRepository(db);
|
||||
phaseRepo = new DrizzlePhaseRepository(db);
|
||||
initiativeRepo = new DrizzleInitiativeRepository(db);
|
||||
|
||||
// Create test initiative and phase for FK constraint
|
||||
const initiative = await initiativeRepo.create({
|
||||
name: 'Test Initiative',
|
||||
});
|
||||
const phase = await phaseRepo.create({
|
||||
initiativeId: initiative.id,
|
||||
number: 1,
|
||||
name: 'Test Phase',
|
||||
});
|
||||
testPhaseId = phase.id;
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a plan with generated id and timestamps', async () => {
|
||||
const plan = await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 1,
|
||||
name: 'Test Plan',
|
||||
description: 'A test plan',
|
||||
});
|
||||
|
||||
expect(plan.id).toBeDefined();
|
||||
expect(plan.id.length).toBeGreaterThan(0);
|
||||
expect(plan.phaseId).toBe(testPhaseId);
|
||||
expect(plan.number).toBe(1);
|
||||
expect(plan.name).toBe('Test Plan');
|
||||
expect(plan.status).toBe('pending');
|
||||
expect(plan.createdAt).toBeInstanceOf(Date);
|
||||
expect(plan.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw for invalid phaseId (FK constraint)', async () => {
|
||||
await expect(
|
||||
planRepo.create({
|
||||
phaseId: 'invalid-phase-id',
|
||||
number: 1,
|
||||
name: 'Invalid Plan',
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null for non-existent plan', async () => {
|
||||
const result = await planRepo.findById('non-existent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should find an existing plan', async () => {
|
||||
const created = await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 1,
|
||||
name: 'Find Me',
|
||||
});
|
||||
|
||||
const found = await planRepo.findById(created.id);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.id).toBe(created.id);
|
||||
expect(found!.name).toBe('Find Me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByPhaseId', () => {
|
||||
it('should return empty array for phase with no plans', async () => {
|
||||
const plans = await planRepo.findByPhaseId(testPhaseId);
|
||||
expect(plans).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only matching plans ordered by number', async () => {
|
||||
// Create plans out of order
|
||||
await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 3,
|
||||
name: 'Plan 3',
|
||||
});
|
||||
await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 1,
|
||||
name: 'Plan 1',
|
||||
});
|
||||
await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 2,
|
||||
name: 'Plan 2',
|
||||
});
|
||||
|
||||
const plans = await planRepo.findByPhaseId(testPhaseId);
|
||||
expect(plans.length).toBe(3);
|
||||
expect(plans[0].name).toBe('Plan 1');
|
||||
expect(plans[1].name).toBe('Plan 2');
|
||||
expect(plans[2].name).toBe('Plan 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update fields and updatedAt', async () => {
|
||||
const created = await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 1,
|
||||
name: 'Original Name',
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updated = await planRepo.update(created.id, {
|
||||
name: 'Updated Name',
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.status).toBe('in_progress');
|
||||
expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime());
|
||||
});
|
||||
|
||||
it('should throw for non-existent plan', async () => {
|
||||
await expect(
|
||||
planRepo.update('non-existent-id', { name: 'New Name' })
|
||||
).rejects.toThrow('Plan not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an existing plan', async () => {
|
||||
const created = await planRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
number: 1,
|
||||
name: 'To Delete',
|
||||
});
|
||||
|
||||
await planRepo.delete(created.id);
|
||||
|
||||
const found = await planRepo.findById(created.id);
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw for non-existent plan', async () => {
|
||||
await expect(planRepo.delete('non-existent-id')).rejects.toThrow(
|
||||
'Plan not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,17 +24,20 @@ export class DrizzlePlanRepository implements PlanRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreatePlanData): Promise<Plan> {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const plan = {
|
||||
id: nanoid(),
|
||||
|
||||
await this.db.insert(plans).values({
|
||||
id,
|
||||
...data,
|
||||
status: data.status ?? 'pending',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
await this.db.insert(plans).values(plan);
|
||||
|
||||
return plan as Plan;
|
||||
// Fetch to get the complete record with all defaults applied
|
||||
const created = await this.findById(id);
|
||||
return created!;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Plan | null> {
|
||||
|
||||
206
src/db/repositories/drizzle/task.test.ts
Normal file
206
src/db/repositories/drizzle/task.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* DrizzleTaskRepository Tests
|
||||
*
|
||||
* Tests for the Task repository adapter.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DrizzleTaskRepository } from './task.js';
|
||||
import { DrizzlePlanRepository } from './plan.js';
|
||||
import { DrizzlePhaseRepository } from './phase.js';
|
||||
import { DrizzleInitiativeRepository } from './initiative.js';
|
||||
import { createTestDatabase } from './test-helpers.js';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
|
||||
describe('DrizzleTaskRepository', () => {
|
||||
let db: DrizzleDatabase;
|
||||
let taskRepo: DrizzleTaskRepository;
|
||||
let planRepo: DrizzlePlanRepository;
|
||||
let phaseRepo: DrizzlePhaseRepository;
|
||||
let initiativeRepo: DrizzleInitiativeRepository;
|
||||
let testPlanId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = createTestDatabase();
|
||||
taskRepo = new DrizzleTaskRepository(db);
|
||||
planRepo = new DrizzlePlanRepository(db);
|
||||
phaseRepo = new DrizzlePhaseRepository(db);
|
||||
initiativeRepo = new DrizzleInitiativeRepository(db);
|
||||
|
||||
// Create full hierarchy for FK constraint
|
||||
const initiative = await initiativeRepo.create({
|
||||
name: 'Test Initiative',
|
||||
});
|
||||
const phase = await phaseRepo.create({
|
||||
initiativeId: initiative.id,
|
||||
number: 1,
|
||||
name: 'Test Phase',
|
||||
});
|
||||
const plan = await planRepo.create({
|
||||
phaseId: phase.id,
|
||||
number: 1,
|
||||
name: 'Test Plan',
|
||||
});
|
||||
testPlanId = plan.id;
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a task with generated id and timestamps', async () => {
|
||||
const task = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Test Task',
|
||||
description: 'A test task',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
expect(task.id).toBeDefined();
|
||||
expect(task.id.length).toBeGreaterThan(0);
|
||||
expect(task.planId).toBe(testPlanId);
|
||||
expect(task.name).toBe('Test Task');
|
||||
expect(task.type).toBe('auto');
|
||||
expect(task.priority).toBe('medium');
|
||||
expect(task.status).toBe('pending');
|
||||
expect(task.order).toBe(1);
|
||||
expect(task.createdAt).toBeInstanceOf(Date);
|
||||
expect(task.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw for invalid planId (FK constraint)', async () => {
|
||||
await expect(
|
||||
taskRepo.create({
|
||||
planId: 'invalid-plan-id',
|
||||
name: 'Invalid Task',
|
||||
order: 1,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should accept custom type and priority', async () => {
|
||||
const task = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Checkpoint Task',
|
||||
type: 'checkpoint:human-verify',
|
||||
priority: 'high',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
expect(task.type).toBe('checkpoint:human-verify');
|
||||
expect(task.priority).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null for non-existent task', async () => {
|
||||
const result = await taskRepo.findById('non-existent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should find an existing task', async () => {
|
||||
const created = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Find Me',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
const found = await taskRepo.findById(created.id);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.id).toBe(created.id);
|
||||
expect(found!.name).toBe('Find Me');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByPlanId', () => {
|
||||
it('should return empty array for plan with no tasks', async () => {
|
||||
const tasks = await taskRepo.findByPlanId(testPlanId);
|
||||
expect(tasks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only matching tasks ordered by order field', async () => {
|
||||
// Create tasks out of order
|
||||
await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Task 3',
|
||||
order: 3,
|
||||
});
|
||||
await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Task 1',
|
||||
order: 1,
|
||||
});
|
||||
await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Task 2',
|
||||
order: 2,
|
||||
});
|
||||
|
||||
const tasks = await taskRepo.findByPlanId(testPlanId);
|
||||
expect(tasks.length).toBe(3);
|
||||
expect(tasks[0].name).toBe('Task 1');
|
||||
expect(tasks[1].name).toBe('Task 2');
|
||||
expect(tasks[2].name).toBe('Task 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update status correctly', async () => {
|
||||
const created = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Status Test',
|
||||
status: 'pending',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
const updated = await taskRepo.update(created.id, {
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
expect(updated.status).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('should update fields and updatedAt', async () => {
|
||||
const created = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'Original Name',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const updated = await taskRepo.update(created.id, {
|
||||
name: 'Updated Name',
|
||||
priority: 'low',
|
||||
});
|
||||
|
||||
expect(updated.name).toBe('Updated Name');
|
||||
expect(updated.priority).toBe('low');
|
||||
expect(updated.updatedAt.getTime()).toBeGreaterThan(created.updatedAt.getTime());
|
||||
});
|
||||
|
||||
it('should throw for non-existent task', async () => {
|
||||
await expect(
|
||||
taskRepo.update('non-existent-id', { name: 'New Name' })
|
||||
).rejects.toThrow('Task not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete an existing task', async () => {
|
||||
const created = await taskRepo.create({
|
||||
planId: testPlanId,
|
||||
name: 'To Delete',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
await taskRepo.delete(created.id);
|
||||
|
||||
const found = await taskRepo.findById(created.id);
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw for non-existent task', async () => {
|
||||
await expect(taskRepo.delete('non-existent-id')).rejects.toThrow(
|
||||
'Task not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,17 +24,23 @@ export class DrizzleTaskRepository implements TaskRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreateTaskData): Promise<Task> {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const task = {
|
||||
id: nanoid(),
|
||||
|
||||
await this.db.insert(tasks).values({
|
||||
id,
|
||||
...data,
|
||||
type: data.type ?? 'auto',
|
||||
priority: data.priority ?? 'medium',
|
||||
status: data.status ?? 'pending',
|
||||
order: data.order ?? 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
|
||||
await this.db.insert(tasks).values(task);
|
||||
|
||||
return task as Task;
|
||||
// Fetch to get the complete record with all defaults applied
|
||||
const created = await this.findById(id);
|
||||
return created!;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Task | null> {
|
||||
|
||||
89
src/db/repositories/drizzle/test-helpers.ts
Normal file
89
src/db/repositories/drizzle/test-helpers.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Test helpers for repository tests.
|
||||
*
|
||||
* Provides utilities for setting up in-memory test databases
|
||||
* with schema applied.
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import * as schema from '../../schema.js';
|
||||
|
||||
/**
|
||||
* SQL statements to create the database schema.
|
||||
* These mirror the schema defined in schema.ts.
|
||||
*/
|
||||
const CREATE_TABLES_SQL = `
|
||||
-- Initiatives table
|
||||
CREATE TABLE IF NOT EXISTS initiatives (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Phases table
|
||||
CREATE TABLE IF NOT EXISTS phases (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
initiative_id TEXT NOT NULL REFERENCES initiatives(id) ON DELETE CASCADE,
|
||||
number INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Plans table
|
||||
CREATE TABLE IF NOT EXISTS plans (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
|
||||
number INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Tasks table
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'auto',
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Task dependencies table
|
||||
CREATE TABLE IF NOT EXISTS task_dependencies (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
depends_on_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create an in-memory test database with schema applied.
|
||||
* Returns a fresh Drizzle instance for each call.
|
||||
*/
|
||||
export function createTestDatabase(): DrizzleDatabase {
|
||||
const sqlite = new Database(':memory:');
|
||||
|
||||
// Enable foreign keys
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
// Create all tables
|
||||
sqlite.exec(CREATE_TABLES_SQL);
|
||||
|
||||
return drizzle(sqlite, { schema });
|
||||
}
|
||||
Reference in New Issue
Block a user