feat: signed releases, release feed, notification (read-only)

This commit is contained in:
fsylvain committed 2026-10-01 15:01:53 +02:00
1 parent a49eec909c
commit 834493a4f5
49 files changed
+4191 -18

No files matched your search

+134 -4
View File
@@ -164,6 +164,65 @@ const verifiedSession: ApiMock = {
},
}
const updateRunningVersion = 'v2.2.0-beta'
const updateLatestTag = 'v2.3.0'
const updateReleaseUrl = 'https://git.floriansylvain.fr/fsylvain/RenewCMS/releases/tag/v2.3.0'
const updateReleasedAt = '2026-09-24T08:00:00Z'
const adminSession: ApiMock = {
status: 200,
body: { ...(verifiedSession.body as object), role: 'admin' },
}
function updateStatusBody(overrides: Record<string, unknown> = {}) {
return {
current: updateRunningVersion,
latest: updateRunningVersion,
available: false,
release_url: updateReleaseUrl,
notes: { version: updateRunningVersion, released_at: updateReleasedAt, items: [] },
checked_at: new Date().toISOString(),
can_apply: false,
state: 'idle',
progress: null,
error: null,
started_at: null,
...overrides,
}
}
function updateNeverChecked() {
return updateStatusBody({
latest: '',
release_url: null,
notes: null,
checked_at: null,
})
}
function updateAvailable() {
return updateStatusBody({
latest: updateLatestTag,
available: true,
notes: { version: updateLatestTag, released_at: updateReleasedAt, items: [] },
})
}
const updateCheckHangs: ApiMock = { status: 200, hang: true }
function updateApi(
role: 'owner' | 'admin',
status: unknown,
check: ApiMock = { status: 200, body: updateStatusBody() },
): Record<string, ApiMock> {
return {
'GET /v1/auth/me': role === 'owner' ? verifiedSession : adminSession,
'GET /v1/settings/': { status: 200, body: { version: updateRunningVersion } },
'GET /v1/system/update': { status: 200, body: status },
'POST /v1/system/update/check': check,
}
}
const editorNow = '2026-09-26T11:02:00Z'
const bathroomBody =
@@ -1981,13 +2040,71 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
'Invite sent': { path: '/users', skip: 'clock-sensitive' },
},
settings: {
Default: {
'Up to date': { path: '/settings', skip: 'clock-sensitive' },
Checking: {
path: '/settings',
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/settings/': { status: 200, body: { version: 'v2.2.0-beta' } },
api: updateApi('owner', updateNeverChecked(), updateCheckHangs),
},
'Check failed': { path: '/settings', skip: 'clock-sensitive' },
'Update available': { path: '/settings', skip: 'pending' },
'Update available, notes open': { path: '/settings', skip: 'pending' },
Downloading: { path: '/settings', skip: 'pending' },
Verifying: { path: '/settings', skip: 'pending' },
'Ready to install': { path: '/settings', skip: 'pending' },
'Restart: confirm': { path: '/settings', skip: 'pending' },
'Restart: request failed': { path: '/settings', skip: 'pending' },
'Restarting to install': { path: '/settings', skip: 'pending' },
'Restarting, taking longer': { path: '/settings', skip: 'pending' },
'Update done': { path: '/settings', skip: 'pending' },
'Update failed, rolled back': { path: '/settings', skip: 'pending' },
'Download failed': { path: '/settings', skip: 'pending' },
'Verification failed': { path: '/settings', skip: 'pending' },
'Too many requests (429)': {
path: '/settings',
api: updateApi('owner', updateNeverChecked(), {
status: 429,
headers: { 'Retry-After': '42' },
}),
},
'Not prepared': {
path: '/settings',
api: updateApi('owner', updateAvailable()),
},
'Not prepared: command copied': {
path: '/settings',
origin: integrationOrigin,
api: updateApi('owner', updateAvailable()),
steps: async (page) => {
await page.getByRole('button', { name: 'Copy', exact: true }).click()
await page.mouse.move(0, 0)
},
},
'Plain restart: confirm': { path: '/settings', skip: 'pending' },
'Plain restart: restarting': { path: '/settings', skip: 'pending' },
'Plain restart: done': { path: '/settings', skip: 'pending' },
'Admin view': {
path: '/settings',
api: updateApi('admin', updateAvailable()),
},
'Role changed (403)': {
path: '/settings',
api: {
...updateApi('owner', updateNeverChecked(), { status: 403 }),
'GET /v1/auth/me': {
status: 200,
bodyFor: (_url, call) => ({
...(verifiedSession.body as object),
role: call === 1 ? 'owner' : 'admin',
}),
},
'GET /v1/system/update': {
status: 200,
bodyFor: (_url, call) =>
call === 1 ? updateNeverChecked() : updateAvailable(),
},
},
},
'Update already running (409)': { path: '/settings', skip: 'pending' },
},
integration: {
Default: {
@@ -2222,6 +2339,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
now: boardNow,
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
},
},
@@ -2230,6 +2348,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
now: boardNow,
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
'GET /v1/article/': { status: 200, body: sixArticlesFixture, hang: true },
},
},
@@ -2238,6 +2357,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
now: boardNow,
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
'GET /v1/article/': { status: 200, body: [] },
},
},
@@ -2246,9 +2366,19 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
now: boardNow,
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
'GET /v1/article/': { status: 500 },
},
},
'Update available (owner)': {
path: '/home',
api: {
'GET /v1/auth/me': verifiedSession,
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
'GET /v1/system/update': { status: 200, body: updateAvailable() },
},
},
'Update ready to install (owner)': { path: '/home', skip: 'pending' },
},
'log-in': {
Default: { path: '/login', api: { 'GET /v1/auth/registration': registrationClosed } },