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(`
`)
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
}