package store import ( "context" "path/filepath" "testing" ) // newTestStore opens a fresh SQLite database in a temp dir. func newTestStore(t *testing.T) *Store { t.Helper() st, err := Open(filepath.Join(t.TempDir(), "school.db")) if err != nil { t.Fatalf("open: %v", err) } t.Cleanup(func() { _ = st.Close() }) if err := st.Migrate(context.Background()); err != nil { t.Fatalf("migrate: %v", err) } if err := st.SeedChildren(context.Background(), "Harry", "Finn"); err != nil { t.Fatalf("seed: %v", err) } return st } // TestItemsAreSeparatedByChild proves the Phase 4 DoD: items are stored against // the right children.id and nothing leaks between children. func TestItemsAreSeparatedByChild(t *testing.T) { ctx := context.Background() st := newTestStore(t) harry, err := st.ChildByName(ctx, "Harry") if err != nil || harry == nil { t.Fatalf("harry: %v %v", harry, err) } finn, err := st.ChildByName(ctx, "Finn") if err != nil || finn == nil { t.Fatalf("finn: %v %v", finn, err) } if harry.ID == finn.ID { t.Fatal("children share an id") } harryItems := []Item{ {Source: "compass", Type: "task", Subject: "English", Title: "Essay", ExternalRef: "h1"}, {Source: "compass", Type: "task", Subject: "Maths", Title: "Chapter 4", ExternalRef: "h2"}, {Source: "compass", Type: "message", Title: "Notice", ExternalRef: "h3"}, } finnItems := []Item{ {Source: "compass", Type: "task", Subject: "Science", Title: "Lab report", ExternalRef: "f1"}, } if _, _, err := st.UpsertItems(ctx, harry.ID, harryItems); err != nil { t.Fatalf("upsert harry: %v", err) } if _, _, err := st.UpsertItems(ctx, finn.ID, finnItems); err != nil { t.Fatalf("upsert finn: %v", err) } // The same external ref under each child must be two rows, not a conflict. if _, _, err := st.UpsertItems(ctx, finn.ID, []Item{ {Source: "compass", Type: "task", Title: "Finn essay", ExternalRef: "h1"}, }); err != nil { t.Fatalf("upsert finn duplicate ref: %v", err) } counts, err := st.CountItemsByType(ctx) if err != nil { t.Fatalf("counts: %v", err) } got := map[string]int{} for _, c := range counts { got[c.Child+"/"+c.Type] = c.Count } want := map[string]int{ "Harry/task": 2, "Harry/message": 1, "Finn/task": 2, } for k, v := range want { if got[k] != v { t.Errorf("count %s = %d, want %d (all: %v)", k, got[k], v, got) } } // A per-child sample must never return the other child's rows. harryRows, err := st.SampleItems(ctx, harry.ID, 50) if err != nil { t.Fatalf("sample harry: %v", err) } if len(harryRows) != 3 { t.Fatalf("harry rows = %d, want 3", len(harryRows)) } for _, r := range harryRows { if r.ChildID != harry.ID { t.Errorf("harry sample returned child %d", r.ChildID) } } finnRows, err := st.SampleItems(ctx, finn.ID, 50) if err != nil { t.Fatalf("sample finn: %v", err) } if len(finnRows) != 2 { t.Fatalf("finn rows = %d, want 2", len(finnRows)) } } // TestSessionRoundTrip proves the per-child session state is stored, read back // and separated. func TestSessionRoundTrip(t *testing.T) { ctx := context.Background() st := newTestStore(t) harry, _ := st.ChildByName(ctx, "Harry") finn, _ := st.ChildByName(ctx, "Finn") if sess, err := st.GetSession(ctx, harry.ID); err != nil || sess != nil { t.Fatalf("empty session = %v, %v", sess, err) } want := `{"cookies":[{"name":"x"}],"origins":[]}` if err := st.PutSession(ctx, harry.ID, want); err != nil { t.Fatalf("put: %v", err) } sess, err := st.GetSession(ctx, harry.ID) if err != nil || sess == nil { t.Fatalf("get: %v %v", sess, err) } if sess.State != want { t.Errorf("state mismatch") } if sess.Child != "Harry" { t.Errorf("child = %q", sess.Child) } // Finn must not inherit Harry's session. if sess, err := st.GetSession(ctx, finn.ID); err != nil || sess != nil { t.Errorf("finn session = %v, %v", sess, err) } // PutSession refreshes the same row. if err := st.PutSession(ctx, harry.ID, want+" "); err != nil { t.Fatalf("refresh: %v", err) } list, err := st.ListSessions(ctx) if err != nil { t.Fatalf("list: %v", err) } if len(list) != 1 { t.Fatalf("sessions = %d, want 1", len(list)) } if err := st.DeleteSession(ctx, harry.ID); err != nil { t.Fatalf("delete: %v", err) } if sess, _ := st.GetSession(ctx, harry.ID); sess != nil { t.Errorf("session survived delete") } } // TestRecordScanFailure proves a failed child is recorded loudly (FR-12/FR-13). func TestRecordScanFailure(t *testing.T) { ctx := context.Background() st := newTestStore(t) harry, _ := st.ChildByName(ctx, "Harry") id, err := st.RecordScan(ctx, &harry.ID, 0, 0, false, "fetcher failed") if err != nil || id == 0 { t.Fatalf("record: %v id=%d", err, id) } last, err := st.LastScan(ctx) if err != nil || last == nil { t.Fatalf("last: %v %v", last, err) } if last.OK { t.Error("failed scan reported as ok") } if last.Error != "fetcher failed" { t.Errorf("error = %q", last.Error) } }