From f3a91ad9154289a7fb016a9b7647120d114953a1 Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Thu, 15 Oct 2026 16:27:49 +0100 Subject: [PATCH] An empty api_root now fails at start, for an enabled find_a_tender or fts_mock source and for fetch once An enabled Find a Tender or mock source with no api_root used to start and then fail on every fetch. Its worker now returns an error naming the missing key before it starts, which stops Run. "fetch once" checks sources.find_a_tender.api_root the same way. TestRunMissingAPIRoot covers both sources, and TestRunLogsSourceErrors sets an api_root so it still tests the zero interval. The section docs and internal/config/README.md say what an empty api_root means. --- internal/cli/fetch/once.go | 4 +++ internal/config/README.md | 4 +-- internal/config/conf_source_findatender.go | 3 +- internal/config/conf_source_ftsmock.go | 4 +-- internal/fetcher/fetcher_test.go | 33 ++++++++++++++++++++++ internal/fetcher/worker_findatender.go | 4 +++ internal/fetcher/worker_ftsmock.go | 4 +++ 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/internal/cli/fetch/once.go b/internal/cli/fetch/once.go index d21e14f..be36247 100644 --- a/internal/cli/fetch/once.go +++ b/internal/cli/fetch/once.go @@ -2,6 +2,7 @@ package fetch import ( "encoding/json" + "errors" "fmt" "log/slog" "slices" @@ -73,6 +74,9 @@ func commandOnce(opts *cli.Options) *cobra.Command { return sinceErr } + if conf.Sources.FindATender.APIRoot == "" { + return errors.New("sources.find_a_tender.api_root is not set") + } findATender := findatender.New(conf, conf.Sources.FindATender.APIRoot) defer findATender.Shutdown() diff --git a/internal/config/README.md b/internal/config/README.md index c55185f..3297663 100644 --- a/internal/config/README.md +++ b/internal/config/README.md @@ -37,8 +37,8 @@ is kept. | `database.file` | string | | Path to the SQLite database file, used by the `migrate` commands. Empty: commands that need the database fail | | `sources..enabled` | bool | `false` | For every source | | `sources..interval` | duration | `10m` | For every source; e.g. `30s`, `1h`. Must be greater than 0; turn a source off with `enabled` | -| `sources.find_a_tender.api_root` | string | `https://www.find-tender.service.gov.uk/api/1.0` | URL of the Find a Tender API | -| `sources.fts_mock.api_root` | string | | URL of the mock FTS server (see `internal/mock`); must be set when the source is enabled | +| `sources.find_a_tender.api_root` | string | `https://www.find-tender.service.gov.uk/api/1.0` | URL of the Find a Tender API. Empty: the source fails to start if enabled, and `fetch once` fails | +| `sources.fts_mock.api_root` | string | | URL of the mock FTS server (see `internal/mock`). Empty: the source fails to start if enabled | | `sources.sleeper.block_shutdown` | bool | `false` | Test source: if true, its sleep ignores shutdown | | `server.host` | string | `127.0.0.1` | Address to listen on; empty: every interface | | `server.port` | int | `9393` | Port to listen on; `0`: any free port | diff --git a/internal/config/conf_source_findatender.go b/internal/config/conf_source_findatender.go index a871f98..da6f239 100644 --- a/internal/config/conf_source_findatender.go +++ b/internal/config/conf_source_findatender.go @@ -6,7 +6,8 @@ import "reflect" const JSCKSourceFindATender string = "find_a_tender" // JSCSourceFindATender configures fetching from the Find a Tender -// OCDS API at APIRoot. +// OCDS API at APIRoot. Empty: the source fails to start if it is enabled, +// and `fetch once` fails. type JSCSourceFindATender struct { JSCSourcesSource APIRoot string `cfg:"api_root" default:"https://www.find-tender.service.gov.uk/api/1.0"` diff --git a/internal/config/conf_source_ftsmock.go b/internal/config/conf_source_ftsmock.go index 98b8a30..8945c07 100644 --- a/internal/config/conf_source_ftsmock.go +++ b/internal/config/conf_source_ftsmock.go @@ -6,8 +6,8 @@ import "reflect" const JSCKSourceFTSMock string = "fts_mock" // JSCSourceFTSMock configures fetching from a Find a Tender mock -// server (cmd/mock-fts) at APIRoot, which has no default and must be set when -// the source is enabled. +// server (cmd/mock-fts) at APIRoot, which has no default. Empty: the source +// fails to start if it is enabled. type JSCSourceFTSMock struct { JSCSourcesSource APIRoot string `cfg:"api_root"` diff --git a/internal/fetcher/fetcher_test.go b/internal/fetcher/fetcher_test.go index f1c7735..e8a51b7 100644 --- a/internal/fetcher/fetcher_test.go +++ b/internal/fetcher/fetcher_test.go @@ -229,7 +229,9 @@ func TestRunLogsSourceErrors(t *testing.T) { // A zero interval makes each worker fail straight away. conf := &config.JobSearchConfig{} conf.Sources.FindATender.Enabled = true + conf.Sources.FindATender.APIRoot = "http://127.0.0.1:1" conf.Sources.FTSMock.Enabled = true + conf.Sources.FTSMock.APIRoot = "http://127.0.0.1:1" conf.Sources.Sleeper.Enabled = true err := Run(context.Background(), conf, time.Time{}) @@ -248,6 +250,37 @@ func TestRunLogsSourceErrors(t *testing.T) { } } +// An enabled Find a Tender or mock source with no api_root fails straight +// away, naming the setting. +func TestRunMissingAPIRoot(t *testing.T) { + quiet := slog.New(slog.DiscardHandler) + tests := []struct { + name string + enable func(*config.JobSearchConfig) + key string + }{ + {"find_a_tender", func(conf *config.JobSearchConfig) { + conf.Sources.FindATender.Enabled = true + conf.Sources.FindATender.Interval = time.Hour + }, "sources.find_a_tender.api_root"}, + {"fts_mock", func(conf *config.JobSearchConfig) { + conf.Sources.FTSMock.Enabled = true + conf.Sources.FTSMock.Interval = time.Hour + }, "sources.fts_mock.api_root"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := &config.JobSearchConfig{} + tt.enable(conf) + + err := Run(context.Background(), conf, time.Time{}, WithLogger(quiet)) + if err == nil || !strings.Contains(err.Error(), tt.key) { + t.Errorf("Run = %v; want an error naming %s", err, tt.key) + } + }) + } +} + func TestRunNoSourcesEnabled(t *testing.T) { done := make(chan struct{}) go func() { diff --git a/internal/fetcher/worker_findatender.go b/internal/fetcher/worker_findatender.go index 519acdb..32811d5 100644 --- a/internal/fetcher/worker_findatender.go +++ b/internal/fetcher/worker_findatender.go @@ -20,6 +20,10 @@ func runFindAOpportunity( conf *config.JobSearchConfig, since time.Time, ) error { + if conf.Sources.FindATender.APIRoot == "" { + return errors.New("fetcher: sources.find_a_tender.api_root is not set") + } + worker := util.Worker{ Logger: tagged(logger), Name: "FindAOpportunity", diff --git a/internal/fetcher/worker_ftsmock.go b/internal/fetcher/worker_ftsmock.go index faee530..a46839b 100644 --- a/internal/fetcher/worker_ftsmock.go +++ b/internal/fetcher/worker_ftsmock.go @@ -20,6 +20,10 @@ func runFTSMock( conf *config.JobSearchConfig, since time.Time, ) error { + if conf.Sources.FTSMock.APIRoot == "" { + return errors.New("fetcher: sources.fts_mock.api_root is not set") + } + worker := util.Worker{ Logger: tagged(logger), Name: "FTSMock",