298 lines
11 KiB
TypeScript
298 lines
11 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 WebPage from '#base_modules/WebManager/model/web_page'
|
|
import WebComponent from '#base_modules/WebManager/model/web_component'
|
|
import { WEB_MANAGER_CACHE_TAG } from '#base_modules/WebManager/services/web_revalidation_service'
|
|
import {
|
|
PageNotPublishedException,
|
|
PageRouteConflictException,
|
|
} from '#exceptions/web_manager_exceptions'
|
|
import { bannerComponent, contentComponent, pageInput } from '#tests/utils/web_manager'
|
|
|
|
/**
|
|
* Lifecycle specs for the draft → published → unpublished state machine.
|
|
*
|
|
* `.env.test` leaves WEB_REVALIDATE_SECRET empty, so revalidation reports
|
|
* `disabled` but still returns the tags it would have purged — the assertions
|
|
* below use those tags to tell a full-site purge from a targeted one.
|
|
*/
|
|
test.group('WebManager page lifecycle', (group) => {
|
|
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
|
|
|
|
const service = () => new WebManagerService()
|
|
|
|
test('createDraft stores a root draft with components in submitted order', async ({ assert }) => {
|
|
const { page } = await service().createDraft(
|
|
pageInput({
|
|
path: 'about-us',
|
|
components: [contentComponent('first'), bannerComponent('second')],
|
|
})
|
|
)
|
|
|
|
const row = await WebPage.findOrFail(page.id)
|
|
assert.equal(row.status, 'draft')
|
|
assert.isNull(row.draftOfId)
|
|
assert.isNull(row.publishedAt)
|
|
|
|
const components = await WebComponent.query().where('page_id', row.id).orderBy('sort_order')
|
|
assert.deepEqual(
|
|
components.map((component) => [component.type, component.sortOrder]),
|
|
[
|
|
['content', 0],
|
|
['banner', 1],
|
|
]
|
|
)
|
|
})
|
|
|
|
test('createDraft normalizes the path before storing it', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput({ path: '/about//team/' }))
|
|
assert.equal(page.path, 'about/team')
|
|
})
|
|
|
|
test('createDraft rejects a route held by any root row, whatever its status', async ({
|
|
assert,
|
|
}) => {
|
|
await service().createDraft(pageInput({ path: 'taken', groupKey: 'taken-one' }))
|
|
|
|
await assert.rejects(
|
|
() => service().createDraft(pageInput({ path: 'taken', groupKey: 'taken-two' })),
|
|
PageRouteConflictException
|
|
)
|
|
})
|
|
|
|
test('saveDraft edits a draft in place and replaces components wholesale', async ({ assert }) => {
|
|
const { page } = await service().createDraft(
|
|
pageInput({ components: [contentComponent(), bannerComponent()] })
|
|
)
|
|
|
|
const { page: saved } = await service().saveDraft(
|
|
page.id,
|
|
pageInput({
|
|
path: page.path,
|
|
groupKey: page.groupKey,
|
|
title: 'Renamed',
|
|
components: [bannerComponent('only one now')],
|
|
})
|
|
)
|
|
|
|
assert.equal(saved.id, page.id)
|
|
assert.equal(saved.title, 'Renamed')
|
|
assert.lengthOf(await WebPage.query(), 1, 'no clone was created for an unpublished draft')
|
|
|
|
const components = await WebComponent.query().where('page_id', page.id)
|
|
assert.lengthOf(components, 1)
|
|
assert.equal(components[0].type, 'banner')
|
|
assert.equal(components[0].sortOrder, 0)
|
|
})
|
|
|
|
test('saveDraft on a published page creates one clone and reuses it', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput({ components: [contentComponent()] }))
|
|
await service().publish(page.id)
|
|
|
|
const input = pageInput({
|
|
path: page.path,
|
|
groupKey: page.groupKey,
|
|
title: 'Edited copy',
|
|
components: [bannerComponent()],
|
|
})
|
|
const { page: firstClone } = await service().saveDraft(page.id, input)
|
|
|
|
assert.notEqual(firstClone.id, page.id)
|
|
assert.equal(firstClone.draftOfId, page.id)
|
|
assert.equal(firstClone.status, 'draft')
|
|
|
|
const original = await WebPage.findOrFail(page.id)
|
|
assert.equal(original.status, 'published')
|
|
assert.equal(original.title, 'Spec page')
|
|
|
|
const { page: secondClone } = await service().saveDraft(page.id, input)
|
|
assert.equal(secondClone.id, firstClone.id)
|
|
|
|
const clones = await WebPage.query().where('draft_of_id', page.id)
|
|
assert.lengthOf(clones, 1)
|
|
})
|
|
|
|
test('publish flips a root draft live and purges the whole site', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput())
|
|
|
|
const result = await service().publish(page.id)
|
|
|
|
assert.equal(result.page.page.status, 'published')
|
|
assert.isNotNull(result.page.page.publishedAt)
|
|
assert.equal(result.revalidation.status, 'disabled')
|
|
assert.deepEqual(result.revalidation.tags, [WEB_MANAGER_CACHE_TAG])
|
|
})
|
|
|
|
test('publish merges a pending clone; a content-only change purges only page tags', async ({
|
|
assert,
|
|
}) => {
|
|
const { page } = await service().createDraft(pageInput({ components: [contentComponent()] }))
|
|
await service().publish(page.id)
|
|
|
|
const { page: clone } = await service().saveDraft(
|
|
page.id,
|
|
pageInput({
|
|
path: page.path,
|
|
groupKey: page.groupKey,
|
|
title: page.title,
|
|
components: [bannerComponent('new content')],
|
|
})
|
|
)
|
|
|
|
const result = await service().publish(page.id)
|
|
|
|
assert.notInclude(result.revalidation.tags, WEB_MANAGER_CACHE_TAG)
|
|
assert.include(result.revalidation.tags, `${WEB_MANAGER_CACHE_TAG}:page:en:${page.path}`)
|
|
|
|
assert.isNull(await WebPage.find(clone.id))
|
|
const components = await WebComponent.query().where('page_id', page.id)
|
|
assert.lengthOf(components, 1)
|
|
assert.equal(components[0].type, 'banner')
|
|
})
|
|
|
|
test('publish treats a changed title as structural and purges the whole site', async ({
|
|
assert,
|
|
}) => {
|
|
const { page } = await service().createDraft(pageInput())
|
|
await service().publish(page.id)
|
|
|
|
await service().saveDraft(
|
|
page.id,
|
|
pageInput({ path: page.path, groupKey: page.groupKey, title: 'New title' })
|
|
)
|
|
const result = await service().publish(page.id)
|
|
|
|
assert.deepEqual(result.revalidation.tags, [WEB_MANAGER_CACHE_TAG])
|
|
assert.equal(result.page.page.title, 'New title')
|
|
})
|
|
|
|
test('publishing an already-published page with no clone only re-purges its cache', async ({
|
|
assert,
|
|
}) => {
|
|
const { page } = await service().createDraft(pageInput())
|
|
const first = await service().publish(page.id)
|
|
|
|
const again = await service().publish(page.id)
|
|
|
|
assert.equal(again.page.page.publishedAt, first.page.page.publishedAt)
|
|
assert.notInclude(again.revalidation.tags, WEB_MANAGER_CACHE_TAG)
|
|
assert.include(again.revalidation.tags, `${WEB_MANAGER_CACHE_TAG}:page:en:${page.path}`)
|
|
})
|
|
|
|
test('publish refuses when another published page holds the address', async ({ assert }) => {
|
|
// Two root rows on the same route can only exist when one is not published
|
|
// (assertNewRouteAvailable blocks the service path), so build them directly.
|
|
await WebPage.create({
|
|
status: 'published',
|
|
draftOfId: null,
|
|
locale: 'en',
|
|
path: 'clash',
|
|
groupKey: 'clash-live',
|
|
pageType: 'landing',
|
|
title: 'Live page',
|
|
hero: null,
|
|
seoTitle: '',
|
|
seoDescription: '',
|
|
})
|
|
const contender = await WebPage.create({
|
|
status: 'draft',
|
|
draftOfId: null,
|
|
locale: 'en',
|
|
path: 'clash',
|
|
groupKey: 'clash-draft',
|
|
pageType: 'landing',
|
|
title: 'Contender',
|
|
hero: null,
|
|
seoTitle: '',
|
|
seoDescription: '',
|
|
})
|
|
|
|
await assert.rejects(() => service().publish(contender.id), PageRouteConflictException)
|
|
})
|
|
|
|
test('unpublish takes the page down but keeps content, date, and address', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput({ components: [contentComponent()] }))
|
|
const published = await service().publish(page.id)
|
|
|
|
const result = await service().unpublish(page.id)
|
|
|
|
assert.equal(result.page.page.status, 'unpublished')
|
|
assert.equal(result.page.page.publishedAt, published.page.page.publishedAt)
|
|
assert.deepEqual(result.revalidation.tags, [WEB_MANAGER_CACHE_TAG])
|
|
assert.lengthOf(await WebComponent.query().where('page_id', page.id), 1)
|
|
|
|
// the address stays reserved for the unpublished page
|
|
await assert.rejects(
|
|
() => service().createDraft(pageInput({ path: page.path, groupKey: 'squatter' })),
|
|
PageRouteConflictException
|
|
)
|
|
})
|
|
|
|
test('unpublish rejects anything that is not currently published', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput())
|
|
await assert.rejects(() => service().unpublish(page.id), PageNotPublishedException)
|
|
})
|
|
|
|
test('an unpublished page is edited in place and can be republished', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput())
|
|
await service().publish(page.id)
|
|
await service().unpublish(page.id)
|
|
|
|
const { page: edited } = await service().saveDraft(
|
|
page.id,
|
|
pageInput({ path: page.path, groupKey: page.groupKey, title: 'Back soon' })
|
|
)
|
|
assert.equal(edited.id, page.id, 'no clone for a page that is not serving')
|
|
|
|
const republished = await service().publish(page.id)
|
|
assert.equal(republished.page.page.status, 'published')
|
|
assert.deepEqual(republished.revalidation.tags, [WEB_MANAGER_CACHE_TAG])
|
|
})
|
|
|
|
test('discarding a clone leaves the published original untouched', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput({ components: [contentComponent()] }))
|
|
await service().publish(page.id)
|
|
const { page: clone } = await service().saveDraft(
|
|
page.id,
|
|
pageInput({ path: page.path, groupKey: page.groupKey, title: 'Scrapped edit' })
|
|
)
|
|
|
|
await service().discard(clone.id)
|
|
|
|
assert.isNull(await WebPage.find(clone.id))
|
|
const original = await WebPage.findOrFail(page.id)
|
|
assert.equal(original.status, 'published')
|
|
assert.lengthOf(await WebComponent.query().where('page_id', page.id), 1)
|
|
})
|
|
|
|
test('deleting a published page cascades its clone and components', async ({ assert }) => {
|
|
const { page } = await service().createDraft(pageInput({ components: [contentComponent()] }))
|
|
await service().publish(page.id)
|
|
const { page: clone } = await service().saveDraft(
|
|
page.id,
|
|
pageInput({ path: page.path, groupKey: page.groupKey, title: 'Open draft' })
|
|
)
|
|
|
|
await service().discard(page.id)
|
|
|
|
assert.isNull(await WebPage.find(page.id))
|
|
assert.isNull(await WebPage.find(clone.id))
|
|
assert.lengthOf(await WebComponent.query().whereIn('page_id', [page.id, clone.id]), 0)
|
|
})
|
|
|
|
test('resolvePublicPage serves published pages only and normalizes the path', async ({
|
|
assert,
|
|
}) => {
|
|
const { page } = await service().createDraft(pageInput({ title: 'Public page' }))
|
|
assert.isNull(await service().resolvePublicPage('en', page.path))
|
|
|
|
await service().publish(page.id)
|
|
|
|
const resolved = await service().resolvePublicPage('en', `/${page.path}/`)
|
|
assert.isNotNull(resolved)
|
|
assert.equal(resolved!.page.title, 'Public page')
|
|
assert.equal(resolved!.page.seo.title, 'Public page', 'seo title falls back to the page title')
|
|
})
|
|
})
|