Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions server/internal/api/apiv1/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ func TestValidateDatabaseSpec(t *testing.T) {
HostIds: []api.Identifier{"host-1"},
ConnectAs: "app",
Port: utils.PointerTo(8080),
Config: map[string]any{},
Config: map[string]any{"db_anon_role": "web_anon"},
},
},
},
Expand Down Expand Up @@ -841,7 +841,7 @@ func TestValidateDatabaseSpec(t *testing.T) {
HostIds: []api.Identifier{"host-2"},
ConnectAs: "app",
Port: utils.PointerTo(8080),
Config: map[string]any{},
Config: map[string]any{"db_anon_role": "web_anon"},
},
},
},
Expand Down Expand Up @@ -912,7 +912,7 @@ func TestValidateDatabaseSpec(t *testing.T) {
Version: "1.0.0",
HostIds: []api.Identifier{"host-1"},
ConnectAs: "app",
Config: map[string]any{},
Config: map[string]any{"db_anon_role": "web_anon"},
},
},
},
Expand Down Expand Up @@ -953,7 +953,7 @@ func TestValidateDatabaseSpec(t *testing.T) {
HostIds: []api.Identifier{"host-1"},
ConnectAs: "app",
Port: utils.PointerTo(0),
Config: map[string]any{},
Config: map[string]any{"db_anon_role": "web_anon"},
},
},
},
Expand Down Expand Up @@ -1341,7 +1341,7 @@ func TestValidateServiceSpec(t *testing.T) {
ServiceType: "postgrest",
Version: "14.5",
HostIds: []api.Identifier{"host-1"},
Config: map[string]any{},
Config: map[string]any{"db_anon_role": "web_anon"},
},
},
{
Expand Down Expand Up @@ -1417,13 +1417,14 @@ func TestValidateServiceSpec(t *testing.T) {
},
},
{
name: "valid postgrest with nil config",
name: "postgrest with nil config fails — db_anon_role required",
svc: &api.ServiceSpec{
ServiceID: "my-postgrest",
ServiceType: "postgrest",
Version: "latest",
HostIds: []api.Identifier{"host-1"},
},
expected: []string{"db_anon_role is required"},
},
{
name: "RAG service with nil config requires pipelines",
Expand All @@ -1438,15 +1439,15 @@ func TestValidateServiceSpec(t *testing.T) {
},
},
{
name: "valid postgrest with defaults",
name: "postgrest with empty config fails — db_anon_role required",
svc: &api.ServiceSpec{
ServiceID: "my-postgrest",
ServiceType: "postgrest",
Version: "latest",
HostIds: []api.Identifier{"host-1"},
Config: map[string]any{},
},
expected: []string{},
expected: []string{"db_anon_role is required"},
},
{
name: "valid postgrest with all config fields",
Expand Down
10 changes: 6 additions & 4 deletions server/internal/database/postgrest_service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// optional; defaults are applied when absent.
type PostgRESTServiceConfig struct {
DBSchemas string `json:"db_schemas"` // default: "public"
DBAnonRole string `json:"db_anon_role"` // default: "pgedge_application_read_only"
DBAnonRole string `json:"db_anon_role"`
DBPool int `json:"db_pool"` // default: 10, range: 1-30
MaxRows int `json:"max_rows"` // default: 1000, range: 1-10000
JWTSecret *string `json:"jwt_secret,omitempty"`
Expand Down Expand Up @@ -53,9 +53,11 @@ func ParsePostgRESTServiceConfig(config map[string]any) (*PostgRESTServiceConfig
}
}

// db_anon_role — optional string, default "pgedge_application_read_only"
dbAnonRole := "pgedge_application_read_only"
if v, ok := config["db_anon_role"]; ok {
// db_anon_role — required string
var dbAnonRole string
if v, ok := config["db_anon_role"]; !ok {
errs = append(errs, fmt.Errorf("db_anon_role is required"))
} else {
s, sOk := v.(string)
if !sOk {
errs = append(errs, fmt.Errorf("db_anon_role must be a string"))
Expand Down
79 changes: 45 additions & 34 deletions server/internal/database/postgrest_service_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ func makeTestConn() database.PostgRESTConnParams {
func TestGenerateConf_CoreFields(t *testing.T) {
cfg := &database.PostgRESTServiceConfig{
DBSchemas: "public",
DBAnonRole: "pgedge_application_read_only",
DBAnonRole: "web_anon",
DBPool: 10,
MaxRows: 1000,
}
data, err := cfg.GenerateConf(makeTestConn())
require.NoError(t, err)
m := parseConf(t, data)
assert.Equal(t, "public", m["db-schemas"])
assert.Equal(t, "pgedge_application_read_only", m["db-anon-role"])
assert.Equal(t, "web_anon", m["db-anon-role"])
assert.Equal(t, "10", m["db-pool"])
assert.Equal(t, "1000", m["db-max-rows"])
}
Expand Down Expand Up @@ -164,17 +164,10 @@ func TestGenerateConf_DBURIMultiHost(t *testing.T) {
}

func TestParsePostgRESTServiceConfig(t *testing.T) {
t.Run("defaults applied for empty config", func(t *testing.T) {
cfg, errs := database.ParsePostgRESTServiceConfig(map[string]any{})
require.Empty(t, errs)
assert.Equal(t, "public", cfg.DBSchemas)
assert.Equal(t, "pgedge_application_read_only", cfg.DBAnonRole)
assert.Equal(t, 10, cfg.DBPool)
assert.Equal(t, 1000, cfg.MaxRows)
assert.Nil(t, cfg.JWTSecret)
assert.Nil(t, cfg.JWTAud)
assert.Nil(t, cfg.JWTRoleClaimKey)
assert.Nil(t, cfg.ServerCORSAllowedOrigins)
t.Run("db_anon_role required — empty config returns error", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_anon_role is required")
})

t.Run("all fields overridden", func(t *testing.T) {
Expand Down Expand Up @@ -205,15 +198,17 @@ func TestParsePostgRESTServiceConfig(t *testing.T) {

t.Run("db_schemas wrong type", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_schemas": 123,
"db_anon_role": "web_anon",
"db_schemas": 123,
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_schemas must be a string")
})

t.Run("db_schemas empty string", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_schemas": "",
"db_anon_role": "web_anon",
"db_schemas": "",
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_schemas must not be empty")
Expand All @@ -237,91 +232,103 @@ func TestParsePostgRESTServiceConfig(t *testing.T) {

t.Run("db_pool below range", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": float64(0),
"db_anon_role": "web_anon",
"db_pool": float64(0),
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_pool must be between 1 and 30")
})

t.Run("db_pool above range", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": float64(31),
"db_anon_role": "web_anon",
"db_pool": float64(31),
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_pool must be between 1 and 30")
})

t.Run("db_pool wrong type", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": "ten",
"db_anon_role": "web_anon",
"db_pool": "ten",
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_pool must be an integer")
})

t.Run("db_pool non-integer float", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": float64(5.5),
"db_anon_role": "web_anon",
"db_pool": float64(5.5),
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "db_pool must be an integer")
})

t.Run("db_pool boundary values", func(t *testing.T) {
cfg, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": float64(1),
"db_anon_role": "web_anon",
"db_pool": float64(1),
})
require.Empty(t, errs)
assert.Equal(t, 1, cfg.DBPool)

cfg, errs = database.ParsePostgRESTServiceConfig(map[string]any{
"db_pool": float64(30),
"db_anon_role": "web_anon",
"db_pool": float64(30),
})
require.Empty(t, errs)
assert.Equal(t, 30, cfg.DBPool)
})

t.Run("max_rows below range", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"max_rows": float64(0),
"db_anon_role": "web_anon",
"max_rows": float64(0),
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "max_rows must be between 1 and 10000")
})

t.Run("max_rows above range", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"max_rows": float64(10001),
"db_anon_role": "web_anon",
"max_rows": float64(10001),
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "max_rows must be between 1 and 10000")
})

t.Run("max_rows boundary values", func(t *testing.T) {
cfg, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"max_rows": float64(1),
"db_anon_role": "web_anon",
"max_rows": float64(1),
})
require.Empty(t, errs)
assert.Equal(t, 1, cfg.MaxRows)

cfg, errs = database.ParsePostgRESTServiceConfig(map[string]any{
"max_rows": float64(10000),
"db_anon_role": "web_anon",
"max_rows": float64(10000),
})
require.Empty(t, errs)
assert.Equal(t, 10000, cfg.MaxRows)
})

t.Run("jwt_secret too short", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"jwt_secret": "short",
"db_anon_role": "web_anon",
"jwt_secret": "short",
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "jwt_secret must be at least 32 characters")
})

t.Run("jwt_secret exactly 32 chars", func(t *testing.T) {
cfg, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"jwt_secret": "12345678901234567890123456789012",
"db_anon_role": "web_anon",
"jwt_secret": "12345678901234567890123456789012",
})
require.Empty(t, errs)
require.NotNil(t, cfg.JWTSecret)
Expand All @@ -330,24 +337,27 @@ func TestParsePostgRESTServiceConfig(t *testing.T) {

t.Run("jwt_secret wrong type", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"jwt_secret": 12345,
"db_anon_role": "web_anon",
"jwt_secret": 12345,
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "jwt_secret must be a string")
})

t.Run("unknown config key", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"unknown_key": "value",
"db_anon_role": "web_anon",
"unknown_key": "value",
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), `unknown config key "unknown_key"`)
})

t.Run("multiple unknown config keys", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"foo": "bar",
"baz": 123,
"db_anon_role": "web_anon",
"foo": "bar",
"baz": 123,
})
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "unknown config keys:")
Expand All @@ -357,9 +367,10 @@ func TestParsePostgRESTServiceConfig(t *testing.T) {

t.Run("multiple errors collected", func(t *testing.T) {
_, errs := database.ParsePostgRESTServiceConfig(map[string]any{
"db_schemas": "",
"db_pool": float64(0),
"max_rows": "not-a-number",
"db_anon_role": "web_anon",
"db_schemas": "",
"db_pool": float64(0),
"max_rows": "not-a-number",
})
require.Len(t, errs, 3)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ func (r *PostgRESTAuthenticatorResource) TypeDependencies() []resource.Type {
}

func (r *PostgRESTAuthenticatorResource) desiredAnonRole() string {
if r.DBAnonRole != "" {
return r.DBAnonRole
}
return "pgedge_application_read_only"
return r.DBAnonRole
}

func (r *PostgRESTAuthenticatorResource) authenticatorUsername() string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ func TestPostgRESTAuthenticatorResource_Dependencies(t *testing.T) {
}

func TestPostgRESTAuthenticatorResource_DesiredAnonRole(t *testing.T) {
t.Run("empty string falls back to default", func(t *testing.T) {
t.Run("empty string returns empty", func(t *testing.T) {
r := &PostgRESTAuthenticatorResource{DBAnonRole: ""}
assert.Equal(t, "pgedge_application_read_only", r.desiredAnonRole())
assert.Equal(t, "", r.desiredAnonRole())
})

t.Run("custom anon role is preserved", func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestPostgRESTConfigResource_WriteConfigFile(t *testing.T) {

cfg := &database.PostgRESTServiceConfig{
DBSchemas: "public",
DBAnonRole: "pgedge_application_read_only",
DBAnonRole: "web_anon",
DBPool: 10,
MaxRows: 1000,
}
Expand All @@ -92,7 +92,7 @@ func TestPostgRESTConfigResource_WriteConfigFile(t *testing.T) {
assert.Contains(t, content, "db-schemas")
assert.Contains(t, content, "public")
assert.Contains(t, content, "db-anon-role")
assert.Contains(t, content, "pgedge_application_read_only")
assert.Contains(t, content, "web_anon")
}

func TestPostgRESTConfigResource_WriteConfigFile_JWTFields(t *testing.T) {
Expand Down