File size: 3,677 Bytes
46252cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import type { Request, Response } from 'express';
import { IngressController } from './ingress.controller';
import { IngressService } from './ingress.service';

// A byte sequence whose JSON.stringify(parse(x)) would NOT round-trip identically: extra whitespace,
// key order, and a trailing newline. The controller must forward the RAW bytes, not a re-serialized body.
const RAW = '{\n  "event": "message_created",\n  "id": 42\n}\n';

function fakeRes() {
  const captured: { status?: number; body?: string; headers?: Record<string, string> } = {};
  const res = {
    status: jest.fn((code: number) => {
      captured.status = code;
      return res;
    }),
    send: jest.fn((body: string) => {
      captured.body = body;
      return res;
    }),
    set: jest.fn((headers: Record<string, string>) => {
      captured.headers = headers;
      return res;
    }),
  } as unknown as Response;
  return { res, captured };
}

describe('IngressController', () => {
  it('forwards the RAW request bytes byte-identically to the pipeline', async () => {
    const handle = jest.fn().mockResolvedValue({ status: 202, body: 'accepted' });
    const controller = new IngressController({ handle } as unknown as IngressService);

    const { res, captured } = fakeRes();
    const req = {
      method: 'POST',
      params: { path: ['chatwoot'] },
      headers: { 'x-delivery': 'd1', 'content-type': 'application/json' },
      rawBody: Buffer.from(RAW, 'utf8'),
    } as unknown as Request & { rawBody?: Buffer };

    await controller.receive('chatwoot', 'acct1', {}, req, res);

    expect(handle).toHaveBeenCalledTimes(1);
    const arg = (handle.mock.calls[0] as [{ rawBody: string; route: string; method: string }])[0];
    // Byte-for-byte identical to what the provider signed — no JSON round-trip.
    expect(arg.rawBody).toBe(RAW);
    expect(arg.route).toBe('chatwoot');
    expect(arg.method).toBe('POST');
    expect(captured.status).toBe(202);
    expect(captured.body).toBe('accepted');
  });

  it('lower-cases headers and tolerates an absent rawBody (empty string)', async () => {
    const handle = jest.fn().mockResolvedValue({ status: 200, body: '' });
    const controller = new IngressController({ handle } as unknown as IngressService);

    const { res } = fakeRes();
    const req = {
      method: 'GET',
      params: { path: ['meta', 'webhook'] },
      headers: { 'X-Verify-Token': 'vtok' },
    } as unknown as Request & { rawBody?: Buffer };

    await controller.receive('meta', 'acct1', { 'hub.challenge': '1' }, req, res);

    const arg = (handle.mock.calls[0] as [{ headers: Record<string, string>; route: string; rawBody: string }])[0];
    // Header keys lower-cased; multi-segment splat reduced to the first route segment.
    expect(arg.headers['x-verify-token']).toBe('vtok');
    expect(arg.route).toBe('meta');
    expect(arg.rawBody).toBe('');
  });

  it('forwards response headers from the pipeline', async () => {
    const handle = jest.fn().mockResolvedValue({
      status: 200,
      body: '{"ok":true}',
      headers: { 'content-type': 'application/json' },
    });
    const controller = new IngressController({ handle } as unknown as IngressService);
    const { res, captured } = fakeRes();
    const req = {
      method: 'POST',
      params: { path: ['send-sms'] },
      headers: { 'x-delivery': 'd1' },
      rawBody: Buffer.from('{}'),
    } as unknown as Request & { rawBody?: Buffer };
    await controller.receive('p', 'i1', {}, req, res);
    expect(captured.status).toBe(200);
    expect(captured.body).toBe('{"ok":true}');
    expect(captured.headers).toEqual({ 'content-type': 'application/json' });
  });
});