package database_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/database" ) // keptField is a non-secret value planted alongside each secret, so // the assertions below cannot pass by the model marshalling to nothing. const keptField = "keepme" // marshalModel encodes a model the way a future JSON handler would. func marshalModel(t *testing.T, v any) string { t.Helper() encoded, err := json.Marshal(v) require.NoError(t, err) return string(encoded) } // TestModelsDoNotMarshalTheirSecrets pins the barrier for the JSON // path. The /api/v1 route group exists and is empty; delivery's // TargetView masks the credential for the HTML path only, so without // these tags the first handler that marshals a model serialises the // secret with it. Each field below is a live credential: // // - Target.Config holds an incoming-webhook URL whose path segments // are the bearer token. // - APIKey.Key is a bearer token outright. // - Setting.Value holds the session encryption key. // - User.Password holds the Argon2 hash, and was already tagged. func TestModelsDoNotMarshalTheirSecrets(t *testing.T) { t.Parallel() const marker = "QQMODELMARKERQQ" cases := []struct { name string model any }{ { name: "target config", model: database.Target{ Name: keptField, Type: database.TargetTypeSlack, Config: `{"webhookUrl":"https://h/s/` + marker + `"}`, }, }, { name: "api key", model: database.APIKey{ Description: keptField, Key: marker, }, }, { name: "setting value", model: database.Setting{ Key: keptField, Value: marker, }, }, { name: "user password hash", model: database.User{ Username: keptField, Password: marker, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() encoded := marshalModel(t, tc.model) assert.NotContains(t, encoded, marker) assert.Contains(t, encoded, keptField) }) } } // TestWebhookMarshalsNoTargetConfig covers the nested case: a webhook // marshalled with its targets preloaded must not carry the credential // through the association either. func TestWebhookMarshalsNoTargetConfig(t *testing.T) { t.Parallel() const marker = "QQNESTEDMARKERQQ" encoded := marshalModel(t, database.Webhook{ Name: keptField, Targets: []database.Target{{ Name: "slack", Config: `{"webhookUrl":"https://h/s/` + marker + `"}`, }}, }) assert.NotContains(t, encoded, marker) assert.Contains(t, encoded, keptField) }