From b5bda8cd744c5bb1f06317d3fa80d98a926782cc Mon Sep 17 00:00:00 2001 From: T Date: Mon, 11 Aug 2025 10:35:03 -0600 Subject: fix magic link handler tests --- magic_link_login_test.go | 184 ++++++++++++----------------------------------- 1 file changed, 45 insertions(+), 139 deletions(-) (limited to 'magic_link_login_test.go') diff --git a/magic_link_login_test.go b/magic_link_login_test.go index da4bfa6..86cad63 100644 --- a/magic_link_login_test.go +++ b/magic_link_login_test.go @@ -10,12 +10,13 @@ import ( "strconv" "strings" "testing" - "time" ) // Mock implementation of MagicLinkMailer for testing -type mockMagicLinkMailer[T any] struct { +type mockMagicLinkUserData[T any] struct { users map[string]T + counter int + tokens map[string]*MagicLinkToken[T] sentEmails []struct { user T token string @@ -23,19 +24,39 @@ type mockMagicLinkMailer[T any] struct { sendEmailFunc func(T, string) error } -func newMockMagicLinkMailer[T any](users map[string]T, sendEmailFunc func(T, string) error) *mockMagicLinkMailer[T] { - return &mockMagicLinkMailer[T]{ +func newMockMagicLinkUserData[T any](users map[string]T, sendEmailFunc func(T, string) error) *mockMagicLinkUserData[T] { + return &mockMagicLinkUserData[T]{ users: users, + tokens: make(map[string]*MagicLinkToken[T]), sendEmailFunc: sendEmailFunc, } } -func (m *mockMagicLinkMailer[T]) Fetch(username string) (T, bool, error) { +func (m *mockMagicLinkUserData[T]) GenerateToken(username, redirectPath string) (*MagicLinkToken[T], error) { user, exists := m.users[username] - return user, exists, nil + if !exists { + return nil, nil + } + token := &MagicLinkToken[T]{ + Identifier: strconv.Itoa(m.counter), + UserData: user, + RedirectPath: redirectPath, + } + m.counter++ + m.tokens[token.Identifier] = token + return token, nil } -func (m *mockMagicLinkMailer[T]) SendEmail(userData T, token string) error { +func (m *mockMagicLinkUserData[T]) ValidateToken(identifier string) (*MagicLinkToken[T], error) { + token, ok := m.tokens[identifier] + if !ok { + return nil, fmt.Errorf("no token %q", identifier) + } + delete(m.tokens, identifier) + return token, nil +} + +func (m *mockMagicLinkUserData[T]) SendEmail(userData T, token string) error { m.sentEmails = append(m.sentEmails, struct { user T token string @@ -89,10 +110,10 @@ func TestMagicLinkHandler(t *testing.T) { "jane@example.com": {Username: "jane@example.com", Hash: "", ID: 2}, } - mockMailer := newMockMagicLinkMailer(users, nil) + mockData := newMockMagicLinkUserData(users, nil) config := MagicLinkConfig[testUser]{ - Mailer: mockMailer, + UserData: mockData, Redirects: Redirects{ Default: "/dashboard", AllowedPrefixes: []string{"/app/", "/admin/"}, @@ -151,7 +172,7 @@ func TestMagicLinkHandler(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockMailer.sentEmails = nil // Reset + mockData.sentEmails = nil // Reset var req *http.Request if tt.method == "POST" { @@ -176,17 +197,17 @@ func TestMagicLinkHandler(t *testing.T) { } if tt.checkEmail { - if len(mockMailer.sentEmails) != 1 { - t.Errorf("expected 1 email sent, got %d", len(mockMailer.sentEmails)) + if len(mockData.sentEmails) != 1 { + t.Errorf("expected 1 email sent, got %d", len(mockData.sentEmails)) } else { - email := mockMailer.sentEmails[0] + email := mockData.sentEmails[0] if email.token == "" { t.Error("expected non-empty token") } } } else { - if len(mockMailer.sentEmails) != 0 { - t.Errorf("expected no emails sent, got %d", len(mockMailer.sentEmails)) + if len(mockData.sentEmails) != 0 { + t.Errorf("expected no emails sent, got %d", len(mockData.sentEmails)) } } }) @@ -203,29 +224,18 @@ func TestMagicLinkVerifyHandler(t *testing.T) { "john@example.com": {Username: "john@example.com", Hash: "", ID: 1}, } - mockMailer := newMockMagicLinkMailer(users, nil) + mockData := newMockMagicLinkUserData(users, nil) config := MagicLinkConfig[testUser]{ - Mailer: mockMailer, - Redirects: Redirects{Default: "/dashbaord"}, - TokenExpiry: time.Minute, - } - - token := magicLinkToken{ - Username: "john@example.com", - Redirect: "/app/settings", - ExpiresAt: time.Now().Add(time.Minute), + UserData: mockData, + Redirects: Redirects{Default: "/dashbaord"}, } - tokenData := []byte(token.serialize()) - validToken := auth.enc.Encrypt(tokenData) - expiredToken := magicLinkToken{ - Username: "john@example.com", - Redirect: "/app/settings", - ExpiresAt: time.Now().Add(-time.Minute), + token, err := mockData.GenerateToken("john@example.com", "/app/settings") + if err != nil { + t.Fatal(err) } - expiredTokenData := []byte(expiredToken.serialize()) - expiredTokenStr := auth.enc.Encrypt(expiredTokenData) + validToken := string(auth.enc.Encrypt([]byte(token.Identifier))) handler := auth.MagicLinkVerifyHandler(config) @@ -259,13 +269,6 @@ func TestMagicLinkVerifyHandler(t *testing.T) { expectedStatus: http.StatusUnauthorized, checkCookie: false, }, - { - name: "expired token", - method: "GET", - token: expiredTokenStr, - expectedStatus: http.StatusUnauthorized, - checkCookie: false, - }, { name: "POST method not allowed", method: "POST", @@ -324,107 +327,11 @@ func TestMagicLinkConfigDefaults(t *testing.T) { if config.TokenField != "token" { t.Errorf("expected TokenField to be 'token', got %s", config.TokenField) } - if config.TokenExpiry != 15*time.Minute { - t.Errorf("expected TokenExpiry to be 15 minutes, got %v", config.TokenExpiry) - } if config.TokenLocation != TokenLocationQuery { t.Errorf("expected TokenLocation to be TokenLocationQuery, got %v", config.TokenLocation) } } -func TestMagicLinkTokenSerialization(t *testing.T) { - now := time.Now().Truncate(time.Second) // Remove nanoseconds for consistent comparison - - tests := []struct { - name string - token magicLinkToken - expected string - }{ - { - name: "simple token", - token: magicLinkToken{ - Redirect: "/dashboard", - ExpiresAt: now, - Username: "user@example.com", - }, - expected: "/dashboard\x00" + now.Format(time.RFC3339) + "\x00user@example.com", - }, - { - name: "redirect with pipes", - token: magicLinkToken{ - Redirect: "/app|section|page", - ExpiresAt: now, - Username: "user@example.com", - }, - expected: "/app|section|page\x00" + now.Format(time.RFC3339) + "\x00user@example.com", - }, - { - name: "empty redirect", - token: magicLinkToken{ - Redirect: "", - ExpiresAt: now, - Username: "user@example.com", - }, - expected: "\x00" + now.Format(time.RFC3339) + "\x00user@example.com", - }, - { - name: "username with pipes", - token: magicLinkToken{ - Redirect: "/dashboard", - ExpiresAt: now, - Username: "user|with|pipes@example.com", - }, - expected: "/dashboard\x00" + now.Format(time.RFC3339) + "\x00user|with|pipes@example.com", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - serialized := tt.token.serialize() - if serialized != tt.expected { - t.Errorf("serialize() = %q, want %q", serialized, tt.expected) - } - - parsed, err := parseMagicLinkToken(serialized) - if err != nil { - t.Errorf("parseMagicLinkToken() error = %v", err) - return - } - - if parsed.Redirect != tt.token.Redirect { - t.Errorf("parsed Redirect = %q, want %q", parsed.Redirect, tt.token.Redirect) - } - if parsed.Username != tt.token.Username { - t.Errorf("parsed Username = %q, want %q", parsed.Username, tt.token.Username) - } - if !parsed.ExpiresAt.Equal(tt.token.ExpiresAt) { - t.Errorf("parsed ExpiresAt = %v, want %v", parsed.ExpiresAt, tt.token.ExpiresAt) - } - }) - } -} - -func TestParseMagicLinkTokenErrors(t *testing.T) { - tests := []struct { - name string - input string - }{ - {"empty string", ""}, - {"single part", "onlyonepart"}, - {"two parts", "two\x00parts"}, - {"invalid time", "redirect\x00invalid-time\x00username"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := parseMagicLinkToken(tt.input) - if err == nil { - t.Errorf("parseMagicLinkToken(%q) expected error, got nil", tt.input) - } - }) - } -} - func TestMagicLinkVerifyHandlerPathToken(t *testing.T) { auth := New("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", AuthConfig[testUser]{ SerDes: testUserSerDes{}, @@ -435,12 +342,11 @@ func TestMagicLinkVerifyHandlerPathToken(t *testing.T) { "test": {Username: "test", ID: 1}, } - mockMailer := newMockMagicLinkMailer(users, nil) + mockData := newMockMagicLinkUserData(users, nil) config := MagicLinkConfig[testUser]{ - Mailer: mockMailer, + UserData: mockData, Redirects: Redirects{Default: "/dashboard"}, - TokenExpiry: time.Minute, TokenLocation: TokenLocationPath, TokenField: "token", } -- cgit v1.2.3