@proulr/bff-core 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proulr/bff-core",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -20,18 +20,21 @@
20
20
  "typecheck": "tsc --noEmit",
21
21
  "test": "vitest run",
22
22
  "test:watch": "vitest",
23
+ "test:integration": "vitest run --config vitest.integration.config.ts",
24
+ "test:integration:coverage": "vitest run --config vitest.integration.config.ts --coverage",
23
25
  "prepublishOnly": "npm run typecheck",
24
26
  "lint": "eslint .",
25
27
  "lint:fix": "eslint . --fix",
26
- "version:sync": "node ../scripts/proulr-bump-and-sync-deps.mjs"
28
+ "version:sync": "node ../scripts/proulr-bump-and-sync-deps.mjs",
29
+ "test:coverage": "vitest run --coverage"
27
30
  },
28
31
  "publishConfig": {
29
32
  "access": "public"
30
33
  },
31
34
  "dependencies": {
32
35
  "@hono/node-server": "^1.19.6",
33
- "@proulr/rosetta": "^0.1.4",
34
- "@proulr/sdui-contract": "^0.1.5",
36
+ "@proulr/rosetta": "^0.1.23",
37
+ "@proulr/sdui-contract": "^0.1.10",
35
38
  "@supabase/supabase-js": "^2.107.0",
36
39
  "dotenv": "^16.6.1",
37
40
  "hono": "^4.10.6",
@@ -40,6 +43,7 @@
40
43
  "devDependencies": {
41
44
  "@eslint/js": "^9.39.4",
42
45
  "@types/node": "^22.10.0",
46
+ "@vitest/coverage-v8": "^3.2.7",
43
47
  "eslint": "^9.0.0",
44
48
  "globals": "^16.0.0",
45
49
  "husky": "^9.1.7",
@@ -0,0 +1,212 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ type ServeCallback = (info: { address: string; family: string; port: number }) => void;
4
+
5
+ const serveMock = vi.fn((opts: unknown, cb?: ServeCallback) => {
6
+ void opts;
7
+ cb?.({ address: '0.0.0.0', family: 'IPv4', port: 9999 });
8
+ return { close: vi.fn() };
9
+ });
10
+
11
+ vi.mock('@hono/node-server', () => ({
12
+ serve: (opts: unknown, cb?: ServeCallback) => serveMock(opts, cb),
13
+ }));
14
+
15
+ import { createSduiBff } from './index';
16
+
17
+ function createTestApp(overrides?: {
18
+ metricsEnv?: string;
19
+ actions?: Parameters<typeof createSduiBff>[0]['actions'];
20
+ }) {
21
+ if (overrides?.metricsEnv !== undefined) {
22
+ vi.stubEnv('BFF_METRICS', overrides.metricsEnv);
23
+ }
24
+ return createSduiBff({
25
+ name: 'Integration BFF',
26
+ port: 9999,
27
+ bffId: 'integration-bff',
28
+ screens: {
29
+ 'demo.home': async (ctx) => ({
30
+ schema_version: 1,
31
+ screen_id: 'demo.home',
32
+ blocks: [],
33
+ data: {
34
+ auth_token: ctx.authToken ?? null,
35
+ locale: ctx.query?.locale ?? null,
36
+ },
37
+ meta: { bff: 'integration-bff' },
38
+ }),
39
+ },
40
+ actions: overrides?.actions ?? {
41
+ ping: async (_ctx, body) => ({ echo: body }),
42
+ boom: async () => {
43
+ throw new Error('boom_failed');
44
+ },
45
+ boom_string: async () => {
46
+ throw 'string_error';
47
+ },
48
+ },
49
+ });
50
+ }
51
+
52
+ describe('createSduiBff HTTP integration', () => {
53
+ beforeEach(() => {
54
+ serveMock.mockClear();
55
+ vi.unstubAllEnvs();
56
+ });
57
+
58
+ afterEach(() => {
59
+ vi.unstubAllEnvs();
60
+ });
61
+
62
+ it('serves /health with screens and metrics flag', async () => {
63
+ const { app } = createTestApp({ metricsEnv: '1' });
64
+ const response = await app.request('/health');
65
+ expect(response.status).toBe(200);
66
+ expect(response.headers.get('X-Request-Id')).toBeTruthy();
67
+ await expect(response.json()).resolves.toMatchObject({
68
+ ok: true,
69
+ bff: 'integration-bff',
70
+ name: 'Integration BFF',
71
+ screens: ['demo.home'],
72
+ metrics_enabled: true,
73
+ });
74
+ });
75
+
76
+ it('returns screen wire and parses Bearer auth + query', async () => {
77
+ const { app } = createTestApp();
78
+ const response = await app.request('/v1/screens/demo.home?locale=pt-BR', {
79
+ headers: { Authorization: 'Bearer tok-abc ' },
80
+ });
81
+ expect(response.status).toBe(200);
82
+ const body = await response.json();
83
+ expect(body.screen_id).toBe('demo.home');
84
+ expect(body.data).toMatchObject({ auth_token: 'tok-abc', locale: 'pt-BR' });
85
+ });
86
+
87
+ it('ignores non-Bearer Authorization header', async () => {
88
+ const { app } = createTestApp();
89
+ const response = await app.request('/v1/screens/demo.home', {
90
+ headers: { Authorization: 'Basic abc' },
91
+ });
92
+ expect(response.status).toBe(200);
93
+ const body = await response.json();
94
+ expect(body.data.auth_token).toBeNull();
95
+ });
96
+
97
+ it('returns 404 for unknown screen', async () => {
98
+ const { app } = createTestApp();
99
+ const response = await app.request('/v1/screens/missing.screen');
100
+ expect(response.status).toBe(404);
101
+ await expect(response.json()).resolves.toMatchObject({
102
+ error: 'screen_not_found',
103
+ screen_id: 'missing.screen',
104
+ });
105
+ });
106
+
107
+ it('invokes registered action with auth and body', async () => {
108
+ const { app } = createTestApp();
109
+ const response = await app.request('/v1/actions/ping', {
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ Authorization: 'Bearer act-tok',
114
+ },
115
+ body: JSON.stringify({ n: 1 }),
116
+ });
117
+ expect(response.status).toBe(200);
118
+ await expect(response.json()).resolves.toMatchObject({
119
+ ok: true,
120
+ action_id: 'ping',
121
+ echo: { n: 1 },
122
+ });
123
+ });
124
+
125
+ it('returns 404 stub for unknown action', async () => {
126
+ const { app } = createTestApp({ actions: {} });
127
+ const response = await app.request('/v1/actions/noop', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({}),
131
+ });
132
+ expect(response.status).toBe(404);
133
+ await expect(response.json()).resolves.toMatchObject({
134
+ ok: false,
135
+ error: 'action_not_found',
136
+ action_id: 'noop',
137
+ });
138
+ });
139
+
140
+ it('returns 500 when action throws Error', async () => {
141
+ const { app } = createTestApp();
142
+ const response = await app.request('/v1/actions/boom', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({}),
146
+ });
147
+ expect(response.status).toBe(500);
148
+ await expect(response.json()).resolves.toMatchObject({
149
+ ok: false,
150
+ error: 'action_failed',
151
+ message: 'boom_failed',
152
+ action_id: 'boom',
153
+ });
154
+ });
155
+
156
+ it('returns 500 with fallback message for non-Error throw', async () => {
157
+ const { app } = createTestApp();
158
+ const response = await app.request('/v1/actions/boom_string', {
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: '{}',
162
+ });
163
+ expect(response.status).toBe(500);
164
+ await expect(response.json()).resolves.toMatchObject({
165
+ ok: false,
166
+ error: 'action_failed',
167
+ message: 'action_failed',
168
+ });
169
+ });
170
+
171
+ it('accepts invalid JSON action body as empty object', async () => {
172
+ const { app } = createTestApp();
173
+ const response = await app.request('/v1/actions/ping', {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: '{not-json',
177
+ });
178
+ expect(response.status).toBe(200);
179
+ await expect(response.json()).resolves.toMatchObject({
180
+ ok: true,
181
+ echo: {},
182
+ });
183
+ });
184
+
185
+ it('serves /metrics when metrics enabled', async () => {
186
+ const { app } = createTestApp({ metricsEnv: '1' });
187
+ await app.request('/health');
188
+ const response = await app.request('/metrics');
189
+ expect(response.status).toBe(200);
190
+ expect(response.headers.get('Content-Type')).toContain('text/plain');
191
+ const text = await response.text();
192
+ expect(text).toContain('bff_http_requests_total');
193
+ });
194
+
195
+ it('does not register /metrics when BFF_METRICS=0', async () => {
196
+ const { app } = createTestApp({ metricsEnv: '0' });
197
+ const health = await app.request('/health');
198
+ await expect(health.json()).resolves.toMatchObject({ metrics_enabled: false });
199
+ const metrics = await app.request('/metrics');
200
+ expect(metrics.status).toBe(404);
201
+ });
202
+
203
+ it('start() binds via @hono/node-server serve', () => {
204
+ const { start } = createTestApp({ metricsEnv: '0' });
205
+ start();
206
+ expect(serveMock).toHaveBeenCalledOnce();
207
+ expect(serveMock.mock.calls[0]?.[0]).toMatchObject({
208
+ port: 9999,
209
+ hostname: '0.0.0.0',
210
+ });
211
+ });
212
+ });
package/src/index.test.ts CHANGED
@@ -1,93 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { createSduiBff } from './index';
3
2
  import { createScreenBuilderContext } from './testing/create-screen-builder-context';
4
3
  import { createMockSupabaseClient } from './testing/create-mock-supabase-client';
5
-
6
- describe('createSduiBff', () => {
7
- it('serves /health with registered screens', async () => {
8
- const { app } = createSduiBff({
9
- name: 'Test BFF',
10
- port: 9999,
11
- bffId: 'test-bff',
12
- screens: {
13
- 'demo.home': async () => ({
14
- schema_version: 1,
15
- screen_id: 'demo.home',
16
- blocks: [],
17
- }),
18
- },
19
- });
20
-
21
- const response = await app.request('/health');
22
- expect(response.status).toBe(200);
23
- const body = await response.json();
24
- expect(body).toMatchObject({
25
- ok: true,
26
- bff: 'test-bff',
27
- name: 'Test BFF',
28
- screens: ['demo.home'],
29
- });
30
- });
31
-
32
- it('returns 404 for unknown screen', async () => {
33
- const { app } = createSduiBff({
34
- name: 'Test BFF',
35
- port: 9999,
36
- bffId: 'test-bff',
37
- screens: {},
38
- });
39
-
40
- const response = await app.request('/v1/screens/missing');
41
- expect(response.status).toBe(404);
42
- const body = await response.json();
43
- expect(body).toMatchObject({ error: 'screen_not_found', screen_id: 'missing' });
44
- });
45
-
46
- it('returns 404 stub for unknown action', async () => {
47
- const { app } = createSduiBff({
48
- name: 'Test BFF',
49
- port: 9999,
50
- bffId: 'test-bff',
51
- screens: {},
52
- });
53
-
54
- const response = await app.request('/v1/actions/noop', {
55
- method: 'POST',
56
- headers: { 'Content-Type': 'application/json' },
57
- body: JSON.stringify({}),
58
- });
59
- expect(response.status).toBe(404);
60
- const body = await response.json();
61
- expect(body).toMatchObject({ ok: false, error: 'action_not_found', action_id: 'noop' });
62
- });
63
-
64
- it('invokes registered screen builder with auth token', async () => {
65
- let seenToken: string | undefined;
66
- const { app } = createSduiBff({
67
- name: 'Test BFF',
68
- port: 9999,
69
- bffId: 'test-bff',
70
- screens: {
71
- 'demo.home': async (ctx) => {
72
- seenToken = ctx.authToken;
73
- return {
74
- schema_version: 1,
75
- screen_id: 'demo.home',
76
- blocks: [{ id: 'root', type: 'stack', props: {} }],
77
- };
78
- },
79
- },
80
- });
81
-
82
- const response = await app.request('/v1/screens/demo.home', {
83
- headers: { Authorization: 'Bearer tok-123' },
84
- });
85
- expect(response.status).toBe(200);
86
- expect(seenToken).toBe('tok-123');
87
- const body = await response.json();
88
- expect(body.screen_id).toBe('demo.home');
89
- });
90
- });
4
+ import { createTestSduiBff } from './testing/create-test-sdui-bff';
91
5
 
92
6
  describe('testing helpers', () => {
93
7
  it('createScreenBuilderContext defaults empty maps', () => {
@@ -114,4 +28,15 @@ describe('testing helpers', () => {
114
28
  const rpc = await client.rpc('ping');
115
29
  expect(rpc.data).toBe(true);
116
30
  });
31
+
32
+ it('createTestSduiBff exposes app.request without start', async () => {
33
+ const { app } = createTestSduiBff({
34
+ name: 'Unit',
35
+ port: 1,
36
+ bffId: 'unit',
37
+ screens: {},
38
+ });
39
+ const response = await app.request('/health');
40
+ expect(response.status).toBe(200);
41
+ });
117
42
  });