package lagoon import ( "bytes" "log/slog" "strings" "testing" ) type fillFixture struct { Name string `gorm:"column:name"` CollectionID uint `gorm:"column:collection_id"` Notes *string `gorm:"column:notes"` } func (fillFixture) Fillable() []string { return []string{"name", "notes"} } func (fillFixture) Hidden() []string { return []string{"collection_id"} } var ( _ HasFillable = fillFixture{} _ HasHidden = fillFixture{} ) func TestFillAllowList(t *testing.T) { var row fillFixture row.CollectionID = 3 err := Fill(&row, []string{"name"}, map[string]any{ "name": "x", "collection_id": uint(9), }, true) if err != nil { t.Fatal(err) } if row.Name != "x" { t.Fatalf("name = %q", row.Name) } if row.CollectionID != 3 { t.Fatalf("collection_id mutated to %d", row.CollectionID) } } func TestFillDroppedKeyLogsOnce(t *testing.T) { var buf bytes.Buffer prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))) defer slog.SetDefault(prev) var row fillFixture requested := map[string]any{"name": "once", "collection_id": uint(1)} if err := Fill(&row, []string{"name"}, requested, false); err != nil { t.Fatal(err) } if err := Fill(&row, []string{"name"}, requested, false); err != nil { t.Fatal(err) } log := buf.String() if strings.Count(log, "collection_id") != 1 { t.Fatalf("dropped key should log once, got %q", log) } if !strings.Contains(log, "lagoon: dropped non-fillable key") { t.Fatalf("missing warn message: %q", log) } } func TestFillProductionSilent(t *testing.T) { var buf bytes.Buffer prev := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))) defer slog.SetDefault(prev) var row fillFixture if err := Fill(&row, []string{"name"}, map[string]any{ "name": "prod", "unknown_field": true, }, true); err != nil { t.Fatal(err) } if buf.Len() != 0 { t.Fatalf("production fill must be silent, got %q", buf.String()) } if row.Name != "prod" { t.Fatalf("name = %q", row.Name) } } func TestFillNilPointerClearsNullable(t *testing.T) { existing := "keep" row := fillFixture{Notes: &existing} if err := Fill(&row, []string{"notes"}, map[string]any{"notes": nil}, true); err != nil { t.Fatal(err) } if row.Notes != nil { t.Fatalf("notes = %v, want nil", row.Notes) } } func TestFillDroppedKeyNeverErrors(t *testing.T) { var row fillFixture if err := Fill(&row, []string{"name"}, map[string]any{"collection_id": uint(9), "nope": 1}, true); err != nil { t.Fatalf("dropped keys must not error: %v", err) } if row.CollectionID != 0 { t.Fatalf("collection_id = %d", row.CollectionID) } } type fillSecretFixture struct { Name string `gorm:"column:name"` APIKey Encrypted `gorm:"column:api_key" json:"-"` Token *Encrypted `gorm:"column:token" json:"-"` } func TestFillEncryptedTakesPlaintext(t *testing.T) { if err := PublishEncryptionKeys(nil, bytes.Repeat([]byte("F"), 32), nil); err != nil { t.Fatal(err) } allowed := []string{"name", "api_key", "token"} var row fillSecretFixture if err := Fill(&row, allowed, map[string]any{"api_key": "sk-plain", "token": "tok-plain"}, true); err != nil { t.Fatal(err) } if row.APIKey.Reveal() != "sk-plain" { t.Fatalf("api_key Reveal = %q", row.APIKey.Reveal()) } if row.Token == nil || row.Token.Reveal() != "tok-plain" { t.Fatalf("token = %v", row.Token) } // Another row's ciphertext is stored as literal text, never decrypted. stolen, err := NewEncrypted("victim-secret").Value() if err != nil { t.Fatal(err) } ciphertext, ok := stolen.(string) if !ok { t.Fatalf("ciphertext driver value is %T", stolen) } var thief fillSecretFixture if err := Fill(&thief, allowed, map[string]any{"api_key": ciphertext}, true); err != nil { t.Fatal(err) } if thief.APIKey.Reveal() != ciphertext { t.Fatal("ciphertext in a request must not be decrypted into the victim's plaintext") } for _, bad := range []any{42, true, map[string]any{"x": 1}, []byte("raw")} { var r fillSecretFixture if err := Fill(&r, allowed, map[string]any{"api_key": bad}, true); err == nil { t.Fatalf("api_key=%T must be rejected", bad) } if r.APIKey.Reveal() != "" { t.Fatalf("api_key=%T left a value behind", bad) } } if err := Fill(&row, allowed, map[string]any{"api_key": nil, "token": nil}, true); err != nil { t.Fatal(err) } if row.APIKey.Reveal() != "" || row.Token != nil { t.Fatalf("nil must clear: api_key=%q token=%v", row.APIKey.Reveal(), row.Token) } }