swallowkit 1.0.0-beta.11 → 1.0.0-beta.13

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.
Files changed (68) hide show
  1. package/README.ja.md +33 -9
  2. package/README.md +33 -9
  3. package/dist/__tests__/fixtures.d.ts +8 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +60 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-connector.d.ts +20 -0
  8. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  9. package/dist/cli/commands/add-connector.js +161 -0
  10. package/dist/cli/commands/add-connector.js.map +1 -0
  11. package/dist/cli/commands/create-model.d.ts +1 -0
  12. package/dist/cli/commands/create-model.d.ts.map +1 -1
  13. package/dist/cli/commands/create-model.js +65 -1
  14. package/dist/cli/commands/create-model.js.map +1 -1
  15. package/dist/cli/commands/dev.d.ts.map +1 -1
  16. package/dist/cli/commands/dev.js +49 -3
  17. package/dist/cli/commands/dev.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +130 -7
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +15 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +3 -2
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +37 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/mock/connector-mock-server.d.ts +67 -0
  28. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  29. package/dist/core/mock/connector-mock-server.js +285 -0
  30. package/dist/core/mock/connector-mock-server.js.map +1 -0
  31. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  32. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  33. package/dist/core/mock/zod-mock-generator.js +163 -0
  34. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  35. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  36. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  37. package/dist/core/scaffold/connector-functions-generator.js +1009 -0
  38. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  39. package/dist/core/scaffold/model-parser.d.ts +6 -0
  40. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  41. package/dist/core/scaffold/model-parser.js +150 -28
  42. package/dist/core/scaffold/model-parser.js.map +1 -1
  43. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  44. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  45. package/dist/core/scaffold/nextjs-generator.js +133 -0
  46. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  47. package/dist/types/index.d.ts +31 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/__tests__/config.test.ts +141 -0
  51. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  52. package/src/__tests__/connector-mock-server.test.ts +252 -0
  53. package/src/__tests__/connector-model-bff.test.ts +162 -0
  54. package/src/__tests__/fixtures.ts +60 -0
  55. package/src/__tests__/scaffold.test.ts +2 -2
  56. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  57. package/src/cli/commands/add-connector.ts +157 -0
  58. package/src/cli/commands/create-model.ts +72 -2
  59. package/src/cli/commands/dev.ts +55 -4
  60. package/src/cli/commands/scaffold.ts +211 -12
  61. package/src/cli/index.ts +16 -0
  62. package/src/core/config.ts +42 -1
  63. package/src/core/mock/connector-mock-server.ts +307 -0
  64. package/src/core/mock/zod-mock-generator.ts +205 -0
  65. package/src/core/scaffold/connector-functions-generator.ts +1106 -0
  66. package/src/core/scaffold/model-parser.ts +172 -31
  67. package/src/core/scaffold/nextjs-generator.ts +154 -0
  68. package/src/types/index.ts +47 -0
@@ -61,6 +61,147 @@ describe("validateConfig", () => {
61
61
  // database is undefined → no connectionString check triggered
62
62
  expect(result.errors.filter((e) => e.includes("endpoint"))).toHaveLength(0);
63
63
  });
64
+
65
+ // ─── Connector Validation ───────────────────────────────────
66
+
67
+ it("validates valid RDB connector config", () => {
68
+ const config: SwallowKitConfig = {
69
+ connectors: {
70
+ mysql: {
71
+ type: "rdb",
72
+ provider: "mysql",
73
+ connectionEnvVar: "MYSQL_CONNECTION_STRING",
74
+ },
75
+ },
76
+ };
77
+ const result = validateConfig(config);
78
+ expect(result.valid).toBe(true);
79
+ expect(result.errors).toHaveLength(0);
80
+ });
81
+
82
+ it("validates valid API connector config", () => {
83
+ const config: SwallowKitConfig = {
84
+ connectors: {
85
+ backlog: {
86
+ type: "api",
87
+ baseUrlEnvVar: "BACKLOG_API_BASE_URL",
88
+ auth: {
89
+ type: "apiKey",
90
+ envVar: "BACKLOG_API_KEY",
91
+ placement: "query",
92
+ paramName: "apiKey",
93
+ },
94
+ },
95
+ },
96
+ };
97
+ const result = validateConfig(config);
98
+ expect(result.valid).toBe(true);
99
+ expect(result.errors).toHaveLength(0);
100
+ });
101
+
102
+ it("returns error for invalid connector type", () => {
103
+ const config = {
104
+ connectors: {
105
+ bad: {
106
+ type: "graphql",
107
+ baseUrlEnvVar: "GRAPHQL_URL",
108
+ },
109
+ },
110
+ } as unknown as SwallowKitConfig;
111
+ const result = validateConfig(config);
112
+ expect(result.valid).toBe(false);
113
+ expect(result.errors[0]).toContain("Connector 'bad'");
114
+ expect(result.errors[0]).toContain("type must be one of");
115
+ });
116
+
117
+ it("returns error for RDB connector without provider", () => {
118
+ const config: SwallowKitConfig = {
119
+ connectors: {
120
+ mydb: {
121
+ type: "rdb",
122
+ provider: "" as any,
123
+ connectionEnvVar: "CONN",
124
+ },
125
+ },
126
+ };
127
+ const result = validateConfig(config);
128
+ expect(result.valid).toBe(false);
129
+ expect(result.errors).toEqual(
130
+ expect.arrayContaining([expect.stringContaining("provider must be one of")])
131
+ );
132
+ });
133
+
134
+ it("returns error for RDB connector without connectionEnvVar", () => {
135
+ const config: SwallowKitConfig = {
136
+ connectors: {
137
+ mydb: {
138
+ type: "rdb",
139
+ provider: "mysql",
140
+ connectionEnvVar: "",
141
+ },
142
+ },
143
+ };
144
+ const result = validateConfig(config);
145
+ expect(result.valid).toBe(false);
146
+ expect(result.errors).toEqual(
147
+ expect.arrayContaining([expect.stringContaining("connectionEnvVar is required")])
148
+ );
149
+ });
150
+
151
+ it("returns error for API connector without baseUrlEnvVar", () => {
152
+ const config: SwallowKitConfig = {
153
+ connectors: {
154
+ ext: {
155
+ type: "api",
156
+ baseUrlEnvVar: "",
157
+ },
158
+ },
159
+ };
160
+ const result = validateConfig(config);
161
+ expect(result.valid).toBe(false);
162
+ expect(result.errors).toEqual(
163
+ expect.arrayContaining([expect.stringContaining("baseUrlEnvVar is required")])
164
+ );
165
+ });
166
+
167
+ it("returns error for API connector with invalid auth type", () => {
168
+ const config: SwallowKitConfig = {
169
+ connectors: {
170
+ ext: {
171
+ type: "api",
172
+ baseUrlEnvVar: "EXT_URL",
173
+ auth: {
174
+ type: "saml" as any,
175
+ envVar: "KEY",
176
+ },
177
+ },
178
+ },
179
+ };
180
+ const result = validateConfig(config);
181
+ expect(result.valid).toBe(false);
182
+ expect(result.errors).toEqual(
183
+ expect.arrayContaining([expect.stringContaining("auth.type must be one of")])
184
+ );
185
+ });
186
+
187
+ it("validates multiple connectors at once", () => {
188
+ const config: SwallowKitConfig = {
189
+ connectors: {
190
+ mysql: {
191
+ type: "rdb",
192
+ provider: "mysql",
193
+ connectionEnvVar: "MYSQL_CONN",
194
+ },
195
+ backlog: {
196
+ type: "api",
197
+ baseUrlEnvVar: "BACKLOG_URL",
198
+ },
199
+ },
200
+ };
201
+ const result = validateConfig(config);
202
+ expect(result.valid).toBe(true);
203
+ expect(result.errors).toHaveLength(0);
204
+ });
64
205
  });
65
206
 
66
207
  describe("loadConfigFromEnv", () => {
@@ -0,0 +1,288 @@
1
+ /**
2
+ * コネクタ Functions ジェネレーターのテスト
3
+ */
4
+
5
+ import {
6
+ generateRdbConnectorFunctionTS,
7
+ generateApiConnectorFunctionTS,
8
+ generateRdbConnectorFunctionCSharp,
9
+ generateApiConnectorFunctionCSharp,
10
+ generateRdbConnectorFunctionPython,
11
+ generateApiConnectorFunctionPython,
12
+ isReadOnlyConnector,
13
+ } from "../core/scaffold/connector-functions-generator";
14
+ import {
15
+ createRdbConnectorModelInfo,
16
+ createApiConnectorModelInfo,
17
+ } from "./fixtures";
18
+ import {
19
+ RdbConnectorConfig,
20
+ ApiConnectorConfig,
21
+ RdbModelConnectorConfig,
22
+ ApiModelConnectorConfig,
23
+ } from "../types";
24
+
25
+ // ─── Shared Test Data ────────────────────────────────────────
26
+
27
+ const mysqlConnector: RdbConnectorConfig = {
28
+ type: "rdb",
29
+ provider: "mysql",
30
+ connectionEnvVar: "MYSQL_CONNECTION_STRING",
31
+ };
32
+
33
+ const postgresConnector: RdbConnectorConfig = {
34
+ type: "rdb",
35
+ provider: "postgres",
36
+ connectionEnvVar: "PG_CONNECTION_STRING",
37
+ };
38
+
39
+ const sqlserverConnector: RdbConnectorConfig = {
40
+ type: "rdb",
41
+ provider: "sqlserver",
42
+ connectionEnvVar: "MSSQL_CONNECTION_STRING",
43
+ };
44
+
45
+ const backlogConnector: ApiConnectorConfig = {
46
+ type: "api",
47
+ baseUrlEnvVar: "BACKLOG_API_BASE_URL",
48
+ auth: {
49
+ type: "apiKey",
50
+ envVar: "BACKLOG_API_KEY",
51
+ placement: "query",
52
+ paramName: "apiKey",
53
+ },
54
+ };
55
+
56
+ const bearerConnector: ApiConnectorConfig = {
57
+ type: "api",
58
+ baseUrlEnvVar: "EXT_API_BASE_URL",
59
+ auth: {
60
+ type: "bearer",
61
+ envVar: "EXT_API_TOKEN",
62
+ },
63
+ };
64
+
65
+ // ─── isReadOnlyConnector ────────────────────────────────────
66
+
67
+ describe("isReadOnlyConnector", () => {
68
+ it("returns true when only read operations are present", () => {
69
+ expect(isReadOnlyConnector(["getAll", "getById"])).toBe(true);
70
+ expect(isReadOnlyConnector(["getAll"])).toBe(true);
71
+ expect(isReadOnlyConnector(["getById"])).toBe(true);
72
+ });
73
+
74
+ it("returns false when write operations are present", () => {
75
+ expect(isReadOnlyConnector(["getAll", "getById", "create"])).toBe(false);
76
+ expect(isReadOnlyConnector(["getAll", "update"])).toBe(false);
77
+ expect(isReadOnlyConnector(["getAll", "delete"])).toBe(false);
78
+ });
79
+ });
80
+
81
+ // ─── TypeScript RDB Connector ───────────────────────────────
82
+
83
+ describe("generateRdbConnectorFunctionTS", () => {
84
+ const model = createRdbConnectorModelInfo();
85
+ const modelConnector = model.connectorConfig as RdbModelConnectorConfig;
86
+
87
+ it("generates MySQL read-only functions", () => {
88
+ const code = generateRdbConnectorFunctionTS(model, "@myapp/shared", mysqlConnector, modelConnector);
89
+ expect(code).toContain("import mysql from 'mysql2/promise'");
90
+ expect(code).toContain("MYSQL_CONNECTION_STRING");
91
+ expect(code).toContain("SELECT * FROM users");
92
+ expect(code).toContain("user-get-all");
93
+ expect(code).toContain("user-get-by-id");
94
+ // Should NOT contain write operations
95
+ expect(code).not.toContain("user-create");
96
+ expect(code).not.toContain("user-update");
97
+ expect(code).not.toContain("user-delete");
98
+ });
99
+
100
+ it("generates PostgreSQL functions", () => {
101
+ const code = generateRdbConnectorFunctionTS(model, "@myapp/shared", postgresConnector, modelConnector);
102
+ expect(code).toContain("import pg from 'pg'");
103
+ expect(code).toContain("PG_CONNECTION_STRING");
104
+ });
105
+
106
+ it("generates SQL Server functions", () => {
107
+ const code = generateRdbConnectorFunctionTS(model, "@myapp/shared", sqlserverConnector, modelConnector);
108
+ expect(code).toContain("import sql from 'mssql'");
109
+ expect(code).toContain("MSSQL_CONNECTION_STRING");
110
+ });
111
+
112
+ it("uses correct table and id column", () => {
113
+ const code = generateRdbConnectorFunctionTS(model, "@myapp/shared", mysqlConnector, modelConnector);
114
+ expect(code).toContain("FROM users");
115
+ expect(code).toContain("WHERE id = ");
116
+ });
117
+
118
+ it("imports schema from shared package", () => {
119
+ const code = generateRdbConnectorFunctionTS(model, "@myapp/shared", mysqlConnector, modelConnector);
120
+ expect(code).toContain("from '@myapp/shared'");
121
+ });
122
+ });
123
+
124
+ // ─── TypeScript API Connector ───────────────────────────────
125
+
126
+ describe("generateApiConnectorFunctionTS", () => {
127
+ const model = createApiConnectorModelInfo();
128
+ const modelConnector = model.connectorConfig as ApiModelConnectorConfig;
129
+
130
+ it("generates read-write functions with apiKey auth", () => {
131
+ const code = generateApiConnectorFunctionTS(model, "@myapp/shared", backlogConnector, modelConnector);
132
+ expect(code).toContain("BACKLOG_API_BASE_URL");
133
+ expect(code).toContain("backlogIssue-get-all");
134
+ expect(code).toContain("backlogIssue-get-by-id");
135
+ expect(code).toContain("backlogIssue-create");
136
+ expect(code).toContain("backlogIssue-update");
137
+ });
138
+
139
+ it("generates bearer auth helper", () => {
140
+ const code = generateApiConnectorFunctionTS(model, "@myapp/shared", bearerConnector, modelConnector);
141
+ expect(code).toContain("Authorization");
142
+ expect(code).toContain("Bearer");
143
+ expect(code).toContain("EXT_API_TOKEN");
144
+ });
145
+
146
+ it("does not generate delete when not in operations", () => {
147
+ const code = generateApiConnectorFunctionTS(model, "@myapp/shared", backlogConnector, modelConnector);
148
+ expect(code).not.toContain("backlogIssue-delete");
149
+ });
150
+
151
+ it("generates read-only API connector when operations are limited", () => {
152
+ const readOnlyModel = createApiConnectorModelInfo({
153
+ connectorConfig: {
154
+ connector: "backlog",
155
+ operations: ["getAll", "getById"],
156
+ endpoints: {
157
+ getAll: "GET /issues",
158
+ getById: "GET /issues/{id}",
159
+ },
160
+ },
161
+ });
162
+ const readOnlyConnector = readOnlyModel.connectorConfig as ApiModelConnectorConfig;
163
+ const code = generateApiConnectorFunctionTS(readOnlyModel, "@myapp/shared", backlogConnector, readOnlyConnector);
164
+ expect(code).toContain("backlogIssue-get-all");
165
+ expect(code).not.toContain("backlogIssue-create");
166
+ expect(code).not.toContain("backlogIssue-update");
167
+ });
168
+ });
169
+
170
+ // ─── C# RDB Connector ──────────────────────────────────────
171
+
172
+ describe("generateRdbConnectorFunctionCSharp", () => {
173
+ const model = createRdbConnectorModelInfo();
174
+ const modelConnector = model.connectorConfig as RdbModelConnectorConfig;
175
+
176
+ it("generates MySQL C# connector functions", () => {
177
+ const code = generateRdbConnectorFunctionCSharp(model, mysqlConnector, modelConnector);
178
+ expect(code).toContain("MySqlConnection");
179
+ expect(code).toContain("MYSQL_CONNECTION_STRING");
180
+ expect(code).toContain("UserConnectorFunctions");
181
+ expect(code).toContain('[Function("userGetAll")]');
182
+ expect(code).toContain('[Function("userGetById")]');
183
+ // Should NOT contain write operations
184
+ expect(code).not.toContain('[Function("userCreate")]');
185
+ expect(code).not.toContain('[Function("userUpdate")]');
186
+ });
187
+
188
+ it("generates PostgreSQL C# connector", () => {
189
+ const code = generateRdbConnectorFunctionCSharp(model, postgresConnector, modelConnector);
190
+ expect(code).toContain("NpgsqlConnection");
191
+ expect(code).toContain("PG_CONNECTION_STRING");
192
+ });
193
+
194
+ it("generates SQL Server C# connector", () => {
195
+ const code = generateRdbConnectorFunctionCSharp(model, sqlserverConnector, modelConnector);
196
+ expect(code).toContain("SqlConnection");
197
+ expect(code).toContain("MSSQL_CONNECTION_STRING");
198
+ });
199
+
200
+ it("uses Route attribute with correct paths", () => {
201
+ const code = generateRdbConnectorFunctionCSharp(model, mysqlConnector, modelConnector);
202
+ expect(code).toContain('Route = "user"');
203
+ expect(code).toContain('Route = "user/{id}"');
204
+ });
205
+ });
206
+
207
+ // ─── C# API Connector ──────────────────────────────────────
208
+
209
+ describe("generateApiConnectorFunctionCSharp", () => {
210
+ const model = createApiConnectorModelInfo();
211
+ const modelConnector = model.connectorConfig as ApiModelConnectorConfig;
212
+
213
+ it("generates API connector with CRUD operations", () => {
214
+ const code = generateApiConnectorFunctionCSharp(model, backlogConnector, modelConnector);
215
+ expect(code).toContain("BacklogIssueConnectorFunctions");
216
+ expect(code).toContain("HttpClient");
217
+ expect(code).toContain("BACKLOG_API_BASE_URL");
218
+ expect(code).toContain('[Function("backlogIssueGetAll")]');
219
+ expect(code).toContain('[Function("backlogIssueGetById")]');
220
+ expect(code).toContain('[Function("backlogIssueCreate")]');
221
+ expect(code).toContain('[Function("backlogIssueUpdate")]');
222
+ });
223
+
224
+ it("includes auth configuration for apiKey", () => {
225
+ const code = generateApiConnectorFunctionCSharp(model, backlogConnector, modelConnector);
226
+ expect(code).toContain("BACKLOG_API_KEY");
227
+ expect(code).toContain("apiKey");
228
+ });
229
+
230
+ it("generates bearer auth for C#", () => {
231
+ const code = generateApiConnectorFunctionCSharp(model, bearerConnector, modelConnector);
232
+ expect(code).toContain("Authorization");
233
+ expect(code).toContain("Bearer");
234
+ expect(code).toContain("EXT_API_TOKEN");
235
+ });
236
+ });
237
+
238
+ // ─── Python RDB Connector ───────────────────────────────────
239
+
240
+ describe("generateRdbConnectorFunctionPython", () => {
241
+ const model = createRdbConnectorModelInfo();
242
+ const modelConnector = model.connectorConfig as RdbModelConnectorConfig;
243
+
244
+ it("generates MySQL Python connector", () => {
245
+ const result = generateRdbConnectorFunctionPython(model, mysqlConnector, modelConnector);
246
+ expect(result.registration).toContain("user");
247
+ expect(result.blueprint).toContain("mysql.connector");
248
+ expect(result.blueprint).toContain("MYSQL_CONNECTION_STRING");
249
+ expect(result.blueprint).toContain("SELECT * FROM users");
250
+ expect(result.blueprint).toContain('route="user"');
251
+ expect(result.blueprint).toContain('methods=["GET"]');
252
+ });
253
+
254
+ it("returns registration and blueprint as object", () => {
255
+ const result = generateRdbConnectorFunctionPython(model, mysqlConnector, modelConnector);
256
+ expect(result).toHaveProperty("registration");
257
+ expect(result).toHaveProperty("blueprint");
258
+ expect(typeof result.registration).toBe("string");
259
+ expect(typeof result.blueprint).toBe("string");
260
+ });
261
+ });
262
+
263
+ // ─── Python API Connector ───────────────────────────────────
264
+
265
+ describe("generateApiConnectorFunctionPython", () => {
266
+ const model = createApiConnectorModelInfo();
267
+ const modelConnector = model.connectorConfig as ApiModelConnectorConfig;
268
+
269
+ it("generates API Python connector with CRUD", () => {
270
+ const result = generateApiConnectorFunctionPython(model, backlogConnector, modelConnector);
271
+ expect(result.blueprint).toContain("BACKLOG_API_BASE_URL");
272
+ expect(result.blueprint).toContain("requests");
273
+ expect(result.blueprint).toContain('route="backlogIssue"');
274
+ });
275
+
276
+ it("includes apiKey auth in Python", () => {
277
+ const result = generateApiConnectorFunctionPython(model, backlogConnector, modelConnector);
278
+ expect(result.blueprint).toContain("BACKLOG_API_KEY");
279
+ expect(result.blueprint).toContain("apiKey");
280
+ });
281
+
282
+ it("generates bearer auth for Python", () => {
283
+ const result = generateApiConnectorFunctionPython(model, bearerConnector, modelConnector);
284
+ expect(result.blueprint).toContain("Authorization");
285
+ expect(result.blueprint).toContain("Bearer");
286
+ expect(result.blueprint).toContain("EXT_API_TOKEN");
287
+ });
288
+ });
@@ -0,0 +1,252 @@
1
+ /**
2
+ * コネクタモックサーバーのテスト
3
+ */
4
+
5
+ import * as http from "http";
6
+ import { ConnectorMockServer } from "../core/mock/connector-mock-server";
7
+ import {
8
+ createRdbConnectorModelInfo,
9
+ createApiConnectorModelInfo,
10
+ } from "./fixtures";
11
+
12
+ // ─── Helpers ────────────────────────────────────────────────
13
+
14
+ function httpRequest(
15
+ port: number,
16
+ method: string,
17
+ path: string,
18
+ body?: unknown
19
+ ): Promise<{ status: number; body: unknown }> {
20
+ return new Promise((resolve, reject) => {
21
+ const opts: http.RequestOptions = {
22
+ hostname: "localhost",
23
+ port,
24
+ path,
25
+ method,
26
+ headers: { "Content-Type": "application/json" },
27
+ };
28
+
29
+ const req = http.request(opts, (res) => {
30
+ let data = "";
31
+ res.on("data", (chunk) => (data += chunk));
32
+ res.on("end", () => {
33
+ try {
34
+ resolve({
35
+ status: res.statusCode || 0,
36
+ body: data ? JSON.parse(data) : null,
37
+ });
38
+ } catch {
39
+ resolve({ status: res.statusCode || 0, body: data });
40
+ }
41
+ });
42
+ });
43
+
44
+ req.on("error", reject);
45
+
46
+ if (body) {
47
+ req.write(JSON.stringify(body));
48
+ }
49
+ req.end();
50
+ });
51
+ }
52
+
53
+ // ─── Tests ──────────────────────────────────────────────────
54
+
55
+ describe("ConnectorMockServer", () => {
56
+ let server: ConnectorMockServer;
57
+ const TEST_PORT = 19876; // Unlikely to conflict
58
+
59
+ afterEach(async () => {
60
+ if (server) {
61
+ await server.stop();
62
+ }
63
+ });
64
+
65
+ it("starts and stops without errors", async () => {
66
+ server = new ConnectorMockServer({
67
+ port: TEST_PORT,
68
+ functionsTarget: "localhost:7071",
69
+ connectorModels: [createRdbConnectorModelInfo()],
70
+ mockCount: 2,
71
+ });
72
+
73
+ await server.start();
74
+ await server.stop();
75
+ });
76
+
77
+ it("serves GET /api/<model> with mock data", async () => {
78
+ server = new ConnectorMockServer({
79
+ port: TEST_PORT,
80
+ functionsTarget: "localhost:7071",
81
+ connectorModels: [createRdbConnectorModelInfo()],
82
+ mockCount: 3,
83
+ });
84
+
85
+ await server.start();
86
+
87
+ const { status, body } = await httpRequest(TEST_PORT, "GET", "/api/user");
88
+ expect(status).toBe(200);
89
+ expect(Array.isArray(body)).toBe(true);
90
+ expect((body as any[]).length).toBe(3);
91
+ expect((body as any[])[0]).toHaveProperty("id");
92
+ expect((body as any[])[0]).toHaveProperty("email");
93
+ });
94
+
95
+ it("serves GET /api/<model>/<id> for single item", async () => {
96
+ server = new ConnectorMockServer({
97
+ port: TEST_PORT,
98
+ functionsTarget: "localhost:7071",
99
+ connectorModels: [createRdbConnectorModelInfo()],
100
+ mockCount: 3,
101
+ });
102
+
103
+ await server.start();
104
+
105
+ const { status, body } = await httpRequest(TEST_PORT, "GET", "/api/user/user-001");
106
+ expect(status).toBe(200);
107
+ expect((body as any).id).toBe("user-001");
108
+ });
109
+
110
+ it("returns 404 for non-existent item", async () => {
111
+ server = new ConnectorMockServer({
112
+ port: TEST_PORT,
113
+ functionsTarget: "localhost:7071",
114
+ connectorModels: [createRdbConnectorModelInfo()],
115
+ mockCount: 2,
116
+ });
117
+
118
+ await server.start();
119
+
120
+ const { status } = await httpRequest(TEST_PORT, "GET", "/api/user/nonexistent");
121
+ expect(status).toBe(404);
122
+ });
123
+
124
+ it("returns 405 for write operations on read-only connector", async () => {
125
+ // RDB connector model has operations: ["getAll", "getById"] (read-only)
126
+ server = new ConnectorMockServer({
127
+ port: TEST_PORT,
128
+ functionsTarget: "localhost:7071",
129
+ connectorModels: [createRdbConnectorModelInfo()],
130
+ mockCount: 2,
131
+ });
132
+
133
+ await server.start();
134
+
135
+ const { status } = await httpRequest(TEST_PORT, "POST", "/api/user", { name: "New" });
136
+ expect(status).toBe(405);
137
+ });
138
+
139
+ it("supports POST for read-write connector models", async () => {
140
+ // API connector model has operations: ["getAll", "getById", "create", "update"]
141
+ server = new ConnectorMockServer({
142
+ port: TEST_PORT,
143
+ functionsTarget: "localhost:7071",
144
+ connectorModels: [createApiConnectorModelInfo()],
145
+ mockCount: 0,
146
+ });
147
+
148
+ await server.start();
149
+
150
+ const { status, body } = await httpRequest(TEST_PORT, "POST", "/api/backlogIssue", {
151
+ summary: "Test issue",
152
+ projectId: "proj-1",
153
+ issueKey: "TEST-001",
154
+ });
155
+ expect(status).toBe(201);
156
+ expect((body as any).summary).toBe("Test issue");
157
+ expect((body as any).id).toBeDefined();
158
+
159
+ // Verify it was stored
160
+ const { body: allBody } = await httpRequest(TEST_PORT, "GET", "/api/backlogIssue");
161
+ expect((allBody as any[]).length).toBe(1);
162
+ });
163
+
164
+ it("supports PUT for update operations", async () => {
165
+ server = new ConnectorMockServer({
166
+ port: TEST_PORT,
167
+ functionsTarget: "localhost:7071",
168
+ connectorModels: [createApiConnectorModelInfo()],
169
+ mockCount: 2,
170
+ });
171
+
172
+ await server.start();
173
+
174
+ const store = server.getStore("BacklogIssue");
175
+ const firstId = store[0].id as string;
176
+
177
+ const { status, body } = await httpRequest(TEST_PORT, "PUT", `/api/backlogIssue/${firstId}`, {
178
+ summary: "Updated summary",
179
+ });
180
+ expect(status).toBe(200);
181
+ expect((body as any).summary).toBe("Updated summary");
182
+ expect((body as any).id).toBe(firstId);
183
+ });
184
+
185
+ it("returns 405 for DELETE when not in operations", async () => {
186
+ // API connector model doesn't include "delete" in operations
187
+ server = new ConnectorMockServer({
188
+ port: TEST_PORT,
189
+ functionsTarget: "localhost:7071",
190
+ connectorModels: [createApiConnectorModelInfo()],
191
+ mockCount: 2,
192
+ });
193
+
194
+ await server.start();
195
+
196
+ const store = server.getStore("BacklogIssue");
197
+ const firstId = store[0].id as string;
198
+
199
+ const { status } = await httpRequest(TEST_PORT, "DELETE", `/api/backlogIssue/${firstId}`);
200
+ expect(status).toBe(405);
201
+ });
202
+
203
+ it("proxies non-connector routes to Functions target (returns 502 when Functions not running)", async () => {
204
+ server = new ConnectorMockServer({
205
+ port: TEST_PORT,
206
+ functionsTarget: "localhost:19877", // No server on this port
207
+ connectorModels: [createRdbConnectorModelInfo()],
208
+ mockCount: 1,
209
+ });
210
+
211
+ await server.start();
212
+
213
+ // /api/todo is NOT a connector model route, so it should be proxied
214
+ const { status, body } = await httpRequest(TEST_PORT, "GET", "/api/todo");
215
+ expect(status).toBe(502);
216
+ expect((body as any).error).toContain("not available");
217
+ });
218
+
219
+ it("handles multiple connector models simultaneously", async () => {
220
+ server = new ConnectorMockServer({
221
+ port: TEST_PORT,
222
+ functionsTarget: "localhost:7071",
223
+ connectorModels: [createRdbConnectorModelInfo(), createApiConnectorModelInfo()],
224
+ mockCount: 2,
225
+ });
226
+
227
+ await server.start();
228
+
229
+ const usersRes = await httpRequest(TEST_PORT, "GET", "/api/user");
230
+ expect(usersRes.status).toBe(200);
231
+ expect((usersRes.body as any[]).length).toBe(2);
232
+
233
+ const issuesRes = await httpRequest(TEST_PORT, "GET", "/api/backlogIssue");
234
+ expect(issuesRes.status).toBe(200);
235
+ expect((issuesRes.body as any[]).length).toBe(2);
236
+ });
237
+
238
+ it("getStore returns current data for a model", async () => {
239
+ server = new ConnectorMockServer({
240
+ port: TEST_PORT,
241
+ functionsTarget: "localhost:7071",
242
+ connectorModels: [createRdbConnectorModelInfo()],
243
+ mockCount: 3,
244
+ });
245
+
246
+ await server.start();
247
+
248
+ const store = server.getStore("User");
249
+ expect(store.length).toBe(3);
250
+ expect(store[0].id).toBe("user-001");
251
+ });
252
+ });