Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.25

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ aklrubbish
*~
.vscode
.idea
.env

# binary files
aklapi
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# AGENTS.md

## Project Snapshot
- Module: `github.com/rusq/aklapi`
- Language: Go
- Purpose: unofficial Auckland Council API wrapper/service for address lookup and rubbish/recycling collection dates.

## Repository Map
- `cmd/`: executable entrypoints.
- `addr.go`: address lookup logic.
- `rubbish.go`: rubbish/recycling API logic and response shaping.
- `caches.go`: cache helpers.
- `time.go`: date/time helpers.
- `*_test.go`: unit tests.
- `test_assets/`: fixtures used by tests.

## Development Commands
- Run tests: `go test ./...`
- Run focused tests: `go test ./... -run <Name>`
- Tidy dependencies: `go mod tidy`
- Build all packages: `go build ./...`

## Working Conventions
- Prefer small, targeted changes over broad refactors.
- Keep API behavior backward compatible unless explicitly requested.
- Add or update tests for behavioral changes.
- Keep exported identifiers and package-level docs concise.

## Validation Checklist
Before finishing a code change, run:
1. `go test ./...`
2. `go build ./...`

If a change only affects docs or comments, note that tests/build were not required.
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ SHELL=/bin/sh

IMAGE=aklapi

SRC=main.go $(wildcard aklapi/*.go)
SRC=$(wildcard aklapi/*.go)
PKG=./cmd/aklapi

server: $(SRC)
go build -o $@
go build -o $@ $(PKG)

test:
go test ./... -race
.PHONY: test

docker:
docker build -t $(IMAGE) .
Expand Down
5 changes: 3 additions & 2 deletions addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
var (
// defined as a variable so it can be overridden in tests.
addrURI = `https://www.aucklandcouncil.govt.nz/nextapi/property`
// defined as a variable so tests can replace it.
addrHTTPClient = &http.Client{Timeout: 15 * time.Second}
)

// AddrRequest is the address request.
Expand Down Expand Up @@ -61,8 +63,7 @@ func MatchingPropertyAddresses(ctx context.Context, addrReq *AddrRequest) (*Addr
req.URL.RawQuery = q.Encode()

start := time.Now()
client := &http.Client{}
resp, err := client.Do(req)
resp, err := addrHTTPClient.Do(req)
if err != nil {
return nil, err
}
Expand Down
12 changes: 9 additions & 3 deletions cmd/aklapi/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (

const dttmLayout = "2006-01-02"

var (
addressLookup = aklapi.AddressLookup
collectionDayDetail = aklapi.CollectionDayDetail
)

type rrResponse struct {
Rubbish string `json:"rubbish,omitempty"`
Recycle string `json:"recycle,omitempty"`
Expand All @@ -24,6 +29,7 @@ func respond(w http.ResponseWriter, data any, code int) {
b, err := json.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(code)
Expand All @@ -35,7 +41,7 @@ func rubbish(r *http.Request) (*aklapi.CollectionDayDetailResult, error) {
if addr == "" {
return nil, errors.New(http.StatusText(http.StatusBadRequest))
}
return aklapi.CollectionDayDetail(r.Context(), addr)
return collectionDayDetail(r.Context(), addr)
}

func addrHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -44,10 +50,10 @@ func addrHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
resp, err := aklapi.AddressLookup(r.Context(), addr)
resp, err := addressLookup(r.Context(), addr)
if err != nil {
slog.Error("address lookup failed", "error", err)
http.NotFound(w, r)
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
return
}
respond(w, resp, http.StatusOK)
Expand Down
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/rusq/aklapi

go 1.24.0

toolchain go1.24.2
go 1.25.0

require (
github.com/PuerkitoBio/goquery v1.11.0
Expand All @@ -15,6 +13,6 @@ require (
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
7 changes: 6 additions & 1 deletion rubbish.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const (
var (
// defined as a variable so it can be overridden in tests.
collectionDayURI = `https://new.aucklandcouncil.govt.nz/en/rubbish-recycling/rubbish-recycling-collections/rubbish-recycling-collection-days/%s.html`
// defined as a variable so tests can replace it.
collectionHTTPClient = &http.Client{Timeout: 15 * time.Second}
)

var errSkip = errors.New("skip this date")
Expand Down Expand Up @@ -118,11 +120,14 @@ func fetchandparse(ctx context.Context, addressID string) (*CollectionDayDetailR
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
resp, err := collectionHTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("collection API returned status code: %d", resp.StatusCode)
}
return parse(resp.Body)
}

Expand Down
23 changes: 23 additions & 0 deletions rubbish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ func TestCollectionDayDetail(t *testing.T) {
}
}

func TestFetchAndParse_StatusCodeError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("temporary error"))
}))
defer srv.Close()

oldURI := collectionDayURI
oldClient := collectionHTTPClient
defer func() {
collectionDayURI = oldURI
collectionHTTPClient = oldClient
}()

collectionDayURI = srv.URL + "/rubbish/%s"
collectionHTTPClient = srv.Client()

_, err := fetchandparse(t.Context(), "42")
if err == nil {
t.Fatal("expected error, got nil")
}
}

func TestCollectionDayDetailResult_NextRubbish(t *testing.T) {
type fields struct {
Collections []RubbishCollection
Expand Down