Files

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')
})
})