From e9659ebbbd9a0b5e9445f1bcfa857c236bd3eca0 Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:07:42 +0300 Subject: [PATCH 1/2] missing tests and typo --- cmd/aklapi/handlers.go | 4 ++-- rubbish_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ time_test.go | 2 +- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/cmd/aklapi/handlers.go b/cmd/aklapi/handlers.go index 766695c..c742c85 100644 --- a/cmd/aklapi/handlers.go +++ b/cmd/aklapi/handlers.go @@ -48,13 +48,13 @@ func rubbish(r *http.Request) (*aklapi.CollectionDayDetailResult, error) { func addrHandler(w http.ResponseWriter, r *http.Request) { addr := r.FormValue("addr") if addr == "" { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + respond(w, rrResponse{Error: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) return } resp, err := addressLookup(r.Context(), addr) if err != nil { slog.Error("address lookup failed", "error", err) - http.Error(w, err.Error(), http.StatusBadGateway) + respond(w, rrResponse{Error: err.Error()}, http.StatusBadGateway) return } respond(w, resp, http.StatusOK) diff --git a/rubbish_test.go b/rubbish_test.go index 4967cb9..e63c41c 100644 --- a/rubbish_test.go +++ b/rubbish_test.go @@ -417,6 +417,50 @@ func TestCollectionDayDetailResult_NextRecycle(t *testing.T) { } } +func TestCollectionDayDetailResult_NextFoodScraps(t *testing.T) { + type fields struct { + Collections []RubbishCollection + Address *Address + } + tests := []struct { + name string + fields fields + want time.Time + }{ + {"found", + fields{ + Collections: []RubbishCollection{ + {Day: "Someday", + Date: time.Date(2000, 2, 1, 0, 0, 0, 0, defaultLoc), + Rubbish: true, + Recycle: false, + FoodScraps: false, + }, + {Day: "This is the day", + Date: time.Date(1991, 9, 16, 0, 0, 0, 0, defaultLoc), + Rubbish: false, + Recycle: false, + FoodScraps: true, + }, + }, + }, + time.Date(1991, 9, 16, 0, 0, 0, 0, defaultLoc), + }, + {"not found", fields{Collections: []RubbishCollection{}}, time.Time{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := &CollectionDayDetailResult{ + Collections: tt.fields.Collections, + Address: tt.fields.Address, + } + if got := res.NextFoodScraps(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CollectionDayDetailResult.NextFoodScraps() = %v, want %v", got, tt.want) + } + }) + } +} + func TestRubbishCollection_parseDate(t *testing.T) { type fields struct { Day string diff --git a/time_test.go b/time_test.go index 321486b..22cffe9 100644 --- a/time_test.go +++ b/time_test.go @@ -41,7 +41,7 @@ func Test_adjustYear(t *testing.T) { args{mustTime(time.Parse(dttmFmt, "0000-04-07 00:00:00"))}, mustTime(time.Parse(dttmFmt, fmt.Sprintf("%d-04-07 00:00:00", time.Now().Year()))), }, - {"differnt days", + {"different days", func() time.Time { t, _ := time.Parse(dttmFmt, fmt.Sprintf("%d-09-15 20:00:00", time.Now().Year())) return t From df278019c00d4fa5f35984d63b2503b65e3d183d Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:11:44 +0300 Subject: [PATCH 2/2] integration tests and organising css selectors --- integration_test.go | 33 +++++++++++++++++++++++++++++++++ rubbish.go | 31 +++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 integration_test.go diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..2814b09 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,33 @@ +//go:build integration + +package aklapi + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestLiveSite_CollectionDayDetail hits the live Auckland Council website and +// verifies that the HTML scraper still returns usable data. Run with: +// +// go test -tags integration -run TestLiveSite +// +// Useful as a periodic smoke test to catch council website redesigns early. +func TestLiveSite_CollectionDayDetail(t *testing.T) { + NoCache = true + t.Cleanup(func() { NoCache = false }) + + // 500 Queen Street, Auckland Central — a stable, well-known address. + const knownAddressID = "12342478585" + + res, err := fetchandparse(context.Background(), knownAddressID) + require.NoError(t, err, "fetchandparse failed — council page structure may have changed") + assert.NotEmpty(t, res.Collections, "expected at least one collection entry") + for _, c := range res.Collections { + assert.NotEmpty(t, c.Day, "collection entry has empty Day") + assert.False(t, c.Date.IsZero(), "collection entry has zero Date") + } +} diff --git a/rubbish.go b/rubbish.go index 6164307..3c4af44 100644 --- a/rubbish.go +++ b/rubbish.go @@ -18,6 +18,17 @@ var NoCache = false const ( dateLayout = "Monday, 2 January" + + // CSS selectors and text constants for the Auckland Council rubbish page. + // Update these if the council website redesigns its HTML structure. + selScheduleCard = ".acpl-schedule-card" + selCardTitle = "h4.card-title" + selCollectionItem = "p.mb-0.lead span.acpl-icon-with-attribute.left" + selIcon = "i.acpl-icon" + cardTitleHousehold = "Household collection" + iconRubbish = "rubbish" + iconRecycle = "recycle" + iconFoodWaste = "food-waste" ) var ( @@ -162,15 +173,19 @@ func (p *refuseParser) parse(r io.Reader) ([]RubbishCollection, error) { return nil, err } - doc.Find(scheduledCardSelector).Each(func(i int, card *goquery.Selection) { + doc.Find(selScheduleCard).Each(func(i int, card *goquery.Selection) { // Only parse Household collection card for now, not Commercial - cardTitle := strings.TrimSpace(card.Find("h4.card-title").Text()) - if !strings.Contains(cardTitle, "Household collection") { + cardTitle := strings.TrimSpace(card.Find(selCardTitle).Text()) + if !strings.Contains(cardTitle, cardTitleHousehold) { return } p.parseScheduleCard(card) }) + if len(p.detail) == 0 { + return nil, errors.New("no collection data found: page structure may have changed") + } + for i := range p.detail { if err := (&p.detail[i]).parseDate(); err != nil { if err == errSkip { @@ -189,19 +204,19 @@ func (p *refuseParser) parse(r io.Reader) ([]RubbishCollection, error) { // parseScheduleCard parses a schedule card and extracts collection information. func (p *refuseParser) parseScheduleCard(card *goquery.Selection) { // Each collection line is a
containing a span.acpl-icon-with-attribute.left - card.Find("p.mb-0.lead span.acpl-icon-with-attribute.left").Each(func(i int, span *goquery.Selection) { - icon := span.Find("i.acpl-icon") + card.Find(selCollectionItem).Each(func(i int, span *goquery.Selection) { + icon := span.Find(selIcon) date := strings.TrimSpace(span.Find("b").First().Text()) if date == "" { // skip empty (e.g. food scraps absent) return } var rc RubbishCollection rc.Day = date - if icon.HasClass("rubbish") { + if icon.HasClass(iconRubbish) { rc.Rubbish = true - } else if icon.HasClass("recycle") { + } else if icon.HasClass(iconRecycle) { rc.Recycle = true - } else if icon.HasClass("food-waste") { + } else if icon.HasClass(iconFoodWaste) { rc.FoodScraps = true } else { // Unknown icon type; ignore