Skip to content

Commit d58bbf3

Browse files
Fix deploy smoke test and normalize Lambda Function URL events (Refs #101)
Normalize Function URL events (requestContext.http.method / rawPath) into the API-Gateway-shaped LambdaEvent the router expects, so / and /login route correctly behind the deployed Function URL. Tighten the deploy smoke step to assert root -> 200/301/302 and /login -> 200, and fix its YAML indentation/continuation alignment. Add unit tests covering the normalization (FU-shaped GET / and /api/health, plus FU-vs-APIGW parity). Refs #101
1 parent 59de6c2 commit d58bbf3

3 files changed

Lines changed: 79 additions & 9 deletions

File tree

.github/workflows/deploy-dataops-v1.yml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,19 @@ jobs:
159159
- name: Smoke test deployed single-origin backend
160160
run: |
161161
backend_url="$(aws cloudformation describe-stacks \
162-
--stack-name dataops-v1 \
163-
--query "Stacks[0].Outputs[?OutputKey=='BackendUrl'].OutputValue" \
164-
--output text)"
165-
# The deployed backend function should respond to any HTTP request.
166-
# Any non-5xx response means the function is alive and routing.
162+
--stack-name dataops-v1 \
163+
--query "Stacks[0].Outputs[?OutputKey=='BackendUrl'].OutputValue" \
164+
--output text)"
165+
# The deployed backend should serve /login (200) and redirect / to /login (302).
166+
# The Function URL event normalization in handler.ts must work for this.
167167
code=$(curl -sS -o /dev/null -w "%{http_code}" "$backend_url/")
168-
if [ "$code" -ge 500 ]; then
169-
echo "Backend smoke test failed: server error $code"
168+
if [ "$code" != "200" ] && [ "$code" != "302" ] && [ "$code" != "301" ]; then
169+
echo "Backend smoke test failed: root expected 200/301/302, got $code"
170170
exit 1
171171
fi
172-
echo "Backend smoke OK: HTTP $code"
173-
172+
login_code=$(curl -sS -o /dev/null -w "%{http_code}" "$backend_url/login")
173+
if [ "$login_code" != "200" ]; then
174+
echo "Backend smoke test failed: /login expected 200, got $login_code"
175+
exit 1
176+
fi
177+
echo "Backend smoke OK: root=$code login=$login_code"

backend/src/handler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ function isScheduledEvent(event: unknown): boolean {
3333
}
3434

3535
async function handler(event: LambdaEvent | Record<string, unknown>, _context?: unknown): Promise<LambdaResponse | CronRunnerResult> {
36+
// Normalize Lambda Function URL events to the API Gateway-shaped LambdaEvent
37+
// the router expects. Function URLs send requestContext.http.method/path and
38+
// rawPath, not httpMethod/path.
39+
if (typeof event === 'object' && event !== null && !('httpMethod' in event)) {
40+
const raw = event as Record<string, unknown>;
41+
const requestContext = raw.requestContext as Record<string, unknown> | undefined;
42+
const http = requestContext?.http as Record<string, unknown> | undefined;
43+
if (http?.method || raw.rawPath) {
44+
event = {
45+
httpMethod: (http?.method as string) || 'GET',
46+
path: (raw.rawPath as string) || (http?.path as string) || '/',
47+
headers: (raw.headers as Record<string, string>) || {},
48+
body: (raw.body as string) ?? null,
49+
isBase64Encoded: (raw.isBase64Encoded as boolean) || false,
50+
queryStringParameters: (raw.queryStringParameters as Record<string, string>) || null,
51+
} as LambdaEvent;
52+
}
53+
}
54+
3655
await ensureInitialized();
3756

3857
// Handle EventBridge scheduled events

backend/tests/handler.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,50 @@ describe('handler', () => {
5050
assert.strictEqual(result.statusCode, 404);
5151
});
5252
});
53+
54+
describe('handler Function URL event normalization', () => {
55+
after(async () => {
56+
await stopLocal();
57+
});
58+
59+
it('FU-shaped GET / (requestContext.http.method + rawPath, no httpMethod) returns SPA HTML with status 200', async () => {
60+
const event = {
61+
requestContext: { http: { method: 'GET', path: '/' } },
62+
rawPath: '/',
63+
};
64+
const result = await handler(event, {});
65+
66+
assert.strictEqual(result.statusCode, 200);
67+
assert.strictEqual(result.headers!['Content-Type'], 'text/html');
68+
assert.ok(result.body.includes('<title>DataOps</title>'));
69+
assert.ok(result.body.includes('id="app"'));
70+
});
71+
72+
it('FU-shaped GET /api/health returns {"status":"ok"} with status 200', async () => {
73+
const event = {
74+
requestContext: { http: { method: 'GET', path: '/api/health' } },
75+
rawPath: '/api/health',
76+
};
77+
const result = await handler(event, {});
78+
79+
assert.strictEqual(result.statusCode, 200);
80+
assert.strictEqual(result.headers!['Content-Type'], 'application/json');
81+
82+
const body = JSON.parse(result.body);
83+
assert.deepStrictEqual(body, { status: 'ok' });
84+
});
85+
86+
it('FU-shaped GET /api/health matches the equivalent API-Gateway-shaped event', async () => {
87+
const fuEvent = {
88+
requestContext: { http: { method: 'GET', path: '/api/health' } },
89+
rawPath: '/api/health',
90+
};
91+
const apiGwEvent = { httpMethod: 'GET', path: '/api/health' };
92+
93+
const fuResult = await handler(fuEvent, {});
94+
const apiGwResult = await handler(apiGwEvent, {});
95+
96+
assert.strictEqual(fuResult.statusCode, apiGwResult.statusCode);
97+
assert.strictEqual(fuResult.body, apiGwResult.body);
98+
});
99+
});

0 commit comments

Comments
 (0)