/** * Unit tests for QueryHelpersService (Plan 3c Task 1) — its first dedicated * test file (inventory §15c: previously exercised only transitively through * days/assignments/places/share suites). Real rows through * `TagsRepository`/`PlaceRatingsRepository`/`AssignmentParticipantsRepository`. */ import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest'; import { createSnapshotTestDb } from '../../helpers/db-mock'; import { resetTestDb } from '../../helpers/test-db'; import { createTestOrm, type TestOrm } from '../../helpers/test-orm'; import { createDay, createDayAssignment, createPlace, createTag, createTrip, createUser } from '../../helpers/factories'; import { Tags } from '../../../src/db/entities/Tags.entity'; import type { TagsRepository } from '../../../src/db/repositories/Tags.repository'; import { PlaceRatings } from '../../../src/db/entities/PlaceRatings.entity'; import type { PlaceRatingsRepository } from '../../../src/db/repositories/PlaceRatings.repository'; import { AssignmentParticipants } from '../../../src/db/entities/AssignmentParticipants.entity'; import type { AssignmentParticipantsRepository } from '../../../src/db/repositories/AssignmentParticipants.repository'; import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service'; import { Users } from '../../../src/db/entities/Users.entity'; import { insertRow, updateRows } from '../../helpers/factories/rows'; import { tagPlace } from '../../helpers/factories/places'; const testDb = createSnapshotTestDb(); let t: TestOrm; let svc: QueryHelpersService; beforeAll(async () => { t = await createTestOrm(testDb); const tags: TagsRepository = t.repo(Tags); const placeRatings: PlaceRatingsRepository = t.repo(PlaceRatings); const assignmentParticipants: AssignmentParticipantsRepository = t.repo(AssignmentParticipants); svc = new QueryHelpersService(tags, placeRatings, assignmentParticipants); }); beforeEach(() => { resetTestDb(testDb); t.clear(); }); afterAll(async () => { await t.close(); testDb.close(); }); async function attachTag(tagId: number, placeId: number): Promise { await tagPlace(t, placeId, [tagId]); } async function rate(placeId: number, userId: number, rating: number): Promise { await insertRow(t, PlaceRatings, { place: placeId, user: userId, rating }); } async function addParticipant(assignmentId: number, userId: number): Promise { await insertRow(t, AssignmentParticipants, { assignment: assignmentId, user: userId }); } describe('QueryHelpersService.loadTagsByPlaceIds', () => { it('QH-001: an empty placeIds array yields an empty Record', async () => { expect(await svc.loadTagsByPlaceIds([])).toEqual({}); }); it('QH-002: indexes tags by place id, non-compact carries every column but the join key', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const place = createPlace(testDb, trip.id); const tag = createTag(testDb, user.id, { name: 'Beach', color: '#ff0000' }); await attachTag(tag.id, place.id); const byPlace = await svc.loadTagsByPlaceIds([place.id]); expect(byPlace[place.id]).toHaveLength(1); expect(byPlace[place.id][0]).toMatchObject({ id: tag.id, user_id: user.id, name: 'Beach', color: '#ff0000' }); expect(byPlace[place.id][0]).not.toHaveProperty('place_id'); }); it('QH-003: compact drops user_id', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const place = createPlace(testDb, trip.id); const tag = createTag(testDb, user.id, { name: 'Compact' }); await attachTag(tag.id, place.id); const byPlace = await svc.loadTagsByPlaceIds([place.id], { compact: true }); expect(byPlace[place.id][0]).toEqual({ id: tag.id, name: 'Compact', color: tag.color, created_at: expect.any(String) }); }); it('QH-004: batches across several places, grouping correctly, a place with no tags gets no key', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const placeA = createPlace(testDb, trip.id, { name: 'A' }); const placeB = createPlace(testDb, trip.id, { name: 'B' }); const placeC = createPlace(testDb, trip.id, { name: 'C' }); const tag = createTag(testDb, user.id); await attachTag(tag.id, placeA.id); await attachTag(tag.id, placeB.id); const byPlace = await svc.loadTagsByPlaceIds([placeA.id, placeB.id, placeC.id]); expect(Object.keys(byPlace).map(Number).sort()).toEqual([placeA.id, placeB.id].sort()); expect(byPlace[placeC.id]).toBeUndefined(); }); it('QH-004b: a second tag on the same place appends to the existing array instead of resetting it', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const place = createPlace(testDb, trip.id); const tagA = createTag(testDb, user.id, { name: 'First' }); const tagB = createTag(testDb, user.id, { name: 'Second' }); await attachTag(tagA.id, place.id); await attachTag(tagB.id, place.id); const byPlace = await svc.loadTagsByPlaceIds([place.id]); expect(byPlace[place.id].map((t) => t.name).sort()).toEqual(['First', 'Second']); }); }); describe('QueryHelpersService.loadRatingsByPlaceIds', () => { it('QH-005: an empty placeIds array yields an empty Record', async () => { expect(await svc.loadRatingsByPlaceIds([])).toEqual({}); }); it('QH-006: indexes ratings by place id with the voter\'s username/avatar', async () => { const { user: owner } = createUser(testDb); const { user: voter } = createUser(testDb, { username: 'rater1' }); const trip = createTrip(testDb, owner.id); const place = createPlace(testDb, trip.id); await rate(place.id, voter.id, 4); const byPlace = await svc.loadRatingsByPlaceIds([place.id]); expect(byPlace[place.id]).toEqual([{ user_id: voter.id, username: 'rater1', avatar: null, rating: 4 }]); }); it('QH-007: a place with no ratings gets no key', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id); const place = createPlace(testDb, trip.id); expect(await svc.loadRatingsByPlaceIds([place.id])).toEqual({}); }); it('QH-007b: a second rating on the same place appends to the existing array instead of resetting it', async () => { const { user: owner } = createUser(testDb); const { user: voterA } = createUser(testDb, { username: 'voter-a' }); const { user: voterB } = createUser(testDb, { username: 'voter-b' }); const trip = createTrip(testDb, owner.id); const place = createPlace(testDb, trip.id); await rate(place.id, voterA.id, 5); await rate(place.id, voterB.id, 2); const byPlace = await svc.loadRatingsByPlaceIds([place.id]); expect(byPlace[place.id].map((r) => r.username).sort()).toEqual(['voter-a', 'voter-b']); }); }); describe('QueryHelpersService.loadParticipantsByAssignmentIds', () => { it('QH-008: an empty assignmentIds array yields an empty Record', async () => { expect(await svc.loadParticipantsByAssignmentIds([])).toEqual({}); }); it('QH-009: indexes participants by assignment id, with NO COALESCE(display_name, username) — raw username only', async () => { const { user: owner } = createUser(testDb); const { user: participant } = createUser(testDb, { username: 'raw-username' }); await updateRows(t, Users, { id: participant.id }, { display_name: 'Ignored Display Name' }); const trip = createTrip(testDb, owner.id); const day = createDay(testDb, trip.id); const place = createPlace(testDb, trip.id); const assignment = createDayAssignment(testDb, day.id, place.id); await addParticipant(assignment.id, participant.id); const byAssignment = await svc.loadParticipantsByAssignmentIds([assignment.id]); expect(byAssignment[assignment.id]).toEqual([{ user_id: participant.id, username: 'raw-username', avatar: null }]); }); it('QH-010: an assignment with no participants gets no key', async () => { const { user: owner } = createUser(testDb); const trip = createTrip(testDb, owner.id); const day = createDay(testDb, trip.id); const place = createPlace(testDb, trip.id); const assignment = createDayAssignment(testDb, day.id, place.id); expect(await svc.loadParticipantsByAssignmentIds([assignment.id])).toEqual({}); }); it('QH-010b: a second participant on the same assignment appends to the existing array instead of resetting it', async () => { const { user: owner } = createUser(testDb); const { user: p1 } = createUser(testDb, { username: 'p1' }); const { user: p2 } = createUser(testDb, { username: 'p2' }); const trip = createTrip(testDb, owner.id); const day = createDay(testDb, trip.id); const place = createPlace(testDb, trip.id); const assignment = createDayAssignment(testDb, day.id, place.id); await addParticipant(assignment.id, p1.id); await addParticipant(assignment.id, p2.id); const byAssignment = await svc.loadParticipantsByAssignmentIds([assignment.id]); expect(byAssignment[assignment.id].map((p) => p.username).sort()).toEqual(['p1', 'p2']); }); });