package main import ( "context" "io" "net/http" "os" "path/filepath" "strings" "testing" "time" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return function(request) } func TestFetchSourceUsesValidatorsAndNotBefore(t *testing.T) { requests := 0 client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { requests++ if request.Header.Get("If-None-Match") == `"generation-1"` { return &http.Response{StatusCode: http.StatusNotModified, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(""))}, nil } header := make(http.Header) header.Set("ETag", `"generation-1"`) return &http.Response{StatusCode: http.StatusOK, Header: header, Body: io.NopCloser(strings.NewReader("source generation"))}, nil })} directory := t.TempDir() dataDirectory := filepath.Join(directory, "data") stateDirectory := filepath.Join(directory, "state") item := source{name: "source.txt", url: "https://source.example.invalid/feed", minimumInterval: time.Hour} now := time.Date(2026, 7, 12, 0, 0, 0, 0, time.UTC) changed, err := fetchSource(context.Background(), client, "source", item, dataDirectory, stateDirectory, now) if err != nil || !changed { t.Fatalf("initial fetch changed=%t err=%v", changed, err) } changed, err = fetchSource(context.Background(), client, "source", item, dataDirectory, stateDirectory, now.Add(30*time.Minute)) if err != nil || changed { t.Fatalf("not-before fetch changed=%t err=%v", changed, err) } changed, err = fetchSource(context.Background(), client, "source", item, dataDirectory, stateDirectory, now.Add(2*time.Hour)) if err != nil || changed { t.Fatalf("conditional fetch changed=%t err=%v", changed, err) } if requests != 2 { t.Fatalf("requests=%d, want 2", requests) } contents, err := os.ReadFile(filepath.Join(dataDirectory, "source.txt")) if err != nil || string(contents) != "source generation" { t.Fatalf("cached source=%q err=%v", contents, err) } } func TestFetchSourcePersistsRetryFloor(t *testing.T) { requests := 0 client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { requests++ header := make(http.Header) header.Set("Retry-After", "3600") return &http.Response{StatusCode: http.StatusTooManyRequests, Header: header, Body: io.NopCloser(strings.NewReader(""))}, nil })} directory := t.TempDir() dataDirectory := filepath.Join(directory, "data") stateDirectory := filepath.Join(directory, "state") if err := os.MkdirAll(dataDirectory, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dataDirectory, "source.txt"), []byte("cached"), 0o600); err != nil { t.Fatal(err) } if err := writeState(filepath.Join(stateDirectory, "source.json"), sourceState{SHA256: "cached-generation"}); err != nil { t.Fatal(err) } item := source{name: "source.txt", url: "https://source.example.invalid/feed", minimumInterval: time.Hour} now := time.Date(2026, 7, 12, 0, 0, 0, 0, time.UTC) if _, err := fetchSource(context.Background(), client, "source", item, dataDirectory, stateDirectory, now); err == nil { t.Fatal("expected rate-limit error") } changed, err := fetchSource(context.Background(), client, "source", item, dataDirectory, stateDirectory, now.Add(time.Minute)) if err != nil || changed { t.Fatalf("retry-floor fetch changed=%t err=%v", changed, err) } if requests != 1 { t.Fatalf("requests=%d, want 1", requests) } }