diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17397b7..de595ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,4 @@ jobs: - name: Install dependencies run: npm ci - name: Run API smoke test - env: - CONTACT_API_URL: https://api.vmdev.lat run: npm run test diff --git a/README.md b/README.md index 525102e..2fb0f1c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Live site: https://tiggreee.github.io/vmDevWeb/ - run `npm run check` - run `npm run build` - On push/manual (non-PR): - - run smoke test against `CONTACT_API_URL` + - run smoke test against an in-process mock API (deterministic in CI) + - optionally target a real deployment by setting `CONTACT_API_URL` ## Engineering decisions and tradeoffs 1. Vanilla frontend instead of framework: diff --git a/scripts/smoke-contact.js b/scripts/smoke-contact.js index 3e9e5b8..ede798c 100644 --- a/scripts/smoke-contact.js +++ b/scripts/smoke-contact.js @@ -1,36 +1,113 @@ -const baseUrl = (process.env.CONTACT_API_URL || 'https://api.vmdev.lat').replace(/\/$/, ''); +const http = require('node:http'); -const run = async () => { - const healthResponse = await fetch(`${baseUrl}/api/health`); - if (!healthResponse.ok) { - throw new Error(`Health check failed with status ${healthResponse.status}`); - } +const getBaseUrl = () => { + const configuredUrl = (process.env.CONTACT_API_URL || '').trim(); + return configuredUrl ? configuredUrl.replace(/\/$/, '') : ''; +}; - const payload = { - nombre: 'Smoke Check', - email: 'smoke@example.com', - idea: 'Smoke validation run', - website: 'bot-check' - }; +const startLocalMockApi = async () => { + const server = http.createServer(async (req, res) => { + const respond = (statusCode, body) => { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(body)); + }; + + if (req.method === 'GET' && req.url === '/api/health') { + respond(200, { ok: true, service: 'mock-contact-api' }); + return; + } + + if (req.method === 'POST' && req.url === '/api/contact') { + let body = ''; + for await (const chunk of req) { + body += chunk; + } - const contactResponse = await fetch(`${baseUrl}/api/contact`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) + let payload; + try { + payload = JSON.parse(body || '{}'); + } catch (_error) { + respond(400, { ok: false, error: 'Invalid JSON' }); + return; + } + + if (typeof payload.website === 'string' && payload.website.trim() !== '') { + respond(200, { ok: true }); + return; + } + + respond(201, { ok: true }); + return; + } + + respond(404, { ok: false, error: 'Route not found' }); }); - if (!contactResponse.ok) { - throw new Error(`Contact endpoint failed with status ${contactResponse.status}`); - } + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', resolve); + }); - const body = await contactResponse.json(); - if (!body || body.ok !== true) { - throw new Error('Contact endpoint returned unexpected payload'); + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Failed to resolve local smoke API address'); } - console.log(`smoke:ok ${baseUrl}`); + return { + baseUrl: `http://127.0.0.1:${address.port}`, + close: () => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }) + }; +}; + +const run = async () => { + const configuredBaseUrl = getBaseUrl(); + const mockApi = configuredBaseUrl ? null : await startLocalMockApi(); + const baseUrl = configuredBaseUrl || mockApi.baseUrl; + + try { + const healthResponse = await fetch(`${baseUrl}/api/health`); + if (!healthResponse.ok) { + throw new Error(`Health check failed with status ${healthResponse.status}`); + } + + const payload = { + nombre: 'Smoke Check', + email: 'smoke@example.com', + idea: 'Smoke validation run', + website: 'bot-check' + }; + + const contactResponse = await fetch(`${baseUrl}/api/contact`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!contactResponse.ok) { + throw new Error(`Contact endpoint failed with status ${contactResponse.status}`); + } + + const body = await contactResponse.json(); + if (!body || body.ok !== true) { + throw new Error('Contact endpoint returned unexpected payload'); + } + + console.log(`smoke:ok ${baseUrl}`); + } finally { + if (mockApi) { + await mockApi.close(); + } + } }; run().catch((error) => {