ai cooking
0
fork

Configure Feed

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

Onemailaweek (#252)

* move mailer to a package

* fix tests

authored by

Paul Miller and committed by
GitHub
1e028929 7d14f1ac

+187 -162
+8 -1
cmd/careme/mail.go internal/mail/mail.go
··· 1 1 // https://app.sendgrid.com/guide/integrate/langs/go 2 2 // using SendGrid's Go Library 3 3 // https://github.com/sendgrid/sendgrid-go 4 - package main 4 + package mail 5 5 6 6 import ( 7 7 "bytes" ··· 123 123 slog.ErrorContext(ctx, "error getting location timezone", "location", user.FavoriteStore, "error", err.Error()) 124 124 return 125 125 } 126 + 127 + uday, _ := utypes.ParseWeekday(user.ShoppingDay) 128 + 129 + if date.Weekday() != uday { 130 + return 131 + } 132 + 126 133 p := recipes.DefaultParams(l, date) 127 134 // p.UserID = user.ID 128 135 for _, last := range user.LastRecipes {
+5 -4
cmd/careme/main.go
··· 3 3 import ( 4 4 "careme/internal/config" 5 5 "careme/internal/logsink" 6 + "careme/internal/mail" 6 7 "careme/internal/static" 7 8 "careme/internal/templates" 8 9 "context" ··· 16 17 ) 17 18 18 19 func main() { 19 - var serve, mail bool 20 + var serve, mailer bool 20 21 var addr string 21 22 22 23 //left for back compat does noting 23 24 flag.BoolVar(&serve, "serve", false, "dead we always serve") 24 - flag.BoolVar(&mail, "mail", false, "Run one-shot mail sender and exit") 25 + flag.BoolVar(&mailer, "mail", false, "Run one-shot mail sender and exit") 25 26 flag.StringVar(&addr, "addr", ":8080", "Address to bind in server mode") 26 27 flag.Parse() 27 28 ··· 58 59 log.Fatalf("failed to initialize templates: %s", err) 59 60 } 60 61 61 - if mail { 62 - mailer, err := NewMailer(cfg) 62 + if mailer { 63 + mailer, err := mail.NewMailer(cfg) 63 64 if err != nil { 64 65 log.Fatalf("failed to create mailer: %v", err) 65 66 }
-153
cmd/careme/web_e2e_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "context" 5 - "encoding/json" 6 4 "io" 7 5 "net/http" 8 6 "net/http/httptest" ··· 13 11 "testing" 14 12 "time" 15 13 16 - "careme/internal/ai" 17 14 "careme/internal/auth" 18 15 "careme/internal/cache" 19 16 "careme/internal/config" ··· 21 18 "careme/internal/recipes" 22 19 "careme/internal/templates" 23 20 "careme/internal/users" 24 - utypes "careme/internal/users/types" 25 21 26 - "github.com/sendgrid/rest" 27 - sgmail "github.com/sendgrid/sendgrid-go/helpers/mail" 28 22 "golang.org/x/net/html" 29 23 ) 30 24 ··· 117 111 118 112 //TODO step 6 make sure recipes are saved to user page? 119 113 120 - } 121 - 122 - type fakeMailCache struct { 123 - shoppingListJSON string 124 - data map[string]string 125 - } 126 - 127 - func newFakeMailCache(t *testing.T) *fakeMailCache { 128 - t.Helper() 129 - listJSON, err := json.Marshal(ai.ShoppingList{ 130 - Recipes: []ai.Recipe{ 131 - {Title: "Test Recipe"}, 132 - }, 133 - }) 134 - if err != nil { 135 - t.Fatalf("failed to marshal shopping list: %v", err) 136 - } 137 - return &fakeMailCache{ 138 - shoppingListJSON: string(listJSON), 139 - data: map[string]string{}, 140 - } 141 - } 142 - 143 - func (c *fakeMailCache) Get(_ context.Context, key string) (io.ReadCloser, error) { 144 - if strings.HasPrefix(key, "shoppinglist/") { 145 - return io.NopCloser(strings.NewReader(c.shoppingListJSON)), nil 146 - } 147 - value, ok := c.data[key] 148 - if !ok { 149 - return nil, cache.ErrNotFound 150 - } 151 - return io.NopCloser(strings.NewReader(value)), nil 152 - } 153 - 154 - func (c *fakeMailCache) Exists(_ context.Context, key string) (bool, error) { 155 - _, ok := c.data[key] 156 - return ok, nil 157 - } 158 - 159 - func (c *fakeMailCache) Put(_ context.Context, key, value string, opts cache.PutOptions) error { 160 - if opts.Condition == cache.PutIfNoneMatch { 161 - if _, exists := c.data[key]; exists { 162 - return cache.ErrAlreadyExists 163 - } 164 - } 165 - c.data[key] = value 166 - return nil 167 - } 168 - 169 - type fakeMailLocServer struct { 170 - location *locations.Location 171 - } 172 - 173 - func (f *fakeMailLocServer) GetLocationByID(_ context.Context, _ string) (*locations.Location, error) { 174 - return f.location, nil 175 - } 176 - 177 - type fakeMailClient struct { 178 - response *rest.Response 179 - err error 180 - } 181 - 182 - func (f *fakeMailClient) Send(_ *sgmail.SGMailV3) (*rest.Response, error) { 183 - return f.response, f.err 184 - } 185 - 186 - func TestSendEmail_DoesNotRecordSentClaimOnNonSuccessSendGridStatus(t *testing.T) { 187 - if err := templates.Init(&config.Config{}, "/assets/tailwind.css"); err != nil { 188 - t.Fatalf("failed to initialize templates: %v", err) 189 - } 190 - 191 - fc := newFakeMailCache(t) 192 - m := &mailer{ 193 - cache: fc, 194 - locServer: &fakeMailLocServer{ 195 - location: &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St"}, 196 - }, 197 - client: &fakeMailClient{ 198 - response: &rest.Response{StatusCode: 500, Body: "sendgrid internal error"}, 199 - }, 200 - } 201 - 202 - m.sendEmail(context.Background(), utypes.User{ 203 - ID: "user-1", 204 - MailOptIn: true, 205 - Email: []string{"u1@example.com"}, 206 - FavoriteStore: "123", 207 - }) 208 - 209 - for key := range fc.data { 210 - if strings.HasPrefix(key, mailSentPrefix) { 211 - t.Fatalf("did not expect sent claim to be recorded for non-success status; got key %q", key) 212 - } 213 - } 214 - } 215 - 216 - func TestSendEmail_RecordsSentClaimOnSuccessSendGridStatus(t *testing.T) { 217 - if err := templates.Init(&config.Config{}, "/assets/tailwind.css"); err != nil { 218 - t.Fatalf("failed to initialize templates: %v", err) 219 - } 220 - 221 - fc := newFakeMailCache(t) 222 - m := &mailer{ 223 - cache: fc, 224 - locServer: &fakeMailLocServer{ 225 - location: &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St", ZipCode: "98005"}, 226 - }, 227 - client: &fakeMailClient{ 228 - response: &rest.Response{StatusCode: 202, Body: "accepted"}, 229 - }, 230 - } 231 - 232 - m.sendEmail(context.Background(), utypes.User{ 233 - ID: "user-1", 234 - MailOptIn: true, 235 - Email: []string{"u1@example.com"}, 236 - FavoriteStore: "123", 237 - }) 238 - 239 - var ( 240 - foundKey string 241 - claimValue string 242 - ) 243 - for key, value := range fc.data { 244 - if strings.HasPrefix(key, mailSentPrefix) { 245 - foundKey = key 246 - claimValue = value 247 - break 248 - } 249 - } 250 - if foundKey == "" { 251 - t.Fatalf("expected sent claim to be recorded for successful status") 252 - } 253 - if !strings.HasSuffix(foundKey, "/user-1") { 254 - t.Fatalf("expected sent claim key to end with /user-1, got %q", foundKey) 255 - } 256 - 257 - var claim mailSentClaim 258 - if err := json.Unmarshal([]byte(claimValue), &claim); err != nil { 259 - t.Fatalf("failed to decode sent claim: %v", err) 260 - } 261 - if claim.UserID != "user-1" { 262 - t.Fatalf("expected claim user id user-1, got %q", claim.UserID) 263 - } 264 - if claim.ParamsHash == "" { 265 - t.Fatalf("expected claim params hash to be set") 266 - } 267 114 } 268 115 269 116 func newTestServer(t *testing.T) *httptest.Server {
+170
internal/mail/mail_test.go
··· 1 + package mail 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "io" 7 + "strings" 8 + "testing" 9 + "time" 10 + 11 + "careme/internal/ai" 12 + "careme/internal/cache" 13 + "careme/internal/config" 14 + "careme/internal/locations" 15 + "careme/internal/templates" 16 + utypes "careme/internal/users/types" 17 + 18 + "github.com/sendgrid/rest" 19 + sgmail "github.com/sendgrid/sendgrid-go/helpers/mail" 20 + ) 21 + 22 + type fakeMailCache struct { 23 + shoppingListJSON string 24 + data map[string]string 25 + } 26 + 27 + func newFakeMailCache(t *testing.T) *fakeMailCache { 28 + t.Helper() 29 + listJSON, err := json.Marshal(ai.ShoppingList{ 30 + Recipes: []ai.Recipe{ 31 + {Title: "Test Recipe"}, 32 + }, 33 + }) 34 + if err != nil { 35 + t.Fatalf("failed to marshal shopping list: %v", err) 36 + } 37 + return &fakeMailCache{ 38 + shoppingListJSON: string(listJSON), 39 + data: map[string]string{}, 40 + } 41 + } 42 + 43 + func (c *fakeMailCache) Get(_ context.Context, key string) (io.ReadCloser, error) { 44 + if strings.HasPrefix(key, "shoppinglist/") { 45 + return io.NopCloser(strings.NewReader(c.shoppingListJSON)), nil 46 + } 47 + value, ok := c.data[key] 48 + if !ok { 49 + return nil, cache.ErrNotFound 50 + } 51 + return io.NopCloser(strings.NewReader(value)), nil 52 + } 53 + 54 + func (c *fakeMailCache) Exists(_ context.Context, key string) (bool, error) { 55 + _, ok := c.data[key] 56 + return ok, nil 57 + } 58 + 59 + func (c *fakeMailCache) Put(_ context.Context, key, value string, opts cache.PutOptions) error { 60 + if opts.Condition == cache.PutIfNoneMatch { 61 + if _, exists := c.data[key]; exists { 62 + return cache.ErrAlreadyExists 63 + } 64 + } 65 + c.data[key] = value 66 + return nil 67 + } 68 + 69 + type fakeMailLocServer struct { 70 + location *locations.Location 71 + } 72 + 73 + func (f *fakeMailLocServer) GetLocationByID(_ context.Context, _ string) (*locations.Location, error) { 74 + return f.location, nil 75 + } 76 + 77 + type fakeMailClient struct { 78 + response *rest.Response 79 + err error 80 + } 81 + 82 + func (f *fakeMailClient) Send(_ *sgmail.SGMailV3) (*rest.Response, error) { 83 + return f.response, f.err 84 + } 85 + 86 + func TestSendEmail_DoesNotRecordSentClaimOnNonSuccessSendGridStatus(t *testing.T) { 87 + if err := templates.Init(&config.Config{}, "/assets/tailwind.css"); err != nil { 88 + t.Fatalf("failed to initialize templates: %v", err) 89 + } 90 + 91 + fc := newFakeMailCache(t) 92 + m := &mailer{ 93 + cache: fc, 94 + locServer: &fakeMailLocServer{ 95 + location: &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St"}, 96 + }, 97 + client: &fakeMailClient{ 98 + response: &rest.Response{StatusCode: 500, Body: "sendgrid internal error"}, 99 + }, 100 + } 101 + 102 + m.sendEmail(context.Background(), utypes.User{ 103 + ID: "user-1", 104 + MailOptIn: true, 105 + Email: []string{"u1@example.com"}, 106 + FavoriteStore: "123", 107 + }) 108 + 109 + for key := range fc.data { 110 + if strings.HasPrefix(key, mailSentPrefix) { 111 + t.Fatalf("did not expect sent claim to be recorded for non-success status; got key %q", key) 112 + } 113 + } 114 + } 115 + 116 + func TestSendEmail_RecordsSentClaimOnSuccessSendGridStatus(t *testing.T) { 117 + if err := templates.Init(&config.Config{}, "/assets/tailwind.css"); err != nil { 118 + t.Fatalf("failed to initialize templates: %v", err) 119 + } 120 + 121 + fc := newFakeMailCache(t) 122 + m := &mailer{ 123 + cache: fc, 124 + locServer: &fakeMailLocServer{ 125 + location: &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St", ZipCode: "98005"}, 126 + }, 127 + client: &fakeMailClient{ 128 + response: &rest.Response{StatusCode: 202, Body: "accepted"}, 129 + }, 130 + } 131 + 132 + m.sendEmail(context.Background(), utypes.User{ 133 + ID: "user-1", 134 + MailOptIn: true, 135 + Email: []string{"u1@example.com"}, 136 + FavoriteStore: "123", 137 + ShoppingDay: time.Now().Weekday().String(), 138 + }) 139 + 140 + var ( 141 + foundKey string 142 + claimValue string 143 + ) 144 + for key, value := range fc.data { 145 + if strings.HasPrefix(key, mailSentPrefix) { 146 + foundKey = key 147 + claimValue = value 148 + break 149 + } 150 + } 151 + if foundKey == "" { 152 + t.Fatalf("expected sent claim to be recorded for successful status") 153 + } 154 + if !strings.HasSuffix(foundKey, "/user-1") { 155 + t.Fatalf("expected sent claim key to end with /user-1, got %q", foundKey) 156 + } 157 + 158 + var claim mailSentClaim 159 + if err := json.Unmarshal([]byte(claimValue), &claim); err != nil { 160 + t.Fatalf("failed to decode sent claim: %v", err) 161 + } 162 + if claim.UserID != "user-1" { 163 + t.Fatalf("expected claim user id user-1, got %q", claim.UserID) 164 + } 165 + if claim.ParamsHash == "" { 166 + t.Fatalf("expected claim params hash to be set") 167 + } 168 + } 169 + 170 + //TODO tests for optin and day of week?
+3 -3
internal/users/types/types.go
··· 28 28 29 29 // need to take a look up to location cache? 30 30 func (u User) Validate() error { 31 - if _, err := parseWeekday(u.ShoppingDay); err != nil { 31 + if _, err := ParseWeekday(u.ShoppingDay); err != nil { 32 32 return err 33 33 } 34 34 if len(u.Email) == 0 { ··· 62 62 time.Saturday.String(), 63 63 } 64 64 65 - func parseWeekday(v string) (time.Weekday, error) { 65 + func ParseWeekday(v string) (time.Weekday, error) { 66 66 for i := range daysOfWeek { 67 67 if strings.EqualFold(daysOfWeek[i], v) { 68 68 return time.Weekday(i), nil 69 69 } 70 70 } 71 71 72 - return time.Sunday, fmt.Errorf("invalid weekday '%s'", v) 72 + return time.Saturday, fmt.Errorf("invalid weekday '%s'", v) 73 73 }
+1 -1
internal/users/types/types_test.go
··· 38 38 39 39 for _, tt := range tests { 40 40 t.Run(tt.name, func(t *testing.T) { 41 - got, err := parseWeekday(tt.input) 41 + got, err := ParseWeekday(tt.input) 42 42 if tt.wantErr { 43 43 if err == nil { 44 44 t.Fatalf("parseWeekday(%q) expected error", tt.input)