-
Notifications
You must be signed in to change notification settings - Fork 242
/
Copy pathconfig_test.go
527 lines (494 loc) · 17.2 KB
/
config_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
package config
import (
"bytes"
_ "embed"
"path"
"strings"
"testing"
fs "testing/fstest"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
//go:embed testdata/config.toml
var testInitConfigEmbed []byte
func TestConfigParsing(t *testing.T) {
t.Run("classic config file", func(t *testing.T) {
config := NewConfig()
// Run test
var buf bytes.Buffer
require.NoError(t, config.Eject(&buf))
file := fs.MapFile{Data: buf.Bytes()}
fsys := fs.MapFS{"config.toml": &file}
// Check error
assert.NoError(t, config.Load("config.toml", fsys))
})
t.Run("optional config file", func(t *testing.T) {
config := NewConfig()
// Run test
err := config.Load("", fs.MapFS{})
// Check error
assert.NoError(t, err)
})
t.Run("config file with environment variables", func(t *testing.T) {
config := NewConfig()
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
"supabase/templates/invite.html": &fs.MapFile{},
}
// Run test
t.Setenv("TWILIO_AUTH_TOKEN", "token")
t.Setenv("AZURE_CLIENT_ID", "hello")
t.Setenv("AZURE_SECRET", "this is cool")
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
t.Setenv("SENDGRID_API_KEY", "sendgrid")
t.Setenv("AUTH_CALLBACK_URL", "http://localhost:3000/auth/callback")
assert.NoError(t, config.Load("", fsys))
// Check error
assert.Equal(t, "hello", config.Auth.External["azure"].ClientId)
assert.Equal(t, "this is cool", config.Auth.External["azure"].Secret.Value)
assert.Equal(t, []string{
"https://127.0.0.1:3000",
"http://localhost:3000/auth/callback",
}, config.Auth.AdditionalRedirectUrls)
})
t.Run("config file with environment variables fails when unset", func(t *testing.T) {
config := NewConfig()
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
}
// Run test
assert.Error(t, config.Load("", fsys))
})
}
func TestRemoteOverride(t *testing.T) {
t.Run("load staging override", func(t *testing.T) {
config := NewConfig()
config.ProjectId = "bvikqvbczudanvggcord"
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
"supabase/templates/invite.html": &fs.MapFile{},
}
// Run test
t.Setenv("SUPABASE_AUTH_SITE_URL", "http://preview.com")
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
assert.NoError(t, config.Load("", fsys))
// Check error
assert.True(t, config.Db.Seed.Enabled)
assert.Equal(t, "http://preview.com", config.Auth.SiteUrl)
assert.Equal(t, []string{"image/png"}, config.Storage.Buckets["images"].AllowedMimeTypes)
})
t.Run("load production override", func(t *testing.T) {
config := NewConfig()
config.ProjectId = "vpefcjyosynxeiebfscx"
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
"supabase/templates/invite.html": &fs.MapFile{},
}
// Run test
t.Setenv("SUPABASE_AUTH_SITE_URL", "http://preview.com")
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
assert.NoError(t, config.Load("", fsys))
// Check error
assert.False(t, config.Db.Seed.Enabled)
assert.Equal(t, "http://feature-auth-branch.com/", config.Auth.SiteUrl)
assert.Equal(t, false, config.Auth.External["azure"].Enabled)
assert.Equal(t, "nope", config.Auth.External["azure"].ClientId)
})
t.Run("config file with remotes", func(t *testing.T) {
config := NewConfig()
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
"supabase/templates/invite.html": &fs.MapFile{},
}
// Run test
t.Setenv("TWILIO_AUTH_TOKEN", "token")
t.Setenv("AZURE_CLIENT_ID", "hello")
t.Setenv("AZURE_SECRET", "this is cool")
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
t.Setenv("SENDGRID_API_KEY", "sendgrid")
t.Setenv("AUTH_CALLBACK_URL", "http://localhost:3000/auth/callback")
assert.NoError(t, config.Load("", fsys))
// Check the default value in the config
assert.Equal(t, "http://127.0.0.1:3000", config.Auth.SiteUrl)
assert.Equal(t, true, config.Auth.EnableSignup)
assert.Equal(t, true, config.Auth.External["azure"].Enabled)
assert.Equal(t, []string{"image/png", "image/jpeg"}, config.Storage.Buckets["images"].AllowedMimeTypes)
// Check the values for remotes override
production, ok := config.Remotes["production"]
assert.True(t, ok)
staging, ok := config.Remotes["staging"]
assert.True(t, ok)
// Check the values for production override
assert.Equal(t, "vpefcjyosynxeiebfscx", production.ProjectId)
assert.Equal(t, "http://feature-auth-branch.com/", production.Auth.SiteUrl)
assert.Equal(t, false, production.Auth.EnableSignup)
assert.Equal(t, false, production.Auth.External["azure"].Enabled)
assert.Equal(t, "nope", production.Auth.External["azure"].ClientId)
// Check seed should be disabled by default for remote configs
assert.Equal(t, false, production.Db.Seed.Enabled)
// Check the values for the staging override
assert.Equal(t, "bvikqvbczudanvggcord", staging.ProjectId)
assert.Equal(t, []string{"image/png"}, staging.Storage.Buckets["images"].AllowedMimeTypes)
assert.Equal(t, true, staging.Db.Seed.Enabled)
})
}
func TestFileSizeLimitConfigParsing(t *testing.T) {
t.Run("test file size limit parsing number", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = 5000000
`, &testConfig)
if assert.NoError(t, err) {
assert.Equal(t, sizeInBytes(5000000), testConfig.Storage.FileSizeLimit)
}
})
t.Run("test file size limit parsing bytes unit", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = "5MB"
`, &testConfig)
if assert.NoError(t, err) {
assert.Equal(t, sizeInBytes(5242880), testConfig.Storage.FileSizeLimit)
}
})
t.Run("test file size limit parsing binary bytes unit", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = "5MiB"
`, &testConfig)
if assert.NoError(t, err) {
assert.Equal(t, sizeInBytes(5242880), testConfig.Storage.FileSizeLimit)
}
})
t.Run("test file size limit parsing string number", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = "5000000"
`, &testConfig)
if assert.NoError(t, err) {
assert.Equal(t, sizeInBytes(5000000), testConfig.Storage.FileSizeLimit)
}
})
t.Run("test file size limit parsing bad datatype", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = []
`, &testConfig)
assert.Error(t, err)
assert.Equal(t, sizeInBytes(0), testConfig.Storage.FileSizeLimit)
})
t.Run("test file size limit parsing bad string data", func(t *testing.T) {
var testConfig config
_, err := toml.Decode(`
[storage]
file_size_limit = "foobar"
`, &testConfig)
assert.Error(t, err)
assert.Equal(t, sizeInBytes(0), testConfig.Storage.FileSizeLimit)
})
}
func TestSanitizeProjectI(t *testing.T) {
// Preserves valid consecutive characters
assert.Equal(t, "abc", sanitizeProjectId("abc"))
assert.Equal(t, "a..b_c", sanitizeProjectId("a..b_c"))
// Removes leading special characters
assert.Equal(t, "abc", sanitizeProjectId("_abc"))
assert.Equal(t, "abc", sanitizeProjectId("_@abc"))
// Replaces consecutive invalid characters with a single _
assert.Equal(t, "a_bc-", sanitizeProjectId("a@@bc-"))
// Truncates to less than 40 characters
sanitized := strings.Repeat("a", maxProjectIdLength)
assert.Equal(t, sanitized, sanitizeProjectId(sanitized+"bb"))
}
const (
defaultAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
defaultServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
)
func TestSigningJWT(t *testing.T) {
t.Run("signs default anon key", func(t *testing.T) {
anonToken := CustomClaims{Role: "anon"}.NewToken()
signed, err := anonToken.SignedString([]byte(defaultJwtSecret))
assert.NoError(t, err)
assert.Equal(t, defaultAnonKey, signed)
})
t.Run("signs default service_role key", func(t *testing.T) {
serviceToken := CustomClaims{Role: "service_role"}.NewToken()
signed, err := serviceToken.SignedString([]byte(defaultJwtSecret))
assert.NoError(t, err)
assert.Equal(t, defaultServiceRoleKey, signed)
})
}
func TestValidateHookURI(t *testing.T) {
tests := []struct {
hookConfig
name string
errorMsg string
}{
{
name: "valid http URL",
hookConfig: hookConfig{
Enabled: true,
URI: "http://example.com",
Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="},
},
},
{
name: "valid https URL",
hookConfig: hookConfig{
Enabled: true,
URI: "https://example.com",
Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="},
},
},
{
name: "valid pg-functions URI",
hookConfig: hookConfig{
Enabled: true,
URI: "pg-functions://functionName",
},
},
{
name: "invalid URI with unsupported scheme",
hookConfig: hookConfig{
Enabled: true,
URI: "ftp://example.com",
Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="},
},
errorMsg: "Invalid hook config: auth.hook.invalid URI with unsupported scheme.uri should be a HTTP, HTTPS, or pg-functions URI",
},
{
name: "invalid URI with parsing error",
hookConfig: hookConfig{
Enabled: true,
URI: "http://a b.com",
Secrets: Secret{Value: "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw=="},
},
errorMsg: "failed to parse template url: parse \"http://a b.com\": invalid character \" \" in host name",
},
{
name: "valid http URL with missing secrets",
hookConfig: hookConfig{
Enabled: true,
URI: "http://example.com",
},
errorMsg: "Missing required field in config: auth.hook.valid http URL with missing secrets.secrets",
},
{
name: "valid pg-functions URI with unsupported secrets",
hookConfig: hookConfig{
Enabled: true,
URI: "pg-functions://functionName",
Secrets: Secret{Value: "test-secret"},
},
errorMsg: "Invalid hook config: auth.hook.valid pg-functions URI with unsupported secrets.secrets is unsupported for pg-functions URI",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.validate(tt.name)
if len(tt.errorMsg) > 0 {
assert.Error(t, err, "Expected an error for %v", tt.name)
assert.EqualError(t, err, tt.errorMsg, "Expected error message does not match for %v", tt.name)
} else {
assert.NoError(t, err, "Expected no error for %v", tt.name)
}
})
}
}
func TestGlobFiles(t *testing.T) {
t.Run("returns seed files matching patterns", func(t *testing.T) {
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/seeds/seed1.sql": &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")},
"supabase/seeds/seed2.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/seed3.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/ignore.sql": &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")},
}
// Mock config patterns
g := Glob{
"supabase/seeds/seed[12].sql",
"supabase/seeds/ano*.sql",
}
// Run test
files, err := g.Files(fsys)
// Check error
assert.NoError(t, err)
// Validate files
assert.ElementsMatch(t, []string{
"supabase/seeds/seed1.sql",
"supabase/seeds/seed2.sql",
"supabase/seeds/another.sql",
}, files)
})
t.Run("returns seed files matching patterns skip duplicates", func(t *testing.T) {
// Setup in-memory fs
fsys := fs.MapFS{
"supabase/seeds/seed1.sql": &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")},
"supabase/seeds/seed2.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/seed3.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
"supabase/seeds/ignore.sql": &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")},
}
// Mock config patterns
g := Glob{
"supabase/seeds/seed[12].sql",
"supabase/seeds/ano*.sql",
"supabase/seeds/seed*.sql",
}
// Run test
files, err := g.Files(fsys)
// Check error
assert.NoError(t, err)
// Validate files
assert.ElementsMatch(t, []string{
"supabase/seeds/seed1.sql",
"supabase/seeds/seed2.sql",
"supabase/seeds/another.sql",
"supabase/seeds/seed3.sql",
}, files)
})
t.Run("returns error on invalid pattern", func(t *testing.T) {
// Setup in-memory fs
fsys := fs.MapFS{}
// Mock config patterns
g := Glob{"[*!#@D#"}
// Run test
files, err := g.Files(fsys)
// Check error
assert.ErrorIs(t, err, path.ErrBadPattern)
// The resuling seed list should be empty
assert.Empty(t, files)
})
t.Run("returns empty list if no files match", func(t *testing.T) {
// Setup in-memory fs
fsys := fs.MapFS{}
// Mock config patterns
g := Glob{"seeds/*.sql"}
// Run test
files, err := g.Files(fsys)
// Check error
assert.ErrorContains(t, err, "no files matched")
// Validate files
assert.Empty(t, files)
})
}
func TestLoadFunctionImportMap(t *testing.T) {
t.Run("uses deno.json as import map when present", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/deno.json": &fs.MapFile{},
"supabase/functions/hello/index.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that deno.json was set as import map
assert.Equal(t, "supabase/functions/hello/deno.json", config.Functions["hello"].ImportMap)
})
t.Run("uses deno.jsonc as import map when present", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
`)},
"supabase/functions/hello/deno.jsonc": &fs.MapFile{},
"supabase/functions/hello/index.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that deno.jsonc was set as import map
assert.Equal(t, "supabase/functions/hello/deno.jsonc", config.Functions["hello"].ImportMap)
})
t.Run("config.toml takes precedence over deno.json", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions]
hello.import_map = "custom_import_map.json"
`)},
"supabase/functions/hello/deno.json": &fs.MapFile{},
"supabase/functions/hello/index.ts": &fs.MapFile{},
}
// Run test
assert.NoError(t, config.Load("", fsys))
// Check that config.toml takes precedence over deno.json
assert.Equal(t, "supabase/custom_import_map.json", config.Functions["hello"].ImportMap)
})
}
func TestLoadFunctionErrorMessageParsing(t *testing.T) {
t.Run("returns error for array-style function config", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[[functions]]
name = "hello"
verify_jwt = true
`)},
}
// Run test
err := config.Load("", fsys)
// Check error contains both decode errors
assert.ErrorContains(t, err, invalidFunctionsConfigFormat)
})
t.Run("returns error with function slug for invalid non-existent field", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
unknown_field = true
`)},
}
// Run test
err := config.Load("", fsys)
// Check error contains both decode errors
assert.ErrorContains(t, err, "'functions[hello]' has invalid keys: unknown_field")
})
t.Run("returns error with function slug for invalid field value", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions.hello]
verify_jwt = "not-a-bool"
`)},
}
// Run test
err := config.Load("", fsys)
// Check error contains both decode errors
assert.ErrorContains(t, err, `cannot parse 'functions[hello].verify_jwt' as bool: strconv.ParseBool: parsing "not-a-bool"`)
})
t.Run("returns error for unknown function fields", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
project_id = "bvikqvbczudanvggcord"
[functions]
name = "hello"
verify_jwt = true
`)},
}
// Run test
err := config.Load("", fsys)
assert.ErrorContains(t, err, `'functions[name]' expected a map, got 'string'`)
assert.ErrorContains(t, err, `'functions[verify_jwt]' expected a map, got 'bool'`)
})
}