1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-01-27 23:46:18 +02:00
pocketbase/core/settings_model_test.go
2024-09-29 21:09:46 +03:00

692 lines
14 KiB
Go

package core_test
import (
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/mailer"
)
func TestSettingsDelete(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
err := app.Delete(app.Settings())
if err == nil {
t.Fatal("Exected settings delete to fail")
}
}
func TestSettingsMerge(t *testing.T) {
s1 := &core.Settings{}
s1.Meta.AppURL = "app_url" // should be unset
s2 := &core.Settings{}
s2.Meta.AppName = "test"
s2.Logs.MaxDays = 123
s2.SMTP.Host = "test"
s2.SMTP.Enabled = true
s2.S3.Enabled = true
s2.S3.Endpoint = "test"
s2.Backups.Cron = "* * * * *"
s2.Batch.Timeout = 15
if err := s1.Merge(s2); err != nil {
t.Fatal(err)
}
s1Encoded, err := json.Marshal(s1)
if err != nil {
t.Fatal(err)
}
s2Encoded, err := json.Marshal(s2)
if err != nil {
t.Fatal(err)
}
if string(s1Encoded) != string(s2Encoded) {
t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", string(s1Encoded), string(s2Encoded))
}
}
func TestSettingsClone(t *testing.T) {
s1 := &core.Settings{}
s1.Meta.AppName = "test_name"
s2, err := s1.Clone()
if err != nil {
t.Fatal(err)
}
s1Bytes, err := json.Marshal(s1)
if err != nil {
t.Fatal(err)
}
s2Bytes, err := json.Marshal(s2)
if err != nil {
t.Fatal(err)
}
if string(s1Bytes) != string(s2Bytes) {
t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes))
}
// verify that it is a deep copy
s2.Meta.AppName = "new_test_name"
if s1.Meta.AppName == s2.Meta.AppName {
t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName)
}
}
func TestSettingsMarshalJSON(t *testing.T) {
settings := &core.Settings{}
// control fields
settings.Meta.AppName = "test123"
settings.SMTP.Username = "abc"
// secrets
testSecret := "test_secret"
settings.SMTP.Password = testSecret
settings.S3.Secret = testSecret
settings.Backups.S3.Secret = testSecret
raw, err := json.Marshal(settings)
if err != nil {
t.Fatal(err)
}
rawStr := string(raw)
expected := `{"smtp":{"enabled":false,"port":0,"host":"","username":"abc","authMethod":"","tls":false,"localName":""},"backups":{"cron":"","cronMaxKeep":0,"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false},"meta":{"appName":"test123","appURL":"","senderName":"","senderAddress":"","hideControls":false},"logs":{"maxDays":0,"minLevel":0,"logIP":false,"logAuthId":false},"batch":{"enabled":false,"maxRequests":0,"timeout":0,"maxBodySize":0},"rateLimits":{"rules":[],"enabled":false},"trustedProxy":{"headers":[],"useLeftmostIP":false}}`
if rawStr != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr)
}
}
func TestSettingsValidate(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
s := app.Settings()
// set invalid settings data
s.Meta.AppName = ""
s.Logs.MaxDays = -10
s.SMTP.Enabled = true
s.SMTP.Host = ""
s.S3.Enabled = true
s.S3.Endpoint = "invalid"
s.Backups.Cron = "invalid"
s.Backups.CronMaxKeep = -10
s.Batch.Enabled = true
s.Batch.MaxRequests = -1
s.Batch.Timeout = -1
s.RateLimits.Enabled = true
s.RateLimits.Rules = nil
// check if Validate() is triggering the members validate methods.
err := app.Validate(s)
if err == nil {
t.Fatalf("Expected error, got nil")
}
expectations := []string{
`"meta":{`,
`"logs":{`,
`"smtp":{`,
`"s3":{`,
`"backups":{`,
`"batch":{`,
`"rateLimits":{`,
}
errBytes, _ := json.Marshal(err)
jsonErr := string(errBytes)
for _, expected := range expectations {
if !strings.Contains(jsonErr, expected) {
t.Errorf("Expected error key %s in %v", expected, jsonErr)
}
}
}
func TestMetaConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.MetaConfig
expectedErrors []string
}{
{
"zero values",
core.MetaConfig{},
[]string{
"appName",
"appURL",
"senderName",
"senderAddress",
},
},
{
"invalid data",
core.MetaConfig{
AppName: strings.Repeat("a", 300),
AppURL: "test",
SenderName: strings.Repeat("a", 300),
SenderAddress: "invalid_email",
},
[]string{
"appName",
"appURL",
"senderName",
"senderAddress",
},
},
{
"valid data",
core.MetaConfig{
AppName: "test",
AppURL: "https://example.com",
SenderName: "test",
SenderAddress: "test@example.com",
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestLogsConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.LogsConfig
expectedErrors []string
}{
{
"zero values",
core.LogsConfig{},
[]string{},
},
{
"invalid data",
core.LogsConfig{MaxDays: -1},
[]string{"maxDays"},
},
{
"valid data",
core.LogsConfig{MaxDays: 2},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestSMTPConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.SMTPConfig
expectedErrors []string
}{
{
"zero values (disabled)",
core.SMTPConfig{},
[]string{},
},
{
"zero values (enabled)",
core.SMTPConfig{Enabled: true},
[]string{"host", "port"},
},
{
"invalid data",
core.SMTPConfig{
Enabled: true,
Host: "test:test:test",
Port: -10,
LocalName: "invalid!",
AuthMethod: "invalid",
},
[]string{"host", "port", "authMethod", "localName"},
},
{
"valid data (no explicit auth method and localName)",
core.SMTPConfig{
Enabled: true,
Host: "example.com",
Port: 100,
TLS: true,
},
[]string{},
},
{
"valid data (explicit auth method and localName)",
core.SMTPConfig{
Enabled: true,
Host: "example.com",
Port: 100,
AuthMethod: mailer.SMTPAuthLogin,
LocalName: "example.com",
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestS3ConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.S3Config
expectedErrors []string
}{
{
"zero values (disabled)",
core.S3Config{},
[]string{},
},
{
"zero values (enabled)",
core.S3Config{Enabled: true},
[]string{
"bucket",
"region",
"endpoint",
"accessKey",
"secret",
},
},
{
"invalid data",
core.S3Config{
Enabled: true,
Endpoint: "test:test:test",
},
[]string{
"bucket",
"region",
"endpoint",
"accessKey",
"secret",
},
},
{
"valid data (url endpoint)",
core.S3Config{
Enabled: true,
Endpoint: "https://localhost:8090",
Bucket: "test",
Region: "test",
AccessKey: "test",
Secret: "test",
},
[]string{},
},
{
"valid data (hostname endpoint)",
core.S3Config{
Enabled: true,
Endpoint: "example.com",
Bucket: "test",
Region: "test",
AccessKey: "test",
Secret: "test",
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestBackupsConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.BackupsConfig
expectedErrors []string
}{
{
"zero value",
core.BackupsConfig{},
[]string{},
},
{
"invalid cron",
core.BackupsConfig{
Cron: "invalid",
CronMaxKeep: 0,
},
[]string{"cron", "cronMaxKeep"},
},
{
"invalid enabled S3",
core.BackupsConfig{
S3: core.S3Config{
Enabled: true,
},
},
[]string{"s3"},
},
{
"valid data",
core.BackupsConfig{
S3: core.S3Config{
Enabled: true,
Endpoint: "example.com",
Bucket: "test",
Region: "test",
AccessKey: "test",
Secret: "test",
},
Cron: "*/10 * * * *",
CronMaxKeep: 1,
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestBatchConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.BatchConfig
expectedErrors []string
}{
{
"zero value",
core.BatchConfig{},
[]string{},
},
{
"zero value (enabled)",
core.BatchConfig{Enabled: true},
[]string{"maxRequests", "timeout"},
},
{
"invalid data (negative values)",
core.BatchConfig{
MaxRequests: -1,
Timeout: -1,
MaxBodySize: -1,
},
[]string{"maxRequests", "timeout", "maxBodySize"},
},
{
"min fields valid data",
core.BatchConfig{
Enabled: true,
MaxRequests: 1,
Timeout: 1,
},
[]string{},
},
{
"all fields valid data",
core.BatchConfig{
Enabled: true,
MaxRequests: 10,
Timeout: 1,
MaxBodySize: 1,
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestRateLimitsConfigValidate(t *testing.T) {
scenarios := []struct {
name string
config core.RateLimitsConfig
expectedErrors []string
}{
{
"zero value (disabled)",
core.RateLimitsConfig{},
[]string{},
},
{
"zero value (enabled)",
core.RateLimitsConfig{Enabled: true},
[]string{"rules"},
},
{
"invalid data",
core.RateLimitsConfig{
Enabled: true,
Rules: []core.RateLimitRule{
{
Label: "/123abc/",
Duration: 1,
MaxRequests: 2,
},
{
Label: "!abc",
Duration: -1,
MaxRequests: -1,
},
},
},
[]string{"rules"},
},
{
"valid data",
core.RateLimitsConfig{
Enabled: true,
Rules: []core.RateLimitRule{
{
Label: "123_abc",
Duration: 1,
MaxRequests: 2,
},
{
Label: "/456-abc",
Duration: 1,
MaxRequests: 2,
},
},
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestRateLimitsFindRateLimitRule(t *testing.T) {
limits := core.RateLimitsConfig{
Rules: []core.RateLimitRule{
{Label: "abc"},
{Label: "POST /test/a/"},
{Label: "/test/a/"},
{Label: "POST /test/a"},
{Label: "/test/a"},
},
}
scenarios := []struct {
labels []string
expected string
}{
{[]string{}, ""},
{[]string{"missing"}, ""},
{[]string{"abc"}, "abc"},
{[]string{"/test"}, ""},
{[]string{"/test/a"}, "/test/a"},
{[]string{"GET /test/a"}, ""},
{[]string{"POST /test/a"}, "POST /test/a"},
{[]string{"/test/a/b/c"}, "/test/a/"},
{[]string{"GET /test/a/b/c"}, ""},
{[]string{"POST /test/a/b/c"}, "POST /test/a/"},
{[]string{"/test/a", "abc"}, "/test/a"}, // priority checks
}
for _, s := range scenarios {
t.Run(strings.Join(s.labels, ""), func(t *testing.T) {
rule, ok := limits.FindRateLimitRule(s.labels)
hasLabel := rule.Label != ""
if hasLabel != ok {
t.Fatalf("Expected hasLabel %v, got %v", hasLabel, ok)
}
if rule.Label != s.expected {
t.Fatalf("Expected rule with label %q, got %q", s.expected, rule.Label)
}
})
}
}
func TestRateLimitRuleValidate(t *testing.T) {
scenarios := []struct {
name string
config core.RateLimitRule
expectedErrors []string
}{
{
"zero value",
core.RateLimitRule{},
[]string{"label", "duration", "maxRequests"},
},
{
"invalid data",
core.RateLimitRule{
Label: "@abc",
Duration: -1,
MaxRequests: -1,
},
[]string{"label", "duration", "maxRequests"},
},
{
"valid data (name)",
core.RateLimitRule{
Label: "abc:123",
Duration: 1,
MaxRequests: 1,
},
[]string{},
},
{
"valid data (name:action)",
core.RateLimitRule{
Label: "abc:123",
Duration: 1,
MaxRequests: 1,
},
[]string{},
},
{
"valid data (*:action)",
core.RateLimitRule{
Label: "*:123",
Duration: 1,
MaxRequests: 1,
},
[]string{},
},
{
"valid data (path /a/b)",
core.RateLimitRule{
Label: "/a/b",
Duration: 1,
MaxRequests: 1,
},
[]string{},
},
{
"valid data (path POST /a/b)",
core.RateLimitRule{
Label: "POST /a/b/",
Duration: 1,
MaxRequests: 1,
},
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := s.config.Validate()
tests.TestValidationErrors(t, result, s.expectedErrors)
})
}
}
func TestRateLimitRuleDurationTime(t *testing.T) {
scenarios := []struct {
config core.RateLimitRule
expected time.Duration
}{
{core.RateLimitRule{}, 0 * time.Second},
{core.RateLimitRule{Duration: 1234}, 1234 * time.Second},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) {
result := s.config.DurationTime()
if result != s.expected {
t.Fatalf("Expected duration %d, got %d", s.expected, result)
}
})
}
}