// Structural lock for src/api/endpoints.js — the single source of truth // for every backend URL. Deliberately checks SHAPE (string-vs-function), // not exact path templates (those are pinned by taskService.test.js and // authService.spec.js). Add a new entity here when you add a new key. import { describe, it, expect } from 'vitest' import { apiEndpoints } from '@/api/endpoints.js' describe('apiEndpoints top-level shape', () => { it('exposes AUTH, STUDENTS, SUBJECTS, TERMS, STORIES, TASKS', () => { expect(Object.keys(apiEndpoints).sort()).toEqual( ['AUTH', 'STORIES', 'STUDENTS', 'SUBJECTS', 'TASKS', 'TERMS'], ) }) it('AUTH endpoints are plain /api/ strings', () => { expect(typeof apiEndpoints.AUTH.LOGIN).toBe('string') expect(typeof apiEndpoints.AUTH.REFRESH).toBe('string') expect(apiEndpoints.AUTH.LOGIN).toMatch(/^\/api\//) expect(apiEndpoints.AUTH.REFRESH).toMatch(/^\/api\//) }) }) describe('apiEndpoints entity resources', () => { // Walk each top-level entity recursively: every leaf must be a string // starting with `/api/`, or a function returning such a string. Nested // groups (TASKS.IMAGES) are recursed into. it.each(['STUDENTS', 'SUBJECTS', 'TERMS', 'STORIES', 'TASKS'])( '%s: every leaf is a /api/ string or a function returning one', (entity) => { const walk = (node, path = entity) => { for (const [k, v] of Object.entries(node)) { const here = `${path}.${k}` if (typeof v === 'string') { expect(v, here).toMatch(/^\/api\//) } else if (typeof v === 'function') { const sample = v(1, 2) expect(typeof sample, here).toBe('string') expect(sample, here).toMatch(/^\/api\//) } else if (v && typeof v === 'object') { walk(v, here) } else { throw new Error(`unexpected leaf at ${here}: ${typeof v}`) } } } walk(apiEndpoints[entity]) }, ) it('TASKS.IMAGES exposes CONTENT, CREATE, DELETE, LIST', () => { expect(Object.keys(apiEndpoints.TASKS.IMAGES).sort()).toEqual( ['CONTENT', 'CREATE', 'DELETE', 'LIST'], ) }) it('TASKS.IMAGES.CONTENT(taskId, imageId) produces the /content/ blob URL', () => { expect(apiEndpoints.TASKS.IMAGES.CONTENT(7, 42)).toBe( '/api/tasks/7/images/42/content/', ) }) it('TASKS.IMAGES.DELETE(taskId, imageId) embeds both ids', () => { expect(apiEndpoints.TASKS.IMAGES.DELETE(7, 42)).toBe( '/api/tasks/7/images/42/', ) }) })