package mailer import ( "context" "fmt" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/supabase/auth/internal/conf" ) func TestEmalValidatorService(t *testing.T) { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, time.Second*60) defer cancel() testResVal := new(atomic.Value) testResVal.Store(`{"valid": true}`) testHdrsVal := new(atomic.Value) testHdrsVal.Store(map[string]string{"apikey": "test"}) // testHeaders := map[string][]string{"apikey": []string{"test"}} testHeaders := `{"apikey": ["test"]}` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("apikey") if key == "" { fmt.Fprintln(w, `{"error": true}`) return } fmt.Fprintln(w, testResVal.Load().(string)) })) defer ts.Close() // Return nil err from service // when svc and extended checks both report email as valid { testResVal.Store(`{"valid": true}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: true, EmailValidationServiceURL: ts.URL, EmailValidationServiceHeaders: testHeaders, } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "chris.stockton@supabase.io") if err != nil { t.Fatalf("exp nil err; got %v", err) } } // Return nil err from service when // extended is disabled for a known invalid address // service reports valid { testResVal.Store(`{"valid": true}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: false, EmailValidationServiceURL: ts.URL, EmailValidationServiceHeaders: testHeaders, } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "test@gmail.com") if err != nil { t.Fatalf("exp nil err; got %v", err) } } // Return nil err from service when // extended is disabled for a known invalid address // service is disabled for a known invalid address { testResVal.Store(`{"valid": false}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: false, EmailValidationServiceURL: "", EmailValidationServiceHeaders: "", } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "test@gmail.com") if err != nil { t.Fatalf("exp nil err; got %v", err) } } // Return err from service when // extended reports invalid // service is disabled for a known invalid address { testResVal.Store(`{"valid": true}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: true, EmailValidationServiceURL: "", EmailValidationServiceHeaders: "", } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "test@gmail.com") if err == nil { t.Fatal("exp non-nil err") } } // Return err from service when // extended reports invalid // service reports valid { testResVal.Store(`{"valid": true}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: true, EmailValidationServiceURL: ts.URL, EmailValidationServiceHeaders: testHeaders, } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "test@gmail.com") if err == nil { t.Fatal("exp non-nil err") } } // Return err from service when // extended reports valid // service reports invalid { testResVal.Store(`{"valid": false}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: true, EmailValidationServiceURL: ts.URL, EmailValidationServiceHeaders: testHeaders, } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "chris.stockton@supabase.io") if err == nil { t.Fatal("exp non-nil err") } } // Return err from service when // extended reports invalid // service reports invalid { testResVal.Store(`{"valid": false}`) cfg := conf.MailerConfiguration{ EmailValidationExtended: false, EmailValidationServiceURL: ts.URL, EmailValidationServiceHeaders: testHeaders, } if err := cfg.Validate(); err != nil { t.Fatal(err) } ev := newEmailValidator(cfg) err := ev.Validate(ctx, "test@gmail.com") if err == nil { t.Fatal("exp non-nil err") } } } func TestValidateEmailExtended(t *testing.T) { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, time.Second*60) defer cancel() cases := []struct { email string timeout time.Duration err string }{ // valid (has mx record) {email: "a@supabase.io"}, {email: "support@supabase.io"}, {email: "chris.stockton@supabase.io"}, // bad format {email: "", err: "invalid_email_format"}, {email: "io", err: "invalid_email_format"}, {email: "supabase.io", err: "invalid_email_format"}, {email: "@supabase.io", err: "invalid_email_format"}, {email: "test@.supabase.io", err: "invalid_email_format"}, // invalid: valid mx records, but invalid and often typed // (invalidEmailMap) {email: "test@email.com", err: "invalid_email_address"}, {email: "test@gmail.com", err: "invalid_email_address"}, {email: "test@test.com", err: "invalid_email_dns"}, // very common typo {email: "test@gamil.com", err: "invalid_email_dns"}, // invalid: valid mx records, but invalid and often typed // (invalidHostMap) {email: "a@example.com", err: "invalid_email_dns"}, {email: "a@example.net", err: "invalid_email_dns"}, {email: "a@example.org", err: "invalid_email_dns"}, // invalid: no mx records {email: "a@test", err: "invalid_email_dns"}, {email: "test@local", err: "invalid_email_dns"}, {email: "test@test.local", err: "invalid_email_dns"}, {email: "test@example", err: "invalid_email_dns"}, {email: "test@invalid", err: "invalid_email_dns"}, // valid but not actually valid and typed a lot {email: "a@invalid", err: "invalid_email_dns"}, {email: "a@a.invalid", err: "invalid_email_dns"}, {email: "test@invalid", err: "invalid_email_dns"}, // various invalid emails {email: "test@test.localhost", err: "invalid_email_dns"}, {email: "test@invalid.example.com", err: "invalid_email_dns"}, {email: "test@no.such.email.host.supabase.io", err: "invalid_email_dns"}, // this low timeout should simulate a dns timeout, which should // not be treated as an invalid email. {email: "validemail@probablyaaaaaaaanotarealdomain.com", timeout: time.Millisecond}, // likewise for a valid email {email: "support@supabase.io", timeout: time.Millisecond}, } cfg := conf.MailerConfiguration{ EmailValidationExtended: true, EmailValidationServiceURL: "", EmailValidationServiceHeaders: "", } ev := newEmailValidator(cfg) for idx, tc := range cases { func(timeout time.Duration) { if timeout == 0 { timeout = validateEmailTimeout } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() now := time.Now() err := ev.Validate(ctx, tc.email) dur := time.Since(now) if max := timeout + (time.Millisecond * 50); max < dur { t.Fatal("timeout was not respected") } t.Logf("tc #%v - email %q", idx, tc.email) if tc.err != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.err) return } require.NoError(t, err) }(tc.timeout) } }