From 42c4f7ae33fad8e5f8f22ce4e5c196ad135e4f3b Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 12:09:00 -0400 Subject: [PATCH] test: add coverage for apostrophe emails in recovery and hook paths Add test cases verifying that email addresses containing apostrophes (e.g., joe.o'sullivan@example.com) are handled correctly through: - The /recover endpoint (user lookup, token generation, one-time token creation) - The pg-functions hook path (JSON payload round-trip through PostgreSQL) - The HTTP hook path (JSON payload serialization and deserialization) Apostrophes are valid in the local part of email addresses per RFC 5321 and are common in Irish/UK names (O'Sullivan, O'Brien, O'Connor). Refs #2329 Co-Authored-By: Claude Opus 4.6 --- internal/api/recover_test.go | 41 +++++++++++++++++++ internal/hooks/hookshttp/hookshttp_test.go | 27 ++++++++++++ .../hooks/hookspgfunc/hookspgfunc_test.go | 16 ++++++++ 3 files changed, 84 insertions(+) diff --git a/internal/api/recover_test.go b/internal/api/recover_test.go index a7e655c596..e4963fa9dd 100644 --- a/internal/api/recover_test.go +++ b/internal/api/recover_test.go @@ -151,3 +151,44 @@ func (ts *RecoverTestSuite) TestRecover_NoSideChannelLeak() { ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusOK, w.Code) } + +func (ts *RecoverTestSuite) TestRecover_WithApostropheEmail() { + // Apostrophes are valid in the local part of email addresses per RFC 5321. + // Irish/UK names commonly use them (O'Sullivan, O'Brien, etc.). + // See: https://github.com/supabase/auth/issues/2329 + email := "joe.o'sullivan@example.com" + + // Create user with apostrophe in email + u, err := models.NewUser("", email, "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model with apostrophe email") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user with apostrophe email") + + u, err = models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud) + require.NoError(ts.T(), err, "Error finding user with apostrophe email") + u.RecoverySentAt = &time.Time{} + require.NoError(ts.T(), ts.API.db.Update(u)) + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": email, + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "http://localhost/recover", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusOK, w.Code) + + u, err = models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + assert.WithinDuration(ts.T(), time.Now(), *u.RecoverySentAt, 1*time.Second) + + // Verify the one-time token was created successfully + _, err = models.FindUserByConfirmationOrRecoveryToken(ts.API.db, u.RecoveryToken) + require.NoError(ts.T(), err, "Recovery token should be retrievable for apostrophe email user") +} diff --git a/internal/hooks/hookshttp/hookshttp_test.go b/internal/hooks/hookshttp/hookshttp_test.go index 8b3fde8643..41e59db271 100644 --- a/internal/hooks/hookshttp/hookshttp_test.go +++ b/internal/hooks/hookshttp/hookshttp_test.go @@ -57,6 +57,33 @@ func TestDispatch(t *testing.T) { }, }, + { + desc: "pass - email with apostrophe in payload", + req: M{ + "user": M{ + "email": "joe.o'sullivan@example.com", + }, + }, + exp: M{"success": true}, + hr: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the apostrophe email is correctly received in the JSON payload + var body M + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + user, _ := body["user"].(map[string]any) + email, _ := user["email"].(string) + if email != "joe.o'sullivan@example.com" { + http.Error(w, fmt.Sprintf("unexpected email: %q", email), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(M{"success": true}) + }), + }, + { desc: "pass - empty content type should not error 204 status", exp: M{}, diff --git a/internal/hooks/hookspgfunc/hookspgfunc_test.go b/internal/hooks/hookspgfunc/hookspgfunc_test.go index 575b878115..428ff97a5d 100644 --- a/internal/hooks/hookspgfunc/hookspgfunc_test.go +++ b/internal/hooks/hookspgfunc/hookspgfunc_test.go @@ -69,6 +69,22 @@ func TestDispatch(t *testing.T) { end; $$ language plpgsql;`, }, + { + desc: "pass - email with apostrophe in jsonb payload", + cfg: conf.ExtensibilityPointConfiguration{ + URI: `pg-functions://postgres/auth/v0pgfunc_test_return_input`, + HookName: `"auth"."v0pgfunc_test_return_input"`, + }, + req: M{"user": M{"email": "joe.o'sullivan@example.com"}}, + exp: M{"user": M{"email": "joe.o'sullivan@example.com"}}, + sql: ` + create or replace function v0pgfunc_test_return_input(input jsonb) + returns json as $ + begin + return input; + end; $ language plpgsql;`, + }, + { desc: "pass - small sleep of 50ms within timeout (100ms)", cfg: conf.ExtensibilityPointConfiguration{