Files
TREK/server/tests/unit/nest/query-helpers.service.test.ts
T
Maurice e3982a8e2a test(server): seed the repository, MCP and e2e specs through the factories
Converts the raw prepare() fixtures and reads in the next set of specs to
the ORM factories and readers: the password reset, webauthn, dawarich,
budget, to-do and vacay repository specs, the journey, settings, addons and
share MCP specs, the plugin POI and operator egress specs, the feeds,
storage stats and query helper services, and the packing, collections,
day reorder, leg mode, upload context and trip access integration suites.

The notifications and google quota e2e suites now seed their users through
makeUser with pinned ids, and the notifications suite moves from hand-rolled
tables to the migrated snapshot. Legacy statements that serve as a parity
oracle and the raw rows the primary-relation spike holds toObject() against
stay raw, each with an allow marker naming why.
2026-10-08 18:46:47 +02:00

187 lines
9.0 KiB
TypeScript

/**
* 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<void> {
await tagPlace(t, placeId, [tagId]);
}
async function rate(placeId: number, userId: number, rating: number): Promise<void> {
await insertRow(t, PlaceRatings, { place: placeId, user: userId, rating });
}
async function addParticipant(assignmentId: number, userId: number): Promise<void> {
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']);
});
});