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.URL is an incoming-webhook URL whose path segments are // the bearer token. // - Target.Headers holds header values, routinely authorization // tokens. // - 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 url", model: database.Target{ Name: keptField, Type: database.TargetTypeSlack, URL: "https://h/s/" + marker, MaxAttempts: 1, }, }, { name: "target headers", model: database.Target{ Name: keptField, Type: database.TargetTypeHTTP, Headers: map[string]string{"Authorization": marker}, MaxAttempts: 1, }, }, { 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) }) } } // TestWebhookMarshalsNoTargetURL covers the nested case: a webhook // marshalled with its targets preloaded must not carry the credential // through the association either. func TestWebhookMarshalsNoTargetURL(t *testing.T) { t.Parallel() const marker = "QQNESTEDMARKERQQ" encoded := marshalModel(t, database.Webhook{ Name: keptField, Targets: []database.Target{{ Name: "slack", URL: "https://h/s/" + marker, MaxAttempts: 1, }}, }) assert.NotContains(t, encoded, marker) assert.Contains(t, encoded, keptField) }