From 88f879d8444ed6222cb9825f21441a517709ff27 Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Fri, 16 Oct 2026 09:19:42 +0100 Subject: [PATCH] Made mock-fts take its base URL from --addr when --base-url isn't given --- cmd/mock-fts/main.go | 43 ++++++++++++++++++++++++++++++++++++--- cmd/mock-fts/main_test.go | 34 +++++++++++++++++++++++++++++++ internal/mock/README.md | 2 +- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 cmd/mock-fts/main_test.go diff --git a/cmd/mock-fts/main.go b/cmd/mock-fts/main.go index a9bdb16..0c73d68 100644 --- a/cmd/mock-fts/main.go +++ b/cmd/mock-fts/main.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "log/slog" + "net" "net/http" "os" "os/signal" @@ -75,6 +76,12 @@ and classifications that releases are generated from is embedded in the build from internal/mock/findatender/data/catalog.json. The mock stops with an error if it is invalid. +Paging: each package's uri and links.next start with --base-url, which +clients follow as given. Without it, the base URL is taken from --addr, with +an unspecified host (":8080", "0.0.0.0", "::") as localhost. Set it when +clients reach the mock by another address, such as through a proxy or a +mapped container port. + Releases are generated from that catalog and appended to /releases.jsonl, so they survive restarts. A background worker publishes a new release every --min-gap to --max-gap. On startup the mock @@ -119,7 +126,16 @@ func newRootCmd() *cobra.Command { fts-mock --data-dir ./mock-data --reset fts-mock --min-gap 5s --max-gap 10s --rate-limit 1000`, PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.validate() + err := opts.validate() + if err != nil { + return err + } + + if opts.baseURL == "" { + opts.baseURL, err = baseURLFromAddr(opts.addr) + } + + return err }, RunE: func(cmd *cobra.Command, args []string) error { return run(cmd.Context(), opts) @@ -133,8 +149,8 @@ func newRootCmd() *cobra.Command { flags.StringVar( &opts.baseURL, "base-url", - "http://localhost:8080", - "public base URL used in uri and links.next", + "", + "public base URL used in uri and links.next (default from --addr)", ) flags.StringVar( &opts.dataDir, @@ -220,6 +236,26 @@ func (o *options) validate() error { return nil } +// baseURLFromAddr returns the base URL clients reach a mock listening on +// addr by: http://host:port, with localhost for an unspecified host. Port 0 +// is refused, as the port isn't known until the mock is listening. +func baseURLFromAddr(addr string) (string, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return "", fmt.Errorf("--addr: %w", err) + } + if port == "0" { + return "", errors.New("--addr with port 0 needs --base-url") + } + + switch host { + case "", "0.0.0.0", "::": + host = "localhost" + } + + return "http://" + net.JoinHostPort(host, port), nil +} + // run starts the mock and blocks until ctx is cancelled or the server fails. func run(ctx context.Context, opts *options) (err error) { location, err := time.LoadLocation(opts.timeZone) // already validated @@ -326,6 +362,7 @@ func run(ctx context.Context, opts *options) (err error) { slog.Info( "mock listening", "addr", opts.addr, + "base_url", opts.baseURL, "path", mock.BasePath, "time_zone", location.String(), ) diff --git a/cmd/mock-fts/main_test.go b/cmd/mock-fts/main_test.go new file mode 100644 index 0000000..24e167b --- /dev/null +++ b/cmd/mock-fts/main_test.go @@ -0,0 +1,34 @@ +package main + +import "testing" + +// Without --base-url, the base URL comes from --addr, with localhost for an +// unspecified host. +func TestBaseURLFromAddr(t *testing.T) { + tests := []struct { + addr string + want string + }{ + {":8080", "http://localhost:8080"}, + {"0.0.0.0:8080", "http://localhost:8080"}, + {"[::]:8080", "http://localhost:8080"}, + {"127.0.0.1:18080", "http://127.0.0.1:18080"}, + {"[::1]:8080", "http://[::1]:8080"}, + {"mock.example.com:80", "http://mock.example.com:80"}, + } + for _, tt := range tests { + got, err := baseURLFromAddr(tt.addr) + if err != nil || got != tt.want { + t.Errorf("baseURLFromAddr(%q) = %q, %v; want %q", tt.addr, got, err, tt.want) + } + } +} + +// An address without a port, or with port 0, is refused. +func TestBaseURLFromAddrRefused(t *testing.T) { + for _, addr := range []string{"localhost", ":0"} { + if got, err := baseURLFromAddr(addr); err == nil { + t.Errorf("baseURLFromAddr(%q) = %q; want an error", addr, got) + } + } +} diff --git a/internal/mock/README.md b/internal/mock/README.md index a318221..6a456a8 100644 --- a/internal/mock/README.md +++ b/internal/mock/README.md @@ -40,7 +40,7 @@ Useful flags (`go run ./cmd/mock-fts --help` lists them all): | Flag | Default | Meaning | |---|---|---| | `--addr` | `:8080` | Listen address | -| `--base-url` | `http://localhost:8080` | Public base URL used in `uri` and `links.next` | +| `--base-url` | from `--addr` | Public base URL used in `uri` and `links.next`. Without it, `http://`, with `localhost` for an unspecified host (`:8080`, `0.0.0.0`, `::`). Set it when clients use another address (a proxy, a mapped container port), or with port 0 | | `--data-dir` | `./mock-data` | Where `releases.jsonl` is stored | | `--reset` | | Delete stored releases and regenerate the backfill | | `--backfill` | `72h` | History to generate on an empty data directory |