console: feed publish rules (required type, monotonic per origin) (geoip spec §10.3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+39
−0
internal/console/geoippublish.go+12 −0internal/console/geoippublish_test.go+27 −0
diff --git a/internal/console/geoippublish.go b/internal/console/geoippublish.go| index 070091bcd..a5bdc44aa 100644 |
| --- a/internal/console/geoippublish.go |
| +++ b/internal/console/geoippublish.go |
| @@ -53,6 +53,12 @@ type geoipPublisher struct { |
| type geoipPublishOpts struct { |
| Origin, Edition string |
| Force bool |
| + // RequireType refuses a database of any other type (the feed's check, |
| + // spec §10.3, including on the first publish). Monotonic refuses a |
| + // build that is not newer than the current pointer's when that pointer |
| + // came from the same Origin. The feed sets both and never Force. |
| + RequireType string |
| + Monotonic bool |
| } |
| // newGeoipPublisher clears tmpRoot (the dedicated geoip-staging directory) |
| @@ -208,7 +214,13 @@ func (p *geoipPublisher) Commit(ctx context.Context, st *geoipStaged, o geoipPub |
| if p.expired(db.Source(), db.BuildEpoch(), p.now()) { |
| return geoip.Pointer{}, refused("the database is already expired (GeoLite2, 30 days after build)") |
| } |
| + if o.RequireType != "" && db.Source() != o.RequireType { |
| + return geoip.Pointer{}, refused("the feed returned a " + db.Source() + " database, not " + o.RequireType) |
| + } |
| cur, etag, present, curErr := st.cur, st.curEtag, st.curPresent, st.curErr |
| + if o.Monotonic && present && curErr == nil && cur.Origin == o.Origin && !db.BuildEpoch().After(cur.Build()) { |
| + return geoip.Pointer{}, refused("not newer than the published edition") |
| + } |
| switch { |
| case curErr != nil: |
| // A pointer exists but does not parse: replace it (etag still guards). |
diff --git a/internal/console/geoippublish_test.go b/internal/console/geoippublish_test.go| index 1563bc46f..f258f6671 100644 |
| --- a/internal/console/geoippublish_test.go |
| +++ b/internal/console/geoippublish_test.go |
| @@ -393,3 +393,30 @@ func TestGeoCountriesKeepsOtherLast(t *testing.T) { |
| t.Fatalf("last row = %+v, want _other rendered as other", rows[len(rows)-1]) |
| } |
| } |
| + |
| +func TestFeedPublishRules(t *testing.T) { |
| + withFixtureProbes(t) |
| + ctx := context.Background() |
| + p, _ := newTestPublisher(t) |
| + city := fixture(t, "GeoIP2-City-Test.mmdb") |
| + if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-Country", Monotonic: true}); !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "not GeoIP2-Country") { |
| + t.Fatalf("wrong type on first publish = %v", err) |
| + } |
| + first, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true}); !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "not newer") { |
| + t.Fatalf("same edition again = %v", err) |
| + } |
| + if cur, _, _, _ := p.Current(ctx); cur.SHA256 != first.SHA256 { |
| + t.Fatal("a refused feed publish moved the pointer") |
| + } |
| + // A manual pointer does not hold the feed back. |
| + if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual", Force: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := p.Publish(ctx, bytes.NewReader(city), geoipPublishOpts{Origin: "feed:x", RequireType: "GeoIP2-City", Monotonic: true}); err != nil { |
| + t.Fatalf("feed over a manual pointer = %v", err) |
| + } |
| +} |