Some checks failed
CI (PR) / e2e (1) (pull_request) Successful in 5m9s
CI / build (push) Successful in 2m20s
CI (PR) / e2e (2) (pull_request) Successful in 5m18s
PR Ready + auto-merge / ready-automerge (pull_request) Failing after 30m17s
CI / e2e (1) (push) Successful in 4m5s
CI (PR) / scope (pull_request) Successful in 32s
CI / deploy homeserver (teqo-1313) (push) Has been cancelled
CI (PR) / lint (pull_request) Successful in 42s
CI (PR) / cycles (pull_request) Successful in 34s
CI (PR) / format (pull_request) Successful in 55s
CI (PR) / docs-guards (pull_request) Successful in 8s
CI (PR) / plans-only-closes (pull_request) Successful in 7s
Main merge flips claimed issues to done + in-prod / issue-done (pull_request) Successful in 13s
CI (PR) / typecheck (pull_request) Successful in 30s
CI (PR) / knip (pull_request) Successful in 31s
CI (PR) / unit (pull_request) Successful in 50s
CI (PR) / checks (pull_request) Successful in 2s
Promote plan-issue Related on main merge / plan-issue-ready (pull_request) Has been skipped
Agent pool supervisor / tick (pull_request) Successful in 27s
CI / static (push) Successful in 1m48s
CI / e2e (2) (push) Successful in 4m7s
CI (PR) / int (pull_request) Successful in 1m49s
CI / int (push) Successful in 1m57s
CI / checks (push) Successful in 2s
CI (PR) / build (pull_request) Successful in 2m31s
Board "Acompanhe de perto" (S1) ganha o feed automático do canal oficial: global SocialFeedSettings (admin-only, kill switch + toggle + maxItems + exclusões por item), loader src/utilities/youtubeFeed.ts (Data API v3 search→statistics, unstable_cache tag social-feed + revalidate 300, fail-closed com snapshot persistido via SQL direto — revalidateTag dentro de unstable_cache lança no Next 15.4), card generalizado (CampaignArticleCard → CampaignContentCard, external com noopener) e merge por recência com o grande = mais recente elegível. Stub HTTP do YouTube no webServer do Playwright (porta dev+1000) + 4 e2e novos no describe serial. Migration 20260818_111748_add_social_feed_settings (não destrutiva). Closes #18
149 lines
3.8 KiB
JavaScript
149 lines
3.8 KiB
JavaScript
/**
|
|
* Deterministic YouTube Data API v3 stub for e2e — the campaign home content
|
|
* board (S2) fetches the feed server-side, so the Next dev/prod server points
|
|
* `YOUTUBE_API_BASE_URL` here (see playwright.config.ts) and this process
|
|
* answers `search`/`videos` with a fixed fixture. `POST /__stub/state` with
|
|
* `{ "state": "ok" | "fail" }` makes the API fail (HTTP 500) so specs can
|
|
* exercise the fail-closed/snapshot paths without any network. Thumbnails are
|
|
* served locally from `/thumbs/<id>.jpg` — the sandbox never reaches
|
|
* `i.ytimg.com`, and next/image would 404 (a console error the e2e guard
|
|
* treats as a failure).
|
|
*/
|
|
import { createServer } from 'node:http'
|
|
|
|
const PORT = Number(process.env.YOUTUBE_STUB_PORT ?? 4000)
|
|
const CHANNEL_ID = 'UCe2eTestChannel'
|
|
const ORIGIN = `http://localhost:${PORT}`
|
|
|
|
// 1x1 transparent PNG — enough for the optimizer to proxy successfully.
|
|
const THUMBNAIL_PNG = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
|
'base64',
|
|
)
|
|
|
|
const minutesAgo = (minutes) => new Date(Date.now() - minutes * 60_000).toISOString()
|
|
|
|
const VIDEOS = [
|
|
{
|
|
id: 'e2e-video-destaque-1',
|
|
title: 'E2e Vídeo em destaque',
|
|
minutesAgo: 30,
|
|
views: 12400,
|
|
maxres: true,
|
|
},
|
|
{
|
|
id: 'e2e-video-caravana-2',
|
|
title: 'E2e Vídeo de caravana',
|
|
minutesAgo: 120,
|
|
views: 8100,
|
|
maxres: true,
|
|
},
|
|
{
|
|
id: 'e2e-video-entrevista-3',
|
|
title: 'E2e Vídeo de entrevista',
|
|
minutesAgo: 26 * 60,
|
|
views: 987,
|
|
maxres: false,
|
|
},
|
|
{
|
|
id: 'e2e-video-excluido-4',
|
|
title: 'E2e Vídeo excluído',
|
|
minutesAgo: 10,
|
|
views: 5_000_000,
|
|
maxres: true,
|
|
},
|
|
]
|
|
|
|
const thumbnails = (video) => {
|
|
const base = `${ORIGIN}/thumbs/${video.id}.jpg`
|
|
const common = {
|
|
default: { url: base },
|
|
medium: { url: base },
|
|
high: { url: base },
|
|
}
|
|
return video.maxres ? { ...common, maxres: { url: base } } : common
|
|
}
|
|
|
|
const searchResponse = () => ({
|
|
items: VIDEOS.map((video) => ({
|
|
id: { videoId: video.id },
|
|
snippet: {
|
|
publishedAt: minutesAgo(video.minutesAgo),
|
|
channelId: CHANNEL_ID,
|
|
title: video.title,
|
|
thumbnails: thumbnails(video),
|
|
},
|
|
})),
|
|
})
|
|
|
|
const statisticsResponse = (ids) => ({
|
|
items: ids
|
|
.split(',')
|
|
.filter(Boolean)
|
|
.map((id) => {
|
|
const video = VIDEOS.find((item) => item.id === id)
|
|
return { id, statistics: { viewCount: video ? String(video.views) : '0' } }
|
|
}),
|
|
})
|
|
|
|
let state = 'ok'
|
|
|
|
const json = (res, status, body) => {
|
|
res.writeHead(status, { 'content-type': 'application/json' })
|
|
res.end(JSON.stringify(body))
|
|
}
|
|
|
|
const server = createServer((req, res) => {
|
|
const url = new URL(req.url, `http://localhost:${PORT}`)
|
|
|
|
if (url.pathname === '/__stub/health') {
|
|
json(res, 200, { ok: true })
|
|
return
|
|
}
|
|
|
|
if (url.pathname === '/__stub/state' && req.method === 'POST') {
|
|
let body = ''
|
|
req.on('data', (chunk) => {
|
|
body += chunk
|
|
})
|
|
req.on('end', () => {
|
|
try {
|
|
const parsed = JSON.parse(body)
|
|
if (parsed.state === 'ok' || parsed.state === 'fail') state = parsed.state
|
|
} catch {
|
|
// keep the current state
|
|
}
|
|
json(res, 200, { state })
|
|
})
|
|
return
|
|
}
|
|
|
|
if (state === 'fail') {
|
|
res.writeHead(500)
|
|
res.end('stub fail')
|
|
return
|
|
}
|
|
|
|
if (url.pathname === '/search') {
|
|
json(res, 200, searchResponse())
|
|
return
|
|
}
|
|
|
|
if (url.pathname === '/videos') {
|
|
json(res, 200, statisticsResponse(url.searchParams.get('id') ?? ''))
|
|
return
|
|
}
|
|
|
|
if (url.pathname.startsWith('/thumbs/')) {
|
|
res.writeHead(200, { 'content-type': 'image/png' })
|
|
res.end(THUMBNAIL_PNG)
|
|
return
|
|
}
|
|
|
|
res.writeHead(404)
|
|
res.end('not found')
|
|
})
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`youtube e2e stub listening on :${PORT}`)
|
|
})
|