package delivery_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/delivery" ) // The path of a Slack incoming webhook URL is the credential: // whoever holds these segments can post to the channel // forever. None of them may reach a stored delivery error, // which lives on disk in the per-webhook database and is // serialized by the JSON tag on DeliveryResult.Error. const ( maskSecretPath = "/services/T00000000/B00000000/" + "XXXXXXXXXXXXXXXXXXXXXXXX" ) // assertNoCredential fails if the whole path or any single // segment of it survived into the message, so a partial leak // fails the test too. func assertNoCredential(t *testing.T, msg string) { t.Helper() segments := []string{ maskSecretPath, "services", "T00000000", "B00000000", "XXXXXXXXXXXXXXXXXXXXXXXX", } for _, segment := range segments { assert.NotContains(t, msg, segment) } } // storedDeliveryError returns the error string persisted for a // delivery, which is what an operator and any future API read. func storedDeliveryError( t *testing.T, db *gorm.DB, deliveryID string, ) string { t.Helper() var result database.DeliveryResult require.NoError(t, db.Where( "delivery_id = ?", deliveryID, ).First(&result).Error) return result.Error } // deliverSlackTo runs a Slack delivery against webhookURL and // returns the error string it persisted. func deliverSlackTo( t *testing.T, webhookURL string, ) string { t.Helper() db := testWebhookDB(t) e := testEngine(t, 1) targetID := uuid.New().String() slackCfg, err := json.Marshal( delivery.SlackTargetConfig{ WebhookURL: webhookURL, }, ) require.NoError(t, err) event := seedEvent(t, db, `{"test":true}`) dlv := seedDelivery( t, db, event.ID, targetID, database.DeliveryStatusPending, ) d := buildSlackDelivery( dlv, event, targetID, "test-slack-mask", string(slackCfg), ) e.ExportDeliverSlack(context.TODO(), db, d) assertDeliveryStatus(t, db, dlv.ID, database.DeliveryStatusFailed, ) return storedDeliveryError(t, db, dlv.ID) } // TestDeliverSlack_TransportErrorMasksWebhookURL is the // load-bearing regression test: a transport failure must not // persist the webhook URL's credential into the database, and // must still say what went wrong and where. func TestDeliverSlack_TransportErrorMasksWebhookURL( t *testing.T, ) { t.Parallel() // A server closed before use gives a deterministic // transport failure against a known host. ts := httptest.NewServer(http.NewServeMux()) host := ts.URL ts.Close() errMsg := deliverSlackTo(t, host+maskSecretPath) require.NotEmpty(t, errMsg) assertNoCredential(t, errMsg) // The diagnostic value survives: the operation, the host // and the transport failure are all still reported, and // only the path is elided. assert.Contains(t, errMsg, "sending request") assert.Contains(t, errMsg, "Post") assert.Contains(t, errMsg, host+"/...") assert.Contains(t, errMsg, "connection refused") } // TestDeliverSlack_UnparsableURLMasksWebhookURL covers the // other error path out of a Slack attempt: url.Parse also // embeds the whole URL in the error it returns. func TestDeliverSlack_UnparsableURLMasksWebhookURL( t *testing.T, ) { t.Parallel() errMsg := deliverSlackTo( t, "https://hooks.slack.com"+maskSecretPath+"\n", ) require.NotEmpty(t, errMsg) assertNoCredential(t, errMsg) assert.Contains(t, errMsg, "invalid control character") } // TestDoHTTPRequest_TransportErrorMasksURL proves the HTTP // target's transport errors are masked too; its destination // URL can carry a token in a query string. func TestDoHTTPRequest_TransportErrorMasksURL(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.NewServeMux()) host := ts.URL ts.Close() e := testEngine(t, 1) cfg, err := e.ExportParseHTTPConfig( newHTTPTargetConfig(host + maskSecretPath), ) require.NoError(t, err) statusCode, _, _, reqErr := e.ExportDoHTTPRequest( context.TODO(), cfg, &database.Event{Body: `{"test":true}`}, ) require.Error(t, reqErr) assert.Zero(t, statusCode) assertNoCredential(t, reqErr.Error()) assert.Contains(t, reqErr.Error(), host+"/...") assert.Contains( t, reqErr.Error(), "connection refused", ) } // TestValidateTargetURL_UnparsableURLIsMasked proves the SSRF // validator's error does not carry the submitted URL, which // the handler both logs and shows. func TestValidateTargetURL_UnparsableURLIsMasked(t *testing.T) { t.Parallel() err := delivery.ValidateTargetURL( context.TODO(), "https://hooks.slack.com"+maskSecretPath+"\n", ) require.Error(t, err) assertNoCredential(t, err.Error()) assert.Contains(t, err.Error(), "invalid URL") }