package api import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "github.com/gobwas/glob" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/supabase/auth/internal/conf" "github.com/supabase/auth/internal/crypto" "github.com/supabase/auth/internal/models" ) type MailTestSuite struct { suite.Suite API *API Config *conf.GlobalConfiguration } func TestMail(t *testing.T) { api, config, err := setupAPIForTest() require.NoError(t, err) ts := &MailTestSuite{ API: api, Config: config, } defer api.db.Close() suite.Run(t, ts) } func (ts *MailTestSuite) SetupTest() { models.TruncateAll(ts.API.db) ts.Config.Mailer.SecureEmailChangeEnabled = true // Create User u, err := models.NewUser("12345678", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating new user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new user") } func (ts *MailTestSuite) TestValidateEmail() { cases := []struct { desc string email string expectedEmail string expectedError error }{ { desc: "valid email", email: "test@example.com", expectedEmail: "test@example.com", expectedError: nil, }, { desc: "email should be normalized", email: "TEST@EXAMPLE.COM", expectedEmail: "test@example.com", expectedError: nil, }, { desc: "empty email should return error", email: "", expectedEmail: "", expectedError: badRequestError(ErrorCodeValidationFailed, "An email address is required"), }, { desc: "email length exceeds 255 characters", // email has 256 characters email: "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest@example.com", expectedEmail: "", expectedError: badRequestError(ErrorCodeValidationFailed, "An email address is too long"), }, } for _, c := range cases { ts.Run(c.desc, func() { email, err := ts.API.validateEmail(c.email) require.Equal(ts.T(), c.expectedError, err) require.Equal(ts.T(), c.expectedEmail, email) }) } } func (ts *MailTestSuite) TestGenerateLink() { // create admin jwt claims := &AccessTokenClaims{ Role: "supabase_admin", } token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(ts.Config.JWT.Secret)) require.NoError(ts.T(), err, "Error generating admin jwt") ts.setURIAllowListMap("http://localhost:8000/**") // create test cases cases := []struct { Desc string Body GenerateLinkParams ExpectedCode int ExpectedResponse map[string]interface{} }{ { Desc: "Generate signup link for new user", Body: GenerateLinkParams{ Email: "new_user@example.com", Password: "secret123", Type: "signup", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate signup link for existing user", Body: GenerateLinkParams{ Email: "test@example.com", Password: "secret123", Type: "signup", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate signup link with custom redirect url", Body: GenerateLinkParams{ Email: "test@example.com", Password: "secret123", Type: "signup", RedirectTo: "http://localhost:8000/welcome", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": "http://localhost:8000/welcome", }, }, { Desc: "Generate magic link", Body: GenerateLinkParams{ Email: "test@example.com", Type: "magiclink", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate invite link", Body: GenerateLinkParams{ Email: "test@example.com", Type: "invite", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate recovery link", Body: GenerateLinkParams{ Email: "test@example.com", Type: "recovery", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate email change link", Body: GenerateLinkParams{ Email: "test@example.com", NewEmail: "new@example.com", Type: "email_change_current", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, { Desc: "Generate email change link", Body: GenerateLinkParams{ Email: "test@example.com", NewEmail: "new@example.com", Type: "email_change_new", }, ExpectedCode: http.StatusOK, ExpectedResponse: map[string]interface{}{ "redirect_to": ts.Config.SiteURL, }, }, } customDomainUrl, err := url.ParseRequestURI("https://example.gotrue.com") require.NoError(ts.T(), err) originalHosts := ts.API.config.Mailer.ExternalHosts ts.API.config.Mailer.ExternalHosts = []string{ "example.gotrue.com", } for _, c := range cases { ts.Run(c.Desc, func() { var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.Body)) req := httptest.NewRequest(http.MethodPost, customDomainUrl.String()+"/admin/generate_link", &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), c.ExpectedCode, w.Code) data := make(map[string]interface{}) require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) require.Contains(ts.T(), data, "action_link") require.Contains(ts.T(), data, "email_otp") require.Contains(ts.T(), data, "hashed_token") require.Contains(ts.T(), data, "redirect_to") require.Equal(ts.T(), c.Body.Type, data["verification_type"]) // check if redirect_to is correct require.Equal(ts.T(), c.ExpectedResponse["redirect_to"], data["redirect_to"]) // check if hashed_token matches hash function of email and the raw otp require.Equal(ts.T(), crypto.GenerateTokenHash(c.Body.Email, data["email_otp"].(string)), data["hashed_token"]) // check if the host used in the email link matches the initial request host u, err := url.ParseRequestURI(data["action_link"].(string)) require.NoError(ts.T(), err) require.Equal(ts.T(), req.Host, u.Host) }) } ts.API.config.Mailer.ExternalHosts = originalHosts } func (ts *MailTestSuite) setURIAllowListMap(uris ...string) { for _, uri := range uris { g := glob.MustCompile(uri, '.', '/') ts.Config.URIAllowListMap[uri] = g } }