package main import ( "os" "strings" "testing" "time" "golang.org/x/net/html" ) func TestInferAssignmentDate(t *testing.T) { now := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC) got := inferAssignmentDate("Jan 20", now) want := time.Date(2026, 1, 20, 0, 0, 0, 0, time.UTC) if !got.Equal(want) { t.Fatalf("got %s want %s", got, want) } } func TestInferAssignmentDateRollsBackYear(t *testing.T) { now := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC) got := inferAssignmentDate("Dec 15", now) want := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC) if !got.Equal(want) { t.Fatalf("got %s want %s", got, want) } } func TestInferUpcomingDateRollsForwardYear(t *testing.T) { now := time.Date(2026, 12, 28, 10, 0, 0, 0, time.UTC) got := inferUpcomingDate("Jan 03", now) want := time.Date(2027, 1, 3, 0, 0, 0, 0, time.UTC) if !got.Equal(want) { t.Fatalf("got %s want %s", got, want) } } func TestFilterUpcomingAssignments(t *testing.T) { now := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC) assignments := []Assignment{ {DueDate: time.Date(2026, 3, 29, 0, 0, 0, 0, time.UTC), Title: "Soon"}, {DueDate: time.Date(2026, 4, 20, 0, 0, 0, 0, time.UTC), Title: "Later"}, } got := filterUpcomingAssignments(assignments, now, 14) if len(got) != 1 || got[0].Title != "Soon" { t.Fatalf("unexpected filtered assignments: %#v", got) } } func TestDedupeAssignments(t *testing.T) { assignment := Assignment{ DueDate: time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC), Title: "Preparedness", Link: "https://example.invalid/a", } got := dedupeAssignments([]Assignment{assignment, assignment}) if len(got) != 1 { t.Fatalf("got %d items", len(got)) } } func TestExtractLikeLiveMissingTable(t *testing.T) { doc := mustParseHTML(`
Mar
25
US History HW: Timeline Events
`) table := findNode(doc, func(n *html.Node) bool { return n.Type == html.ElementNode && n.Data == "table" && hasClass(n, "missing-assignments") }) if table == nil { t.Fatal("missing table not found") } row := findNode(table, func(n *html.Node) bool { return n.Type == html.ElementNode && n.Data == "tr" }) cells := directChildrenByTag(row, "td") if got := nodeText(cells[0]); got != "Mar 25" { t.Fatalf("got %q", got) } link := findNode(cells[1], func(n *html.Node) bool { return n.Type == html.ElementNode && n.Data == "a" && attr(n, "data-alma-modal") != "" }) if got := nodeText(link); got != "HW: Timeline Events" { t.Fatalf("got %q", got) } } func TestLoadAlmaCredsFromEnv(t *testing.T) { t.Setenv("ALMA_USERNAME", "student") t.Setenv("ALMA_PASSWORD", "secret") got, err := loadAlmaCreds("") if err != nil { t.Fatalf("loadAlmaCreds returned error: %v", err) } if got["username"] != "student" || got["password"] != "secret" { t.Fatalf("unexpected creds: %#v", got) } } func TestLoadAlmaCredsRequiresPasswordWithEnvUsername(t *testing.T) { t.Setenv("ALMA_USERNAME", "student") t.Setenv("ALMA_PASSWORD", "") if _, err := loadAlmaCreds(""); err == nil { t.Fatal("expected error when ALMA_PASSWORD is missing") } } func TestLoadAlmaCredsFromFile(t *testing.T) { dir := t.TempDir() path := dir + "/alma.creds" if err := os.WriteFile(path, []byte("username: student\npassword: secret\n"), 0o600); err != nil { t.Fatalf("write creds file: %v", err) } got, err := loadAlmaCreds(path) if err != nil { t.Fatalf("loadAlmaCreds returned error: %v", err) } if got["username"] != "student" || got["password"] != "secret" { t.Fatalf("unexpected creds: %#v", got) } } func mustParseHTML(text string) *html.Node { doc, err := html.Parse(strings.NewReader(text)) if err != nil { panic(err) } return doc }