import { test } from '@japa/runner' import testUtils from '@adonisjs/core/services/test_utils' import WebManagerService from '#base_modules/WebManager/services/web_manager_service' import { createAdminWithToken } from '#tests/utils/auth' import { contentComponent, pageInput } from '#tests/utils/web_manager' const ALL_PERMISSIONS = [ 'web_manager.view', 'web_manager.create', 'web_manager.update', 'web_manager.publish', 'web_manager.delete', 'web_manager.settings', ] /** [method, path, permission the action authorizes against] */ const ADMIN_ROUTES = [ ['get', '/web-manager/pages', 'web_manager.view'], ['get', '/web-manager/pages/group-keys', 'web_manager.view'], ['post', '/web-manager/pages', 'web_manager.create'], ['get', '/web-manager/pages/1', 'web_manager.view'], ['put', '/web-manager/pages/1/draft', 'web_manager.update'], ['post', '/web-manager/pages/1/publish', 'web_manager.publish'], ['post', '/web-manager/pages/1/unpublish', 'web_manager.publish'], ['post', '/web-manager/pages/1/preview-token', 'web_manager.view'], ['delete', '/web-manager/pages/1', 'web_manager.delete'], ['get', '/web-manager/settings', 'web_manager.view'], ['put', '/web-manager/settings', 'web_manager.settings'], ] as const test.group('WebManager admin API auth', (group) => { group.each.setup(() => testUtils.db().wrapInGlobalTransaction()) test('every admin route rejects an unauthenticated request', async ({ client }) => { for (const [method, url] of ADMIN_ROUTES) { const response = await client[method](url) response.assertStatus(401) } }) test('every action rejects a token that lacks its permission', async ({ client }) => { // holds every permission except the one each action checks for (const [method, url, permission] of ADMIN_ROUTES) { const { token } = await createAdminWithToken( ALL_PERMISSIONS.filter((slug) => slug !== permission) ) const response = await client[method](url).bearerToken(token) response.assertStatus(403) } }) test('a viewer can read but not mutate', async ({ client }) => { const { token } = await createAdminWithToken(['web_manager.view']) const list = await client.get('/web-manager/pages').bearerToken(token) list.assertStatus(200) const create = await client.post('/web-manager/pages').bearerToken(token).json(pageInput()) create.assertStatus(403) }) }) test.group('WebManager admin API', (group) => { group.each.setup(() => testUtils.db().wrapInGlobalTransaction()) test('create → publish → unpublish round trip reports revalidation', async ({ assert, client, }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const created = await client .post('/web-manager/pages') .bearerToken(token) .json(pageInput({ components: [contentComponent()] })) created.assertStatus(201) const pageId = created.body().data.page.id assert.equal(created.body().data.page.status, 'draft') const published = await client.post(`/web-manager/pages/${pageId}/publish`).bearerToken(token) published.assertStatus(200) assert.equal(published.body().data.page.status, 'published') assert.equal(published.body().meta.revalidation.status, 'disabled') assert.isNotEmpty(published.body().meta.revalidation.tags) const unpublished = await client .post(`/web-manager/pages/${pageId}/unpublish`) .bearerToken(token) unpublished.assertStatus(200) assert.equal(unpublished.body().data.page.status, 'unpublished') const again = await client.post(`/web-manager/pages/${pageId}/unpublish`).bearerToken(token) again.assertStatus(409) assert.equal(again.body().code, 'E_PAGE_NOT_PUBLISHED') }) test('page list filters by status and search', async ({ assert, client }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const service = new WebManagerService() const { page: draft } = await service.createDraft(pageInput({ title: 'Needle draft' })) const { page: live } = await service.createDraft(pageInput({ title: 'Other page' })) await service.publish(live.id) const drafts = await client .get('/web-manager/pages') .qs({ status: 'draft', search: 'needle' }) .bearerToken(token) drafts.assertStatus(200) assert.deepEqual( drafts.body().data.map((row: { id: number }) => row.id), [draft.id] ) }) test('validation rejects a malformed path with 422', async ({ client }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const response = await client .post('/web-manager/pages') .bearerToken(token) .json(pageInput({ path: 'About/Us' })) response.assertStatus(422) }) test('an unknown block type is rejected with 422', async ({ assert, client }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const response = await client .post('/web-manager/pages') .bearerToken(token) .json(pageInput({ components: [{ type: 'carousel', data: {} }] })) response.assertStatus(422) assert.equal(response.body().code, 'E_INVALID_PAGE_PAYLOAD') }) test('a multipart payload field is parsed as JSON', async ({ assert, client }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const response = await client .post('/web-manager/pages') .bearerToken(token) .field('payload', JSON.stringify(pageInput({ components: [contentComponent()] }))) response.assertStatus(201) assert.equal(response.body().data.components.length, 1) }) test('empty strings survive the multipart payload but not a JSON body', async ({ assert, client, }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const input = pageInput({ components: [ { type: 'content', data: { title: 'Section', subtitle: '', columns: [] }, }, ], }) // The JSON bodyparser converts '' to null, so the strict block validator // rejects it — this is why the admin sends the page as one JSON string. const asJson = await client.post('/web-manager/pages').bearerToken(token).json(input) asJson.assertStatus(422) assert.equal(asJson.body().code, 'E_INVALID_PAGE_PAYLOAD') const asPayload = await client .post('/web-manager/pages') .bearerToken(token) .field('payload', JSON.stringify(input)) asPayload.assertStatus(201) }) test('a malformed multipart payload is rejected with 422', async ({ assert, client }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const response = await client .post('/web-manager/pages') .bearerToken(token) .field('payload', 'not-json') response.assertStatus(422) assert.equal(response.body().code, 'E_INVALID_PAGE_PAYLOAD') }) test('a media value referencing a missing upload is rejected with 422', async ({ assert, client, }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const input = pageInput({ hero: { type: 'middle', ctaItems: [], media: { name: 'x.png', fileKey: 'file-0' } }, }) const response = await client .post('/web-manager/pages') .bearerToken(token) .field('payload', JSON.stringify(input)) response.assertStatus(422) assert.equal(response.body().code, 'E_PAGE_MEDIA_FILE_MISSING') }) test('settings upsert requires its own permission and reports revalidation', async ({ assert, client, }) => { const settings = { locale: 'en', header: { menus: [], ctaItems: [] }, footer: { copyright: '© Spec', menus: [] }, legal: { privacyPath: '/privacy', termsPath: '/terms' }, } const { token: viewer } = await createAdminWithToken(['web_manager.view']) const denied = await client.put('/web-manager/settings').bearerToken(viewer).json(settings) denied.assertStatus(403) const { token } = await createAdminWithToken(ALL_PERMISSIONS) const saved = await client.put('/web-manager/settings').bearerToken(token).json(settings) saved.assertStatus(200) assert.equal(saved.body().meta.revalidation.status, 'disabled') assert.deepEqual(saved.body().meta.revalidation.tags, ['web-manager:settings:en']) }) }) test.group('WebManager public API', (group) => { group.each.setup(() => testUtils.db().wrapInGlobalTransaction()) test('resolve serves published pages only', async ({ assert, client }) => { const service = new WebManagerService() const { page } = await service.createDraft(pageInput({ title: 'Public page' })) const asDraft = await client .get('/web-manager/public/pages/resolve') .qs({ locale: 'en', path: page.path }) asDraft.assertStatus(404) await service.publish(page.id) const asPublished = await client .get('/web-manager/public/pages/resolve') .qs({ locale: 'en', path: page.path }) asPublished.assertStatus(200) assert.equal(asPublished.body().page.title, 'Public page') assert.equal(asPublished.body().page.status, 'published') assert.isArray(asPublished.body().breadcrumbs) assert.isArray(asPublished.body().languageAlternatives) }) test('preview serves a draft through a signed token and rejects tampering', async ({ assert, client, }) => { const { token } = await createAdminWithToken(ALL_PERMISSIONS) const service = new WebManagerService() const { page } = await service.createDraft(pageInput({ title: 'Draft preview' })) const minted = await client .post(`/web-manager/pages/${page.id}/preview-token`) .bearerToken(token) .json({}) minted.assertStatus(201) const previewToken: string = minted.body().data.token const preview = await client .get('/web-manager/public/pages/preview') .qs({ token: previewToken }) preview.assertStatus(200) assert.equal(preview.body().page.title, 'Draft preview') assert.equal(preview.body().page.status, 'draft') const tampered = await client .get('/web-manager/public/pages/preview') .qs({ token: `${previewToken}x` }) tampered.assertStatus(404) }) test('sitemap lists published root pages with pagination', async ({ assert, client }) => { const service = new WebManagerService() const { page } = await service.createDraft(pageInput()) await service.publish(page.id) await service.createDraft(pageInput()) // stays draft, must not appear const response = await client.get('/web-manager/public/pages/sitemap') response.assertStatus(200) const items = response.body().items as Array<{ id: number }> assert.deepEqual( items.map((item) => item.id), [page.id] ) assert.properties(response.body().pagination, ['page', 'perPage', 'total', 'lastPage']) }) test('blog listing returns published blog items, newest first', async ({ assert, client }) => { const service = new WebManagerService() const { page: older } = await service.createDraft( pageInput({ pageType: 'blog-item', title: 'Older post' }) ) await service.publish(older.id) const { page: newer } = await service.createDraft( pageInput({ pageType: 'blog-item', title: 'Newer post' }) ) await service.publish(newer.id) const { page: landing } = await service.createDraft(pageInput({ title: 'Not a post' })) await service.publish(landing.id) const response = await client.get('/web-manager/public/blog/pages').qs({ locale: 'en' }) response.assertStatus(200) const titles = (response.body().items as Array<{ title: string }>).map((item) => item.title) assert.deepEqual(titles, ['Newer post', 'Older post']) }) })