ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #202 from paulgmiller/pmiller/usertests

Pmiller/usertests

authored by

Paul Miller and committed by
GitHub
76eccaa4 3ef7ea6e

+364 -66
+35
internal/users/normalize_test.go
··· 1 + package users 2 + 3 + import "testing" 4 + 5 + func TestNormalizeEmail(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + input string 9 + want string 10 + }{ 11 + { 12 + name: "trim and lower", 13 + input: " Alice@Example.COM ", 14 + want: "alice@example.com", 15 + }, 16 + { 17 + name: "newline trimmed", 18 + input: "bob@example.com\n", 19 + want: "bob@example.com", 20 + }, 21 + { 22 + name: "already normalized", 23 + input: "carol@example.com", 24 + want: "carol@example.com", 25 + }, 26 + } 27 + 28 + for _, tt := range tests { 29 + t.Run(tt.name, func(t *testing.T) { 30 + if got := normalizeEmail(tt.input); got != tt.want { 31 + t.Fatalf("normalizeEmail(%q) = %q, want %q", tt.input, got, tt.want) 32 + } 33 + }) 34 + } 35 + }
+1 -1
internal/users/storage.go
··· 151 151 } 152 152 153 153 func normalizeEmail(email string) string { 154 - // remove . from before @? 154 + // remove . from before @? or +<suffix? 155 155 return strings.TrimSpace(strings.ToLower(email)) 156 156 }
+197 -65
internal/users/storage_test.go
··· 1 1 package users 2 2 3 3 import ( 4 - utypes "careme/internal/users/types" 5 - "strings" 4 + "context" 5 + "net/http" 6 + "net/http/httptest" 6 7 "testing" 7 8 "time" 9 + 10 + "careme/internal/auth" 11 + "careme/internal/cache" 12 + "careme/internal/config" 13 + utypes "careme/internal/users/types" 8 14 ) 9 15 10 - func TestUserValidate(t *testing.T) { 11 - t.Run("valid user sorts recipes", func(t *testing.T) { 12 - newer := time.Date(2024, time.December, 1, 0, 0, 0, 0, time.UTC) 13 - older := newer.Add(-24 * time.Hour) 14 - oldest := newer.Add(-48 * time.Hour) 15 - user := &utypes.User{ 16 - ID: "user-1", 17 - ShoppingDay: time.Monday.String(), 18 - Email: []string{"alice@example.com"}, 19 - FavoriteStore: "1234", 20 - LastRecipes: []utypes.Recipe{ 21 - {Title: "newer", CreatedAt: newer}, 22 - {Title: "oldest", CreatedAt: oldest}, 23 - {Title: "older", CreatedAt: older}, 24 - }, 25 - } 16 + type stubEmailFetcher struct { 17 + email string 18 + err error 19 + calls int 20 + } 26 21 27 - if err := user.Validate(); err != nil { 28 - t.Fatalf("expected no error, got %v", err) 29 - } 22 + func (s *stubEmailFetcher) GetUserEmail(_ context.Context, _ string) (string, error) { 23 + s.calls++ 24 + return s.email, s.err 25 + } 26 + 27 + func TestStorageUpdateAndGetByID(t *testing.T) { 28 + fc := cache.NewFileCache(t.TempDir()) 29 + storage := NewStorage(fc) 30 30 31 - for i, name := range []string{"newer", "older", "oldest"} { 32 - if got, want := user.LastRecipes[i].Title, name; got != want { 33 - t.Fatalf("recipes not sorted by CreatedAt: got %s want %s", got, want) 34 - } 35 - } 36 - }) 31 + user := &utypes.User{ 32 + ID: "user-1", 33 + Email: []string{"Alice@Example.com"}, 34 + ShoppingDay: time.Monday.String(), 35 + } 37 36 38 - t.Run("invalid shopping day", func(t *testing.T) { 39 - user := &utypes.User{ 40 - ShoppingDay: "Caturday", 41 - Email: []string{"bob@example.com"}, 42 - } 37 + if err := storage.Update(user); err != nil { 38 + t.Fatalf("Update() error: %v", err) 39 + } 43 40 44 - err := user.Validate() 45 - if err == nil || !strings.Contains(err.Error(), "invalid weekday") { 46 - t.Fatalf("expected invalid weekday error, got %v", err) 47 - } 48 - }) 41 + got, err := storage.GetByID("user-1") 42 + if err != nil { 43 + t.Fatalf("GetByID() error: %v", err) 44 + } 45 + if got.ID != user.ID { 46 + t.Fatalf("GetByID() ID = %q, want %q", got.ID, user.ID) 47 + } 48 + if got.ShoppingDay != user.ShoppingDay { 49 + t.Fatalf("GetByID() ShoppingDay = %q, want %q", got.ShoppingDay, user.ShoppingDay) 50 + } 51 + if len(got.Email) != 1 || got.Email[0] != user.Email[0] { 52 + t.Fatalf("GetByID() Email = %v, want %v", got.Email, user.Email) 53 + } 54 + } 49 55 50 - t.Run("missing email", func(t *testing.T) { 51 - user := &utypes.User{ShoppingDay: time.Friday.String()} 56 + func TestStorageGetByIDNotFound(t *testing.T) { 57 + fc := cache.NewFileCache(t.TempDir()) 58 + storage := NewStorage(fc) 52 59 53 - err := user.Validate() 54 - if err == nil || err.Error() != "at least one email is required" { 55 - t.Fatalf("expected missing email error, got %v", err) 56 - } 57 - }) 60 + _, err := storage.GetByID("missing") 61 + if err == nil || err != ErrNotFound { 62 + t.Fatalf("GetByID() error = %v, want %v", err, ErrNotFound) 63 + } 64 + } 58 65 59 - t.Run("invalid email address", func(t *testing.T) { 60 - user := &utypes.User{ 61 - ShoppingDay: time.Saturday.String(), 62 - Email: []string{"not-an-email"}, 63 - } 66 + func TestStorageGetByEmailNotFound(t *testing.T) { 67 + fc := cache.NewFileCache(t.TempDir()) 68 + storage := NewStorage(fc) 64 69 65 - err := user.Validate() 66 - if err == nil || err.Error() != "invalid email address: not-an-email" { 67 - t.Fatalf("expected invalid email error, got %v", err) 68 - } 69 - }) 70 + _, err := storage.GetByEmail("missing@example.com") 71 + if err == nil || err != ErrNotFound { 72 + t.Fatalf("GetByEmail() error = %v, want %v", err, ErrNotFound) 73 + } 74 + } 70 75 71 - t.Run("invalid favorite store", func(t *testing.T) { 72 - user := &utypes.User{ 73 - ShoppingDay: time.Sunday.String(), 74 - Email: []string{"charlie@example.com"}, 75 - FavoriteStore: "store-99", 76 - } 76 + func TestStorageGetByEmail(t *testing.T) { 77 + fc := cache.NewFileCache(t.TempDir()) 78 + storage := NewStorage(fc) 77 79 78 - err := user.Validate() 79 - if err == nil || !strings.Contains(err.Error(), "invalid favorite store id") { 80 - t.Fatalf("expected invalid favorite store error, got %v", err) 81 - } 82 - }) 80 + user := &utypes.User{ 81 + ID: "user-2", 82 + Email: []string{"Alice@Example.com"}, 83 + ShoppingDay: time.Tuesday.String(), 84 + } 85 + 86 + if err := storage.Update(user); err != nil { 87 + t.Fatalf("Update() error: %v", err) 88 + } 89 + if err := fc.Put(context.Background(), emailPrefix+normalizeEmail(user.Email[0]), user.ID, cache.Unconditional()); err != nil { 90 + t.Fatalf("failed to index email: %v", err) 91 + } 92 + 93 + got, err := storage.GetByEmail("ALICE@EXAMPLE.COM") 94 + if err != nil { 95 + t.Fatalf("GetByEmail() error: %v", err) 96 + } 97 + if got.ID != user.ID { 98 + t.Fatalf("GetByEmail() ID = %q, want %q", got.ID, user.ID) 99 + } 100 + } 101 + 102 + func TestFindOrCreateFromClerkExistingUser(t *testing.T) { 103 + fc := cache.NewFileCache(t.TempDir()) 104 + storage := NewStorage(fc) 105 + 106 + user := &utypes.User{ 107 + ID: "user-3", 108 + Email: []string{"dana@example.com"}, 109 + ShoppingDay: time.Wednesday.String(), 110 + } 111 + if err := storage.Update(user); err != nil { 112 + t.Fatalf("Update() error: %v", err) 113 + } 114 + 115 + fetcher := &stubEmailFetcher{email: "should-not-call@example.com"} 116 + got, err := storage.FindOrCreateFromClerk(context.Background(), "user-3", fetcher) 117 + if err != nil { 118 + t.Fatalf("FindOrCreateFromClerk() error: %v", err) 119 + } 120 + if fetcher.calls != 0 { 121 + t.Fatalf("expected email fetcher to not be called, calls=%d", fetcher.calls) 122 + } 123 + if got.ID != user.ID { 124 + t.Fatalf("FindOrCreateFromClerk() ID = %q, want %q", got.ID, user.ID) 125 + } 126 + } 127 + 128 + func TestFindOrCreateFromClerkCreatesUser(t *testing.T) { 129 + fc := cache.NewFileCache(t.TempDir()) 130 + storage := NewStorage(fc) 131 + 132 + fetcher := &stubEmailFetcher{email: "NewUser@Example.com"} 133 + start := time.Now() 134 + got, err := storage.FindOrCreateFromClerk(context.Background(), "user-4", fetcher) 135 + end := time.Now() 136 + if err != nil { 137 + t.Fatalf("FindOrCreateFromClerk() error: %v", err) 138 + } 139 + if fetcher.calls != 1 { 140 + t.Fatalf("expected email fetcher to be called once, calls=%d", fetcher.calls) 141 + } 142 + if got.ID != "user-4" { 143 + t.Fatalf("FindOrCreateFromClerk() ID = %q, want %q", got.ID, "user-4") 144 + } 145 + if len(got.Email) != 1 || got.Email[0] != "newuser@example.com" { 146 + t.Fatalf("FindOrCreateFromClerk() Email = %v, want [newuser@example.com]", got.Email) 147 + } 148 + if got.ShoppingDay != time.Saturday.String() { 149 + t.Fatalf("FindOrCreateFromClerk() ShoppingDay = %q, want %q", got.ShoppingDay, time.Saturday.String()) 150 + } 151 + if got.CreatedAt.Before(start) || got.CreatedAt.After(end) { 152 + t.Fatalf("FindOrCreateFromClerk() CreatedAt = %v, expected between %v and %v", got.CreatedAt, start, end) 153 + } 154 + 155 + stored, err := storage.GetByID("user-4") 156 + if err != nil { 157 + t.Fatalf("GetByID() error: %v", err) 158 + } 159 + if stored.ID != got.ID { 160 + t.Fatalf("stored ID = %q, want %q", stored.ID, got.ID) 161 + } 162 + } 163 + 164 + func TestFromRequestCreatesUserWithMockAuth(t *testing.T) { 165 + fc := cache.NewFileCache(t.TempDir()) 166 + storage := NewStorage(fc) 167 + 168 + cfg := &config.Config{ 169 + Mocks: config.MockConfig{Email: "NewUser@Example.com"}, 170 + } 171 + client := auth.Mock(cfg) 172 + 173 + req := httptest.NewRequest(http.MethodGet, "/", nil) 174 + got, err := storage.FromRequest(context.Background(), req, client) 175 + if err != nil { 176 + t.Fatalf("FromRequest() error: %v", err) 177 + } 178 + if got.ID != "mock-clerk-user-id" { 179 + t.Fatalf("FromRequest() ID = %q, want %q", got.ID, "mock-clerk-user-id") 180 + } 181 + if len(got.Email) != 1 || got.Email[0] != "newuser@example.com" { 182 + t.Fatalf("FromRequest() Email = %v, want [newuser@example.com]", got.Email) 183 + } 184 + } 185 + 186 + func TestFromRequestReturnsExistingUser(t *testing.T) { 187 + fc := cache.NewFileCache(t.TempDir()) 188 + storage := NewStorage(fc) 189 + 190 + existing := &utypes.User{ 191 + ID: "mock-clerk-user-id", 192 + Email: []string{"existing@example.com"}, 193 + ShoppingDay: time.Thursday.String(), 194 + } 195 + if err := storage.Update(existing); err != nil { 196 + t.Fatalf("Update() error: %v", err) 197 + } 198 + 199 + cfg := &config.Config{ 200 + Mocks: config.MockConfig{Email: "ignored@example.com"}, 201 + } 202 + client := auth.Mock(cfg) 203 + 204 + req := httptest.NewRequest(http.MethodGet, "/", nil) 205 + got, err := storage.FromRequest(context.Background(), req, client) 206 + if err != nil { 207 + t.Fatalf("FromRequest() error: %v", err) 208 + } 209 + if got.ID != existing.ID { 210 + t.Fatalf("FromRequest() ID = %q, want %q", got.ID, existing.ID) 211 + } 212 + if len(got.Email) != 1 || got.Email[0] != "existing@example.com" { 213 + t.Fatalf("FromRequest() Email = %v, want [existing@example.com]", got.Email) 214 + } 83 215 }
+131
internal/users/types/types_test.go
··· 1 + package types 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + "time" 7 + ) 8 + 9 + func TestParseWeekday(t *testing.T) { 10 + tests := []struct { 11 + name string 12 + input string 13 + want time.Weekday 14 + wantErr bool 15 + }{ 16 + { 17 + name: "sunday", 18 + input: "Sunday", 19 + want: time.Sunday, 20 + }, 21 + { 22 + name: "case insensitive", 23 + input: "mOnDaY", 24 + want: time.Monday, 25 + }, 26 + { 27 + name: "lowercase", 28 + input: "tuesday", 29 + want: time.Tuesday, 30 + }, 31 + { 32 + name: "invalid", 33 + input: "Caturday", 34 + want: time.Sunday, 35 + wantErr: true, 36 + }, 37 + } 38 + 39 + for _, tt := range tests { 40 + t.Run(tt.name, func(t *testing.T) { 41 + got, err := parseWeekday(tt.input) 42 + if tt.wantErr { 43 + if err == nil { 44 + t.Fatalf("parseWeekday(%q) expected error", tt.input) 45 + } 46 + return 47 + } 48 + if err != nil { 49 + t.Fatalf("parseWeekday(%q) unexpected error: %v", tt.input, err) 50 + } 51 + if got != tt.want { 52 + t.Fatalf("parseWeekday(%q) = %v, want %v", tt.input, got, tt.want) 53 + } 54 + }) 55 + } 56 + } 57 + 58 + func TestUserValidate(t *testing.T) { 59 + t.Run("valid user sorts recipes", func(t *testing.T) { 60 + newer := time.Date(2024, time.December, 1, 0, 0, 0, 0, time.UTC) 61 + older := newer.Add(-24 * time.Hour) 62 + oldest := newer.Add(-48 * time.Hour) 63 + user := &User{ 64 + ID: "user-1", 65 + ShoppingDay: time.Monday.String(), 66 + Email: []string{"alice@example.com"}, 67 + FavoriteStore: "1234", 68 + LastRecipes: []Recipe{ 69 + {Title: "newer", CreatedAt: newer}, 70 + {Title: "oldest", CreatedAt: oldest}, 71 + {Title: "older", CreatedAt: older}, 72 + }, 73 + } 74 + 75 + if err := user.Validate(); err != nil { 76 + t.Fatalf("expected no error, got %v", err) 77 + } 78 + 79 + for i, name := range []string{"newer", "older", "oldest"} { 80 + if got, want := user.LastRecipes[i].Title, name; got != want { 81 + t.Fatalf("recipes not sorted by CreatedAt: got %s want %s", got, want) 82 + } 83 + } 84 + }) 85 + 86 + t.Run("invalid shopping day", func(t *testing.T) { 87 + user := &User{ 88 + ShoppingDay: "Caturday", 89 + Email: []string{"bob@example.com"}, 90 + } 91 + 92 + err := user.Validate() 93 + if err == nil || !strings.Contains(err.Error(), "invalid weekday") { 94 + t.Fatalf("expected invalid weekday error, got %v", err) 95 + } 96 + }) 97 + 98 + t.Run("missing email", func(t *testing.T) { 99 + user := &User{ShoppingDay: time.Friday.String()} 100 + 101 + err := user.Validate() 102 + if err == nil || err.Error() != "at least one email is required" { 103 + t.Fatalf("expected missing email error, got %v", err) 104 + } 105 + }) 106 + 107 + t.Run("invalid email address", func(t *testing.T) { 108 + user := &User{ 109 + ShoppingDay: time.Saturday.String(), 110 + Email: []string{"not-an-email"}, 111 + } 112 + 113 + err := user.Validate() 114 + if err == nil || err.Error() != "invalid email address: not-an-email" { 115 + t.Fatalf("expected invalid email error, got %v", err) 116 + } 117 + }) 118 + 119 + t.Run("invalid favorite store", func(t *testing.T) { 120 + user := &User{ 121 + ShoppingDay: time.Sunday.String(), 122 + Email: []string{"charlie@example.com"}, 123 + FavoriteStore: "store-99", 124 + } 125 + 126 + err := user.Validate() 127 + if err == nil || !strings.Contains(err.Error(), "invalid favorite store id") { 128 + t.Fatalf("expected invalid favorite store error, got %v", err) 129 + } 130 + }) 131 + }