/** * Deterministic Instagram Graph API stub for e2e — the campaign home content * board (S3) fetches the feed server-side, so the Next dev/prod server points * `INSTAGRAM_API_BASE_URL` here (see playwright.config.ts) and this process * answers `/media`, `/user` and `/refresh_access_token` 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/.jpg` — the * sandbox never reaches `scontent.cdninstagram.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.INSTAGRAM_STUB_PORT ?? 5000) const USER_ID = '17841400000000000' const USERNAME = 'depjorgesolla' 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 POSTS = [ { id: 'e2e-ig-muro-1', caption: 'E2e Post do muro', media_type: 'IMAGE', minutesAgo: 20, }, { id: 'e2e-ig-reel-2', caption: 'E2e Reel da caravana', media_type: 'REEL', minutesAgo: 90, }, { id: 'e2e-ig-sem-legenda-3', caption: null, media_type: 'IMAGE', minutesAgo: 26 * 60, }, { id: 'e2e-ig-carrossel-4', caption: 'E2e Carrossel de fotos', media_type: 'CAROUSEL_ALBUM', minutesAgo: 30 * 60, }, { id: 'e2e-ig-grade-excluido-5', caption: 'E2e Post de grade', media_type: 'IMAGE', minutesAgo: 5, }, ] const mediaItem = (post) => { const base = `${ORIGIN}/thumbs/${post.id}.jpg` const item = { id: post.id, caption: post.caption, media_type: post.media_type, permalink: `https://www.instagram.com/p/${post.id}/`, timestamp: minutesAgo(post.minutesAgo), media_url: base, thumbnail_url: base, } if (post.media_type === 'CAROUSEL_ALBUM') { item.children = { data: [{ media_url: base, thumbnail_url: base }] } } return item } const mediaResponse = () => ({ data: POSTS.map(mediaItem) }) 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.endsWith('/media')) { json(res, 200, mediaResponse()) return } if (url.pathname === '/refresh_access_token') { json(res, 200, { access_token: 'e2e-refreshed-token' }) return } if (url.pathname.endsWith(`/${USER_ID}`) || url.pathname === '/user') { json(res, 200, { id: USER_ID, username: USERNAME }) 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(`instagram e2e stub listening on :${PORT}`) })