summaryrefslogtreecommitdiff
path: root/magic_link_login_test.go
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-11 10:35:03 -0600
committerT <t@tjp.lol>2025-08-11 10:35:16 -0600
commitb5bda8cd744c5bb1f06317d3fa80d98a926782cc (patch)
treed192540e8a87324748140be6fd9b935c3c9891b5 /magic_link_login_test.go
parent52e03c2658c376b452871e16eda65a2ca4243458 (diff)
fix magic link handler testsHEADmain
Diffstat (limited to 'magic_link_login_test.go')
-rw-r--r--magic_link_login_test.go184
1 files changed, 45 insertions, 139 deletions
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)
@@ -260,13 +270,6 @@ func TestMagicLinkVerifyHandler(t *testing.T) {
checkCookie: false,
},
{
- name: "expired token",
- method: "GET",
- token: expiredTokenStr,
- expectedStatus: http.StatusUnauthorized,
- checkCookie: false,
- },
- {
name: "POST method not allowed",
method: "POST",
token: validToken,
@@ -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",
}