home.Service aggregates the Home screen from the services that own each value, at "now" in the household's zone: the date label, tonight's dinner from the current week's plan (null with no plan), the pending Side quest count from the deck, the Supply run status of the upcoming week or else the current one, and recentUpdates.unread. unread is whether a notification was stored in notification_outbox after household_settings.updates_seen_at (or at all while it is null), read through the new notify.StoredSince over the HasNotificationSince query. Nothing writes updates_seen_at yet; the updates task adds that. Tests under America/Chicago show tonight's dinner flips at local midnight, not UTC midnight, and cover no plan, no suggestions, the Supply run choice and unread. Maestro task 9ccfc3a6-cc7b-493a-aa4b-2b455d26f4df. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
4.3 KiB
Go
139 lines
4.3 KiB
Go
package server
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
// newMediaRouter returns a router over a photo cache whose data directory
|
|
// holds both sizes of two photos, the id of the one with a photos row, and
|
|
// the id of the orphan without one. Each size's file holds its size as
|
|
// text.
|
|
func newMediaRouter(t *testing.T) (r http.Handler, id, orphan string) {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
st := newTestStore(t)
|
|
photos, err := recipes.NewPhotoCache(recipes.NewFetcher(), st, dataDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, orphan = store.NewID(), store.NewID()
|
|
for _, stem := range []string{id, orphan} {
|
|
for _, size := range []string{"400", "1200"} {
|
|
p := filepath.Join(dataDir, "media", "photos", stem+"-"+size+".jpg")
|
|
if err := os.WriteFile(p, []byte("jpeg "+size), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
now := store.Now().String()
|
|
if _, err := db.New(st.Writer).CreatePhoto(t.Context(), db.CreatePhotoParams{
|
|
ID: id,
|
|
SourceUrl: "https://example.com/a.jpg",
|
|
LocalPath: "media/photos/" + id + "-1200.jpg",
|
|
Width: 1600,
|
|
Height: 1200,
|
|
Sha256: "sha",
|
|
FetchedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
r, err = NewRouter(logger, "dev", nil, stubAuth{}, stubSessions{}, newTestSettings(t), newTestSearch(t), newTestAI(t), newTestChat(t), newTestDinners(t), newTestSuggest(t), newTestPlan(t), newTestShopping(t), newTestGoogle(t), newTestPush(t), newTestIdeas(t), newTestHome(t), newTestRunner(t),
|
|
photos, fstest.MapFS{"index.html": {Data: []byte(fixtureIndex)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r, id, orphan
|
|
}
|
|
|
|
func TestGetPhotoServesEachSize(t *testing.T) {
|
|
t.Parallel()
|
|
router, id, _ := newMediaRouter(t)
|
|
for _, size := range []string{"400", "1200"} {
|
|
req := httptest.NewRequest(http.MethodGet, "/media/photos/"+id+"-"+size+".jpg", nil)
|
|
req.AddCookie(validSessionCookie)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("size %s: status = %d, want 200", size, rec.Code)
|
|
}
|
|
if got := rec.Body.String(); got != "jpeg "+size {
|
|
t.Errorf("size %s: body = %q", size, got)
|
|
}
|
|
if got := rec.Header().Get("Content-Type"); got != "image/jpeg" {
|
|
t.Errorf("size %s: Content-Type = %q, want image/jpeg", size, got)
|
|
}
|
|
if got := rec.Header().Get("Cache-Control"); got != "private, max-age=31536000, immutable" {
|
|
t.Errorf("size %s: Cache-Control = %q", size, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetPhotoRequiresSession(t *testing.T) {
|
|
t.Parallel()
|
|
router, id, _ := newMediaRouter(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/media/photos/"+id+"-400.jpg", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", rec.Code)
|
|
}
|
|
if got := rec.Body.String(); got != unauthenticatedJSON {
|
|
t.Errorf("body = %s, want %s", got, unauthenticatedJSON)
|
|
}
|
|
if got := rec.Header().Get("Cache-Control"); got != "" {
|
|
t.Errorf("Cache-Control = %q on a refused request", got)
|
|
}
|
|
}
|
|
|
|
func TestGetPhotoUnknownNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
router, id, orphan := newMediaRouter(t)
|
|
for _, name := range []string{
|
|
store.NewID() + "-400.jpg", // no such photo
|
|
orphan + "-400.jpg", // file stored, no photos row
|
|
id + "-800.jpg", // size not stored
|
|
orphan + "-1200.jpg",
|
|
id + "-0400.jpg",
|
|
id + "-+400.jpg",
|
|
id + "-400.png",
|
|
id + ".jpg",
|
|
id + "-400",
|
|
"..%2F" + id + "-400.jpg",
|
|
"not-an-id-400.jpg",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
req := httptest.NewRequest(http.MethodGet, "/media/photos/"+name, nil)
|
|
req.AddCookie(validSessionCookie)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", rec.Code)
|
|
}
|
|
if got := rec.Body.String(); got != notFoundJSON {
|
|
t.Errorf("body = %s, want %s", got, notFoundJSON)
|
|
}
|
|
if got := rec.Header().Get("Cache-Control"); got != "" {
|
|
t.Errorf("Cache-Control = %q on a 404", got)
|
|
}
|
|
})
|
|
}
|
|
}
|