@specific.dev/spectest 0.45.0 → 0.47.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/dist/daemon.js CHANGED
@@ -933,8 +933,15 @@ async function ensureCaTrustedImage(serviceName, tag) {
933
933
  const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
934
934
  await fs.mkdir(ctxDir, { recursive: true });
935
935
  await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
936
+ // An image that declares a non-root `USER` runs this step as that user,
937
+ // and writing the trust store then fails ("cannot create
938
+ // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
939
+ // silently never lands. Take root for the one command, then hand the
940
+ // image back its own user so containers still run as it.
941
+ const declaredUser = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
942
+ const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
936
943
  const dockerfile = `FROM ${tag}
937
- COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
944
+ ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
938
945
  RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
939
946
  update-ca-certificates; \\
940
947
  elif command -v update-ca-trust >/dev/null 2>&1; then \\
@@ -942,7 +949,7 @@ RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
942
949
  else \\
943
950
  echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
944
951
  fi
945
- `;
952
+ ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
946
953
  await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
947
954
  const build = await docker(["build", "-t", tag, ctxDir], 300_000);
948
955
  if (build.code !== 0) {
@@ -29,6 +29,12 @@
29
29
  * showed the problem, which is why it went unnoticed: only the appending
30
30
  * variable was in play.
31
31
  *
32
+ * **Deno is a third mechanism.** It reads none of those variables and
33
+ * trusts only its bundled Mozilla roots, so an edge function calling a
34
+ * fake fails with `UnknownIssuer` however the image is set up.
35
+ * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
+ * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
+ *
32
38
  * **`command` and `args` are different overrides.** `command` replaces the
33
39
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
34
40
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -29,6 +29,12 @@
29
29
  * showed the problem, which is why it went unnoticed: only the appending
30
30
  * variable was in play.
31
31
  *
32
+ * **Deno is a third mechanism.** It reads none of those variables and
33
+ * trusts only its bundled Mozilla roots, so an edge function calling a
34
+ * fake fails with `UnknownIssuer` however the image is set up.
35
+ * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
+ * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
+ *
32
38
  * **`command` and `args` are different overrides.** `command` replaces the
33
39
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
34
40
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -87,6 +93,13 @@ export function runContainerArgs(input) {
87
93
  // replacing ones take the combined bundle.
88
94
  args.push(`--volume=${input.caPath}:${input.caPath}:ro`);
89
95
  args.push("-e", `NODE_EXTRA_CA_CERTS=${input.caPath}`);
96
+ // Deno reads none of the above, and its default store is the bundled
97
+ // Mozilla roots alone — so a Deno runtime (a Supabase edge function is
98
+ // the usual one) rejects every certificate this environment mints, with
99
+ // or without the CA layered into the image. `system` adds the platform
100
+ // store, which resolves through `SSL_CERT_FILE`: the bundle below.
101
+ // `mozilla` stays first so the public roots survive.
102
+ args.push("-e", "DENO_TLS_CA_STORE=mozilla,system");
90
103
  const bundle = input.caBundle;
91
104
  if (bundle) {
92
105
  if (bundle !== input.caPath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,261 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { mapSupabaseConfig } from "./supabase-config.js";
4
+
5
+ const toml = (s: string): unknown =>
6
+ (globalThis as unknown as { Bun: { TOML: { parse(s: string): unknown } } }).Bun.TOML.parse(s);
7
+
8
+ describe("mapSupabaseConfig", () => {
9
+ test("maps the auth settings a test can observe", () => {
10
+ const out = mapSupabaseConfig(
11
+ toml(`
12
+ [auth]
13
+ jwt_expiry = 7200
14
+ minimum_password_length = 10
15
+
16
+ [auth.email]
17
+ otp_length = 8
18
+
19
+ [auth.hook.before_user_created]
20
+ enabled = true
21
+ uri = "pg-functions://postgres/public/hook_restrict_signup"
22
+ `),
23
+ );
24
+
25
+ expect(out.auth).toMatchObject({
26
+ GOTRUE_JWT_EXP: "7200",
27
+ GOTRUE_PASSWORD_MIN_LENGTH: "10",
28
+ GOTRUE_MAILER_OTP_LENGTH: "8",
29
+ GOTRUE_HOOK_BEFORE_USER_CREATED_ENABLED: "true",
30
+ GOTRUE_HOOK_BEFORE_USER_CREATED_URI: "pg-functions://postgres/public/hook_restrict_signup",
31
+ });
32
+ expect(out.warnings).toEqual([]);
33
+ });
34
+
35
+ // The two mappings a copy-paste gets backwards without anyone noticing.
36
+ test("negates enable_signup and enable_confirmations", () => {
37
+ const on = mapSupabaseConfig(
38
+ toml(`
39
+ [auth]
40
+ enable_signup = true
41
+ [auth.email]
42
+ enable_confirmations = true
43
+ `),
44
+ );
45
+ expect(on.auth.GOTRUE_DISABLE_SIGNUP).toBe("false");
46
+ expect(on.auth.GOTRUE_MAILER_AUTOCONFIRM).toBe("false");
47
+
48
+ const off = mapSupabaseConfig(
49
+ toml(`
50
+ [auth]
51
+ enable_signup = false
52
+ [auth.email]
53
+ enable_confirmations = false
54
+ `),
55
+ );
56
+ expect(off.auth.GOTRUE_DISABLE_SIGNUP).toBe("true");
57
+ expect(off.auth.GOTRUE_MAILER_AUTOCONFIRM).toBe("true");
58
+ });
59
+
60
+ test("collects mail templates by id", () => {
61
+ const out = mapSupabaseConfig(
62
+ toml(`
63
+ [auth.email.template.invite]
64
+ subject = "You've been invited"
65
+ content_path = "./supabase/templates/invite.html"
66
+ `),
67
+ );
68
+ expect(out.templates.invite).toEqual({
69
+ path: "./supabase/templates/invite.html",
70
+ subject: "You've been invited",
71
+ });
72
+ });
73
+
74
+ // A subject with no content_path is how the CLI spells "default body, custom
75
+ // subject" — it must not invent a template path.
76
+ test("keeps a subject-only template out of the served set", () => {
77
+ const out = mapSupabaseConfig(
78
+ toml(`
79
+ [auth.email.template.recovery]
80
+ subject = "Reset it"
81
+ content_path = ""
82
+ `),
83
+ );
84
+ expect(out.templates.recovery).toEqual({ path: "", subject: "Reset it" });
85
+ });
86
+
87
+ test("reads per-function verify_jwt and the api schemas", () => {
88
+ const out = mapSupabaseConfig(
89
+ toml(`
90
+ [api]
91
+ schemas = ["public", "graphql_public"]
92
+ max_rows = 500
93
+ port = 54321
94
+
95
+ [functions.invoice-api]
96
+ verify_jwt = false
97
+ [functions.account-api]
98
+ verify_jwt = true
99
+ `),
100
+ );
101
+ expect(out.functionVerifyJwt).toEqual({ "invoice-api": false, "account-api": true });
102
+ expect(out.rest).toEqual({ PGRST_DB_SCHEMAS: "public,graphql_public", PGRST_DB_MAX_ROWS: "500" });
103
+ });
104
+
105
+ // Addresses are this environment's, not the file's: its URLs name the
106
+ // developer's machine, where nothing is listening inside a VM.
107
+ test("never takes site_url or the redirect allow list from the file", () => {
108
+ const out = mapSupabaseConfig(
109
+ toml(`
110
+ [auth]
111
+ site_url = "http://localhost:3000"
112
+ additional_redirect_urls = ["http://localhost:3000/**"]
113
+ `),
114
+ );
115
+ expect(out.auth).toEqual({});
116
+ expect(out.unsupported).toEqual([]);
117
+ expect(out.warnings).toEqual([]);
118
+ });
119
+
120
+ // Reported, never fatal — the file belongs to the project and describes more
121
+ // environments than this one.
122
+ test("reports a setting it cannot apply, with the reason", () => {
123
+ const out = mapSupabaseConfig(
124
+ toml(`
125
+ [auth.captcha]
126
+ enabled = true
127
+ provider = "hcaptcha"
128
+ secret = "env(HCAPTCHA_SECRET)"
129
+ `),
130
+ );
131
+ expect(out.unsupported).toHaveLength(1);
132
+ expect(out.unsupported[0]!.key).toBe("auth.captcha");
133
+ expect(out.unsupported[0]!.reason).toMatch(/captcha/);
134
+ });
135
+
136
+ // Social sign-in is collected, not refused: the component serves each
137
+ // provider it has an emulator for at that provider's real endpoints.
138
+ test("collects the social providers and where their credentials come from", () => {
139
+ const out = mapSupabaseConfig(
140
+ toml(`
141
+ [auth.external.google]
142
+ enabled = true
143
+ client_id = "env(GOOGLE_OAUTH_CLIENT_ID)"
144
+ secret = "env(GOOGLE_OAUTH_SECRET)"
145
+
146
+ [auth.external.azure]
147
+ enabled = true
148
+ client_id = "literal-azure-id"
149
+ url = "https://login.microsoftonline.com/tenant-1/v2.0"
150
+
151
+ [auth.external.facebook]
152
+ enabled = false
153
+ `),
154
+ );
155
+ expect(out.externalProviders.google).toEqual({
156
+ enabled: true,
157
+ clientIdFrom: "GOOGLE_OAUTH_CLIENT_ID",
158
+ secretFrom: "GOOGLE_OAUTH_SECRET",
159
+ });
160
+ expect(out.externalProviders.azure).toEqual({
161
+ enabled: true,
162
+ clientId: "literal-azure-id",
163
+ url: "https://login.microsoftonline.com/tenant-1/v2.0",
164
+ });
165
+ expect(out.externalProviders.facebook!.enabled).toBe(false);
166
+ expect(out.unsupported).toEqual([]);
167
+ });
168
+
169
+ // The common shape: a config lists a dozen providers with every one off.
170
+ // Nothing is lost by not applying them, so nothing is said.
171
+ test("stays quiet about an unsupported feature that is switched off", () => {
172
+ const out = mapSupabaseConfig(
173
+ toml(`
174
+ [auth.captcha]
175
+ enabled = false
176
+ provider = "hcaptcha"
177
+
178
+ [auth.sms]
179
+ enable_signup = false
180
+ template = "code {{ .Code }}"
181
+ `),
182
+ );
183
+ expect(out.unsupported).toEqual([]);
184
+ });
185
+
186
+ test("reports a section where only one feature is enabled", () => {
187
+ const out = mapSupabaseConfig(
188
+ toml(`
189
+ [auth.mfa.phone]
190
+ enroll_enabled = true
191
+ verify_enabled = false
192
+ `),
193
+ );
194
+ expect(out.unsupported.map((u) => u.key)).toEqual(["auth.mfa.phone"]);
195
+ });
196
+
197
+ // Both ways the file defers a value to the surrounding environment. Passing
198
+ // either through would hand a function the string `env(NAME)` or a base64
199
+ // ciphertext — the latter looking enough like a value to go unnoticed.
200
+ test("separates literal secrets from the two deferred forms", () => {
201
+ const out = mapSupabaseConfig(
202
+ toml(`
203
+ [edge_runtime.secrets]
204
+ FEATURE_FLAG = "true"
205
+ STRIPE_KEY = "env(STRIPE_SECRET_KEY)"
206
+ SENDGRID_KEY = "encrypted:c2VjcmV0LXZhbHVl"
207
+ `),
208
+ );
209
+ expect(out.functionSecrets).toEqual({ FEATURE_FLAG: "true" });
210
+ // `env(NAME)` records the name to read it from…
211
+ expect(out.deferredSecrets).toEqual({ STRIPE_KEY: "STRIPE_SECRET_KEY" });
212
+ // …while an inline ciphertext names nothing and cannot be supplied here.
213
+ expect(out.encryptedValues).toEqual(["edge_runtime.secrets.SENDGRID_KEY"]);
214
+ });
215
+
216
+ // A provider credential reads from the same `.env` as everything else — it
217
+ // is a field on the project's auth settings rather than a named secret, but
218
+ // the value gets there the same way.
219
+ test("records where a provider credential is read from", () => {
220
+ const out = mapSupabaseConfig(
221
+ toml(`
222
+ [auth.external.google]
223
+ enabled = true
224
+ client_id = "env(GOOGLE_OAUTH_CLIENT_ID)"
225
+ secret = "encrypted:c2hoaA=="
226
+ `),
227
+ );
228
+ expect(out.externalProviders.google).toEqual({
229
+ enabled: true,
230
+ clientIdFrom: "GOOGLE_OAUTH_CLIENT_ID",
231
+ });
232
+ // The ciphertext names nothing, so it is reported instead of guessed at.
233
+ expect(out.encryptedValues).toEqual(["auth.external.google.secret"]);
234
+ });
235
+
236
+ test("warns rather than fails on a key it has never seen", () => {
237
+ const out = mapSupabaseConfig(toml(`\n[auth]\nfuture_option = true\n`));
238
+ expect(out.warnings).toEqual(["auth.future_option"]);
239
+ });
240
+
241
+ // Sections spectest owns outright never reach the user as noise.
242
+ test("drops the topology sections in silence", () => {
243
+ const out = mapSupabaseConfig(
244
+ toml(`
245
+ project_id = "test"
246
+ [db]
247
+ port = 54322
248
+ [db.pooler]
249
+ enabled = true
250
+ [studio]
251
+ port = 54323
252
+ [inbucket]
253
+ port = 54324
254
+ [remotes.production.auth]
255
+ site_url = "https://example.com"
256
+ `),
257
+ );
258
+ expect(out.warnings).toEqual([]);
259
+ expect(out.auth).toEqual({});
260
+ });
261
+ });