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)) }) 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.hookConfig.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 TestLoadEnv(t *testing.T) { t.Setenv("SUPABASE_AUTH_JWT_SECRET", "test-secret") t.Setenv("SUPABASE_DB_ROOT_KEY", "test-root-key") config := NewConfig() // Run test err := config.loadFromEnv() // Check error assert.NoError(t, err) assert.Equal(t, "test-secret", config.Auth.JwtSecret) assert.Equal(t, "test-root-key", config.Db.RootKey) } 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'`) }) }