From 17d74da2a87926c6b94c1225a7499a25a0f63436 Mon Sep 17 00:00:00 2001 From: Jules Date: Thu, 13 Aug 2026 11:47:13 +0000 Subject: [PATCH 1/3] feat(edge): implement HTTP 301 redirects for capitalized valid SPA pages with spec coverage --- src/worker.spec.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++ src/worker.ts | 24 +++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/worker.spec.ts diff --git a/src/worker.spec.ts b/src/worker.spec.ts new file mode 100644 index 00000000..5bfb7107 --- /dev/null +++ b/src/worker.spec.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import worker, { Env } from './worker'; + +// Mock the dynamically generated headers text so the test doesn't depend on build artifacts +vi.mock('../dist/app/browser/_headers', () => { + return { + default: ` +# Dummy headers for testing +/* + X-Test-Header: WorkerTest +` + }; +}); + +describe('Edge Worker Case Redirection', () => { + let mockFetch: any; + let env: Env; + + beforeEach(() => { + mockFetch = vi.fn(async (request: Request | string) => { + const urlStr = typeof request === 'string' ? request : request.url; + const url = new URL(urlStr); + // Mock different behavior based on file types + if (url.pathname.endsWith('.PNG') || url.pathname.endsWith('.png')) { + return new Response('Mock Image content', { + status: 200, + headers: { 'Content-Type': 'image/png' } + }); + } + return new Response('Mock Asset Response', { + status: 200, + headers: { 'Content-Type': 'text/html' } + }); + }); + + env = { + ASSETS: { + fetch: mockFetch + } + }; + }); + + it('should redirect capitalized SPA pages to their lowercase equivalents (301)', async () => { + const request = new Request('https://example.com/ABOUT', { method: 'GET' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(301); + expect(response.headers.get('Location')).toBe('https://example.com/about'); + }); + + it('should preserve and append existing query parameters during redirect', async () => { + const request = new Request('https://example.com/Generator?ref=search&utm_source=test', { method: 'GET' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(301); + expect(response.headers.get('Location')).toBe('https://example.com/generator?ref=search&utm_source=test'); + }); + + it('should handle trailing slash redirection while preserving query parameters', async () => { + const request = new Request('https://example.com/Verify/?test=true', { method: 'GET' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(301); + expect(response.headers.get('Location')).toBe('https://example.com/verify/?test=true'); + }); + + it('should bypass redirect for valid lowercase SPA pages and return 200 OK (SPA Fallback)', async () => { + const request = new Request('https://example.com/about', { method: 'GET' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(200); + expect(mockFetch).toHaveBeenCalled(); + }); + + it('should bypass redirect for truly non-existent paths (capitalized or not)', async () => { + const request1 = new Request('https://example.com/Non-Existent-Route', { method: 'GET' }); + const response1 = await worker.fetch(request1, env); + expect(response1.status).toBe(200); + + const request2 = new Request('https://example.com/non-existent-route', { method: 'GET' }); + const response2 = await worker.fetch(request2, env); + expect(response2.status).toBe(200); + }); + + it('should bypass redirect for static assets and media files (even with uppercase)', async () => { + const request = new Request('https://example.com/assets/LOGO.PNG', { method: 'GET' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(200); + }); + + it('should only redirect GET and HEAD requests', async () => { + const request = new Request('https://example.com/ABOUT', { method: 'POST' }); + const response = await worker.fetch(request, env); + + expect(response.status).toBe(200); + }); +}); diff --git a/src/worker.ts b/src/worker.ts index 4c325400..3afb0c4d 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -144,9 +144,31 @@ export default { return env.ASSETS.fetch(request); } - const rules = await getRules(request, env); const isFile = isFileRequest(pathname); + // Redirect capitalized valid SPA pages to lowercase + if (!isFile) { + const lowercasePath = pathname.toLowerCase(); + const normalizedPath = lowercasePath.endsWith('/') && lowercasePath.length > 1 + ? lowercasePath.slice(0, -1) + : lowercasePath; + + const validPages = ['/about', '/generator', '/verify', '/exception-report']; + + if ((validPages.includes(normalizedPath) || normalizedPath === '/') && /[A-Z]/.test(pathname)) { + const redirectUrl = new URL(request.url); + redirectUrl.pathname = lowercasePath; + return new Response(null, { + status: 301, + headers: { + 'Location': redirectUrl.toString() + } + }); + } + } + + const rules = await getRules(request, env); + if (isFile) { // Fetch the file directly const assetResponse = await env.ASSETS.fetch(request); From 0e486575c8268f88e59b28fa666c6022597b39cd Mon Sep 17 00:00:00 2001 From: Jules Date: Thu, 13 Aug 2026 12:00:24 +0000 Subject: [PATCH 2/3] chore: exclude Cloudflare worker files from Angular unit-test builder --- angular.json | 7 ++++++- tsconfig.spec.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/angular.json b/angular.json index a1849902..0b727b62 100644 --- a/angular.json +++ b/angular.json @@ -87,7 +87,12 @@ "defaultConfiguration": "development" }, "test": { - "builder": "@angular/build:unit-test" + "builder": "@angular/build:unit-test", + "options": { + "exclude": [ + "src/worker.spec.ts" + ] + } }, "lint": { "builder": "@angular-eslint/builder:lint", diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 8f9324dd..f4bf9237 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -6,5 +6,6 @@ "outDir": "./out-tsc/spec", "types": ["vitest/globals", "node"] }, - "include": ["src/**/*.d.ts", "src/**/*.spec.ts"] + "include": ["src/**/*.d.ts", "src/**/*.spec.ts"], + "exclude": ["src/worker.ts", "src/worker.spec.ts"] } From 2b4ee80dba7fae6503f124e2407496c34f2572a0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" Date: Thu, 13 Aug 2026 12:12:47 +0000 Subject: [PATCH 3/3] fix(ci): create separate tsconfig for Cloudflare worker and test files to resolve ESLint project service parsing error --- tsconfig.json | 3 +++ tsconfig.worker.json | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 tsconfig.worker.json diff --git a/tsconfig.json b/tsconfig.json index 221e1427..49b0cf2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,9 @@ }, { "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.worker.json" } ] } diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 00000000..21a13aef --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/worker", + "types": ["vitest/globals", "node"] + }, + "include": ["src/worker.ts", "src/worker.spec.ts"] +}