312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
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'])
|
|
})
|
|
})
|