ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
86 files +3943 -1190

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

tests/coaching-views.test.tsadded+125-0
Changes to tests/coaching-views.test.ts
@@ -0,0 +1,125 @@
1import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2import { resolve } from 'node:path'
3import { processDirectives } from '@stacksjs/stx'
4import { coachingModules } from '../resources/data/coaching'
5import { forgetBrand } from '../resources/functions/coaching-source'
6
7/**
8 * The eight module routes, rendered the way the site renders them.
9 *
10 * Each view is four lines that name a slug three times, so the failure these
11 * guard against is a copied file that still says 'calendar' somewhere: a page
12 * whose nav highlights the wrong tab, whose canonical points at its neighbour,
13 * or which quietly loses its noindex.
14 */
15
16const payload = await Bun.file('tests/fixtures/brand-last-soul-ultra.json').json()
17const realFetch = globalThis.fetch
18
19/** Answer the brand endpoint from the fixture; refuse everything else. */
20function stubHq(body: unknown = payload): void {
21 globalThis.fetch = (async (input: any) => {
22 const url = String(typeof input === 'string' ? input : input.url)
23 if (url.includes('/api/brands/'))
24 return body === null ? new Response('not found', { status: 404 }) : Response.json(body)
25 throw new Error(`unexpected request: ${url}`)
26 }) as typeof fetch
27}
28
29async function render(slug: string): Promise<string> {
30 const path = resolve(`resources/views/coaching/${slug}.stx`)
31 return processDirectives(await Bun.file(path).text(), {}, path, {
32 enabled: true,
33 cache: false,
34 componentsDir: resolve('resources/components'),
35 layoutsDir: resolve('resources/layouts'),
36 partialsDir: resolve('resources/partials'),
37 }, new Set())
38}
39
40beforeEach(() => forgetBrand())
41afterEach(() => { globalThis.fetch = realFetch })
42
43describe('a module route, with a brand published', () => {
44 beforeEach(() => stubHq())
45
46 it('has a view on disk for every registered module', async () => {
47 for (const module of coachingModules)
48 expect(await Bun.file(`resources/views/coaching/${module.slug}.stx`).exists()).toBe(true)
49 })
50
51 it('titles, describes and canonicalises itself from the registry', async () => {
52 for (const module of coachingModules) {
53 const html = await render(module.slug)
54
55 expect(html).toContain(`<title>${module.title} - Last Soul Ultra coaching</title>`)
56 expect(html).toContain(`content="${module.summary}"`)
57 expect(html).toContain(`rel="canonical" href="https://lastsoulultra.com/coaching/${module.slug}"`)
58 }
59 })
60
61 it('highlights its own tab, once', async () => {
62 for (const module of coachingModules) {
63 const html = await render(module.slug)
64 const current = [...html.matchAll(/href="\/coaching\/([a-z]+)"\s+aria-current="page"/g)].map(match => match[1])
65
66 expect(current).toEqual([module.slug])
67 }
68 })
69
70 it('links out to the section it is standing in for', async () => {
71 for (const module of coachingModules) {
72 const html = await render(module.slug)
73 expect(html).toContain(`/brands/last-soul-ultra?section=${module.slug}`)
74 }
75 })
76
77 it('keeps the modules that need an account out of search results', async () => {
78 for (const module of coachingModules) {
79 const html = await render(module.slug)
80 const noindex = html.includes('name="robots" content="noindex,nofollow"')
81
82 expect(noindex).toBe(module.scope !== 'public')
83 }
84 })
85
86 it('says which scope a panel is before the click', async () => {
87 expect(await render('plans')).not.toContain('coach-scope')
88 expect(await render('calendar')).toContain('Needs a training account')
89 expect(await render('health')).toContain('Only you see this')
90 expect(await render('messages')).toContain('Only you see this')
91 })
92
93 it('renders the storefront on the storefront, and nowhere else', async () => {
94 const plans = await render('plans')
95
96 expect(plans).toContain('coach-offers')
97 expect(plans).toContain('€19.99 a month')
98 expect(await render('calendar')).not.toContain('coach-offers')
99 })
100})
101
102describe('a module route, with nothing published', () => {
103 beforeEach(() => stubHq(null))
104
105 it('says so rather than showing an empty storefront', async () => {
106 const plans = await render('plans')
107
108 expect(plans).toContain('Nothing is open yet')
109 expect(plans).not.toContain('coach-offers')
110 })
111
112 it('offers no nav to panels that cannot load', async () => {
113 const plans = await render('plans')
114
115 expect(plans).not.toContain('coach-nav')
116 expect(plans).not.toContain('/coaching/calendar')
117 })
118
119 it('still renders the page it was asked for', async () => {
120 const calendar = await render('calendar')
121
122 expect(calendar).toContain('<title>Your week - Last Soul Ultra coaching</title>')
123 expect(calendar).toContain('coach-panel')
124 })
125})
tests/setup.tsmodified+9-0
Changes to tests/setup.ts
@@ -10,6 +10,15 @@
1010if (!Bun.env.STRIPE_SECRET_KEY)
1111 Bun.env.STRIPE_SECRET_KEY = 'sk_test_fake_key_for_testing'
1212
13// config/coaching defaults HQ_ORIGIN to the real hq.training, and the coaching
14// components read the published brand while rendering. Left alone, a suite run
15// would make live requests to another service - slow, flaky, and answered
16// differently depending on who is running it. Loopback port 1 refuses at once,
17// so the default is a deterministic "nothing published"; a test that wants a
18// brand stubs fetch itself.
19if (!Bun.env.HQ_ORIGIN)
20 Bun.env.HQ_ORIGIN = 'http://127.0.0.1:1'
21
1322import { applyRuntimeDirectoryEnv } from '@stacksjs/path'
1423import { setupTestEnvironment } from '@stacksjs/testing'
1524