otavia 0.1.0
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/bun.lock +589 -0
- package/package.json +35 -0
- package/src/cli.ts +153 -0
- package/src/commands/__tests__/aws-auth.test.ts +32 -0
- package/src/commands/__tests__/cell.test.ts +44 -0
- package/src/commands/__tests__/dev.test.ts +49 -0
- package/src/commands/__tests__/init.test.ts +47 -0
- package/src/commands/__tests__/setup.test.ts +263 -0
- package/src/commands/aws-auth.ts +32 -0
- package/src/commands/aws.ts +59 -0
- package/src/commands/cell.ts +33 -0
- package/src/commands/clean.ts +32 -0
- package/src/commands/deploy.ts +508 -0
- package/src/commands/dev/__tests__/fixtures/gateway-cell/cell.yaml +8 -0
- package/src/commands/dev/__tests__/gateway-backend-routes.test.ts +13 -0
- package/src/commands/dev/__tests__/gateway-forward-url.test.ts +20 -0
- package/src/commands/dev/__tests__/gateway-sso-base-url.test.ts +93 -0
- package/src/commands/dev/__tests__/tunnel.test.ts +93 -0
- package/src/commands/dev/__tests__/vite-dev-proxy-rules.test.ts +220 -0
- package/src/commands/dev/__tests__/well-known.test.ts +88 -0
- package/src/commands/dev/forward-url.ts +7 -0
- package/src/commands/dev/gateway.ts +421 -0
- package/src/commands/dev/main-frontend-runtime/main-entry.ts +35 -0
- package/src/commands/dev/main-frontend-runtime/vite-config.ts +210 -0
- package/src/commands/dev/mount-selection.ts +9 -0
- package/src/commands/dev/tunnel.ts +176 -0
- package/src/commands/dev/vite-dev.ts +382 -0
- package/src/commands/dev/well-known.ts +76 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/init.ts +69 -0
- package/src/commands/lint.ts +49 -0
- package/src/commands/setup.ts +887 -0
- package/src/commands/test.ts +331 -0
- package/src/commands/typecheck.ts +36 -0
- package/src/config/__tests__/load-cell-yaml.test.ts +248 -0
- package/src/config/__tests__/load-otavia-yaml.test.ts +492 -0
- package/src/config/__tests__/ports.test.ts +48 -0
- package/src/config/__tests__/resolve-cell-dir.test.ts +60 -0
- package/src/config/__tests__/resolve-params.test.ts +137 -0
- package/src/config/__tests__/resource-names.test.ts +62 -0
- package/src/config/cell-yaml-schema.ts +115 -0
- package/src/config/load-cell-yaml.ts +87 -0
- package/src/config/load-otavia-yaml.ts +256 -0
- package/src/config/otavia-yaml-schema.ts +49 -0
- package/src/config/ports.ts +57 -0
- package/src/config/resolve-cell-dir.ts +55 -0
- package/src/config/resolve-params.ts +160 -0
- package/src/config/resource-names.ts +60 -0
- package/src/deploy/__tests__/template.test.ts +137 -0
- package/src/deploy/api-gateway.ts +96 -0
- package/src/deploy/cloudflare-dns.ts +261 -0
- package/src/deploy/cloudfront.ts +228 -0
- package/src/deploy/dynamodb.ts +68 -0
- package/src/deploy/lambda.ts +121 -0
- package/src/deploy/s3.ts +57 -0
- package/src/deploy/template.ts +264 -0
- package/src/deploy/types.ts +16 -0
- package/src/local/docker.ts +175 -0
- package/src/local/dynamodb-local.ts +124 -0
- package/src/local/minio-local.ts +44 -0
- package/src/utils/env.test.ts +74 -0
- package/src/utils/env.ts +79 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { loadOtaviaYaml } from "../load-otavia-yaml.js";
|
|
6
|
+
import { isEnvRef, isParamRef, isSecretRef } from "../cell-yaml-schema.js";
|
|
7
|
+
|
|
8
|
+
function writeYaml(dir: string, content: string) {
|
|
9
|
+
const filePath = path.join(dir, "otavia.yaml");
|
|
10
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
11
|
+
return filePath;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("loadOtaviaYaml", () => {
|
|
15
|
+
test("returns parsed object when valid otavia.yaml exists", () => {
|
|
16
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
17
|
+
try {
|
|
18
|
+
writeYaml(
|
|
19
|
+
tmp,
|
|
20
|
+
`
|
|
21
|
+
stackName: my-stack
|
|
22
|
+
cells:
|
|
23
|
+
- cell-a
|
|
24
|
+
- cell-b
|
|
25
|
+
domain:
|
|
26
|
+
host: example.com
|
|
27
|
+
dns:
|
|
28
|
+
provider: route53
|
|
29
|
+
zone: example.com
|
|
30
|
+
zoneId: Z123
|
|
31
|
+
params:
|
|
32
|
+
foo: bar
|
|
33
|
+
`
|
|
34
|
+
);
|
|
35
|
+
const result = loadOtaviaYaml(tmp);
|
|
36
|
+
expect(result.stackName).toBe("my-stack");
|
|
37
|
+
expect(result.cells).toEqual({ "cell-a": "@otavia/cell-a", "cell-b": "@otavia/cell-b" });
|
|
38
|
+
expect(result.cellsList).toEqual([
|
|
39
|
+
{ mount: "cell-a", package: "@otavia/cell-a" },
|
|
40
|
+
{ mount: "cell-b", package: "@otavia/cell-b" },
|
|
41
|
+
]);
|
|
42
|
+
expect(result.domain.host).toBe("example.com");
|
|
43
|
+
expect(result.domain.dns?.provider).toBe("route53");
|
|
44
|
+
expect(result.domain.dns?.zone).toBe("example.com");
|
|
45
|
+
expect(result.domain.dns?.zoneId).toBe("Z123");
|
|
46
|
+
expect(result.params).toEqual({ foo: "bar" });
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(tmp, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("parses defaultCell when configured", () => {
|
|
53
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
54
|
+
try {
|
|
55
|
+
writeYaml(
|
|
56
|
+
tmp,
|
|
57
|
+
`
|
|
58
|
+
stackName: my-stack
|
|
59
|
+
defaultCell: drive
|
|
60
|
+
cells:
|
|
61
|
+
sso: "@otavia/sso"
|
|
62
|
+
drive: "@otavia/drive"
|
|
63
|
+
domain:
|
|
64
|
+
host: example.com
|
|
65
|
+
`
|
|
66
|
+
);
|
|
67
|
+
const result = loadOtaviaYaml(tmp);
|
|
68
|
+
expect(result.defaultCell).toBe("drive");
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(tmp, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("parses cells as object (mount -> package)", () => {
|
|
75
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
76
|
+
try {
|
|
77
|
+
writeYaml(
|
|
78
|
+
tmp,
|
|
79
|
+
`
|
|
80
|
+
stackName: my-stack
|
|
81
|
+
cells:
|
|
82
|
+
sso: "@otavia/sso"
|
|
83
|
+
drive: "@otavia/drive"
|
|
84
|
+
domain:
|
|
85
|
+
host: example.com
|
|
86
|
+
`
|
|
87
|
+
);
|
|
88
|
+
const result = loadOtaviaYaml(tmp);
|
|
89
|
+
expect(result.cells).toEqual({ sso: "@otavia/sso", drive: "@otavia/drive" });
|
|
90
|
+
expect(result.cellsList).toEqual([
|
|
91
|
+
{ mount: "sso", package: "@otavia/sso" },
|
|
92
|
+
{ mount: "drive", package: "@otavia/drive" },
|
|
93
|
+
]);
|
|
94
|
+
} finally {
|
|
95
|
+
fs.rmSync(tmp, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("parses cells object values as { package, params }", () => {
|
|
100
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
101
|
+
try {
|
|
102
|
+
writeYaml(
|
|
103
|
+
tmp,
|
|
104
|
+
`
|
|
105
|
+
stackName: my-stack
|
|
106
|
+
cells:
|
|
107
|
+
sso:
|
|
108
|
+
package: "@otavia/sso"
|
|
109
|
+
params:
|
|
110
|
+
issuer: "https://issuer.example.com"
|
|
111
|
+
drive:
|
|
112
|
+
package: "@otavia/drive"
|
|
113
|
+
domain:
|
|
114
|
+
host: example.com
|
|
115
|
+
`
|
|
116
|
+
);
|
|
117
|
+
const result = loadOtaviaYaml(tmp);
|
|
118
|
+
expect(result.cells).toEqual({ sso: "@otavia/sso", drive: "@otavia/drive" });
|
|
119
|
+
expect(result.cellsList).toEqual([
|
|
120
|
+
{
|
|
121
|
+
mount: "sso",
|
|
122
|
+
package: "@otavia/sso",
|
|
123
|
+
params: { issuer: "https://issuer.example.com" },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
mount: "drive",
|
|
127
|
+
package: "@otavia/drive",
|
|
128
|
+
params: undefined,
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(tmp, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("parses canonical cells list with package/mount/params", () => {
|
|
137
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
138
|
+
try {
|
|
139
|
+
writeYaml(
|
|
140
|
+
tmp,
|
|
141
|
+
`
|
|
142
|
+
stackName: my-stack
|
|
143
|
+
cells:
|
|
144
|
+
- package: "@otavia/sso"
|
|
145
|
+
mount: "auth"
|
|
146
|
+
params:
|
|
147
|
+
issuer: "https://issuer.example.com"
|
|
148
|
+
- package: "@otavia/drive"
|
|
149
|
+
domain:
|
|
150
|
+
host: example.com
|
|
151
|
+
`
|
|
152
|
+
);
|
|
153
|
+
const result = loadOtaviaYaml(tmp);
|
|
154
|
+
expect(result.cells).toEqual({ auth: "@otavia/sso", drive: "@otavia/drive" });
|
|
155
|
+
expect(result.cellsList).toEqual([
|
|
156
|
+
{
|
|
157
|
+
mount: "auth",
|
|
158
|
+
package: "@otavia/sso",
|
|
159
|
+
params: { issuer: "https://issuer.example.com" },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
mount: "drive",
|
|
163
|
+
package: "@otavia/drive",
|
|
164
|
+
params: undefined,
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
} finally {
|
|
168
|
+
fs.rmSync(tmp, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("parses !Env and !Secret in otavia params", () => {
|
|
173
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
174
|
+
try {
|
|
175
|
+
writeYaml(
|
|
176
|
+
tmp,
|
|
177
|
+
`
|
|
178
|
+
stackName: my-stack
|
|
179
|
+
cells:
|
|
180
|
+
sso: "@otavia/sso"
|
|
181
|
+
domain:
|
|
182
|
+
host: example.com
|
|
183
|
+
params:
|
|
184
|
+
SSO_BASE_URL: !Env SSO_BASE_URL
|
|
185
|
+
BFL_API_KEY: !Secret BFL_API_KEY
|
|
186
|
+
`
|
|
187
|
+
);
|
|
188
|
+
const result = loadOtaviaYaml(tmp);
|
|
189
|
+
const ssoBaseUrl = result.params?.SSO_BASE_URL;
|
|
190
|
+
const bflApiKey = result.params?.BFL_API_KEY;
|
|
191
|
+
expect(isEnvRef(ssoBaseUrl)).toBe(true);
|
|
192
|
+
expect(isSecretRef(bflApiKey)).toBe(true);
|
|
193
|
+
if (isEnvRef(ssoBaseUrl)) {
|
|
194
|
+
expect(ssoBaseUrl.env).toBe("SSO_BASE_URL");
|
|
195
|
+
}
|
|
196
|
+
if (isSecretRef(bflApiKey)) {
|
|
197
|
+
expect(bflApiKey.secret).toBe("BFL_API_KEY");
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(tmp, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("parses !Param in cell-level params", () => {
|
|
205
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
206
|
+
try {
|
|
207
|
+
writeYaml(
|
|
208
|
+
tmp,
|
|
209
|
+
`
|
|
210
|
+
stackName: my-stack
|
|
211
|
+
cells:
|
|
212
|
+
- package: "@otavia/artist"
|
|
213
|
+
mount: "artist"
|
|
214
|
+
params:
|
|
215
|
+
BFL_API_KEY: !Param BFL_API_KEY
|
|
216
|
+
domain:
|
|
217
|
+
host: example.com
|
|
218
|
+
params:
|
|
219
|
+
BFL_API_KEY: !Secret BFL_API_KEY
|
|
220
|
+
`
|
|
221
|
+
);
|
|
222
|
+
const result = loadOtaviaYaml(tmp);
|
|
223
|
+
const bflApiKey = result.cellsList[0]?.params?.BFL_API_KEY;
|
|
224
|
+
expect(isParamRef(bflApiKey)).toBe(true);
|
|
225
|
+
if (isParamRef(bflApiKey)) {
|
|
226
|
+
expect(bflApiKey.param).toBe("BFL_API_KEY");
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
fs.rmSync(tmp, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("throws when top-level params uses !Param", () => {
|
|
234
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
235
|
+
try {
|
|
236
|
+
writeYaml(
|
|
237
|
+
tmp,
|
|
238
|
+
`
|
|
239
|
+
stackName: my-stack
|
|
240
|
+
cells:
|
|
241
|
+
sso: "@otavia/sso"
|
|
242
|
+
domain:
|
|
243
|
+
host: example.com
|
|
244
|
+
params:
|
|
245
|
+
SSO_BASE_URL: !Param OTHER_KEY
|
|
246
|
+
`
|
|
247
|
+
);
|
|
248
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow(
|
|
249
|
+
"otavia.yaml: params.SSO_BASE_URL cannot use !Param; top-level params only allow plain values, !Env, !Secret"
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
fs.rmSync(tmp, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("throws when cell-level params uses !Env/!Secret", () => {
|
|
257
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
258
|
+
try {
|
|
259
|
+
writeYaml(
|
|
260
|
+
tmp,
|
|
261
|
+
`
|
|
262
|
+
stackName: my-stack
|
|
263
|
+
cells:
|
|
264
|
+
- package: "@otavia/sso"
|
|
265
|
+
mount: "sso"
|
|
266
|
+
params:
|
|
267
|
+
AUTH_COOKIE_DOMAIN: !Env AUTH_COOKIE_DOMAIN
|
|
268
|
+
domain:
|
|
269
|
+
host: example.com
|
|
270
|
+
`
|
|
271
|
+
);
|
|
272
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow(
|
|
273
|
+
'otavia.yaml: cells["sso"].params.AUTH_COOKIE_DOMAIN cannot use !Env/!Secret; use !Param to reference top-level params'
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
fs.rmSync(tmp, { recursive: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("throws when stackName is missing or empty", () => {
|
|
281
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
282
|
+
try {
|
|
283
|
+
writeYaml(
|
|
284
|
+
tmp,
|
|
285
|
+
`
|
|
286
|
+
stackName:
|
|
287
|
+
cells: [a]
|
|
288
|
+
domain:
|
|
289
|
+
host: x.com
|
|
290
|
+
`
|
|
291
|
+
);
|
|
292
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow("otavia.yaml: missing stackName");
|
|
293
|
+
} finally {
|
|
294
|
+
fs.rmSync(tmp, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("throws when stackName is empty string", () => {
|
|
299
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
300
|
+
try {
|
|
301
|
+
writeYaml(
|
|
302
|
+
tmp,
|
|
303
|
+
`
|
|
304
|
+
stackName: ""
|
|
305
|
+
cells: [a]
|
|
306
|
+
domain:
|
|
307
|
+
host: x.com
|
|
308
|
+
`
|
|
309
|
+
);
|
|
310
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow("otavia.yaml: missing stackName");
|
|
311
|
+
} finally {
|
|
312
|
+
fs.rmSync(tmp, { recursive: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("throws when cells is missing or empty array", () => {
|
|
317
|
+
const tmpMissing = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
318
|
+
const tmpEmpty = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
319
|
+
try {
|
|
320
|
+
writeYaml(
|
|
321
|
+
tmpMissing,
|
|
322
|
+
`
|
|
323
|
+
stackName: s
|
|
324
|
+
domain:
|
|
325
|
+
host: x.com
|
|
326
|
+
`
|
|
327
|
+
);
|
|
328
|
+
expect(() => loadOtaviaYaml(tmpMissing)).toThrow("otavia.yaml: missing cells");
|
|
329
|
+
|
|
330
|
+
writeYaml(
|
|
331
|
+
tmpEmpty,
|
|
332
|
+
`
|
|
333
|
+
stackName: s
|
|
334
|
+
cells: []
|
|
335
|
+
domain:
|
|
336
|
+
host: x.com
|
|
337
|
+
`
|
|
338
|
+
);
|
|
339
|
+
expect(() => loadOtaviaYaml(tmpEmpty)).toThrow("otavia.yaml: cells must be a non-empty array or object");
|
|
340
|
+
} finally {
|
|
341
|
+
fs.rmSync(tmpMissing, { recursive: true });
|
|
342
|
+
fs.rmSync(tmpEmpty, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("throws when domain or domain.host is missing", () => {
|
|
347
|
+
const tmpNoDomain = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
348
|
+
const tmpNoHost = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
349
|
+
try {
|
|
350
|
+
writeYaml(
|
|
351
|
+
tmpNoDomain,
|
|
352
|
+
`
|
|
353
|
+
stackName: s
|
|
354
|
+
cells: [a]
|
|
355
|
+
`
|
|
356
|
+
);
|
|
357
|
+
expect(() => loadOtaviaYaml(tmpNoDomain)).toThrow("otavia.yaml: missing domain");
|
|
358
|
+
|
|
359
|
+
writeYaml(
|
|
360
|
+
tmpNoHost,
|
|
361
|
+
`
|
|
362
|
+
stackName: s
|
|
363
|
+
cells: [a]
|
|
364
|
+
domain: {}
|
|
365
|
+
`
|
|
366
|
+
);
|
|
367
|
+
expect(() => loadOtaviaYaml(tmpNoHost)).toThrow("otavia.yaml: missing domain.host");
|
|
368
|
+
} finally {
|
|
369
|
+
fs.rmSync(tmpNoDomain, { recursive: true });
|
|
370
|
+
fs.rmSync(tmpNoHost, { recursive: true });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("throws when defaultCell is not a string", () => {
|
|
375
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
376
|
+
try {
|
|
377
|
+
writeYaml(
|
|
378
|
+
tmp,
|
|
379
|
+
`
|
|
380
|
+
stackName: s
|
|
381
|
+
defaultCell: 123
|
|
382
|
+
cells: [sso]
|
|
383
|
+
domain:
|
|
384
|
+
host: x.com
|
|
385
|
+
`
|
|
386
|
+
);
|
|
387
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow("otavia.yaml: defaultCell must be a string");
|
|
388
|
+
} finally {
|
|
389
|
+
fs.rmSync(tmp, { recursive: true });
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("throws when defaultCell is not in cells", () => {
|
|
394
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
395
|
+
try {
|
|
396
|
+
writeYaml(
|
|
397
|
+
tmp,
|
|
398
|
+
`
|
|
399
|
+
stackName: s
|
|
400
|
+
defaultCell: drive
|
|
401
|
+
cells: [sso]
|
|
402
|
+
domain:
|
|
403
|
+
host: x.com
|
|
404
|
+
`
|
|
405
|
+
);
|
|
406
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow(
|
|
407
|
+
'otavia.yaml: defaultCell "drive" must match one of configured cell mounts'
|
|
408
|
+
);
|
|
409
|
+
} finally {
|
|
410
|
+
fs.rmSync(tmp, { recursive: true });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("parses oauth callback config", () => {
|
|
415
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
416
|
+
try {
|
|
417
|
+
writeYaml(
|
|
418
|
+
tmp,
|
|
419
|
+
`
|
|
420
|
+
stackName: my-stack
|
|
421
|
+
cells:
|
|
422
|
+
sso: "@otavia/sso"
|
|
423
|
+
domain:
|
|
424
|
+
host: example.com
|
|
425
|
+
oauth:
|
|
426
|
+
callback:
|
|
427
|
+
cell: sso
|
|
428
|
+
path: /oauth/callback
|
|
429
|
+
`
|
|
430
|
+
);
|
|
431
|
+
const result = loadOtaviaYaml(tmp);
|
|
432
|
+
expect(result.oauth).toEqual({
|
|
433
|
+
callback: {
|
|
434
|
+
cell: "sso",
|
|
435
|
+
path: "/oauth/callback",
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
} finally {
|
|
439
|
+
fs.rmSync(tmp, { recursive: true });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("throws when oauth callback path does not start with slash", () => {
|
|
444
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
445
|
+
try {
|
|
446
|
+
writeYaml(
|
|
447
|
+
tmp,
|
|
448
|
+
`
|
|
449
|
+
stackName: my-stack
|
|
450
|
+
cells:
|
|
451
|
+
sso: "@otavia/sso"
|
|
452
|
+
domain:
|
|
453
|
+
host: example.com
|
|
454
|
+
oauth:
|
|
455
|
+
callback:
|
|
456
|
+
cell: sso
|
|
457
|
+
path: oauth/callback
|
|
458
|
+
`
|
|
459
|
+
);
|
|
460
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow(
|
|
461
|
+
"otavia.yaml: oauth.callback.path must start with '/'"
|
|
462
|
+
);
|
|
463
|
+
} finally {
|
|
464
|
+
fs.rmSync(tmp, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("throws when oauth callback cell is not in cells", () => {
|
|
469
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "otavia-test-"));
|
|
470
|
+
try {
|
|
471
|
+
writeYaml(
|
|
472
|
+
tmp,
|
|
473
|
+
`
|
|
474
|
+
stackName: my-stack
|
|
475
|
+
cells:
|
|
476
|
+
sso: "@otavia/sso"
|
|
477
|
+
domain:
|
|
478
|
+
host: example.com
|
|
479
|
+
oauth:
|
|
480
|
+
callback:
|
|
481
|
+
cell: drive
|
|
482
|
+
path: /oauth/callback
|
|
483
|
+
`
|
|
484
|
+
);
|
|
485
|
+
expect(() => loadOtaviaYaml(tmp)).toThrow(
|
|
486
|
+
'otavia.yaml: oauth.callback.cell "drive" must match one of configured cells'
|
|
487
|
+
);
|
|
488
|
+
} finally {
|
|
489
|
+
fs.rmSync(tmp, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolvePortsFromEnv, resolvePortsFromPortBase } from "../ports.js";
|
|
3
|
+
|
|
4
|
+
describe("resolvePortsFromPortBase", () => {
|
|
5
|
+
test("derives dev ports from PORT_BASE + dev offsets", () => {
|
|
6
|
+
expect(resolvePortsFromPortBase("dev", 7000)).toEqual({
|
|
7
|
+
portBase: 7000,
|
|
8
|
+
frontend: 7100,
|
|
9
|
+
backend: 8900,
|
|
10
|
+
dynamodb: 9001,
|
|
11
|
+
minio: 9000,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("derives test ports from PORT_BASE + test offsets", () => {
|
|
16
|
+
expect(resolvePortsFromPortBase("test", 8000)).toEqual({
|
|
17
|
+
portBase: 8000,
|
|
18
|
+
frontend: 8100,
|
|
19
|
+
backend: 8910,
|
|
20
|
+
dynamodb: 8012,
|
|
21
|
+
minio: 9014,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("resolvePortsFromEnv", () => {
|
|
27
|
+
test("reads PORT_BASE from env map", () => {
|
|
28
|
+
expect(resolvePortsFromEnv("dev", { PORT_BASE: "7000" })).toEqual({
|
|
29
|
+
portBase: 7000,
|
|
30
|
+
frontend: 7100,
|
|
31
|
+
backend: 8900,
|
|
32
|
+
dynamodb: 9001,
|
|
33
|
+
minio: 9000,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("throws when PORT_BASE is missing", () => {
|
|
38
|
+
expect(() => resolvePortsFromEnv("dev", {})).toThrow(
|
|
39
|
+
'Missing PORT_BASE for stage "dev". Define it in .env.dev/.env.test or process env.'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("throws when PORT_BASE is invalid", () => {
|
|
44
|
+
expect(() => resolvePortsFromEnv("test", { PORT_BASE: "abc" })).toThrow(
|
|
45
|
+
'Invalid PORT_BASE for stage "test": "abc"'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { resolveCellDir } from "../resolve-cell-dir.js";
|
|
6
|
+
|
|
7
|
+
function writeCellYaml(dir: string) {
|
|
8
|
+
writeFileSync(join(dir, "cell.yaml"), "name: test\n", "utf-8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("resolveCellDir", () => {
|
|
12
|
+
test("mount-only prefers cells/<mount> when cell.yaml exists", () => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "otavia-rcd-"));
|
|
14
|
+
try {
|
|
15
|
+
const cells = join(root, "cells", "sso");
|
|
16
|
+
mkdirSync(cells, { recursive: true });
|
|
17
|
+
writeCellYaml(cells);
|
|
18
|
+
expect(resolveCellDir(root, "sso")).toBe(cells);
|
|
19
|
+
} finally {
|
|
20
|
+
rmSync(root, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("scoped package prefers cells/<slug> over node_modules when both have cell.yaml", () => {
|
|
25
|
+
const root = mkdtempSync(join(tmpdir(), "otavia-rcd-"));
|
|
26
|
+
try {
|
|
27
|
+
const cells = join(root, "cells", "sso");
|
|
28
|
+
mkdirSync(cells, { recursive: true });
|
|
29
|
+
writeCellYaml(cells);
|
|
30
|
+
const nm = join(root, "node_modules", "@otavia", "sso");
|
|
31
|
+
mkdirSync(nm, { recursive: true });
|
|
32
|
+
writeCellYaml(nm);
|
|
33
|
+
expect(resolveCellDir(root, "@otavia/sso")).toBe(cells);
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("scoped package uses node_modules when cells has no cell.yaml", () => {
|
|
40
|
+
const root = mkdtempSync(join(tmpdir(), "otavia-rcd-"));
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(join(root, "cells", "sso"), { recursive: true });
|
|
43
|
+
const nm = join(root, "node_modules", "@otavia", "sso");
|
|
44
|
+
mkdirSync(nm, { recursive: true });
|
|
45
|
+
writeCellYaml(nm);
|
|
46
|
+
expect(resolveCellDir(root, "@otavia/sso")).toBe(nm);
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("scoped package defaults to cells/<slug> path when nothing resolves", () => {
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), "otavia-rcd-"));
|
|
54
|
+
try {
|
|
55
|
+
expect(resolveCellDir(root, "@otavia/sso")).toBe(join(root, "cells", "sso"));
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(root, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|