diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml new file mode 100644 index 00000000..bbefb4dc --- /dev/null +++ b/.github/actions/setup-windows/action.yml @@ -0,0 +1,40 @@ +name: setup-windows +description: 'Set up a Windows CI runner for the B2C developer tooling monorepo (Node, pnpm, dependency install, pnpm-store cache).' + +# Mirrors the SalesforceCommerceCloud/pwa-kit `setup_windows` composite action. +# Windows runs everything under Git Bash (`shell: bash`) so the inline `env VAR=value ...` +# style used in the package.json test scripts keeps working on windows-latest. + +inputs: + node-version: + description: 'Node.js version to install (matches `actions/setup-node` input).' + required: false + default: '22.x' + +runs: + using: composite + steps: + - name: Setup Node.js ${{ inputs.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + + - name: Setup pnpm + uses: pnpm/action-setup@v5 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV" + + - name: Setup pnpm cache + uses: actions/cache@v5 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 192b990f..782ccb29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,3 +124,105 @@ jobs: packages/b2c-tooling-sdk/coverage/ packages/b2c-cli/coverage/ retention-days: 30 + + test-windows: + runs-on: windows-latest + # Advisory rollout: Windows coverage is new; surface failures without + # blocking Linux CI while remaining Windows-specific test issues are + # addressed in follow-up PRs. Remove once Windows tests are green. + continue-on-error: true + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + node-version: [22.x, 24.x] + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Setup Windows machine + uses: ./.github/actions/setup-windows + with: + node-version: ${{ matrix.node-version }} + - name: Build packages + run: pnpm -r run build + - name: Run SDK tests + id: sdk-test + working-directory: packages/b2c-tooling-sdk + run: pnpm run pretest && pnpm run test:ci && pnpm run lint + - name: Run MCP tests + id: mcp-test + if: always() && steps.sdk-test.conclusion != 'cancelled' + working-directory: packages/b2c-dx-mcp + run: pnpm run pretest && pnpm run test:ci && pnpm run lint + - name: Run CLI tests + id: cli-test + if: always() && steps.mcp-test.conclusion != 'cancelled' + working-directory: packages/b2c-cli + # Use the Windows-specific script (test:ci:win) to bypass c8's + # coverage threshold check. V8 coverage on Windows double-counts + # some TS source files (distinct URL casing emitted by the tsx + # loader), which drops the reported function-coverage number below + # the Linux-calibrated 70% threshold even though the tests pass. + # Coverage is still generated and uploaded for inspection. + run: pnpm run pretest && pnpm run test:ci:win && pnpm run lint + - name: Run VS Extension checks + if: always() && steps.cli-test.conclusion != 'cancelled' + working-directory: packages/b2c-vs-extension + run: pnpm run typecheck:agent && pnpm run lint + - name: Print Windows test failures + # Mocha's JSON reporter swallows stdout, so Windows-only failures are + # invisible in the step log. Parse the per-package test-results.json + # and echo failing test titles + error messages to the job log for triage. + if: always() && steps.sdk-test.conclusion != 'cancelled' + shell: bash + run: | + node -e " + const fs = require('node:fs'); + const path = require('node:path'); + const reports = [ + 'packages/b2c-tooling-sdk/test-results.json', + 'packages/b2c-dx-mcp/test-results.json', + 'packages/b2c-cli/test-results.json', + ]; + let totalFailures = 0; + for (const report of reports) { + if (!fs.existsSync(report)) continue; + const data = JSON.parse(fs.readFileSync(report, 'utf8')); + const failures = data.failures || []; + if (failures.length === 0) continue; + totalFailures += failures.length; + console.log('\n=== ' + report + ' (' + failures.length + ' failures) ==='); + for (const f of failures) { + console.log('\nāœ– ' + f.fullTitle); + if (f.err && f.err.message) console.log(' message: ' + f.err.message.split('\n')[0]); + if (f.err && f.err.stack) console.log(' stack: ' + f.err.stack.split('\n').slice(0, 5).join('\n ')); + } + } + if (totalFailures === 0) console.log('No Windows test failures reported.'); + else console.log('\nTotal Windows failures: ' + totalFailures); + " + - name: Test Report + uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 + if: always() && steps.sdk-test.conclusion != 'cancelled' + with: + name: Test Results (Windows Node ${{ matrix.node-version }}) + path: 'packages/*/test-results.json' + reporter: mocha-json + - name: Upload test results (for Windows triage) + if: always() && steps.sdk-test.conclusion != 'cancelled' + uses: actions/upload-artifact@v7 + with: + name: test-results-windows-node-${{ matrix.node-version }} + path: 'packages/*/test-results.json' + retention-days: 30 + - name: Upload coverage reports + if: always() && steps.sdk-test.conclusion != 'cancelled' + uses: actions/upload-artifact@v7 + with: + name: coverage-reports-windows-node-${{ matrix.node-version }} + path: | + packages/b2c-tooling-sdk/coverage/ + packages/b2c-cli/coverage/ + retention-days: 30 diff --git a/.github/workflows/e2e-shell-tests.yml b/.github/workflows/e2e-shell-tests.yml index 37f0b93b..5dcdb928 100644 --- a/.github/workflows/e2e-shell-tests.yml +++ b/.github/workflows/e2e-shell-tests.yml @@ -87,3 +87,67 @@ jobs: echo "Running E2E shell tests with realm: ${TEST_REALM}" cd packages/b2c-cli ./test/functional/e2e_cli_test.sh + + e2e-shell-tests-windows: + runs-on: windows-latest + # Advisory rollout: surface Windows shell-test failures without blocking + # the Linux shell-test job while Windows-specific issues are triaged. + continue-on-error: true + environment: e2e-dev + timeout-minutes: 45 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v6 + + - name: Check for required secrets and vars + id: check-secrets + env: + SFCC_CLIENT_ID: ${{ vars.SFCC_CLIENT_ID }} + SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }} + TEST_REALM: ${{ vars.TEST_REALM }} + SFCC_ACCOUNT_MANAGER_HOST: ${{ vars.SFCC_ACCOUNT_MANAGER_HOST }} + SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }} + SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }} + run: | + missing="" + [ -z "$SFCC_CLIENT_ID" ] && missing="$missing SFCC_CLIENT_ID" + [ -z "$SFCC_CLIENT_SECRET" ] && missing="$missing SFCC_CLIENT_SECRET" + [ -z "$TEST_REALM" ] && missing="$missing TEST_REALM" + [ -z "$SFCC_ACCOUNT_MANAGER_HOST" ] && missing="$missing SFCC_ACCOUNT_MANAGER_HOST" + [ -z "$SFCC_SANDBOX_API_HOST" ] && missing="$missing SFCC_SANDBOX_API_HOST" + [ -z "$SFCC_SHORTCODE" ] && missing="$missing SFCC_SHORTCODE" + + if [ -z "$missing" ]; then + echo "has-secrets=true" >> "$GITHUB_OUTPUT" + else + echo "has-secrets=false" >> "$GITHUB_OUTPUT" + echo "Windows E2E shell tests skipped - missing required variables:$missing" >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Setup Windows machine + if: steps.check-secrets.outputs.has-secrets == 'true' + uses: ./.github/actions/setup-windows + with: + node-version: '24' + + - name: Build packages + if: steps.check-secrets.outputs.has-secrets == 'true' + run: pnpm -r run build + + - name: Run E2E Shell Tests + if: steps.check-secrets.outputs.has-secrets == 'true' + env: + SFCC_CLIENT_ID: ${{ vars.SFCC_CLIENT_ID }} + SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }} + SFCC_ACCOUNT_MANAGER_HOST: ${{ vars.SFCC_ACCOUNT_MANAGER_HOST }} + SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }} + SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }} + TEST_REALM: ${{ vars.TEST_REALM }} + SFCC_EXTRA_HEADERS: ${{ secrets.SFCC_EXTRA_HEADERS }} + CURL_EXTRA_HEADERS: ${{ secrets.CURL_EXTRA_HEADERS }} + run: | + echo "Running Windows E2E shell tests with realm: ${TEST_REALM}" + cd packages/b2c-cli + ./test/functional/e2e_cli_test.sh diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d0451772..8a73c0bd 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -165,6 +165,83 @@ jobs: github.rest.issues.create(issue); } + e2e-tests-windows: + name: e2e-tests (windows) + # Advisory rollout: Windows E2E coverage is new; surface failures without + # blocking Linux E2E while remaining Windows-specific test issues are + # addressed in follow-up PRs. Remove once Windows E2E is green. + continue-on-error: true + strategy: + fail-fast: false + matrix: + node-version: [22.x] + runs-on: windows-latest + environment: e2e-dev + timeout-minutes: 60 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v6 + - name: Check for required secrets and vars + id: check-secrets + env: + SFCC_CLIENT_ID: ${{ vars.SFCC_CLIENT_ID }} + SFCC_CLIENT_SECRET: ${{ secrets.SFCC_CLIENT_SECRET }} + TEST_REALM: ${{ vars.TEST_REALM }} + SFCC_ACCOUNT_MANAGER_HOST: ${{ vars.SFCC_ACCOUNT_MANAGER_HOST }} + SFCC_SANDBOX_API_HOST: ${{ vars.SFCC_SANDBOX_API_HOST }} + SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }} + run: | + if [ -n "$SFCC_CLIENT_ID" ] && [ -n "$SFCC_CLIENT_SECRET" ] && [ -n "$TEST_REALM" ] && [ -n "$SFCC_ACCOUNT_MANAGER_HOST" ] && [ -n "$SFCC_SANDBOX_API_HOST" ] && [ -n "$SFCC_SHORTCODE" ]; then + echo "has-secrets=true" >> "$GITHUB_OUTPUT" + else + echo "has-secrets=false" >> "$GITHUB_OUTPUT" + echo "Windows E2E tests skipped - missing required variables" >> "$GITHUB_STEP_SUMMARY" + fi + - name: Setup Windows machine + if: steps.check-secrets.outputs.has-secrets == 'true' + uses: ./.github/actions/setup-windows + with: + node-version: ${{ matrix.node-version }} + - name: Build package + if: steps.check-secrets.outputs.has-secrets == 'true' + run: pnpm -r run build + - name: Run E2E Tests + if: steps.check-secrets.outputs.has-secrets == 'true' + id: e2e-test + working-directory: packages/b2c-cli + env: + SFCC_CLIENT_ID: ${{ inputs.sfcc_client_id || vars.SFCC_CLIENT_ID }} + SFCC_CLIENT_SECRET: ${{ inputs.sfcc_client_secret || secrets.SFCC_CLIENT_SECRET }} + SFCC_ACCOUNT_MANAGER_HOST: ${{ inputs.sfcc_account_manager_host || vars.SFCC_ACCOUNT_MANAGER_HOST }} + SFCC_SANDBOX_API_HOST: ${{ inputs.sfcc_sandbox_api_host || vars.SFCC_SANDBOX_API_HOST }} + TEST_REALM: ${{ inputs.test_realm || vars.TEST_REALM }} + SFCC_SHORTCODE: ${{ vars.SFCC_SHORTCODE }} + SFCC_EXTRA_HEADERS: ${{ secrets.SFCC_EXTRA_HEADERS }} + SFCC_MRT_CLOUD_ORIGIN: ${{ vars.SFCC_MRT_CLOUD_ORIGIN }} + SFCC_MRT_API_KEY: ${{ secrets.SFCC_MRT_API_KEY }} + NODE_ENV: test + SFCC_LOG_LEVEL: silent + run: | + echo "Running Windows E2E tests with realm: $TEST_REALM" + echo "Node version: $(node --version)" + pnpm run test:e2e:ci && pnpm run lint + - name: E2E Test Report + uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 + if: always() && steps.e2e-test.conclusion != 'cancelled' && steps.check-secrets.outputs.has-secrets == 'true' + with: + name: E2E Test Results (Windows Node ${{ matrix.node-version }}) + path: 'packages/b2c-cli/test-results.json' + reporter: mocha-json + - name: Upload E2E Test Results + if: always() && steps.e2e-test.conclusion != 'cancelled' && steps.check-secrets.outputs.has-secrets == 'true' + uses: actions/upload-artifact@v7 + with: + name: e2e-test-results-windows-node-${{ matrix.node-version }}-${{ github.run_number }} + path: packages/b2c-cli/test-results.json + retention-days: 30 + mcp-e2e-tests: name: MCP E2E runs-on: ubuntu-latest diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index c62f8951..7b307a95 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -332,6 +332,7 @@ "pretest": "tsc --noEmit -p test", "test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"", "test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"", + "test:ci:win": "c8 --check-coverage=false env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"", "test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"", "test:agent": "env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter min --exclude \"test/functional/e2e/**\" \"test/**/*.test.ts\"", "test:e2e": "env TEST_USE_SHARED_SANDBOX=true OCLIF_TEST_ROOT=. mocha --forbid-only --require test/functional/e2e/hooks.ts --node-option import=tsx --timeout 30000 --retries 2 --reporter spec \"test/functional/e2e/**/*.test.ts\"", diff --git a/packages/b2c-cli/test/commands/mrt/save-credentials.test.ts b/packages/b2c-cli/test/commands/mrt/save-credentials.test.ts index 40c91f29..733b11c3 100644 --- a/packages/b2c-cli/test/commands/mrt/save-credentials.test.ts +++ b/packages/b2c-cli/test/commands/mrt/save-credentials.test.ts @@ -55,7 +55,14 @@ describe('mrt save-credentials', () => { expect(content).to.deep.equal({username: 'user@example.com', api_key: 'abc123'}); }); - it('sets file permissions to 0o600', async () => { + it('sets file permissions to 0o600', async function () { + // NTFS does not honor POSIX permission bits: fs.chmod(0o600) silently + // leaves the mode as 0o666, so the assertion can only be validated on + // POSIX platforms. Credential files on Windows are protected via ACLs, + // not mode bits. + if (process.platform === 'win32') { + this.skip(); + } const credFile = path.join(tempDir, '.mobify'); const command = createCommand({ user: 'user@example.com', diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts index 15d9bb0f..022ac7c1 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/site-theming/theming-store.ts @@ -5,7 +5,8 @@ */ import {readFileSync, existsSync} from 'node:fs'; -import {join, dirname, basename} from 'node:path'; +import nodePath from 'node:path'; +const {join, dirname, basename} = nodePath; import {createRequire} from 'node:module'; import {getLogger} from '@salesforce/b2c-tooling-sdk/logging'; @@ -56,21 +57,21 @@ export interface ThemingGuidance { type ParsedQuestion = {id: string; question: string; category: string; required: boolean}; function parseWorkflowSection(content: string): ThemingGuidance['workflow'] { - const workflowMatch = content.match(/##\s*šŸ”„\s*WORKFLOW[^#]*(?=##|$)/is); + const workflowMatch = content.match(/##\s*\u{1F504}\s*WORKFLOW[^#]*(?=##|$)/isu); if (!workflowMatch) return undefined; - const workflowContent = workflowMatch[0].replace(/##\s*šŸ”„\s*WORKFLOW[^\n]*\n?/i, '').trim(); + const workflowContent = workflowMatch[0].replace(/##\s*\u{1F504}\s*WORKFLOW[^\n]*\n?/iu, '').trim(); const stepMatches = workflowContent.match(/^\d+\.\s+(.+)$/gm); const steps = stepMatches ? stepMatches.map((step) => step.replace(/^\d+\.\s+/, '').trim()) : []; - const extractionMatch = workflowContent.match(/###\s*šŸ“\s*EXTRACTION[^#]*(?=###|$)/is); + const extractionMatch = workflowContent.match(/###\s*\u{1F4DD}\s*EXTRACTION[^#]*(?=###|$)/isu); const extractionInstructions = extractionMatch - ? extractionMatch[0].replace(/###\s*šŸ“\s*EXTRACTION[^\n]*\n?/i, '').trim() + ? extractionMatch[0].replace(/###\s*\u{1F4DD}\s*EXTRACTION[^\n]*\n?/iu, '').trim() : undefined; - const checklistMatch = workflowContent.match(/###\s*āœ…\s*PRE-IMPLEMENTATION[^#]*(?=###|$)/is); + const checklistMatch = workflowContent.match(/###\s*\u2705\s*PRE-IMPLEMENTATION[^#]*(?=###|$)/is); const preImplementationChecklist = checklistMatch - ? checklistMatch[0].replace(/###\s*āœ…\s*PRE-IMPLEMENTATION[^\n]*\n?/i, '').trim() + ? checklistMatch[0].replace(/###\s*\u2705\s*PRE-IMPLEMENTATION[^\n]*\n?/i, '').trim() : undefined; if (steps.length > 0 || extractionInstructions || preImplementationChecklist) { @@ -80,10 +81,10 @@ function parseWorkflowSection(content: string): ThemingGuidance['workflow'] { } function parseValidationSection(content: string): ThemingGuidance['validation'] { - const validationMatch = content.match(/##\s*āœ…\s*VALIDATION[^#]*(?=##|$)/is); + const validationMatch = content.match(/##\s*\u2705\s*VALIDATION[^#]*(?=##|$)/is); if (!validationMatch) return undefined; - const validationContent = validationMatch[0].replace(/##\s*āœ…\s*VALIDATION[^\n]*\n?/i, '').trim(); + const validationContent = validationMatch[0].replace(/##\s*\u2705\s*VALIDATION[^\n]*\n?/i, '').trim(); const colorValidationMatch = validationContent.match(/###\s*A\.\s*Color[^#]*(?=###|$)/is); const colorValidation = colorValidationMatch @@ -414,27 +415,31 @@ function parseThemingMDC(content: string, filePath: string): ThemingGuidance { const validation = parseValidationSection(content); if (validation) guidance.validation = validation; - const criticalSections = content.match(/##\s*āš ļø\s*CRITICAL[^#]*/gi) || []; - const specificationSections = content.match(/##\s*šŸ“‹[^#]*/gi) || []; + // Emoji prefixes use explicit code points (U+26A0 WARNING SIGN with optional + // U+FE0F VARIATION SELECTOR-16; U+1F4CB CLIPBOARD) so that regex literals do + // not depend on how the source file is decoded by the TS/Node runtime on + // different platforms. Also tolerate \r before newlines for CRLF checkouts. + const criticalSections = content.match(/##\s*\u26A0\uFE0F?\s*CRITICAL[^#]*/gi) || []; + const specificationSections = content.match(/##\s*\u{1F4CB}[^#]*/giu) || []; for (const section of criticalSections) { - const titleMatch = section.match(/##\s*āš ļø\s*CRITICAL:\s*(.+?)\n/); + const titleMatch = section.match(/##\s*\u26A0\uFE0F?\s*CRITICAL:\s*(.+?)\r?\n/); const title = titleMatch ? titleMatch[1].trim() : 'Critical Rule'; guidance.guidelines.push({ category: 'critical', title, - content: section.replace(/##\s*āš ļø\s*CRITICAL[^\n]*\n/, '').trim(), + content: section.replace(/##\s*\u26A0\uFE0F?\s*CRITICAL[^\n]*\n/, '').trim(), critical: true, }); } for (const section of specificationSections) { - const titleMatch = section.match(/##\s*šŸ“‹\s*(.+?)\n/); + const titleMatch = section.match(/##\s*\u{1F4CB}\s*(.+?)\r?\n/u); const title = titleMatch ? titleMatch[1].trim() : 'Specification Rule'; guidance.guidelines.push({ category: 'specification', title, - content: section.replace(/##\s*šŸ“‹[^\n]*\n/, '').trim(), + content: section.replace(/##\s*\u{1F4CB}[^\n]*\n/u, '').trim(), critical: false, }); } @@ -541,7 +546,9 @@ class ThemingStore { private loadThemingFilesFromEnv(envValue: string, root: string): void { const files = JSON.parse(envValue) as Array<{key: string; path: string}>; for (const {key, path: filePath} of files) { - const fullPath = filePath.startsWith('/') ? filePath : join(root, filePath); + // Use path.isAbsolute to detect absolute paths on both POSIX (/foo) and + // Windows (C:\foo); filePath.startsWith('/') misses Windows drive paths. + const fullPath = nodePath.isAbsolute(filePath) ? filePath : join(root, filePath); this.tryLoadEnvFile(key, fullPath); } } diff --git a/packages/b2c-dx-mcp/test/services.test.ts b/packages/b2c-dx-mcp/test/services.test.ts index 5e6784b1..3c991131 100644 --- a/packages/b2c-dx-mcp/test/services.test.ts +++ b/packages/b2c-dx-mcp/test/services.test.ts @@ -215,7 +215,10 @@ describe('services', () => { const config = createMockResolvedConfig({projectDirectory: projectDir}); const services = new Services({resolvedConfig: config}); - expect(services.resolveWithProjectDirectory('subdir')).to.equal('/path/to/project/subdir'); + // Use path.resolve so the expectation matches the production code on both + // POSIX (/path/to/project/subdir) and Windows (where path.resolve on a + // rooted POSIX-style path produces a drive-prefixed, backslash-separated path). + expect(services.resolveWithProjectDirectory('subdir')).to.equal(path.resolve(projectDir, 'subdir')); }); }); diff --git a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts index 71193f0f..1c80f1fb 100644 --- a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts @@ -483,7 +483,10 @@ describe('tools/mrt', () => { it('should use default build directory when buildDirectory is not provided', async () => { const projectDir = '/path/to/project'; - const expectedDefaultPath = path.join(projectDir, 'build'); + // services.resolveWithProjectDirectory() uses path.resolve() which on + // Windows prepends the current drive letter; path.join() does not, so + // use path.resolve() here to match production on both POSIX and Windows. + const expectedDefaultPath = path.resolve(projectDir, 'build'); const mockResult: PushResult = { bundleId: 888, @@ -628,7 +631,7 @@ describe('tools/mrt', () => { describe('mrt_bundle_push project-type defaults', () => { it('should use Storefront Next defaults when storefront-next is detected and args omitted', async () => { const projectDir = '/path/to/sfnext-project'; - const expectedResolvedPath = path.join(projectDir, 'build'); + const expectedResolvedPath = path.resolve(projectDir, 'build'); const mockResult: PushResult = { bundleId: 100, @@ -662,7 +665,7 @@ describe('tools/mrt', () => { it('should use PWA Kit v3 defaults when pwa-kit-v3 is detected and args omitted', async () => { const projectDir = '/path/to/pwakit-project'; - const expectedResolvedPath = path.join(projectDir, 'build'); + const expectedResolvedPath = path.resolve(projectDir, 'build'); const mockResult: PushResult = { bundleId: 101, @@ -696,7 +699,7 @@ describe('tools/mrt', () => { it('should use generic defaults when no project type detected', async () => { const projectDir = '/path/to/unknown-project'; - const expectedResolvedPath = path.join(projectDir, 'build'); + const expectedResolvedPath = path.resolve(projectDir, 'build'); const mockResult: PushResult = { bundleId: 102, diff --git a/packages/b2c-tooling-sdk/src/plugins/loader.ts b/packages/b2c-tooling-sdk/src/plugins/loader.ts index deab783c..cddd3bcd 100644 --- a/packages/b2c-tooling-sdk/src/plugins/loader.ts +++ b/packages/b2c-tooling-sdk/src/plugins/loader.ts @@ -76,7 +76,12 @@ export async function invokeHook( logger?: Logger, ): Promise { try { - const mod = await dynamicImport(hookFilePath); + // On Windows, Node's dynamic import() rejects raw filesystem paths (backslashes, + // drive letters) and requires a file:// URL. pathToFileURL handles both POSIX + // and Windows correctly, so normalize before importing. + const {pathToFileURL} = await import('node:url'); + const importSpecifier = pathToFileURL(hookFilePath).href; + const mod = await dynamicImport(importSpecifier); const hookFn = (mod.default ?? mod) as (...args: unknown[]) => Promise; if (typeof hookFn !== 'function') { diff --git a/packages/b2c-tooling-sdk/test/discovery/utils.test.ts b/packages/b2c-tooling-sdk/test/discovery/utils.test.ts index 4fc923e2..c730ad7c 100644 --- a/packages/b2c-tooling-sdk/test/discovery/utils.test.ts +++ b/packages/b2c-tooling-sdk/test/discovery/utils.test.ts @@ -97,7 +97,10 @@ describe('discovery/utils', () => { const result = await glob('**/*.js', {cwd: tempDir}); - expect(result).to.include('src/index.js'); + // glob returns platform-native separators (backslashes on Windows), so + // assert against the normalized POSIX form so the expectation is portable. + const normalized = result.map((p) => p.split(path.sep).join('/')); + expect(normalized).to.include('src/index.js'); }); it('ignores node_modules', async () => { @@ -108,8 +111,9 @@ describe('discovery/utils', () => { const result = await glob('**/*.js', {cwd: tempDir}); - expect(result).to.include('app.js'); - expect(result).to.not.include('node_modules/pkg/index.js'); + const normalized = result.map((p) => p.split(path.sep).join('/')); + expect(normalized).to.include('app.js'); + expect(normalized).to.not.include('node_modules/pkg/index.js'); }); it('returns empty array when no matches', async () => { diff --git a/packages/b2c-tooling-sdk/test/operations/code/download.test.ts b/packages/b2c-tooling-sdk/test/operations/code/download.test.ts index d1920a23..7c349b58 100644 --- a/packages/b2c-tooling-sdk/test/operations/code/download.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/code/download.test.ts @@ -254,8 +254,13 @@ describe('operations/code/download', () => { expect(result.cartridges).to.deep.equal(['app_storefront']); }); - it('should preserve existing file permissions', async () => { - // Create a file with specific permissions + it('should preserve existing file permissions', async function () { + // NTFS does not honor POSIX permission bits: fs.chmod(0o755) silently + // leaves the mode as 0o666, so this assertion can only be validated on + // POSIX platforms. + if (process.platform === 'win32') { + this.skip(); + } const cartridgeDir = path.join(tempDir, 'app_storefront'); fs.mkdirSync(cartridgeDir, {recursive: true}); const filePath = path.join(cartridgeDir, 'main.js'); diff --git a/packages/b2c-tooling-sdk/test/operations/debug/source-mapping.test.ts b/packages/b2c-tooling-sdk/test/operations/debug/source-mapping.test.ts index a21bccff..cc81bbfc 100644 --- a/packages/b2c-tooling-sdk/test/operations/debug/source-mapping.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/debug/source-mapping.test.ts @@ -45,17 +45,21 @@ describe('operations/debug/source-mapping', () => { }); describe('toLocalPath', () => { + // The mapper resolves cartridge src paths via path.resolve(), which on + // Windows prepends the current drive letter (e.g. D:\workspace\...), so + // expected values must be built with path.resolve() to match. On POSIX + // path.resolve() is a no-op for already-absolute paths. it('converts a server script path to a local path', () => { const server = '/app_storefront/cartridge/controllers/Cart.js'; expect(mapper.toLocalPath(server)).to.equal( - path.join('/workspace/cartridges/app_storefront', 'cartridge/controllers/Cart.js'), + path.resolve('/workspace/cartridges/app_storefront', 'cartridge/controllers/Cart.js'), ); }); it('handles different cartridges', () => { const server = '/bm_extensions/cartridge/scripts/helper.js'; expect(mapper.toLocalPath(server)).to.equal( - path.join('/workspace/cartridges/bm_extensions', 'cartridge/scripts/helper.js'), + path.resolve('/workspace/cartridges/bm_extensions', 'cartridge/scripts/helper.js'), ); }); @@ -70,7 +74,7 @@ describe('operations/debug/source-mapping', () => { it('handles paths without leading slash', () => { const server = 'app_storefront/cartridge/controllers/Cart.js'; expect(mapper.toLocalPath(server)).to.equal( - path.join('/workspace/cartridges/app_storefront', 'cartridge/controllers/Cart.js'), + path.resolve('/workspace/cartridges/app_storefront', 'cartridge/controllers/Cart.js'), ); }); }); @@ -80,7 +84,9 @@ describe('operations/debug/source-mapping', () => { const local = '/workspace/cartridges/app_storefront/cartridge/controllers/Cart.js'; const server = mapper.toServerPath(local); expect(server).to.not.be.undefined; - expect(mapper.toLocalPath(server!)).to.equal(local); + // Production resolves cartridge src via path.resolve(), so the round-trip + // returns an absolute path rooted to the current drive on Windows. + expect(mapper.toLocalPath(server!)).to.equal(path.resolve(local)); }); }); }); diff --git a/packages/b2c-tooling-sdk/test/scaffold/registry.test.ts b/packages/b2c-tooling-sdk/test/scaffold/registry.test.ts index 13d36212..fc6f6865 100644 --- a/packages/b2c-tooling-sdk/test/scaffold/registry.test.ts +++ b/packages/b2c-tooling-sdk/test/scaffold/registry.test.ts @@ -4,6 +4,7 @@ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 */ import {expect} from 'chai'; +import path from 'node:path'; import {createScaffoldRegistry, ScaffoldRegistry, SCAFFOLDS_DATA_DIR} from '../../src/scaffold/index.js'; describe('scaffold/registry', () => { @@ -125,7 +126,9 @@ describe('scaffold/registry', () => { describe('SCAFFOLDS_DATA_DIR', () => { it('should be a valid path', () => { expect(SCAFFOLDS_DATA_DIR).to.be.a('string'); - expect(SCAFFOLDS_DATA_DIR).to.include('data/scaffolds'); + // Use path.join so the assertion works on both POSIX (data/scaffolds) + // and Windows (data\scaffolds) where path.join uses native separators. + expect(SCAFFOLDS_DATA_DIR).to.include(path.join('data', 'scaffolds')); }); }); }); diff --git a/packages/mrt-utilities/test/middleware.test.ts b/packages/mrt-utilities/test/middleware.test.ts index 11597622..fbd0d4e0 100644 --- a/packages/mrt-utilities/test/middleware.test.ts +++ b/packages/mrt-utilities/test/middleware.test.ts @@ -220,11 +220,7 @@ describe('middleware', () => { it('forwards request processor errors via next', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mrt-processor-')); const processorPath = path.join(tempDir, 'processor.mjs'); - fs.writeFileSync( - processorPath, - "export function processRequest() { throw new Error('boom'); }", - 'utf-8', - ); + fs.writeFileSync(processorPath, "export function processRequest() { throw new Error('boom'); }", 'utf-8'); const middleware = createMRTRequestProcessorMiddleware(processorPath, []); diff --git a/packages/mrt-utilities/test/streaming/create-lambda-adapter.test.ts b/packages/mrt-utilities/test/streaming/create-lambda-adapter.test.ts index ad471b9f..c9ea7c94 100644 --- a/packages/mrt-utilities/test/streaming/create-lambda-adapter.test.ts +++ b/packages/mrt-utilities/test/streaming/create-lambda-adapter.test.ts @@ -31,7 +31,8 @@ const mockHttpResponseStream = { .callsFake( ( stream: WritableWithChunksAndMetadata, - _metadata: {statusCode: number; headers: Record; cookies?: string[]}) => { + _metadata: {statusCode: number; headers: Record; cookies?: string[]}, + ) => { stream.metadata = _metadata; return stream; }, @@ -99,7 +100,7 @@ function createMockWritable(): MockWritable { stream.getWrittenData = function (encoding?: BufferEncoding) { const data = Buffer.concat(this.chunks); return encoding ? data.toString(encoding) : data; -}; + }; return stream as MockWritable; } @@ -270,167 +271,166 @@ describe('create-lambda-adapter', () => { expect(typeof handler).to.equal('function'); }); - it('should handle successful request', async function () { - this.timeout(10000); - mockApp.get('/test', (req, res) => { - res.status(200).json({message: 'success'}); - }); - - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/test'}); - const context = createMockContext(); + it('should handle successful request', async function () { + this.timeout(10000); + mockApp.get('/test', (req, res) => { + res.status(200).json({message: 'success'}); + }); - await handler(event, context); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/test'}); + const context = createMockContext(); - // Response should have been written and ended - expect(mockResponseStream.write.called).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + await handler(event, context); - it('should handle errors and write error response', async () => { - // Create an app that throws an error synchronously - mockApp.get('/test', () => { - throw new Error('Test error'); - }); + // Response should have been written and ended + expect(mockResponseStream.write.called).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/test'}); - const context = createMockContext(); + it('should handle errors and write error response', async () => { + // Create an app that throws an error synchronously + mockApp.get('/test', () => { + throw new Error('Test error'); + }); - await handler(event, context); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/test'}); + const context = createMockContext(); - expect(mockResponseStream.write.firstCall.args[0]).to.include('Error'); - expect(mockResponseStream.end.called).to.be.true; - }); + await handler(event, context); - it('should handle non-Error objects thrown', async () => { - mockApp.get('/test', () => { - throw new Error('String error'); - }); + expect(mockResponseStream.write.firstCall.args[0]).to.include('Error'); + expect(mockResponseStream.end.called).to.be.true; + }); - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/test'}); - const context = createMockContext(); + it('should handle non-Error objects thrown', async () => { + mockApp.get('/test', () => { + throw new Error('String error'); + }); - await handler(event, context); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/test'}); + const context = createMockContext(); - expect(mockResponseStream.write.firstCall.args[0]).to.include('Error'); - expect(mockResponseStream.end.called).to.be.true; - }); + await handler(event, context); - it('should handle closed stream in error handler', async () => { - mockApp.get('/test', () => { - throw new Error('Test error'); - }); + expect(mockResponseStream.write.firstCall.args[0]).to.include('Error'); + expect(mockResponseStream.end.called).to.be.true; + }); - const closedStream = createMockWritable(); - (closedStream as any).writable = false; - (closedStream as any).destroyed = true; + it('should handle closed stream in error handler', async () => { + mockApp.get('/test', () => { + throw new Error('Test error'); + }); - const handler = createStreamingLambdaAdapter(mockApp, closedStream); - const event = createMockEvent({path: '/test'}); - const context = createMockContext(); + const closedStream = createMockWritable(); + (closedStream as any).writable = false; + (closedStream as any).destroyed = true; - await handler(event, context); + const handler = createStreamingLambdaAdapter(mockApp, closedStream); + const event = createMockEvent({path: '/test'}); + const context = createMockContext(); - // Should not throw, even with closed stream - expect(closedStream.write.called).to.be.false; - }); + await handler(event, context); - it('should return status code 400 response for requests to streaming route with invalid path', async () => { - // We need a catch-all route to force the router to try to decode the path - const dummyCatchAllRoute = (_: any, res: any) => { - res.status(200).send('dummy catch-all route response'); - }; - try { - //express 4 style catch-all route, throws an error when installed - // in express 5, see https://github.com/pillarjs/path-to-regexp#errors - mockApp.get('/*', dummyCatchAllRoute); - } catch (error) { - //express 5 style catch-all route - mockApp.get('/{*splat}', dummyCatchAllRoute); - } + // Should not throw, even with closed stream + expect(closedStream.write.called).to.be.false; + }); + it('should return status code 400 response for requests to streaming route with invalid path', async () => { + // We need a catch-all route to force the router to try to decode the path + const dummyCatchAllRoute = (_: any, res: any) => { + res.status(200).send('dummy catch-all route response'); + }; + try { + //express 4 style catch-all route, throws an error when installed + // in express 5, see https://github.com/pillarjs/path-to-regexp#errors + mockApp.get('/*', dummyCatchAllRoute); + } catch (error) { + //express 5 style catch-all route + mockApp.get('/{*splat}', dummyCatchAllRoute); + } - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/%80'}); - const context = createMockContext(); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/%80'}); + const context = createMockContext(); - await handler(event, context); + await handler(event, context); - expect(mockResponseStream.metadata.statusCode).to.equal(400); - expect(mockResponseStream.write.called).to.be.true; - expect(mockResponseStream.end.called).to.be.true; + expect(mockResponseStream.metadata.statusCode).to.equal(400); + expect(mockResponseStream.write.called).to.be.true; + expect(mockResponseStream.end.called).to.be.true; - const responseData = mockResponseStream.getWrittenData('utf-8'); + const responseData = mockResponseStream.getWrittenData('utf-8'); - // The response body returned by finalhandler depends on the NODE_ENV environment variable, when it is set to "production", a brief error page with the message "Bad Request" is returned, otherwise it returns a more detailed error page with a stack trace for a URIError exception - expect(responseData).to.contain('Error'); - expect(responseData).to.match(/URIError|Bad Request/); - }); + // The response body returned by finalhandler depends on the NODE_ENV environment variable, when it is set to "production", a brief error page with the message "Bad Request" is returned, otherwise it returns a more detailed error page with a stack trace for a URIError exception + expect(responseData).to.contain('Error'); + expect(responseData).to.match(/URIError|Bad Request/); + }); - it('should return status code 404 response for requests to streaming route that explicitly raises to return a 404', async () => { - // A 404 implemented by throwing - mockApp.get('/intentional404', () => { - type ErrorWithStatus = Error & {status: number}; - const err = new Error('Not Found') as ErrorWithStatus; - err.message = 'Not Found'; - err.status = 404; - throw err; - }); + it('should return status code 404 response for requests to streaming route that explicitly raises to return a 404', async () => { + // A 404 implemented by throwing + mockApp.get('/intentional404', () => { + type ErrorWithStatus = Error & {status: number}; + const err = new Error('Not Found') as ErrorWithStatus; + err.message = 'Not Found'; + err.status = 404; + throw err; + }); - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/intentional404'}); - const context = createMockContext(); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/intentional404'}); + const context = createMockContext(); - await handler(event, context); + await handler(event, context); - //expect(mockResponseStream.write).toHaveBeenCalled(); - expect(mockResponseStream.end.called).to.be.true; - expect(mockResponseStream.metadata.statusCode).to.equal(404); + //expect(mockResponseStream.write).toHaveBeenCalled(); + expect(mockResponseStream.end.called).to.be.true; + expect(mockResponseStream.metadata.statusCode).to.equal(404); - expect(mockResponseStream.getWrittenData('utf-8')).to.contain('Not Found'); - }); + expect(mockResponseStream.getWrittenData('utf-8')).to.contain('Not Found'); + }); - it('should stream status code 200 response for requests to an finalhandler style route', async () => { - mockApp.get('/simpleroute', (req, res) => { - // A hanlder that uses the response object in the style of https://github.com/pillarjs/finalhandler/blob/v2.1.0/index.js#L245-L280 which is used by express to handle the final response in case of an error - res.statusCode = 200; - res.statusMessage = 'OK'; - const body = 'hello world'; - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')); - res.end(body); - }); + it('should stream status code 200 response for requests to an finalhandler style route', async () => { + mockApp.get('/simpleroute', (req, res) => { + // A hanlder that uses the response object in the style of https://github.com/pillarjs/finalhandler/blob/v2.1.0/index.js#L245-L280 which is used by express to handle the final response in case of an error + res.statusCode = 200; + res.statusMessage = 'OK'; + const body = 'hello world'; + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')); + res.end(body); + }); - const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); - const event = createMockEvent({path: '/simpleroute'}); - const context = createMockContext(); + const handler = createStreamingLambdaAdapter(mockApp, mockResponseStream); + const event = createMockEvent({path: '/simpleroute'}); + const context = createMockContext(); - await handler(event, context); + await handler(event, context); - expect(mockResponseStream.metadata.statusCode).to.equal(200); - expect(mockResponseStream.getWrittenData('utf-8')).to.equal('hello world'); - }); + expect(mockResponseStream.metadata.statusCode).to.equal(200); + expect(mockResponseStream.getWrittenData('utf-8')).to.equal('hello world'); + }); - it('should handle stream without end method in finally', async () => { - mockApp.get('/test', (req, res) => { - res.status(200).send('OK'); - }); + it('should handle stream without end method in finally', async () => { + mockApp.get('/test', (req, res) => { + res.status(200).send('OK'); + }); - const streamWithoutEnd = createMockWritable(); - delete (streamWithoutEnd as any).end; + const streamWithoutEnd = createMockWritable(); + delete (streamWithoutEnd as any).end; - const handler = createStreamingLambdaAdapter(mockApp, streamWithoutEnd); - const event = createMockEvent({path: '/test'}); - const context = createMockContext(); + const handler = createStreamingLambdaAdapter(mockApp, streamWithoutEnd); + const event = createMockEvent({path: '/test'}); + const context = createMockContext(); - await handler(event, context); + await handler(event, context); - // Should not throw - expect(streamWithoutEnd.write.called).to.be.true; - }); - }); + // Should not throw + expect(streamWithoutEnd.write.called).to.be.true; + }); + }); describe('createExpressRequest', () => { it('should create Express-like request object', () => { @@ -445,220 +445,220 @@ describe('create-lambda-adapter', () => { // These are handled by Express middleware }); - it('should decode base64 encoded body', () => { - const body = Buffer.from('test body').toString('base64'); - const event = createMockEvent({ - body, - isBase64Encoded: true, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should decode base64 encoded body', () => { + const body = Buffer.from('test body').toString('base64'); + const event = createMockEvent({ + body, + isBase64Encoded: true, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest returns body as Buffer - expect(Buffer.isBuffer(req.body)).to.equal(true); - expect(req.body.toString('utf-8')).to.equal('test body'); - }); + // ServerlessRequest returns body as Buffer + expect(Buffer.isBuffer(req.body)).to.equal(true); + expect(req.body.toString('utf-8')).to.equal('test body'); + }); - it('should handle query string parameters', () => { - const event = createMockEvent({ - queryStringParameters: { - foo: 'bar', - baz: 'qux', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should handle query string parameters', () => { + const event = createMockEvent({ + queryStringParameters: { + foo: 'bar', + baz: 'qux', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // Query parameters are in the URL, Express will parse them - expect(req.url).to.include('foo=bar'); - expect(req.url).to.include('baz=qux'); - }); + // Query parameters are in the URL, Express will parse them + expect(req.url).to.include('foo=bar'); + expect(req.url).to.include('baz=qux'); + }); - it('should handle path parameters', () => { - const event = createMockEvent({ - pathParameters: { - id: '123', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should handle path parameters', () => { + const event = createMockEvent({ + pathParameters: { + id: '123', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // Path parameters are handled by Express routing, not directly on request - expect(req.url).to.exist; - }); + // Path parameters are handled by Express routing, not directly on request + expect(req.url).to.exist; + }); - it('should set protocol from X-Forwarded-Proto header', () => { - const event = createMockEvent({ - headers: { - 'X-Forwarded-Proto': 'http', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should set protocol from X-Forwarded-Proto header', () => { + const event = createMockEvent({ + headers: { + 'X-Forwarded-Proto': 'http', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest doesn't expose protocol directly - // It's available via headers if needed - expect(req.headers['x-forwarded-proto']).to.equal('http'); - }); + // ServerlessRequest doesn't expose protocol directly + // It's available via headers if needed + expect(req.headers['x-forwarded-proto']).to.equal('http'); + }); - it('should default to https protocol', () => { - const event = createMockEvent({ - headers: {}, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should default to https protocol', () => { + const event = createMockEvent({ + headers: {}, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest doesn't expose protocol directly - // Without X-Forwarded-Proto header, protocol is not set - expect(req.headers['x-forwarded-proto']).to.be.undefined; - }); + // ServerlessRequest doesn't expose protocol directly + // Without X-Forwarded-Proto header, protocol is not set + expect(req.headers['x-forwarded-proto']).to.be.undefined; + }); - it('should set hostname from Host header', () => { - const event = createMockEvent({ - headers: { - Host: 'example.com', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should set hostname from Host header', () => { + const event = createMockEvent({ + headers: { + Host: 'example.com', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest doesn't expose hostname directly - // It's available via headers - expect(req.headers.host).to.equal('example.com'); - }); + // ServerlessRequest doesn't expose hostname directly + // It's available via headers + expect(req.headers.host).to.equal('example.com'); + }); - it('should set IP from X-Forwarded-For header', () => { - const event = createMockEvent({ - headers: { - 'X-Forwarded-For': '192.168.1.1, 10.0.0.1', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should set IP from X-Forwarded-For header', () => { + const event = createMockEvent({ + headers: { + 'X-Forwarded-For': '192.168.1.1, 10.0.0.1', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest uses remoteAddress, which comes from requestContext.identity.sourceIp - // X-Forwarded-For is in headers but remoteAddress is set from sourceIp - expect(req.headers['x-forwarded-for']).to.equal('192.168.1.1, 10.0.0.1'); - }); + // ServerlessRequest uses remoteAddress, which comes from requestContext.identity.sourceIp + // X-Forwarded-For is in headers but remoteAddress is set from sourceIp + expect(req.headers['x-forwarded-for']).to.equal('192.168.1.1, 10.0.0.1'); + }); - it('should implement get method for headers', () => { - const event = createMockEvent({ - headers: { - 'Content-Type': 'application/json', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should implement get method for headers', () => { + const event = createMockEvent({ + headers: { + 'Content-Type': 'application/json', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - expect(req.get('Content-Type')).to.equal('application/json'); - expect(req.get('content-type')).to.equal('application/json'); - expect(req.header('Content-Type')).to.equal('application/json'); - }); + expect(req.get('Content-Type')).to.equal('application/json'); + expect(req.get('content-type')).to.equal('application/json'); + expect(req.header('Content-Type')).to.equal('application/json'); + }); - it('should handle missing headers', () => { - const event = createMockEvent({ - headers: null as any, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should handle missing headers', () => { + const event = createMockEvent({ + headers: null as any, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - expect(req.headers).to.deep.equal({}); - expect(req.get('Content-Type')).to.be.undefined; - }); + expect(req.headers).to.deep.equal({}); + expect(req.get('Content-Type')).to.be.undefined; + }); - it('should handle empty headers object', () => { - const event = createMockEvent({ - headers: {}, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should handle empty headers object', () => { + const event = createMockEvent({ + headers: {}, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - expect(req.headers).to.deep.equal({}); - expect(req.get('Any-Header')).to.be.undefined; - }); + expect(req.headers).to.deep.equal({}); + expect(req.get('Any-Header')).to.be.undefined; + }); - it('should handle headers with array values', () => { - const event = createMockEvent({ - headers: { - 'X-Custom': ['value1', 'value2'] as any, - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - - // ServerlessRequest stores the value as-is (array) - // The get method returns the header value directly - const value = req.get('X-Custom'); - expect(Array.isArray(value)).to.equal(true); - expect(value).to.deep.equal(['value1', 'value2']); - }); + it('should handle headers with array values', () => { + const event = createMockEvent({ + headers: { + 'X-Custom': ['value1', 'value2'] as any, + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - it('should handle missing requestContext', () => { - const event = createMockEvent({ - requestContext: null, - } as any); - const context = createMockContext(); - const req = createExpressRequest(event, context); + // ServerlessRequest stores the value as-is (array) + // The get method returns the header value directly + const value = req.get('X-Custom'); + expect(Array.isArray(value)).to.equal(true); + expect(value).to.deep.equal(['value1', 'value2']); + }); - // ServerlessRequest uses remoteAddress which defaults to empty string - // We can't directly access it, but the request should still be created - expect(req.method).to.equal('GET'); - }); + it('should handle missing requestContext', () => { + const event = createMockEvent({ + requestContext: null, + } as any); + const context = createMockContext(); + const req = createExpressRequest(event, context); - it('should handle missing identity in requestContext', () => { - const event = createMockEvent({ - requestContext: { - identity: null, - } as any, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + // ServerlessRequest uses remoteAddress which defaults to empty string + // We can't directly access it, but the request should still be created + expect(req.method).to.equal('GET'); + }); - // ServerlessRequest uses remoteAddress which defaults to empty string - // We can't directly access it, but the request should still be created - expect(req.method).to.equal('GET'); - }); + it('should handle missing identity in requestContext', () => { + const event = createMockEvent({ + requestContext: { + identity: null, + } as any, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - it('should handle empty query string parameters', () => { - const event = createMockEvent({ - queryStringParameters: {}, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + // ServerlessRequest uses remoteAddress which defaults to empty string + // We can't directly access it, but the request should still be created + expect(req.method).to.equal('GET'); + }); - // ServerlessRequest doesn't expose query directly - // Empty query string parameters should not add '?' to URL - expect(req.url).to.equal('/test'); - }); + it('should handle empty query string parameters', () => { + const event = createMockEvent({ + queryStringParameters: {}, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - it('should handle null body', () => { - const event = createMockEvent({ - body: null, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - - // When body is null, requestBody is undefined, so req.body may be undefined - // or ServerlessRequest may convert it to an empty Buffer - expect(req.body === undefined || Buffer.isBuffer(req.body)).to.equal(true); - if (Buffer.isBuffer(req.body)) { - expect(req.body.length).to.equal(0); - } - }); + // ServerlessRequest doesn't expose query directly + // Empty query string parameters should not add '?' to URL + expect(req.url).to.equal('/test'); + }); - it('should handle body without base64 encoding', () => { - const event = createMockEvent({ - body: 'plain text body', - isBase64Encoded: false, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); + it('should handle null body', () => { + const event = createMockEvent({ + body: null, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); - // ServerlessRequest returns body as Buffer - expect(Buffer.isBuffer(req.body)).to.equal(true); - expect(req.body.toString('utf-8')).to.equal('plain text body'); - }); - }); + // When body is null, requestBody is undefined, so req.body may be undefined + // or ServerlessRequest may convert it to an empty Buffer + expect(req.body === undefined || Buffer.isBuffer(req.body)).to.equal(true); + if (Buffer.isBuffer(req.body)) { + expect(req.body.length).to.equal(0); + } + }); + + it('should handle body without base64 encoding', () => { + const event = createMockEvent({ + body: 'plain text body', + isBase64Encoded: false, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + + // ServerlessRequest returns body as Buffer + expect(Buffer.isBuffer(req.body)).to.equal(true); + expect(req.body.toString('utf-8')).to.equal('plain text body'); + }); + }); describe('createExpressResponse', () => { describe('writeHead', () => { @@ -673,1938 +673,1938 @@ describe('create-lambda-adapter', () => { expect(res.headersSent).to.be.true; }); - it('should handle status message', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(404, 'Not Found'); + it('should handle status message', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(404, 'Not Found'); - expect(res.statusCode).to.equal(404); - expect(res.statusMessage).to.equal('Not Found'); - }); + expect(res.statusCode).to.equal(404); + expect(res.statusMessage).to.equal('Not Found'); + }); - it('should handle object as second parameter', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(200, {'Content-Type': 'application/json'}); + it('should handle object as second parameter', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(200, {'Content-Type': 'application/json'}); - expect(res.statusCode).to.equal(200); - }); + expect(res.statusCode).to.equal(200); + }); - it('should handle writeHead with only status code', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(201); + it('should handle writeHead with only status code', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(201); - expect(res.statusCode).to.equal(201); - expect(mockHttpResponseStream.from.called).to.be.true; - }); + expect(res.statusCode).to.equal(201); + expect(mockHttpResponseStream.from.called).to.be.true; + }); - it('should handle writeHead with status code and status message', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(500, 'Internal Server Error', {'X-Custom': 'value'}); + it('should handle writeHead with status code and status message', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(500, 'Internal Server Error', {'X-Custom': 'value'}); - expect(res.statusCode).to.equal(500); - expect(res.statusMessage).to.equal('Internal Server Error'); - expect(res.getHeader('X-Custom')).to.equal('value'); - }); + expect(res.statusCode).to.equal(500); + expect(res.statusMessage).to.equal('Internal Server Error'); + expect(res.getHeader('X-Custom')).to.equal('value'); + }); - it('should not send headers twice if writeHead called multiple times', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(200); - const firstCallCount = mockHttpResponseStream.from.callCount; - res.writeHead(201); + it('should not send headers twice if writeHead called multiple times', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(200); + const firstCallCount = mockHttpResponseStream.from.callCount; + res.writeHead(201); - // Should only call from once (headers already sent) - expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); - }); + // Should only call from once (headers already sent) + expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); + }); - it('should handle writeHead with array header values', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(200, {'X-Custom': ['value1', 'value2']}); + it('should handle writeHead with array header values', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(200, {'X-Custom': ['value1', 'value2']}); - expect(res.statusCode).to.equal(200); - expect(mockHttpResponseStream.from.called).to.be.true; - }); - }); + expect(res.statusCode).to.equal(200); + expect(mockHttpResponseStream.from.called).to.be.true; + }); + }); - describe('write', () => { - it('should write chunk to stream', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.write('test'); + describe('write', () => { + it('should write chunk to stream', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.write('test'); - expect(result).to.be.true; - expect(mockResponseStream.write.calledWith('test')).to.be.true; - }); + expect(result).to.be.true; + expect(mockResponseStream.write.calledWith('test')).to.be.true; + }); - it('should auto-send headers on first write', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.write('test'); + it('should auto-send headers on first write', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.write('test'); - expect(mockHttpResponseStream.from.called).to.be.true; - expect(res.headersSent).to.be.true; - }); + expect(mockHttpResponseStream.from.called).to.be.true; + expect(res.headersSent).to.be.true; + }); - it('should handle Buffer chunks', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const buffer = Buffer.from('test'); - res.write(buffer); + it('should handle Buffer chunks', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const buffer = Buffer.from('test'); + res.write(buffer); - expect(mockResponseStream.write.calledWith(buffer)).to.be.true; - }); + expect(mockResponseStream.write.calledWith(buffer)).to.be.true; + }); - it('should handle multiple writes', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.write('chunk1'); - res.write('chunk2'); - res.write('chunk3'); - - expect(mockResponseStream.write.callCount).to.equal(3); - expect(mockResponseStream.write.getCall(1 - 1).args).to.deep.equal(['chunk1']); - expect(mockResponseStream.write.getCall(2 - 1).args).to.deep.equal(['chunk2']); - expect(mockResponseStream.write.getCall(3 - 1).args).to.deep.equal(['chunk3']); - }); + it('should handle multiple writes', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.write('chunk1'); + res.write('chunk2'); + res.write('chunk3'); + + expect(mockResponseStream.write.callCount).to.equal(3); + expect(mockResponseStream.write.getCall(1 - 1).args).to.deep.equal(['chunk1']); + expect(mockResponseStream.write.getCall(2 - 1).args).to.deep.equal(['chunk2']); + expect(mockResponseStream.write.getCall(3 - 1).args).to.deep.equal(['chunk3']); + }); - it('should handle empty string chunk', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.write(''); + it('should handle empty string chunk', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.write(''); - // Empty strings should be written - expect(result).to.be.true; - expect(mockResponseStream.write.calledWith('')).to.be.true; - }); + // Empty strings should be written + expect(result).to.be.true; + expect(mockResponseStream.write.calledWith('')).to.be.true; + }); - it('should handle Uint8Array chunks', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const uint8Array = new Uint8Array([1, 2, 3, 4]); - res.write(uint8Array); + it('should handle Uint8Array chunks', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const uint8Array = new Uint8Array([1, 2, 3, 4]); + res.write(uint8Array); - expect(mockResponseStream.write.calledWith(uint8Array)).to.be.true; - }); + expect(mockResponseStream.write.calledWith(uint8Array)).to.be.true; + }); - it('should return false if stream write fails', () => { - const failingStream = createMockWritable(); - failingStream.write = sinon.stub().callsFake(() => { - throw new Error('Write failed'); - }); + it('should return false if stream write fails', () => { + const failingStream = createMockWritable(); + failingStream.write = sinon.stub().callsFake(() => { + throw new Error('Write failed'); + }); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(failingStream, event, context); - const result = res.write('test'); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(failingStream, event, context); + const result = res.write('test'); - expect(result).to.be.false; - }); + expect(result).to.be.false; + }); - it('should handle write after headers are sent', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(200); - mockHttpResponseStream.from.resetHistory(); + it('should handle write after headers are sent', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(200); + mockHttpResponseStream.from.resetHistory(); - res.write('test'); + res.write('test'); - // Should still write, but not call from again - expect(mockResponseStream.write.calledWith('test')).to.be.true; - expect(mockHttpResponseStream.from.called).to.be.false; - }); - }); + // Should still write, but not call from again + expect(mockResponseStream.write.calledWith('test')).to.be.true; + expect(mockHttpResponseStream.from.called).to.be.false; + }); + }); - describe('end', () => { - it('should end stream', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end(); + describe('end', () => { + it('should end stream', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end(); - expect(mockResponseStream.end.called).to.be.true; - expect(res.finished).to.be.true; - }); + expect(mockResponseStream.end.called).to.be.true; + expect(res.finished).to.be.true; + }); - it('should write final chunk before ending', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end('final'); + it('should write final chunk before ending', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end('final'); - expect(mockResponseStream.write.calledWith('final')).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + expect(mockResponseStream.write.calledWith('final')).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - it('should auto-send headers on end', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end(); + it('should auto-send headers on end', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end(); - expect(mockHttpResponseStream.from.called).to.be.true; - expect(res.headersSent).to.be.true; - }); + expect(mockHttpResponseStream.from.called).to.be.true; + expect(res.headersSent).to.be.true; + }); - it('should emit finish event', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const finishSpy = sinon.stub(); - res.on('finish', finishSpy); - res.end(); + it('should emit finish event', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const finishSpy = sinon.stub(); + res.on('finish', finishSpy); + res.end(); - expect(finishSpy.called).to.be.true; - }); + expect(finishSpy.called).to.be.true; + }); - it('should handle end with Buffer chunk', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const buffer = Buffer.from('final'); - res.end(buffer); + it('should handle end with Buffer chunk', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const buffer = Buffer.from('final'); + res.end(buffer); - expect(mockResponseStream.write.calledWith(buffer)).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + expect(mockResponseStream.write.calledWith(buffer)).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - it('should handle end with empty string', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end(''); + it('should handle end with empty string', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end(''); - // Empty strings should be written - expect(mockResponseStream.write.calledWith('')).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + // Empty strings should be written + expect(mockResponseStream.write.calledWith('')).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - it('should handle end after write', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.write('chunk1'); - res.end('chunk2'); + it('should handle end after write', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.write('chunk1'); + res.end('chunk2'); - expect(mockResponseStream.write.callCount).to.equal(2); - expect(mockResponseStream.end.called).to.be.true; - }); + expect(mockResponseStream.write.callCount).to.equal(2); + expect(mockResponseStream.end.called).to.be.true; + }); - it('should handle end error gracefully', () => { - const failingStream = createMockWritable(); - failingStream.end = sinon.stub().callsFake(() => { - throw new Error('End failed'); + it('should handle end error gracefully', () => { + const failingStream = createMockWritable(); + failingStream.end = sinon.stub().callsFake(() => { + throw new Error('End failed'); + }); + + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(failingStream, event, context); + const result = res.end(); + + expect(result).to.equal(res); + expect(res.finished).to.be.true; + }); }); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(failingStream, event, context); - const result = res.end(); + describe('status', () => { + it('should set status code', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.status(404); + + expect(res.statusCode).to.equal(404); + expect(result).to.equal(res); + }); - expect(result).to.equal(res); - expect(res.finished).to.be.true; - }); - }); + it('should set status message', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // @ts-expect-error - ExpressResponse type doesn't include the message parameter, but our implementation supports it + res.status(404, 'Not Found'); - describe('status', () => { - it('should set status code', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.status(404); + expect(res.statusCode).to.equal(404); + expect(res.statusMessage).to.equal('Not Found'); + }); + }); - expect(res.statusCode).to.equal(404); - expect(result).to.equal(res); - }); + describe('set', () => { + it('should set single header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.set('Content-Type', 'application/json'); - it('should set status message', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // @ts-expect-error - ExpressResponse type doesn't include the message parameter, but our implementation supports it - res.status(404, 'Not Found'); + expect(res.getHeader('Content-Type')).to.equal('application/json'); + expect(result).to.equal(res); + }); - expect(res.statusCode).to.equal(404); - expect(res.statusMessage).to.equal('Not Found'); - }); - }); + it('should set multiple headers from object', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set({ + 'Content-Type': 'application/json', + 'X-Custom': 'value', + }); - describe('set', () => { - it('should set single header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.set('Content-Type', 'application/json'); + expect(res.getHeader('Content-Type')).to.equal('application/json'); + expect(res.getHeader('X-Custom')).to.equal('value'); + }); - expect(res.getHeader('Content-Type')).to.equal('application/json'); - expect(result).to.equal(res); - }); + it('should overwrite existing header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', 'value1'); + res.set('X-Custom', 'value2'); + + expect(res.getHeader('X-Custom')).to.equal('value2'); + }); - it('should set multiple headers from object', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set({ - 'Content-Type': 'application/json', - 'X-Custom': 'value', + it('should set header with array value', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', ['value1', 'value2']); + + expect(res.getHeader('X-Custom')).to.deep.equal(['value1', 'value2']); + }); + + it('should handle setting undefined value', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', 'value1'); + res.set('X-Custom', undefined as any); + + // Should not throw + expect(res.getHeader('X-Custom')).to.equal('value1'); + }); }); - expect(res.getHeader('Content-Type')).to.equal('application/json'); - expect(res.getHeader('X-Custom')).to.equal('value'); - }); + describe('append', () => { + it('should append to existing header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', 'value1'); + res.append('X-Custom', 'value2'); - it('should overwrite existing header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', 'value1'); - res.set('X-Custom', 'value2'); + const header = res.getHeader('X-Custom'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + }); - expect(res.getHeader('X-Custom')).to.equal('value2'); - }); + it('should set header if it does not exist', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.append('X-Custom', 'value'); - it('should set header with array value', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', ['value1', 'value2']); + expect(res.getHeader('X-Custom')).to.equal('value'); + }); - expect(res.getHeader('X-Custom')).to.deep.equal(['value1', 'value2']); - }); + it('should append to existing array header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', ['value1', 'value2']); + res.append('X-Custom', 'value3'); + + const header = res.getHeader('X-Custom'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + expect(header).to.include('value3'); + }); - it('should handle setting undefined value', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', 'value1'); - res.set('X-Custom', undefined as any); + it('should append array to existing header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Custom', 'value1'); + res.append('X-Custom', ['value2', 'value3']); + + const header = res.getHeader('X-Custom'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + expect(header).to.include('value3'); + }); + }); - // Should not throw - expect(res.getHeader('X-Custom')).to.equal('value1'); - }); - }); + describe('flushHeaders', () => { + it('should send headers immediately', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('Content-Type', 'application/json'); + res.flushHeaders(); - describe('append', () => { - it('should append to existing header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', 'value1'); - res.append('X-Custom', 'value2'); - - const header = res.getHeader('X-Custom'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - }); + expect(mockHttpResponseStream.from.called).to.be.true; + expect(res.headersSent).to.be.true; + }); - it('should set header if it does not exist', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.append('X-Custom', 'value'); + it('should not send headers twice', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.flushHeaders(); + const firstCallCount = mockHttpResponseStream.from.callCount; + res.flushHeaders(); - expect(res.getHeader('X-Custom')).to.equal('value'); - }); + expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); + }); - it('should append to existing array header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', ['value1', 'value2']); - res.append('X-Custom', 'value3'); - - const header = res.getHeader('X-Custom'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - expect(header).to.include('value3'); - }); + it('should include all set headers', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('Content-Type', 'application/json'); + res.set('X-Custom', 'value'); + res.status(201); - it('should append array to existing header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Custom', 'value1'); - res.append('X-Custom', ['value2', 'value3']); - - const header = res.getHeader('X-Custom'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - expect(header).to.include('value3'); - }); - }); + // Verify headers are set on the response object + expect(res.getHeader('Content-Type')).to.equal('application/json'); + expect(res.getHeader('X-Custom')).to.equal('value'); - describe('flushHeaders', () => { - it('should send headers immediately', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('Content-Type', 'application/json'); - res.flushHeaders(); + res.flushHeaders(); - expect(mockHttpResponseStream.from.called).to.be.true; - expect(res.headersSent).to.be.true; - }); + expect(mockHttpResponseStream.from.called).to.be.true; + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata.statusCode).to.equal(201); + // Headers should be included in metadata (case-insensitive check) + const headers = metadata.headers; + expect(headers['content-type'] || headers['Content-Type']).to.equal('application/json'); + expect(headers['x-custom'] || headers['X-Custom']).to.equal('value'); + }); + }); - it('should not send headers twice', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.flushHeaders(); - const firstCallCount = mockHttpResponseStream.from.callCount; - res.flushHeaders(); + describe('json', () => { + it('should send JSON response', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.json({message: 'test'}); - expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); - }); + expect(res.getHeader('Content-Type')).to.equal('application/json'); + expect(mockResponseStream.write.calledWith(JSON.stringify({message: 'test'}))).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - it('should include all set headers', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('Content-Type', 'application/json'); - res.set('X-Custom', 'value'); - res.status(201); - - // Verify headers are set on the response object - expect(res.getHeader('Content-Type')).to.equal('application/json'); - expect(res.getHeader('X-Custom')).to.equal('value'); - - res.flushHeaders(); - - expect(mockHttpResponseStream.from.called).to.be.true; - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata.statusCode).to.equal(201); - // Headers should be included in metadata (case-insensitive check) - const headers = metadata.headers; - expect(headers['content-type'] || headers['Content-Type']).to.equal('application/json'); - expect(headers['x-custom'] || headers['X-Custom']).to.equal('value'); - }); - }); + it('should handle complex JSON objects', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const complexObj = { + nested: {value: 123}, + array: [1, 2, 3], + string: 'test', + }; + res.json(complexObj); + + expect(mockResponseStream.write.calledWith(JSON.stringify(complexObj))).to.be.true; + }); - describe('json', () => { - it('should send JSON response', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.json({message: 'test'}); + it('should handle null JSON', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.json(null); - expect(res.getHeader('Content-Type')).to.equal('application/json'); - expect(mockResponseStream.write.calledWith(JSON.stringify({message: 'test'}))).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + expect(mockResponseStream.write.calledWith('null')).to.be.true; + }); - it('should handle complex JSON objects', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const complexObj = { - nested: {value: 123}, - array: [1, 2, 3], - string: 'test', - }; - res.json(complexObj); - - expect(mockResponseStream.write.calledWith(JSON.stringify(complexObj))).to.be.true; - }); + it('should handle array JSON', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.json([1, 2, 3]); - it('should handle null JSON', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.json(null); + expect(mockResponseStream.write.calledWith(JSON.stringify([1, 2, 3]))).to.be.true; + }); + }); - expect(mockResponseStream.write.calledWith('null')).to.be.true; - }); + describe('send', () => { + it('should send string response', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.send('test'); - it('should handle array JSON', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.json([1, 2, 3]); + expect(mockResponseStream.write.calledWith('test')).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - expect(mockResponseStream.write.calledWith(JSON.stringify([1, 2, 3]))).to.be.true; - }); - }); + it('should send object as JSON', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.send({message: 'test'}); - describe('send', () => { - it('should send string response', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.send('test'); + expect(res.getHeader('Content-Type')).to.equal('application/json'); + expect(mockResponseStream.write.calledWith(JSON.stringify({message: 'test'}))).to.be.true; + }); - expect(mockResponseStream.write.calledWith('test')).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + it('should send empty string', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.send(''); - it('should send object as JSON', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.send({message: 'test'}); + // Empty strings should be written + expect(mockResponseStream.write.calledWith('')).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); - expect(res.getHeader('Content-Type')).to.equal('application/json'); - expect(mockResponseStream.write.calledWith(JSON.stringify({message: 'test'}))).to.be.true; - }); + it('should send number as string', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // send() converts numbers to strings + res.send(123 as any); - it('should send empty string', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.send(''); + // Numbers are converted to strings and sent + expect(mockResponseStream.write.calledWith('123')).to.be.true; + expect(mockResponseStream.end.called).to.be.true; + }); + }); - // Empty strings should be written - expect(mockResponseStream.write.calledWith('')).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); + describe('redirect', () => { + it('should redirect to URL', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.redirect('https://example.com'); - it('should send number as string', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // send() converts numbers to strings - res.send(123 as any); + expect(res.statusCode).to.equal(302); + expect(res.getHeader('Location')).to.equal('https://example.com'); + expect(mockResponseStream.end.called).to.be.true; + }); - // Numbers are converted to strings and sent - expect(mockResponseStream.write.calledWith('123')).to.be.true; - expect(mockResponseStream.end.called).to.be.true; - }); - }); + it('should redirect to relative URL', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.redirect('/other/path'); - describe('redirect', () => { - it('should redirect to URL', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.redirect('https://example.com'); + expect(res.statusCode).to.equal(302); + expect(res.getHeader('Location')).to.equal('/other/path'); + }); + }); - expect(res.statusCode).to.equal(302); - expect(res.getHeader('Location')).to.equal('https://example.com'); - expect(mockResponseStream.end.called).to.be.true; - }); + describe('headersSent property', () => { + it('should be false initially', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + expect(res.headersSent).to.be.false; + }); - it('should redirect to relative URL', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.redirect('/other/path'); + it('should be true after writeHead', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.writeHead(200); + expect(res.headersSent).to.be.true; + }); - expect(res.statusCode).to.equal(302); - expect(res.getHeader('Location')).to.equal('/other/path'); - }); - }); + it('should be true after write', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.write('test'); + expect(res.headersSent).to.be.true; + }); - describe('headersSent property', () => { - it('should be false initially', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - expect(res.headersSent).to.be.false; - }); + it('should be true after end', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end(); + expect(res.headersSent).to.be.true; + }); + }); - it('should be true after writeHead', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.writeHead(200); - expect(res.headersSent).to.be.true; - }); + describe('flush', () => { + it('should flush stream if supported', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it + res.flush(); - it('should be true after write', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.write('test'); - expect(res.headersSent).to.be.true; - }); + expect((mockResponseStream as any).flush.called).to.be.true; + }); - it('should be true after end', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end(); - expect(res.headersSent).to.be.true; - }); - }); + it('should auto-send headers on flush', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it + res.flush(); + + expect(mockHttpResponseStream.from.called).to.be.true; + expect(res.headersSent).to.be.true; + }); - describe('flush', () => { - it('should flush stream if supported', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it - res.flush(); + it('should handle stream without flush method', () => { + const streamWithoutFlush = createMockWritable(); + delete (streamWithoutFlush as any).flush; - expect((mockResponseStream as any).flush.called).to.be.true; - }); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(streamWithoutFlush, event, context); + // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it + const result = res.flush(); - it('should auto-send headers on flush', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it - res.flush(); + expect(result).to.equal(res); + }); - expect(mockHttpResponseStream.from.called).to.be.true; - expect(res.headersSent).to.be.true; - }); + it('should handle flush error gracefully', () => { + const failingStream = createMockWritable(); + (failingStream as any).flush = sinon.stub().callsFake(() => { + throw new Error('Flush failed'); + }); - it('should handle stream without flush method', () => { - const streamWithoutFlush = createMockWritable(); - delete (streamWithoutFlush as any).flush; + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(failingStream, event, context); + // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it + const result = res.flush(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(streamWithoutFlush, event, context); - // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it - const result = res.flush(); + expect(result).to.equal(res); + }); + }); - expect(result).to.equal(res); - }); + describe('pipe', () => { + it('should pipe to destination', () => { + const destination = createMockWritable(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.pipe(destination); + + expect(result).to.equal(destination); + }); - it('should handle flush error gracefully', () => { - const failingStream = createMockWritable(); - (failingStream as any).flush = sinon.stub().callsFake(() => { - throw new Error('Flush failed'); + it('should auto-send headers on pipe', () => { + const destination = createMockWritable(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.pipe(destination); + + expect(mockHttpResponseStream.from.called).to.be.true; + expect(res.headersSent).to.be.true; + }); + + it('should handle pipe with options', () => { + const destination = createMockWritable(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const result = res.pipe(destination, {end: false} as any); + + expect(result).to.equal(destination); + }); }); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(failingStream, event, context); - // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it - const result = res.flush(); + describe('unpipe', () => { + it('should unpipe specific destination', () => { + const destination = createMockWritable(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.pipe(destination); + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + const result = res.unpipe(destination); - expect(result).to.equal(res); - }); - }); + expect(result).to.equal(res); + }); - describe('pipe', () => { - it('should pipe to destination', () => { - const destination = createMockWritable(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.pipe(destination); + it('should unpipe all destinations', () => { + const destination1 = createMockWritable(); + const destination2 = createMockWritable(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.pipe(destination1); + res.pipe(destination2); + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + const result = res.unpipe(); - expect(result).to.equal(destination); - }); + expect(result).to.equal(res); + }); - it('should auto-send headers on pipe', () => { - const destination = createMockWritable(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.pipe(destination); + it('should handle unpipe when no destinations', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + const result = res.unpipe(); - expect(mockHttpResponseStream.from.called).to.be.true; - expect(res.headersSent).to.be.true; - }); + expect(result).to.equal(res); + }); + }); - it('should handle pipe with options', () => { - const destination = createMockWritable(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const result = res.pipe(destination, {end: false} as any); + describe('status code handling', () => { + it('should default to 200 status code', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + expect(res.statusCode).to.equal(200); + }); - expect(result).to.equal(destination); - }); - }); + it('should update status code multiple times', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.status(201); + expect(res.statusCode).to.equal(201); + res.status(404); + expect(res.statusCode).to.equal(404); + }); - describe('unpipe', () => { - it('should unpipe specific destination', () => { - const destination = createMockWritable(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.pipe(destination); - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - const result = res.unpipe(destination); - - expect(result).to.equal(res); - }); + it('should preserve status code through writeHead', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.status(201); + res.writeHead(200); - it('should unpipe all destinations', () => { - const destination1 = createMockWritable(); - const destination2 = createMockWritable(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.pipe(destination1); - res.pipe(destination2); - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - const result = res.unpipe(); - - expect(result).to.equal(res); - }); + expect(res.statusCode).to.equal(200); + }); + }); - it('should handle unpipe when no destinations', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - const result = res.unpipe(); + describe('header operations', () => { + it('should handle getHeader for non-existent header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + expect(res.getHeader('X-Non-Existent')).to.be.undefined; + }); - expect(result).to.equal(res); - }); - }); + it('should handle setHeader with number value', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Length', 123); - describe('status code handling', () => { - it('should default to 200 status code', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - expect(res.statusCode).to.equal(200); - }); + expect(res.getHeader('Content-Length')).to.equal(123); + }); - it('should update status code multiple times', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.status(201); - expect(res.statusCode).to.equal(201); - res.status(404); - expect(res.statusCode).to.equal(404); - }); + it('should handle multiple setHeader calls', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Header1', 'value1'); + res.setHeader('X-Header2', 'value2'); + res.setHeader('X-Header3', 'value3'); - it('should preserve status code through writeHead', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.status(201); - res.writeHead(200); + expect(res.getHeader('X-Header1')).to.equal('value1'); + expect(res.getHeader('X-Header2')).to.equal('value2'); + expect(res.getHeader('X-Header3')).to.equal('value3'); + }); + }); - expect(res.statusCode).to.equal(200); - }); - }); + describe('flushable property', () => { + it('should be set to true', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + expect(res.flushable).to.be.true; + }); + }); - describe('header operations', () => { - it('should handle getHeader for non-existent header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - expect(res.getHeader('X-Non-Existent')).to.be.undefined; - }); + describe('multi-value headers', () => { + it('should convert array headers to comma-separated strings in metadata', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Multi-Value-Header', ['value1', 'value2', 'value3']); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata?.headers['x-multi-value-header']).to.equal('value1,value2,value3'); + }); - it('should handle setHeader with number value', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Length', 123); + it('should handle single value headers normally', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Single-Header', 'value1'); + res.end('test'); - expect(res.getHeader('Content-Length')).to.equal(123); - }); + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata?.headers['x-single-header']).to.equal('value1'); + }); + }); + + describe('cookies', () => { + it('should extract cookies from set-cookie header and add to metadata', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Set-Cookie', ['cookie1=value1', 'cookie2=value2']); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + cookies?: string[]; + headers: Record; + }; + expect(metadata?.cookies).to.deep.equal(['cookie1=value1', 'cookie2=value2']); + expect(metadata?.headers['set-cookie']).to.be.undefined; + }); - it('should handle multiple setHeader calls', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Header1', 'value1'); - res.setHeader('X-Header2', 'value2'); - res.setHeader('X-Header3', 'value3'); - - expect(res.getHeader('X-Header1')).to.equal('value1'); - expect(res.getHeader('X-Header2')).to.equal('value2'); - expect(res.getHeader('X-Header3')).to.equal('value3'); + it('should handle single cookie string', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Set-Cookie', 'cookie1=value1'); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as {cookies?: string[]}; + expect(metadata?.cookies).to.deep.equal(['cookie1=value1']); + }); + }); + + describe('request header copying', () => { + it('should copy x-correlation-id from request to response headers', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'x-correlation-id': 'test-correlation-123', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.equal('test-correlation-123'); + }); + + it('should not include x-correlation-id in response headers when not present in request', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: {}, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.be.undefined; + }); + + it('should copy x-correlation-id when using writeHead', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'x-correlation-id': 'correlation-456', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.writeHead(200); + res.end(); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.equal('correlation-456'); + }); + + it('should copy x-correlation-id when using write', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'x-correlation-id': 'correlation-789', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.write('chunk'); + res.end(); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.equal('correlation-789'); + }); + + it('should copy x-correlation-id when using flushHeaders', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'x-correlation-id': 'correlation-flush', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.flushHeaders(); + res.end(); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.equal('correlation-flush'); + }); + + it('should handle x-correlation-id with case-insensitive matching', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'X-Correlation-ID': 'correlation-case-test', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + expect(metadata?.headers['x-correlation-id']).to.equal('correlation-case-test'); + }); + + it('should overwrite x-correlation-id on response with value from request', () => { + const event = createMockEvent({ + httpMethod: 'GET', + headers: { + 'x-correlation-id': 'request-correlation', + }, + }); + const context = createMockContext(); + const req = createExpressRequest(event, context); + const res = createExpressResponse(mockResponseStream, event, context, req); + res.setHeader('x-correlation-id', 'response-correlation'); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + headers: Record; + }; + // Request header should overwrite response header since request headers are copied after + // response headers are collected in initializeResponse + expect(metadata?.headers['x-correlation-id']).to.equal('request-correlation'); + }); + }); }); - }); - describe('flushable property', () => { - it('should be set to true', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - expect(res.flushable).to.be.true; + describe('createExpressRequest', () => { + describe('multiValueHeaders processing', () => { + it('should handle multiValueHeaders with length > 1', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + headers: {}, + multiValueHeaders: { + 'x-custom': ['value1', 'value2', 'value3'], // Use lowercase key + }, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context = createMockContext(); + const req = createExpressRequest(event, context); + // Should join multi-value headers (key is used as-is from multiValueHeaders) + expect(req.headers['x-custom']).to.equal('value1,value2,value3'); + }); + + it('should skip multiValueHeaders with length <= 1', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + headers: {}, + multiValueHeaders: { + 'X-Custom': ['value1'], // Length is 1, should be skipped + }, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context = createMockContext(); + const req = createExpressRequest(event, context); + // Should not add header with length <= 1 + expect(req.headers['x-custom']).to.be.undefined; + }); + }); + + describe('query parameter merging', () => { + it('should handle duplicate values in merged query parameters', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: { + param1: 'value1', + }, + multiValueQueryStringParameters: { + param1: ['value1', 'value2'], // value1 is duplicate + }, + headers: {}, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context = createMockContext(); + const req = createExpressRequest(event, context); + // Should not duplicate value1 + expect(req.url).to.include('param1=value1'); + expect(req.url).to.include('param1=value2'); + }); + + it('should merge single-value and multi-value query parameters', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: { + param1: 'value1', + param2: 'value2', + }, + multiValueQueryStringParameters: { + param1: ['value1', 'value3'], + param3: ['value4', 'value5'], + }, + headers: {}, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '$LATEST', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test', + getRemainingTimeInMillis: () => 30000, + done: () => {}, + fail: () => {}, + succeed: () => {}, + }; + + const req = createExpressRequest(event, context); + // The URL should contain all query parameters + expect(req.url).to.include('param1'); + expect(req.url).to.include('param2'); + expect(req.url).to.include('param3'); + }); + + it('should handle only single-value query parameters', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: { + param1: 'value1', + param2: 'value2', + }, + multiValueQueryStringParameters: null, + headers: {}, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '$LATEST', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test', + getRemainingTimeInMillis: () => 30000, + done: () => {}, + fail: () => {}, + succeed: () => {}, + }; + + const req = createExpressRequest(event, context); + expect(req.url).to.include('param1=value1'); + expect(req.url).to.include('param2=value2'); + }); + + it('should handle only multi-value query parameters', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: { + param1: ['value1', 'value2'], + }, + headers: {}, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '$LATEST', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test', + getRemainingTimeInMillis: () => 30000, + done: () => {}, + fail: () => {}, + succeed: () => {}, + }; + + const req = createExpressRequest(event, context); + expect(req.url).to.include('param1=value1'); + expect(req.url).to.include('param1=value2'); + }); + + it('should handle path without query parameters', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + headers: {}, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '$LATEST', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test', + getRemainingTimeInMillis: () => 30000, + done: () => {}, + fail: () => {}, + succeed: () => {}, + }; + + const req = createExpressRequest(event, context); + expect(req.url).to.equal('/test'); + }); + }); }); - }); - describe('multi-value headers', () => { - it('should convert array headers to comma-separated strings in metadata', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Multi-Value-Header', ['value1', 'value2', 'value3']); - res.end('test'); + describe('Edge cases and error handling', () => { + describe('initializeResponse edge cases', () => { + it('should handle closed stream in initializeResponse', () => { + const closedStream = createMockWritable(); + (closedStream as any).writable = false; + (closedStream as any).destroyed = true; + + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(closedStream, event, context); + res.setHeader('Content-Type', 'text/html'); + res.write('test'); // This should trigger initializeResponse + + // Should not throw, even with closed stream + expect(mockHttpResponseStream.from.called).to.be.false; + }); + + it('should handle initializeResponse called multiple times', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); + res.write('test'); + const firstCallCount = mockHttpResponseStream.from.callCount; + res.write('more'); // Second write should not re-initialize + + // Should only initialize once + expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); + }); + }); + + describe('convertHeaders edge cases', () => { + it('should handle number header values', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Length', 123); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + // Content-Length should be removed for streaming responses + expect(metadata?.headers['content-length']).to.be.undefined; + }); + + it('should handle array header values in convertHeaders', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Custom', ['value1', 'value2', 'value3']); + res.end('test'); - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata?.headers['x-multi-value-header']).to.equal('value1,value2,value3'); - }); + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata?.headers['x-custom']).to.equal('value1,value2,value3'); + }); - it('should handle single value headers normally', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Single-Header', 'value1'); - res.end('test'); + it('should skip undefined header values', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // Set a header then remove it + res.setHeader('X-Test', 'value'); + res.removeHeader('X-Test'); + res.end('test'); - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata?.headers['x-single-header']).to.equal('value1'); - }); - }); + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata?.headers['x-test']).to.be.undefined; + }); - describe('cookies', () => { - it('should extract cookies from set-cookie header and add to metadata', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Set-Cookie', ['cookie1=value1', 'cookie2=value2']); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - cookies?: string[]; - headers: Record; - }; - expect(metadata?.cookies).to.deep.equal(['cookie1=value1', 'cookie2=value2']); - expect(metadata?.headers['set-cookie']).to.be.undefined; - }); + it('should handle string header values', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-String', 'simple-value'); + res.end('test'); - it('should handle single cookie string', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Set-Cookie', 'cookie1=value1'); - res.end('test'); + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + expect(metadata?.headers['x-string']).to.equal('simple-value'); + }); + }); - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as {cookies?: string[]}; - expect(metadata?.cookies).to.deep.equal(['cookie1=value1']); - }); - }); + describe('cookie handling edge cases', () => { + it('should handle single cookie string (not array)', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Set-Cookie', 'single-cookie=value'); + res.end('test'); + + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + cookies?: string[]; + headers: Record; + }; + expect(metadata?.cookies).to.deep.equal(['single-cookie=value']); + expect(metadata?.headers['set-cookie']).to.be.undefined; + }); - describe('request header copying', () => { - it('should copy x-correlation-id from request to response headers', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'x-correlation-id': 'test-correlation-123', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.equal('test-correlation-123'); - }); + it('should handle no cookies', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.end('test'); - it('should not include x-correlation-id in response headers when not present in request', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: {}, + const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { + cookies?: string[]; + }; + expect(metadata?.cookies).to.be.undefined; + }); }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.be.undefined; - }); - it('should copy x-correlation-id when using writeHead', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'x-correlation-id': 'correlation-456', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.writeHead(200); - res.end(); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.equal('correlation-456'); - }); + describe('status message handling', () => { + it('should set status message when provided', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + // @ts-expect-error - status method signature doesn't match ExpressResponse type exactly + res.status(404, 'Not Found'); + res.end('test'); - it('should copy x-correlation-id when using write', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'x-correlation-id': 'correlation-789', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.write('chunk'); - res.end(); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.equal('correlation-789'); - }); + expect(res.statusMessage).to.equal('Not Found'); + }); - it('should copy x-correlation-id when using flushHeaders', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'x-correlation-id': 'correlation-flush', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.flushHeaders(); - res.end(); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.equal('correlation-flush'); - }); + it('should not set status message when undefined', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.status(200); + res.end('test'); - it('should handle x-correlation-id with case-insensitive matching', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'X-Correlation-ID': 'correlation-case-test', - }, + expect(res.statusMessage).to.be.undefined; + }); }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - expect(metadata?.headers['x-correlation-id']).to.equal('correlation-case-test'); - }); - it('should overwrite x-correlation-id on response with value from request', () => { - const event = createMockEvent({ - httpMethod: 'GET', - headers: { - 'x-correlation-id': 'request-correlation', - }, - }); - const context = createMockContext(); - const req = createExpressRequest(event, context); - const res = createExpressResponse(mockResponseStream, event, context, req); - res.setHeader('x-correlation-id', 'response-correlation'); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - headers: Record; - }; - // Request header should overwrite response header since request headers are copied after - // response headers are collected in initializeResponse - expect(metadata?.headers['x-correlation-id']).to.equal('request-correlation'); - }); - }); - }); + describe('res.set edge cases', () => { + it('should handle res.set with object', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set({ + 'X-Header1': 'value1', + 'X-Header2': 'value2', + }); + res.end('test'); + + expect(res.getHeader('X-Header1')).to.equal('value1'); + expect(res.getHeader('X-Header2')).to.equal('value2'); + }); - describe('createExpressRequest', () => { - describe('multiValueHeaders processing', () => { - it('should handle multiValueHeaders with length > 1', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: null, - multiValueQueryStringParameters: null, - headers: {}, - multiValueHeaders: { - 'x-custom': ['value1', 'value2', 'value3'], // Use lowercase key - }, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context = createMockContext(); - const req = createExpressRequest(event, context); - // Should join multi-value headers (key is used as-is from multiValueHeaders) - expect(req.headers['x-custom']).to.equal('value1,value2,value3'); - }); + it('should skip undefined values in res.set object', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set({ + 'X-Header1': 'value1', + 'X-Header2': undefined, + } as any); + res.end('test'); + + expect(res.getHeader('X-Header1')).to.equal('value1'); + expect(res.getHeader('X-Header2')).to.be.undefined; + }); - it('should skip multiValueHeaders with length <= 1', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: null, - multiValueQueryStringParameters: null, - headers: {}, - multiValueHeaders: { - 'X-Custom': ['value1'], // Length is 1, should be skipped - }, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context = createMockContext(); - const req = createExpressRequest(event, context); - // Should not add header with length <= 1 - expect(req.headers['x-custom']).to.be.undefined; - }); - }); + it('should handle res.set with undefined value', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.set('X-Header', undefined as any); + res.end('test'); - describe('query parameter merging', () => { - it('should handle duplicate values in merged query parameters', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: { - param1: 'value1', - }, - multiValueQueryStringParameters: { - param1: ['value1', 'value2'], // value1 is duplicate - }, - headers: {}, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context = createMockContext(); - const req = createExpressRequest(event, context); - // Should not duplicate value1 - expect(req.url).to.include('param1=value1'); - expect(req.url).to.include('param1=value2'); - }); + expect(res.getHeader('X-Header')).to.be.undefined; + }); + }); - it('should merge single-value and multi-value query parameters', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: { - param1: 'value1', - param2: 'value2', - }, - multiValueQueryStringParameters: { - param1: ['value1', 'value3'], - param3: ['value4', 'value5'], - }, - headers: {}, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context: Context = { - callbackWaitsForEmptyEventLoop: false, - functionName: 'test-function', - functionVersion: '$LATEST', - invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', - memoryLimitInMB: '128', - awsRequestId: 'test-request-id', - logGroupName: '/aws/lambda/test-function', - logStreamName: '2023/01/01/[$LATEST]test', - getRemainingTimeInMillis: () => 30000, - done: () => {}, - fail: () => {}, - succeed: () => {}, - }; - - const req = createExpressRequest(event, context); - // The URL should contain all query parameters - expect(req.url).to.include('param1'); - expect(req.url).to.include('param2'); - expect(req.url).to.include('param3'); - }); + describe('res.append edge cases', () => { + it('should append to existing array header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Header', ['value1', 'value2']); + res.append('X-Header', 'value3'); + res.end('test'); + + const header = res.getHeader('X-Header'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + expect(header).to.include('value3'); + }); - it('should handle only single-value query parameters', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: { - param1: 'value1', - param2: 'value2', - }, - multiValueQueryStringParameters: null, - headers: {}, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context: Context = { - callbackWaitsForEmptyEventLoop: false, - functionName: 'test-function', - functionVersion: '$LATEST', - invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', - memoryLimitInMB: '128', - awsRequestId: 'test-request-id', - logGroupName: '/aws/lambda/test-function', - logStreamName: '2023/01/01/[$LATEST]test', - getRemainingTimeInMillis: () => 30000, - done: () => {}, - fail: () => {}, - succeed: () => {}, - }; - - const req = createExpressRequest(event, context); - expect(req.url).to.include('param1=value1'); - expect(req.url).to.include('param2=value2'); - }); + it('should append array to existing string header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Header', 'value1'); + res.append('X-Header', ['value2', 'value3']); + res.end('test'); + + const header = res.getHeader('X-Header'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + expect(header).to.include('value3'); + }); - it('should handle only multi-value query parameters', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: null, - multiValueQueryStringParameters: { - param1: ['value1', 'value2'], - }, - headers: {}, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context: Context = { - callbackWaitsForEmptyEventLoop: false, - functionName: 'test-function', - functionVersion: '$LATEST', - invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', - memoryLimitInMB: '128', - awsRequestId: 'test-request-id', - logGroupName: '/aws/lambda/test-function', - logStreamName: '2023/01/01/[$LATEST]test', - getRemainingTimeInMillis: () => 30000, - done: () => {}, - fail: () => {}, - succeed: () => {}, - }; - - const req = createExpressRequest(event, context); - expect(req.url).to.include('param1=value1'); - expect(req.url).to.include('param1=value2'); - }); + it('should append string to existing string header', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('X-Header', 'value1'); + res.append('X-Header', 'value2'); + res.end('test'); + + const header = res.getHeader('X-Header'); + expect(Array.isArray(header)).to.equal(true); + expect(header).to.include('value1'); + expect(header).to.include('value2'); + }); - it('should handle path without query parameters', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: null, - multiValueQueryStringParameters: null, - headers: {}, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context: Context = { - callbackWaitsForEmptyEventLoop: false, - functionName: 'test-function', - functionVersion: '$LATEST', - invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', - memoryLimitInMB: '128', - awsRequestId: 'test-request-id', - logGroupName: '/aws/lambda/test-function', - logStreamName: '2023/01/01/[$LATEST]test', - getRemainingTimeInMillis: () => 30000, - done: () => {}, - fail: () => {}, - succeed: () => {}, - }; - - const req = createExpressRequest(event, context); - expect(req.url).to.equal('/test'); - }); - }); - }); + it('should set header if it does not exist in append', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.append('X-Header', 'value1'); + res.end('test'); - describe('Edge cases and error handling', () => { - describe('initializeResponse edge cases', () => { - it('should handle closed stream in initializeResponse', () => { - const closedStream = createMockWritable(); - (closedStream as any).writable = false; - (closedStream as any).destroyed = true; - - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(closedStream, event, context); - res.setHeader('Content-Type', 'text/html'); - res.write('test'); // This should trigger initializeResponse - - // Should not throw, even with closed stream - expect(mockHttpResponseStream.from.called).to.be.false; - }); + expect(res.getHeader('X-Header')).to.equal('value1'); + }); + }); - it('should handle initializeResponse called multiple times', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); - res.write('test'); - const firstCallCount = mockHttpResponseStream.from.callCount; - res.write('more'); // Second write should not re-initialize - - // Should only initialize once - expect(mockHttpResponseStream.from.callCount).to.equal(firstCallCount); - }); - }); + describe('pipeToDestination edge cases', () => { + it('should handle pipeToDestination with closed stream', async () => { + const closedStream = createMockWritable(); + (closedStream as any).writable = false; + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(closedStream, event, context); + const destination = createMockWritable(); - describe('convertHeaders edge cases', () => { - it('should handle number header values', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Length', 123); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - // Content-Length should be removed for streaming responses - expect(metadata?.headers['content-length']).to.be.undefined; - }); + res.pipe(destination); - it('should handle array header values in convertHeaders', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Custom', ['value1', 'value2', 'value3']); - res.end('test'); + // Should handle gracefully + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(destination.write.called).to.be.false; + }); - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata?.headers['x-custom']).to.equal('value1,value2,value3'); - }); + it('should handle pipeToDestination with no source stream', async () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const destination = createMockWritable(); - it('should skip undefined header values', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // Set a header then remove it - res.setHeader('X-Test', 'value'); - res.removeHeader('X-Test'); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata?.headers['x-test']).to.be.undefined; - }); + // Try to pipe before initialization - this will initialize response + res.pipe(destination); - it('should handle string header values', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-String', 'simple-value'); - res.end('test'); + await new Promise((resolve) => setTimeout(resolve, 50)); + // Should handle gracefully - httpResponseStream should be created + expect(mockHttpResponseStream.from.called).to.be.true; + }); - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - expect(metadata?.headers['x-string']).to.equal('simple-value'); - }); - }); + it('should handle pipeToDestination pipeline error', async () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const destination = createMockWritable(); + // Make destination throw an error + (destination as any).write = () => { + throw new Error('Pipeline error'); + }; - describe('cookie handling edge cases', () => { - it('should handle single cookie string (not array)', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Set-Cookie', 'single-cookie=value'); - res.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - cookies?: string[]; - headers: Record; - }; - expect(metadata?.cookies).to.deep.equal(['single-cookie=value']); - expect(metadata?.headers['set-cookie']).to.be.undefined; - }); + res.setHeader('Content-Type', 'text/html'); + res.pipe(destination); - it('should handle no cookies', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.end('test'); + await new Promise((resolve) => setTimeout(resolve, 50)); + // Should handle error gracefully + }); + }); - const metadata = mockHttpResponseStream.from.getCall(0).args[1] as { - cookies?: string[]; - }; - expect(metadata?.cookies).to.be.undefined; - }); - }); + describe('res.unpipe edge cases', () => { + it('should unpipe specific destination', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const dest1 = createMockWritable(); + const dest2 = createMockWritable(); - describe('status message handling', () => { - it('should set status message when provided', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - // @ts-expect-error - status method signature doesn't match ExpressResponse type exactly - res.status(404, 'Not Found'); - res.end('test'); + res.pipe(dest1); + res.pipe(dest2); + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + res.unpipe(dest1); - expect(res.statusMessage).to.equal('Not Found'); - }); + // Should only have dest2 in piped destinations + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + res.unpipe(dest2); + }); - it('should not set status message when undefined', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.status(200); - res.end('test'); + it('should unpipe all destinations', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + const dest1 = createMockWritable(); + const dest2 = createMockWritable(); - expect(res.statusMessage).to.be.undefined; - }); - }); + res.pipe(dest1); + res.pipe(dest2); + // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it + res.unpipe(); - describe('res.set edge cases', () => { - it('should handle res.set with object', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set({ - 'X-Header1': 'value1', - 'X-Header2': 'value2', + // All destinations should be removed + }); }); - res.end('test'); - - expect(res.getHeader('X-Header1')).to.equal('value1'); - expect(res.getHeader('X-Header2')).to.equal('value2'); - }); - it('should skip undefined values in res.set object', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set({ - 'X-Header1': 'value1', - 'X-Header2': undefined, - } as any); - res.end('test'); - - expect(res.getHeader('X-Header1')).to.equal('value1'); - expect(res.getHeader('X-Header2')).to.be.undefined; - }); + describe('writeChunk edge cases', () => { + it('should handle writeChunk with empty chunk', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); + const result = res.write(''); - it('should handle res.set with undefined value', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.set('X-Header', undefined as any); - res.end('test'); + expect(result).to.be.true; + }); - expect(res.getHeader('X-Header')).to.be.undefined; - }); - }); + it('should handle writeChunk with closed stream', () => { + const stream = createCollectingStream(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context); - describe('res.append edge cases', () => { - it('should append to existing array header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Header', ['value1', 'value2']); - res.append('X-Header', 'value3'); - res.end('test'); - - const header = res.getHeader('X-Header'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - expect(header).to.include('value3'); - }); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // Close the stream + stream.destroy(); - it('should append array to existing string header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Header', 'value1'); - res.append('X-Header', ['value2', 'value3']); - res.end('test'); - - const header = res.getHeader('X-Header'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - expect(header).to.include('value3'); - }); + // Should handle gracefully + const result = response.write('more'); + expect(result).to.be.false; + }); - it('should append string to existing string header', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('X-Header', 'value1'); - res.append('X-Header', 'value2'); - res.end('test'); - - const header = res.getHeader('X-Header'); - expect(Array.isArray(header)).to.equal(true); - expect(header).to.include('value1'); - expect(header).to.include('value2'); - }); + it('should handle writeChunk when compression stream is not writable', async () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - it('should set header if it does not exist in append', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.append('X-Header', 'value1'); - res.end('test'); + response.setHeader('Content-Type', 'text/html'); + // First write should succeed + const firstWrite = response.write('test'); + expect(firstWrite).to.be.true; - expect(res.getHeader('X-Header')).to.equal('value1'); - }); - }); + // Write more data + response.write('more'); - describe('pipeToDestination edge cases', () => { - it('should handle pipeToDestination with closed stream', async () => { - const closedStream = createMockWritable(); - (closedStream as any).writable = false; - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(closedStream, event, context); - const destination = createMockWritable(); + // End the response + response.end('done'); - res.pipe(destination); + await stream.waitForEnd(); + await new Promise((resolve) => setTimeout(resolve, 50)); - // Should handle gracefully - await new Promise((resolve) => setTimeout(resolve, 50)); - expect(destination.write.called).to.be.false; - }); + // Should have written data + expect(stream.getData().length).to.be.greaterThan(0); + }); - it('should handle pipeToDestination with no source stream', async () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const destination = createMockWritable(); + it('should handle writeChunk when httpResponseStream is not writable', () => { + const stream = createCollectingStream(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context); - // Try to pipe before initialization - this will initialize response - res.pipe(destination); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // Destroy stream + stream.destroy(); - await new Promise((resolve) => setTimeout(resolve, 50)); - // Should handle gracefully - httpResponseStream should be created - expect(mockHttpResponseStream.from.called).to.be.true; - }); + // Should handle gracefully + const result = response.write('more'); + expect(result).to.be.false; + }); - it('should handle pipeToDestination pipeline error', async () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const destination = createMockWritable(); - // Make destination throw an error - (destination as any).write = () => { - throw new Error('Pipeline error'); - }; - - res.setHeader('Content-Type', 'text/html'); - res.pipe(destination); - - await new Promise((resolve) => setTimeout(resolve, 50)); - // Should handle error gracefully - }); - }); + it('should handle writeChunk when neither compression nor httpResponseStream is available', () => { + const closedStream = createMockWritable(); + (closedStream as any).writable = false; + (closedStream as any).destroyed = true; + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const response = createExpressResponse(closedStream, event, context); - describe('res.unpipe edge cases', () => { - it('should unpipe specific destination', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const dest1 = createMockWritable(); - const dest2 = createMockWritable(); - - res.pipe(dest1); - res.pipe(dest2); - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - res.unpipe(dest1); - - // Should only have dest2 in piped destinations - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - res.unpipe(dest2); - }); + response.setHeader('Content-Type', 'text/html'); + // Should return false when stream is closed + const result = response.write('test'); + expect(result).to.be.false; + }); - it('should unpipe all destinations', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - const dest1 = createMockWritable(); - const dest2 = createMockWritable(); + it('should handle writeChunk error gracefully', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); - res.pipe(dest1); - res.pipe(dest2); - // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it - res.unpipe(); + // Mock write to throw an error + const originalWrite = mockResponseStream.write; + (mockResponseStream as any).write = sinon.stub().callsFake(() => { + throw new Error('Write error'); + }); - // All destinations should be removed - }); - }); + // Should handle error gracefully + const result = res.write('test'); + expect(result).to.be.false; - describe('writeChunk edge cases', () => { - it('should handle writeChunk with empty chunk', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); - const result = res.write(''); + // Restore original write + mockResponseStream.write = originalWrite; + }); + }); - expect(result).to.be.true; - }); + describe('endStream edge cases', () => { + it('should handle endStream with compression stream error', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - it('should handle writeChunk with closed stream', () => { - const stream = createCollectingStream(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // End should handle compression stream errors gracefully + response.end('more'); + }); - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // Close the stream - stream.destroy(); + it('should handle endStream with httpResponseStream error', () => { + const stream = createCollectingStream(); + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context); - // Should handle gracefully - const result = response.write('more'); - expect(result).to.be.false; - }); + response.setHeader('Content-Type', 'text/html'); + // End should handle httpResponseStream errors gracefully + response.end('test'); + }); - it('should handle writeChunk when compression stream is not writable', async () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); + it('should handle endStream when compression stream is not writable', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); + + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // Close the stream before ending + stream.destroy(); + // Should handle gracefully + response.end('more'); + }); - response.setHeader('Content-Type', 'text/html'); - // First write should succeed - const firstWrite = response.write('test'); - expect(firstWrite).to.be.true; + it('should handle endStream when compression stream has flush method', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - // Write more data - response.write('more'); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // End should call flush if available + response.end('more'); + }); - // End the response - response.end('done'); + it('should handle endStream when compression stream does not have flush method', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('deflate'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'deflate'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - await stream.waitForEnd(); - await new Promise((resolve) => setTimeout(resolve, 50)); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // End should work even without flush method + response.end('more'); + }); + }); - // Should have written data - expect(stream.getData().length).to.be.greaterThan(0); - }); + describe('getBestEncoding edge cases', () => { + it('should handle getBestEncoding with compression disabled', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const compressionConfig = { + enabled: false, + }; + const response = createExpressResponse(stream, event, context, request, compressionConfig); - it('should handle writeChunk when httpResponseStream is not writable', () => { - const stream = createCollectingStream(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context); + response.setHeader('Content-Type', 'text/html'); + response.end('test'); - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // Destroy stream - stream.destroy(); + const metadata = stream.getMetadata(); + expect(metadata?.headers['content-encoding']).to.be.undefined; + }); - // Should handle gracefully - const result = response.write('more'); - expect(result).to.be.false; - }); + it('should handle getBestEncoding with Accept-Encoding header', () => { + const event: APIGatewayProxyEvent = { + httpMethod: 'GET', + path: '/test', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + headers: { + 'Accept-Encoding': 'br, gzip', // Put br first to ensure it's selected + }, + multiValueHeaders: {}, + body: null, + isBase64Encoded: false, + requestContext: createMockEvent().requestContext, + resource: '/test', + stageVariables: null, + } as APIGatewayProxyEvent; - it('should handle writeChunk when neither compression nor httpResponseStream is available', () => { - const closedStream = createMockWritable(); - (closedStream as any).writable = false; - (closedStream as any).destroyed = true; - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const response = createExpressResponse(closedStream, event, context); - - response.setHeader('Content-Type', 'text/html'); - // Should return false when stream is closed - const result = response.write('test'); - expect(result).to.be.false; - }); + const context = createMockContext(); + const request = createExpressRequest(event, context); + const response = createExpressResponse(mockResponseStream, event, context, request); - it('should handle writeChunk error gracefully', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); + response.setHeader('Content-Type', 'text/html'); + response.end('test'); - // Mock write to throw an error - const originalWrite = mockResponseStream.write; - (mockResponseStream as any).write = sinon.stub().callsFake(() => { - throw new Error('Write error'); + const metadata = mockHttpResponseStream.from.getCall(0).args[1]; + // Should prefer br over gzip based on preference order + expect(metadata?.headers['content-encoding']).to.equal('br'); + }); }); - // Should handle error gracefully - const result = res.write('test'); - expect(result).to.be.false; + describe('initializeCompression edge cases', () => { + it('should not initialize compression if already initialized', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - // Restore original write - mockResponseStream.write = originalWrite; - }); - }); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // Second write should not re-initialize compression + response.write('more'); + response.end('done'); + }); - describe('endStream edge cases', () => { - it('should handle endStream with compression stream error', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // End should handle compression stream errors gracefully - response.end('more'); - }); + it('should not initialize compression when enabled is false', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const compressionConfig = { + enabled: false, + }; + const response = createExpressResponse(stream, event, context, request, compressionConfig); - it('should handle endStream with httpResponseStream error', () => { - const stream = createCollectingStream(); - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context); + response.setHeader('Content-Type', 'text/html'); + response.end('test'); - response.setHeader('Content-Type', 'text/html'); - // End should handle httpResponseStream errors gracefully - response.end('test'); - }); + const metadata = stream.getMetadata(); + expect(metadata?.headers['content-encoding']).to.be.undefined; + }); - it('should handle endStream when compression stream is not writable', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // Close the stream before ending - stream.destroy(); - // Should handle gracefully - response.end('more'); - }); + it('should handle compression stream creation error', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('invalid-encoding'); + const event = createMockEvent({ + httpMethod: 'GET', + headers: {'Accept-Encoding': 'invalid-encoding'}, + }); + const context = createMockContext(); + // This should not crash, but handle gracefully + // Note: getBestEncoding will return null for invalid encoding + const response = createExpressResponse(stream, event, context, request); - it('should handle endStream when compression stream has flush method', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // End should call flush if available - response.end('more'); - }); + response.setHeader('Content-Type', 'text/html'); + response.end('test'); + }); - it('should handle endStream when compression stream does not have flush method', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('deflate'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'deflate'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // End should work even without flush method - response.end('more'); - }); - }); + it('should handle initializeCompression error during stream creation', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - describe('getBestEncoding edge cases', () => { - it('should handle getBestEncoding with compression disabled', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const compressionConfig = { - enabled: false, - }; - const response = createExpressResponse(stream, event, context, request, compressionConfig); - - response.setHeader('Content-Type', 'text/html'); - response.end('test'); - - const metadata = stream.getMetadata(); - expect(metadata?.headers['content-encoding']).to.be.undefined; - }); + response.setHeader('Content-Type', 'text/html'); + // Compression should initialize successfully + response.write('test'); + response.end('more'); + }); - it('should handle getBestEncoding with Accept-Encoding header', () => { - const event: APIGatewayProxyEvent = { - httpMethod: 'GET', - path: '/test', - pathParameters: null, - queryStringParameters: null, - multiValueQueryStringParameters: null, - headers: { - 'Accept-Encoding': 'br, gzip', // Put br first to ensure it's selected - }, - multiValueHeaders: {}, - body: null, - isBase64Encoded: false, - requestContext: createMockEvent().requestContext, - resource: '/test', - stageVariables: null, - } as APIGatewayProxyEvent; - - const context = createMockContext(); - const request = createExpressRequest(event, context); - const response = createExpressResponse(mockResponseStream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.end('test'); - - const metadata = mockHttpResponseStream.from.getCall(0).args[1]; - // Should prefer br over gzip based on preference order - expect(metadata?.headers['content-encoding']).to.equal('br'); - }); - }); + it('should handle compression stream error event', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - describe('initializeCompression edge cases', () => { - it('should not initialize compression if already initialized', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // Second write should not re-initialize compression - response.write('more'); - response.end('done'); - }); + response.setHeader('Content-Type', 'text/html'); + response.write('test'); + // Compression stream error should be handled gracefully + response.end('more'); + }); - it('should not initialize compression when enabled is false', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const compressionConfig = { - enabled: false, - }; - const response = createExpressResponse(stream, event, context, request, compressionConfig); - - response.setHeader('Content-Type', 'text/html'); - response.end('test'); - - const metadata = stream.getMetadata(); - expect(metadata?.headers['content-encoding']).to.be.undefined; - }); + it('should handle initializeCompression catch block', async () => { + // This is hard to test directly, but we can verify the error handling path exists + // by ensuring compression still works normally + const stream = createCollectingStream(); + + // Override the mock for this test to use the collecting stream mock + const originalFrom = (globalThis as any).awslambda.HttpResponseStream.from; + (globalThis as any).awslambda.HttpResponseStream.from = (s: Writable, m: any) => { + const originalStream = (s as any).__originalStream || s; + originalStream.__metadata = m; + const passThrough = new PassThrough(); + passThrough.pipe(s); + return passThrough; + }; + + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - it('should handle compression stream creation error', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('invalid-encoding'); - const event = createMockEvent({ - httpMethod: 'GET', - headers: {'Accept-Encoding': 'invalid-encoding'}, - }); - const context = createMockContext(); - // This should not crash, but handle gracefully - // Note: getBestEncoding will return null for invalid encoding - const response = createExpressResponse(stream, event, context, request); + response.setHeader('Content-Type', 'text/html'); + response.end('test'); - response.setHeader('Content-Type', 'text/html'); - response.end('test'); - }); + await stream.waitForEnd(); + await new Promise((resolve) => setTimeout(resolve, 50)); - it('should handle initializeCompression error during stream creation', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - // Compression should initialize successfully - response.write('test'); - response.end('more'); - }); + // Compression should work normally + const metadata = stream.getMetadata(); + expect(metadata?.headers['content-encoding']).to.equal('gzip'); - it('should handle compression stream error event', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.write('test'); - // Compression stream error should be handled gracefully - response.end('more'); - }); + // Restore original mock + (globalThis as any).awslambda.HttpResponseStream.from = originalFrom; + }); + }); - it('should handle initializeCompression catch block', async () => { - // This is hard to test directly, but we can verify the error handling path exists - // by ensuring compression still works normally - const stream = createCollectingStream(); - - // Override the mock for this test to use the collecting stream mock - const originalFrom = (globalThis as any).awslambda.HttpResponseStream.from; - (globalThis as any).awslambda.HttpResponseStream.from = (s: Writable, m: any) => { - const originalStream = (s as any).__originalStream || s; - originalStream.__metadata = m; - const passThrough = new PassThrough(); - passThrough.pipe(s); - return passThrough; - }; - - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - response.end('test'); - - await stream.waitForEnd(); - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Compression should work normally - const metadata = stream.getMetadata(); - expect(metadata?.headers['content-encoding']).to.equal('gzip'); - - // Restore original mock - (globalThis as any).awslambda.HttpResponseStream.from = originalFrom; - }); - }); + describe('writeChunk with backpressure', () => { + it('should handle writeChunk returning false (backpressure)', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); - describe('writeChunk with backpressure', () => { - it('should handle writeChunk returning false (backpressure)', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - // Write should handle backpressure - const result = response.write('test'); - expect(typeof result).to.equal('boolean'); - }); - }); + response.setHeader('Content-Type', 'text/html'); + // Write should handle backpressure + const result = response.write('test'); + expect(typeof result).to.equal('boolean'); + }); + }); - describe('res.end with backpressure', () => { - it('should handle res.end with backpressure and wait for drain', () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - - response.setHeader('Content-Type', 'text/html'); - // End with chunk should handle backpressure - response.end('test'); - }); + describe('res.end with backpressure', () => { + it('should handle res.end with backpressure and wait for drain', () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); + + response.setHeader('Content-Type', 'text/html'); + // End with chunk should handle backpressure + response.end('test'); + }); - it('should handle res.end without chunk', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); - res.write('test'); - res.end(); // End without chunk + it('should handle res.end without chunk', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); + res.write('test'); + res.end(); // End without chunk - expect(mockResponseStream.end.called).to.be.true; - }); + expect(mockResponseStream.end.called).to.be.true; + }); - it('should handle res.end with chunk and backpressure - wait for drain', async () => { - const stream = createCollectingStream(); - const request = createRequestWithEncoding('gzip'); - const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); - const context = createMockContext(); - const response = createExpressResponse(stream, event, context, request); - response.setHeader('Content-Type', 'text/html'); + it('should handle res.end with chunk and backpressure - wait for drain', async () => { + const stream = createCollectingStream(); + const request = createRequestWithEncoding('gzip'); + const event = createMockEvent({httpMethod: 'GET', headers: {'Accept-Encoding': 'gzip'}}); + const context = createMockContext(); + const response = createExpressResponse(stream, event, context, request); + response.setHeader('Content-Type', 'text/html'); - // Create a mock compression stream that returns false on write (backpressure) - // This is tricky to test directly, so we'll just verify the end works - response.write('test'); - response.end('more'); + // Create a mock compression stream that returns false on write (backpressure) + // This is tricky to test directly, so we'll just verify the end works + response.write('test'); + response.end('more'); - await stream.waitForEnd(); - await new Promise((resolve) => setTimeout(resolve, 50)); + await stream.waitForEnd(); + await new Promise((resolve) => setTimeout(resolve, 50)); - // Should handle backpressure gracefully - expect(stream.getData().length).to.be.greaterThan(0); - }); + // Should handle backpressure gracefully + expect(stream.getData().length).to.be.greaterThan(0); + }); - it('should handle res.end with backpressure when no stream to wait for', () => { - const closedStream = createMockWritable(); - (closedStream as any).writable = false; - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(closedStream, event, context); - res.setHeader('Content-Type', 'text/html'); + it('should handle res.end with backpressure when no stream to wait for', () => { + const closedStream = createMockWritable(); + (closedStream as any).writable = false; + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(closedStream, event, context); + res.setHeader('Content-Type', 'text/html'); - // Write should fail (stream closed), then end should handle the else branch - res.write('test'); - res.end('more'); + // Write should fail (stream closed), then end should handle the else branch + res.write('test'); + res.end('more'); - // Should handle gracefully even when stream is closed - }); + // Should handle gracefully even when stream is closed + }); - it('should handle res.end with chunk and no backpressure', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); + it('should handle res.end with chunk and no backpressure', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); - // End with chunk when there's no backpressure - res.end('test'); + // End with chunk when there's no backpressure + res.end('test'); - expect(mockResponseStream.write.called).to.be.true; - }); - }); + expect(mockResponseStream.write.called).to.be.true; + }); + }); - describe('res.write edge cases', () => { - it('should handle res.write with Buffer', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); - const result = res.write(Buffer.from('test')); + describe('res.write edge cases', () => { + it('should handle res.write with Buffer', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); + const result = res.write(Buffer.from('test')); - expect(result).to.be.true; - }); + expect(result).to.be.true; + }); - it('should handle res.write with Uint8Array', () => { - const event = createMockEvent({httpMethod: 'GET'}); - const context = createMockContext(); - const res = createExpressResponse(mockResponseStream, event, context); - res.setHeader('Content-Type', 'text/html'); - const uint8Array = new Uint8Array([116, 101, 115, 116]); - const result = res.write(uint8Array); + it('should handle res.write with Uint8Array', () => { + const event = createMockEvent({httpMethod: 'GET'}); + const context = createMockContext(); + const res = createExpressResponse(mockResponseStream, event, context); + res.setHeader('Content-Type', 'text/html'); + const uint8Array = new Uint8Array([116, 101, 115, 116]); + const result = res.write(uint8Array); - expect(result).to.be.true; - }); - }); + expect(result).to.be.true; + }); + }); }); }); });