Repository: pmcelhaney/counterfact (ts)

Issue:
Custom headers not being correctly added from the response builder
The docs indicate a given mock can add a custom header like this:

```
return $.response[200].header('some-custom-header', 'someValue').json({ whatever: 'value'});
```

The custom value never gets added to the response.

Pull request description:
fix(server): fixes Custom headers not being correctly added from the response builder #593
fix #593

Corrects the logic in `koa-middleware.js` to properly add any custom response headers. Also fleshes out some of the types in `dispatcher` to be more explicit about the response types coming to the `koa-middleware`.

Patch:
diff --git a/.changeset/famous-humans-juggle.md b/.changeset/famous-humans-juggle.md
new file mode 100644
index 0000000..65f4376
--- /dev/null
+++ b/.changeset/famous-humans-juggle.md
@@ -0,0 +1,5 @@
+---
+"counterfact": patch
+---
+
+Fixes issue #593 where custom response headers were not being set
diff --git a/src/server/dispatcher.ts b/src/server/dispatcher.ts
index 19d4f07..a90d244 100644
--- a/src/server/dispatcher.ts
+++ b/src/server/dispatcher.ts
@@ -10,6 +10,7 @@ import type { ContextRegistry } from "./context-registry.js";
 import type {
   CounterfactResponseObject,
   HttpMethods,
+  NormalizedCounterfactResponseObject,
   Registry,
 } from "./registry.js";
 import {
@@ -133,7 +134,7 @@ export class Dispatcher {
   private normalizeResponse(
     response: CounterfactResponseObject,
     acceptHeader: string,
-  ) {
+  ): NormalizedCounterfactResponseObject {
     if (typeof response === "string") {
       return {
         body: response,
@@ -156,7 +157,8 @@ export class Dispatcher {
 
       const normalizedResponse = {
         ...response,
-        body: content.body,
+        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+        body: content.body as string | undefined,
         contentType: content.type,
       };
 
@@ -233,7 +235,7 @@ export class Dispatcher {
     req: {
       path?: string;
     };
-  }) {
+  }): Promise<NormalizedCounterfactResponseObject> {
     debug(`request: ${method} ${path}`);
 
     const { matchedPath } = this.registry.handler(path);
@@ -290,10 +292,13 @@ export class Dispatcher {
 
     if (
       !mediaTypes(headers.accept ?? "*/*").some((type) =>
-        this.isMediaType(normalizedResponse.contentType, type),
+        this.isMediaType(normalizedResponse.contentType ?? "", type),
       )
     ) {
-      return { body: mediaTypes(headers.accept ?? "*/*"), status: 406 };
+      return {
+        body: JSON.stringify(mediaTypes(headers.accept ?? "*/*")),
+        status: 406,
+      };
     }
 
     return normalizedResponse;
diff --git a/src/server/koa-middleware.ts b/src/server/koa-middleware.ts
index 5e5ac1e..1608620 100644
--- a/src/server/koa-middleware.ts
+++ b/src/server/koa-middleware.ts
@@ -61,6 +61,11 @@ export function koaMiddleware(
 
     /* eslint-disable require-atomic-updates */
     ctx.body = response.body;
+    if (response.headers) {
+      for (const [key, value] of Object.entries(response.headers)) {
+        ctx.set(key, value.toString());
+      }
+    }
     ctx.status = response.status ?? HTTP_STATUS_CODE_OK;
     /* eslint-enable require-atomic-updates */
 
diff --git a/src/server/registry.ts b/src/server/registry.ts
index 9a88d40..8de9bae 100644
--- a/src/server/registry.ts
+++ b/src/server/registry.ts
@@ -60,6 +60,7 @@ type CounterfactResponseObject =
         body: unknown;
         type: MediaType;
       }[];
+      contentType?: string;
       headers?: { [key: string]: number | string };
       status?: number;
     };
@@ -68,6 +69,17 @@ type CounterfactResponse =
   | CounterfactResponseObject
   | Promise<CounterfactResponseObject>;
 
+interface NormalizedCounterfactResponseObject {
+  body?: string;
+  content?: {
+    body: unknown;
+    type: MediaType;
+  }[];
+  contentType?: string;
+  headers?: { [key: string]: number | string };
+  status?: number;
+}
+
 function castParameters(
   parameters: { [key: string]: number | string },
   parameterTypes?: { [key: string]: string },
@@ -245,5 +257,6 @@ export type {
   CounterfactResponseObject,
   HttpMethods,
   Module,
+  NormalizedCounterfactResponseObject,
   RequestDataWithBody,
 };


Test patch:
diff --git a/test/server/koa-middleware.test.ts b/test/server/koa-middleware.test.ts
index 2a25919..07b887c 100644
--- a/test/server/koa-middleware.test.ts
+++ b/test/server/koa-middleware.test.ts
@@ -216,4 +216,51 @@ describe("koa middleware", () => {
       "X-My-Header,X-Another-Header",
     );
   });
+
+  it("adds custom response builder headers", async () => {
+    const registry = new Registry();
+
+    registry.add("/hello", {
+      POST({ body }) {
+        return {
+          // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+          body: `Hello, ${(body as { name: string }).name}!`,
+
+          headers: {
+            "X-Custom-Header": "custom value",
+          },
+        };
+      },
+    });
+
+    const dispatcher = new Dispatcher(registry, new ContextRegistry());
+    const middleware = koaMiddleware(dispatcher);
+    const ctx = {
+      body: undefined,
+
+      req: {
+        path: "/hello",
+      },
+
+      request: {
+        body: { name: "Homer" },
+        method: "POST",
+        path: "/hello",
+      },
+
+      set: jest.fn(),
+
+      status: undefined,
+    };
+
+    // @ts-expect-error - not obvious how to make TS happy here, and it's just a unit test
+    await middleware(ctx, async () => {
+      await Promise.resolve(undefined);
+    });
+
+    expect(ctx.status).toBe(200);
+    expect(ctx.body).toBe("Hello, Homer!");
+
+    expect(ctx.set).toHaveBeenCalledWith("X-Custom-Header", "custom value");
+  });
 });