@specific.dev/spectest 0.45.0 → 0.46.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/components/supabase-config.d.ts +89 -0
- package/dist/components/supabase-config.js +447 -0
- package/dist/components/supabase.d.ts +124 -84
- package/dist/components/supabase.js +395 -136
- package/package.json +1 -1
- package/src/components/supabase-config.test.ts +261 -0
- package/src/components/supabase-config.ts +543 -0
- package/src/components/supabase-dir.test.ts +287 -0
- package/src/components/supabase.ts +581 -230
package/package.json
CHANGED
|
@@ -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
|
+
});
|