test(categories): integration tests for layered category ordering (#782)
Real-DB coverage: migration 158 backfill; global default reorder + a non-customised event following it; per-event override + isolation from other events; override accepts globals / rejects a foreign event's category; reset clears the override; create appends.
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Layered per-event category ordering (#782).
|
||||||
|
*
|
||||||
|
* Two ordering layers, resolved per event:
|
||||||
|
* - GLOBAL default — photo_categories.display_order (migration 158),
|
||||||
|
* set via POST /reorder-global; applies everywhere.
|
||||||
|
* - PER-EVENT override — event_category_order (migration 159), set via
|
||||||
|
* POST /reorder; overrides the default for one gallery.
|
||||||
|
* - DELETE /reorder/:eventId clears an event's override.
|
||||||
|
*
|
||||||
|
* Verified against a real SQLite DB with the full core-migration set applied.
|
||||||
|
*/
|
||||||
|
const request = require('supertest');
|
||||||
|
const {
|
||||||
|
bootCrmDb, seedMinimal, assignAdminRole, mintAdminToken, buildRouteApp,
|
||||||
|
} = require('./helpers/crmDb');
|
||||||
|
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
|
||||||
|
describe('category ordering (#782)', () => {
|
||||||
|
let db;
|
||||||
|
let cleanup;
|
||||||
|
let token;
|
||||||
|
let app;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
({ db, cleanup } = await bootCrmDb());
|
||||||
|
const { adminId } = await seedMinimal(db);
|
||||||
|
await assignAdminRole(db, adminId, 'super_admin');
|
||||||
|
token = mintAdminToken(adminId);
|
||||||
|
app = buildRouteApp('/api/admin/categories', require('../../src/routes/adminCategories'));
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
afterAll(async () => { if (cleanup) await cleanup(); });
|
||||||
|
|
||||||
|
const auth = (r) => r.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
async function insertEvent(slug) {
|
||||||
|
await db('events').insert({
|
||||||
|
event_type: 'wedding', password_hash: 'x',
|
||||||
|
expires_at: new Date(Date.now() + 9e9).toISOString(),
|
||||||
|
is_active: true, is_archived: false, slug, share_link: slug,
|
||||||
|
event_name: slug, event_date: '2026-01-01',
|
||||||
|
});
|
||||||
|
return (await db('events').where({ slug }).first()).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertCat(name, { is_global = false, event_id = null, display_order = 0 } = {}) {
|
||||||
|
const res = await db('photo_categories').insert({
|
||||||
|
name,
|
||||||
|
slug: name.toLowerCase().replace(/\s+/g, '-'),
|
||||||
|
is_global: is_global ? 1 : 0,
|
||||||
|
event_id,
|
||||||
|
display_order,
|
||||||
|
}).returning('id');
|
||||||
|
return res[0]?.id ?? res[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEvent = (eventId) => auth(request(app).get(`/api/admin/categories/event/${eventId}`)).expect(200);
|
||||||
|
|
||||||
|
describe('migration 158 backfill', () => {
|
||||||
|
it('seeds display_order from alphabetical order, scoped per event', async () => {
|
||||||
|
const eventId = await insertEvent('backfill-ev');
|
||||||
|
await insertCat('Reception', { event_id: eventId });
|
||||||
|
await insertCat('Ceremony', { event_id: eventId });
|
||||||
|
await insertCat('Pre-Ceremony', { event_id: eventId });
|
||||||
|
|
||||||
|
// Re-run the migration: addColumn is guarded (no-op); the backfill loop
|
||||||
|
// re-runs and assigns per-scope alphabetical order — what an upgrade does.
|
||||||
|
await require('../../migrations/core/158_add_category_display_order').up(db);
|
||||||
|
|
||||||
|
const evCats = await db('photo_categories').where({ event_id: eventId }).orderBy('display_order', 'asc');
|
||||||
|
expect(evCats.map((c) => c.name)).toEqual(['Ceremony', 'Pre-Ceremony', 'Reception']);
|
||||||
|
expect(evCats.map((c) => c.display_order)).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('global default order (POST /reorder-global)', () => {
|
||||||
|
it('reverses the global order and every non-customised event follows it', async () => {
|
||||||
|
const before = (await auth(request(app).get('/api/admin/categories/global')).expect(200)).body;
|
||||||
|
expect(before.length).toBeGreaterThan(1);
|
||||||
|
const reversedIds = before.map((c) => c.id).reverse();
|
||||||
|
|
||||||
|
const res = await auth(request(app).post('/api/admin/categories/reorder-global'))
|
||||||
|
.send({ orderedIds: reversedIds })
|
||||||
|
.expect(200);
|
||||||
|
expect(res.body.map((c) => c.id)).toEqual(reversedIds);
|
||||||
|
|
||||||
|
// A fresh event (no override) shows globals in the new global order.
|
||||||
|
const eventId = await insertEvent('follows-global');
|
||||||
|
const globalsInEvent = (await getEvent(eventId)).body.filter((c) => c.is_global).map((c) => c.id);
|
||||||
|
expect(globalsInEvent).toEqual(reversedIds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('per-event override (POST /reorder)', () => {
|
||||||
|
it('pins a custom order for one event without affecting another', async () => {
|
||||||
|
const eventA = await insertEvent('override-a');
|
||||||
|
const eventB = await insertEvent('override-b');
|
||||||
|
const a1 = await insertCat('A-Ceremony', { event_id: eventA });
|
||||||
|
const a2 = await insertCat('A-Reception', { event_id: eventA });
|
||||||
|
|
||||||
|
// Current resolved list for A (globals + A's two categories).
|
||||||
|
const listA = (await getEvent(eventA)).body;
|
||||||
|
// Put A-Reception first, then A-Ceremony, then the globals in their order.
|
||||||
|
const globalsA = listA.filter((c) => c.is_global).map((c) => c.id);
|
||||||
|
const desired = [a2, a1, ...globalsA];
|
||||||
|
|
||||||
|
const res = await auth(request(app).post('/api/admin/categories/reorder'))
|
||||||
|
.send({ event_id: eventA, orderedIds: desired })
|
||||||
|
.expect(200);
|
||||||
|
expect(res.body.map((c) => c.id)).toEqual(desired);
|
||||||
|
// override_position is set on every row for a customised event.
|
||||||
|
expect(res.body.every((c) => c.override_position != null)).toBe(true);
|
||||||
|
|
||||||
|
// Event B is untouched — no override, follows the global default.
|
||||||
|
const listB = (await getEvent(eventB)).body;
|
||||||
|
expect(listB.every((c) => c.override_position == null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts global ids but rejects another event’s category', async () => {
|
||||||
|
const eventId = await insertEvent('scope-ev');
|
||||||
|
const own = await insertCat('Own', { event_id: eventId });
|
||||||
|
const global = (await db('photo_categories').where('is_global', 1).first()).id;
|
||||||
|
const foreign = await insertCat('Foreign', { event_id: await insertEvent('other-ev') });
|
||||||
|
|
||||||
|
// A global id is allowed (globals can be arranged per event).
|
||||||
|
await auth(request(app).post('/api/admin/categories/reorder'))
|
||||||
|
.send({ event_id: eventId, orderedIds: [own, global] })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
// A foreign event's category is out of scope.
|
||||||
|
await auth(request(app).post('/api/admin/categories/reorder'))
|
||||||
|
.send({ event_id: eventId, orderedIds: [own, foreign] })
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset (DELETE /reorder/:eventId)', () => {
|
||||||
|
it('clears the override and reverts to the global default', async () => {
|
||||||
|
const eventId = await insertEvent('reset-ev');
|
||||||
|
const c1 = await insertCat('R-One', { event_id: eventId });
|
||||||
|
const list = (await getEvent(eventId)).body;
|
||||||
|
const globals = list.filter((c) => c.is_global).map((c) => c.id);
|
||||||
|
|
||||||
|
await auth(request(app).post('/api/admin/categories/reorder'))
|
||||||
|
.send({ event_id: eventId, orderedIds: [c1, ...globals] })
|
||||||
|
.expect(200);
|
||||||
|
expect((await getEvent(eventId)).body.some((c) => c.override_position != null)).toBe(true);
|
||||||
|
|
||||||
|
const res = await auth(request(app).delete(`/api/admin/categories/reorder/${eventId}`)).expect(200);
|
||||||
|
expect(res.body.every((c) => c.override_position == null)).toBe(true);
|
||||||
|
expect(await db('event_category_order').where({ event_id: eventId }).first()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST / (create) appends to the end of its scope', () => {
|
||||||
|
it('assigns display_order = max + 1 within the event', async () => {
|
||||||
|
const eventId = await insertEvent('append-ev');
|
||||||
|
await insertCat('First', { event_id: eventId, display_order: 1 });
|
||||||
|
await insertCat('Second', { event_id: eventId, display_order: 2 });
|
||||||
|
|
||||||
|
const res = await auth(request(app).post('/api/admin/categories'))
|
||||||
|
.send({ name: 'Third', is_global: false, event_id: eventId })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(res.body.display_order).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user