package email import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "unicode/utf8" "git.sendico.io/tech/sxchnge/notifications/model" ) func TestProviderErrorDiagnostics(t *testing.T) { for _, tc := range []struct { name, body, code, message string }{ {"resend", `{"name":"validation_error","message":"The sxchnge.com domain is not verified"}`, "validation_error", "The sxchnge.com domain is not verified"}, {"sendgrid", `{"errors":[{"message":"The sender identity is not verified","field":"from","help":"private URL"}]}`, "", "The sender identity is not verified"}, {"no message", `{"unrecognized":"private response"}`, "", "Provider did not include an error message"}, {"malformed", `{"message":"private response"`, "", "Provider error details unavailable: invalid JSON response"}, {"wrong type", `{"message":{"secret":"private response"}}`, "", "Provider error details unavailable: invalid JSON response"}, {"trailing", `{"message":"private response"} {}`, "", "Provider error details unavailable: invalid JSON response"}, {"oversized", `{"message":"` + strings.Repeat("x", maxErrorBody) + `"}`, "", "Provider error details unavailable: incomplete or oversized response"}, } { t.Run(tc.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = io.WriteString(w, tc.body) })) defer server.Close() c := client{http: server.Client(), endpoint: server.URL} out := c.post(context.Background(), "", struct{}{}) if out.State != model.Failed || out.Code != "provider_authentication_failed" || out.HTTPStatus != 403 || out.ProviderError != tc.code || out.ProviderMessage != tc.message { t.Fatalf("unexpected provider outcome: %+v", out) } }) } } func TestProviderDiagnosticsRedactRequestDataBeforeTruncation(t *testing.T) { key := "re_credential-secret" request := []byte(`{"from":"Sender ","to":["recipient@example.com"],"personalizations":[{"dynamic_template_data":{"secret":"short-private-value"}}],"subject":"Private subject","text":"Private text","html":"

Private HTML

","content":[{"value":"Your verification code is 123456; account 9876543210"}]}`) token := strings.Repeat("a", 43) message := "Credential " + key + "; recipient@example.com; Private subject; Private text;

Private HTML

; short-private-value; https://example.com/verify?token=" + token + "; " + token + "; 123456; 9876543210; job-secret\n\r\x00\u202eforged-log" body, err := json.Marshal(map[string]string{"name": "validation_error", "message": message}) if err != nil { t.Fatal(err) } name, result := providerFailure(body, request, key, "job-secret") if name != "validation_error" || !strings.Contains(result, "forged-log") || !strings.Contains(result, "[redacted]") { t.Fatal("lost useful diagnostic", name, result) } for _, forbidden := range []string{key, "recipient@example.com", "Private subject", "Private text", "Private HTML", "short-private-value", "https://", token, "123456", "9876543210", "job-secret", "\n", "\r", "\x00", "\u202e"} { if strings.Contains(result, forbidden) { t.Fatalf("diagnostic contains sensitive value or control %q", forbidden) } } body, err = json.Marshal(map[string]string{"name": "validation_error", "message": strings.Repeat("ą", maxErrorMessage+20) + key}) if err != nil { t.Fatal(err) } _, result = providerFailure(body, request, key) if !utf8.ValidString(result) || utf8.RuneCountInString(result) != maxErrorMessage+1 || !strings.HasSuffix(result, "…") || strings.Contains(result, "re_") { t.Fatal("invalid or unbounded diagnostic") } } func TestSuccessfulResponsesDoNotExposeProviderContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{"name":"private","message":"private provider response"}`) })) defer server.Close() c := client{http: server.Client(), endpoint: server.URL} if out := c.post(context.Background(), "", struct{}{}); out.State != model.Accepted || out.HTTPStatus != 200 || out.ProviderMessage != "" || out.ProviderError != "" { t.Fatal("success leaked provider content", out) } } func TestTransportFailureHasSanitizedDiagnostic(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() c := client{http: http.DefaultClient, key: "credential-secret", endpoint: "https://example.com/emails?credential=credential-secret"} out := c.post(ctx, "job-secret", struct{}{}) if out.State != model.Unknown || !strings.Contains(out.ProviderMessage, "context canceled") || strings.Contains(out.ProviderMessage, "credential-secret") || strings.Contains(out.ProviderMessage, "https://") { t.Fatal("transport cause missing or unsafe", out) } }