package console import ( "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/geoip" "github.com/carlosframework/platform/internal/release" ) // The repo's fixtures hold none of the production probe addresses // (FACTS.md), so tests swap in addresses the fixtures do resolve. func withFixtureProbes(t *testing.T) { prev := geoipProbeAddrs geoipProbeAddrs = []string{"81.2.69.142", "216.160.83.56", "89.160.20.112", "2001:218::1", "67.43.156.1"} t.Cleanup(func() { geoipProbeAddrs = prev }) } func fixture(t *testing.T, name string) []byte { b, err := os.ReadFile("../geo/testdata/" + name) if err != nil { t.Fatal(err) } return b } func newTestPublisher(t *testing.T) (*geoipPublisher, release.Store) { s := release.NewDirStore(t.TempDir()) return newGeoipPublisher(s, t.TempDir()), s } func blobKeys(t *testing.T, s release.Store) []string { keys, err := s.List(context.Background(), geoip.Prefix) if err != nil { t.Fatal(err) } var out []string for _, k := range keys { if strings.HasSuffix(k, ".mmdb") { out = append(out, k) } } return out } func TestPublishWritesBlobThenPointer(t *testing.T) { withFixtureProbes(t) p, s := newTestPublisher(t) ptr, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } if ptr.DatabaseType != "GeoIP2-City" || ptr.NodeCount != 1547 || ptr.Origin != "manual" || ptr.Key != geoip.BlobKey(ptr.SHA256) { t.Fatalf("pointer = %+v", ptr) } raw, _, err := s.Get(context.Background(), geoip.PointerKey) if err != nil { t.Fatal(err) } if got, err := geoip.ParsePointer(raw); err != nil || got.SHA256 != ptr.SHA256 { t.Fatalf("stored pointer = (%+v, %v)", got, err) } if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != ptr.Key { t.Fatalf("blobs = %v", keys) } } func TestPublishRefusals(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, _ := newTestPublisher(t) for name, body := range map[string][]byte{ "not a database": []byte("junk"), "no country data": fixture(t, "MaxMind-DB-test-ipv4-24.mmdb"), } { if _, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) { t.Errorf("%s: err = %v, want errGeoipRefused", name, err) } } } func TestPublishShrinkCheckOnlyForTheSameType(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil { t.Fatal(err) } // Inflate the recorded node_count so a same-type republish looks like a // truncation (fewer than half). raw, etag, _ := s.Get(ctx, geoip.PointerKey) inflated := bytes.Replace(raw, []byte(`"node_count": 1704`), []byte(`"node_count": 99999`), 1) if bytes.Equal(inflated, raw) { t.Fatalf("pointer JSON shape changed; adjust the test: %s", raw) } if _, err := s.Put(ctx, geoip.PointerKey, inflated, etag); err != nil { t.Fatal(err) } same := fixture(t, "GeoIP2-Country-Test.mmdb") if _, err := p.Publish(ctx, bytes.NewReader(same), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) { t.Fatalf("same-type shrink = %v, want refused", err) } if _, err := p.Publish(ctx, bytes.NewReader(same), geoipPublishOpts{Origin: "manual", Force: true}); err != nil { t.Fatalf("forced = %v", err) } // A different type skips the check entirely. Re-inflate first, or the // City fixture (1547 nodes) would pass against Country's real 1704 even // if the check were wrongly applied across types (plan review R1-20). raw, etag, _ = s.Get(ctx, geoip.PointerKey) inflated = bytes.Replace(raw, []byte(`"node_count": 1704`), []byte(`"node_count": 99999`), 1) if _, err := s.Put(ctx, geoip.PointerKey, inflated, etag); err != nil { t.Fatal(err) } if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil { t.Fatalf("different type = %v", err) } } func TestPublishSameFileTwiceAndXYXKeepsTheCurrentBlob(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) x, y := fixture(t, "GeoIP2-City-Test.mmdb"), fixture(t, "GeoIP2-Country-Test.mmdb") for i, body := range [][]byte{x, x, y, x} { ptr, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual", Force: true}) if err != nil { t.Fatalf("publish %d: %v", i, err) } keys := blobKeys(t, s) if len(keys) != 1 || keys[0] != ptr.Key { t.Fatalf("after publish %d blobs = %v, want only %s", i, keys, ptr.Key) } } } func TestPublishIsOneAtATime(t *testing.T) { withFixtureProbes(t) p, _ := newTestPublisher(t) p.mu.Lock() // a publish in progress _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) p.mu.Unlock() if !errors.Is(err, errGeoipBusy) { t.Fatalf("concurrent publish = %v, want errGeoipBusy", err) } } func TestPublishRefusesOversizeWhileCopying(t *testing.T) { withFixtureProbes(t) p, _ := newTestPublisher(t) p.maxBytes = 1024 _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if !errors.Is(err, errGeoipRefused) { t.Fatalf("oversize = %v", err) } if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 { t.Fatalf("temp left behind: %v", entries) } } func TestOffAndSweep(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) ptr, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } // An orphan from a crashed publish (spec §3 orphan sweep). orphan := geoip.BlobKey(strings.Repeat("a", 64)) if _, err := s.Put(ctx, orphan, []byte("x"), ""); err != nil { t.Fatal(err) } if err := p.Sweep(ctx); err != nil { t.Fatal(err) } if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != ptr.Key { t.Fatalf("after sweep = %v", keys) } if err := p.Off(ctx); err != nil { t.Fatal(err) } if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) { t.Fatalf("pointer after off: %v", err) } if keys := blobKeys(t, s); len(keys) != 0 { t.Fatalf("blobs after off = %v", keys) } } func TestExpireIfDueMarksGeoLiteExpired(t *testing.T) { ctx := context.Background() p, s := newTestPublisher(t) sha := strings.Repeat("b", 64) ptr := geoip.Pointer{V: 1, SHA256: sha, Key: geoip.BlobKey(sha), Size: 1, DatabaseType: "GeoLite2-City", BuildEpoch: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC).Unix(), Origin: "manual"} putPointer(t, s, ptr) if _, err := s.Put(ctx, ptr.Key, []byte("x"), ""); err != nil { t.Fatal(err) } if did, err := p.ExpireIfDue(ctx, time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC)); did || err != nil { t.Fatalf("early = (%v, %v)", did, err) } if did, err := p.ExpireIfDue(ctx, time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)); !did || err != nil { t.Fatalf("due = (%v, %v)", did, err) } got, _, present, err := p.Current(ctx) if err != nil || !present || !got.Expired { t.Fatalf("pointer = (%+v, %v, %v)", got, present, err) } if keys := blobKeys(t, s); len(keys) != 0 { t.Fatalf("blob not destroyed: %v", keys) } } func putPointer(t *testing.T, s release.Store, p geoip.Pointer) { t.Helper() b, err := marshalPointer(p) if err != nil { t.Fatal(err) } if _, err := s.Put(context.Background(), geoip.PointerKey, b, ""); err != nil { t.Fatal(err) } } var _ = sync.Mutex{} func TestPublisherStartupClearsAbandonedStaging(t *testing.T) { withFixtureProbes(t) root := filepath.Join(t.TempDir(), "geoip-staging") abandoned := filepath.Join(root, "geoip-publish-123") if err := os.MkdirAll(abandoned, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(abandoned, "db.mmdb"), []byte("half an upload"), 0o600); err != nil { t.Fatal(err) } p := newGeoipPublisher(release.NewDirStore(t.TempDir()), root) if entries, _ := os.ReadDir(root); len(entries) != 0 { t.Fatalf("staging not cleared at startup: %v", entries) } if _, err := p.Publish(context.Background(), bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}); err != nil { t.Fatalf("publish after restart: %v", err) } } func TestPublishRefusesAWrongObjectAtTheBlobKey(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) body := fixture(t, "GeoIP2-City-Test.mmdb") sum := sha256.Sum256(body) key := geoip.BlobKey(hex.EncodeToString(sum[:])) if _, err := s.Put(ctx, key, body[:len(body)/2], ""); err != nil { // a truncated object squatting the name t.Fatal(err) } if _, err := p.Publish(ctx, bytes.NewReader(body), geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) { t.Fatalf("publish over a wrong object = %v, want refused", err) } if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) { t.Fatal("the pointer moved onto a wrong blob") } } func TestPublishRefusesAnExpiredDatabaseEvenWithForce(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } p.expired = func(string, time.Time, time.Time) bool { return true } _, err = p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb")), geoipPublishOpts{Origin: "manual", Force: true}) if !errors.Is(err, errGeoipRefused) { t.Fatalf("expired candidate with force = %v, want refused (codex R7-1)", err) } cur, _, _, _ := p.Current(ctx) if cur.SHA256 != good.SHA256 { t.Fatal("a refused expired candidate moved the pointer") } if keys := blobKeys(t, s); len(keys) != 1 || keys[0] != good.Key { t.Fatalf("blobs after refusal = %v, want only the previous good one", keys) } } func TestCommitRefusesAForeignPointerWrite(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) first, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } st, err := p.Stage(ctx, bytes.NewReader(fixture(t, "GeoIP2-Country-Test.mmdb"))) if err != nil { t.Fatal(err) } // Something outside this console rewrites the pointer between staging // and commit (spec §3 step 3). raw, etag, _ := s.Get(ctx, geoip.PointerKey) if _, err := s.Put(ctx, geoip.PointerKey, append(raw, ' '), etag); err != nil { t.Fatal(err) } if _, err := p.Commit(ctx, st, geoipPublishOpts{Origin: "manual", Force: true}); err == nil { t.Fatal("commit over a foreign pointer write succeeded") } cur, _, _, _ := p.Current(ctx) if cur.SHA256 != first.SHA256 { t.Fatal("the foreign pointer was overwritten") } if !p.mu.TryLock() { t.Fatal("a failed commit must release the publish slot") } p.mu.Unlock() } func TestOffRefusesDuringAPublishThenSucceeds(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, s := newTestPublisher(t) st, err := p.Stage(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb"))) if err != nil { t.Fatal(err) } // Off refuses rather than waits while the slot is held (codex R9-2)... if err := p.Off(ctx); !errors.Is(err, errGeoipBusy) { t.Fatalf("off during a publish = %v, want errGeoipBusy", err) } if _, err := p.Commit(ctx, st, geoipPublishOpts{Origin: "manual"}); err != nil { t.Fatal(err) } // ...and succeeds once the publish has finished. if err := p.Off(ctx); err != nil { t.Fatal(err) } if _, _, err := s.Get(ctx, geoip.PointerKey); !errors.Is(err, os.ErrNotExist) { t.Fatal("off, serialised after the publish, must leave no pointer") } if keys := blobKeys(t, s); len(keys) != 0 { t.Fatalf("blobs after off = %v", keys) } } // Code review R1-3: an oversized object at the pointer key is never read // whole, and is reported as present-but-not-a-pointer. func TestCurrentRefusesAnOversizedPointer(t *testing.T) { ctx := context.Background() p, s := newTestPublisher(t) if _, err := s.Put(ctx, geoip.PointerKey, bytes.Repeat([]byte("x"), geoip.MaxPointerBytes+1), ""); err != nil { t.Fatal(err) } _, etag, present, err := p.Current(ctx) if !errors.Is(err, errPointerTooLarge) || !present { t.Fatalf("Current = (%v, %v), want errPointerTooLarge, present", present, err) } _ = etag } type panicReader struct{} func (panicReader) Read([]byte) (int, error) { panic("a busy Stage read its body") } // Code review R1-5: a Stage refused for a held slot never touches its body. func TestStageNeverReadsWhenTheSlotIsHeld(t *testing.T) { p, _ := newTestPublisher(t) p.mu.Lock() defer p.mu.Unlock() if _, err := p.Stage(context.Background(), panicReader{}); !errors.Is(err, errGeoipBusy) { t.Fatalf("Stage = %v, want errGeoipBusy", err) } } // Code review R1-6: the other row sorts last whatever its size. func TestGeoCountriesKeepsOtherLast(t *testing.T) { m := map[string]int64{} for i := 0; i < 15; i++ { m[fmt.Sprintf("C%02d", i)] = 10 } m["GB"] = 11 rows := sortedKV(geoCountries(m)) if rows[len(rows)-1].Key != "_other" || geoLabel(rows[len(rows)-1].Key) != "other" { 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) } } type stallingStore struct{ release.Store } func (stallingStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) { <-ctx.Done() return nil, "", ctx.Err() } func (s stallingStore) Get(ctx context.Context, key string) ([]byte, string, error) { <-ctx.Done() return nil, "", ctx.Err() } // Code review R4-1: a stalled bucket cannot hold the publish slot forever. func TestStageGivesUpOnAStalledStoreAndFreesTheSlot(t *testing.T) { p, s := newTestPublisher(t) p.store = stallingStore{s} p.storeTimeout = 50 * time.Millisecond start := time.Now() if _, err := p.Stage(context.Background(), bytes.NewReader([]byte("x"))); err == nil { t.Fatal("Stage succeeded against a stalled store") } if time.Since(start) > 5*time.Second { t.Fatal("Stage waited on the stalled store") } if !p.mu.TryLock() { t.Fatal("the slot stayed held after giving up") } p.mu.Unlock() } // Code review R8-1: a crafted tree is refused in time, the slot is freed, // staging is cleaned and the previous pointer survives. func TestPublishRefusesAPathologicalTreeInTime(t *testing.T) { withFixtureProbes(t) ctx := context.Background() p, _ := newTestPublisher(t) good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } start := time.Now() _, err = p.Publish(ctx, bytes.NewReader(fixture(t, "pathological-dag.mmdb")), geoipPublishOpts{Origin: "manual", Force: true}) if !errors.Is(err, errGeoipRefused) || !strings.Contains(err.Error(), "search tree") { t.Fatalf("err = %v", err) } if time.Since(start) > 5*time.Second { t.Fatal("took too long") } if cur, _, _, _ := p.Current(ctx); cur.SHA256 != good.SHA256 { t.Fatal("the previous pointer was replaced") } if !p.mu.TryLock() { t.Fatal("slot still held") } p.mu.Unlock() if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 { t.Fatalf("staging left behind: %v", entries) } } // Code review R9: databases that hang or panic the parser are refused in // time, by the child, and the console survives with its slot and pointer. func TestPublishSurvivesDatabasesThatHangOrPanicTheParser(t *testing.T) { withFixtureProbes(t) prev := geoipVerifyTimeout geoipVerifyTimeout = 2 * time.Second t.Cleanup(func() { geoipVerifyTimeout = prev }) ctx := context.Background() p, _ := newTestPublisher(t) good, err := p.Publish(ctx, bytes.NewReader(fixture(t, "GeoIP2-City-Test.mmdb")), geoipPublishOpts{Origin: "manual"}) if err != nil { t.Fatal(err) } for _, f := range []string{"pathological-empty-dag.mmdb", "pathological-metadata.mmdb", "pathological-bigmap.mmdb"} { start := time.Now() if _, err := p.Publish(ctx, bytes.NewReader(fixture(t, f)), geoipPublishOpts{Origin: "manual", Force: true}); !errors.Is(err, errGeoipRefused) { t.Fatalf("%s: err = %v", f, err) } if d := time.Since(start); d > 15*time.Second { t.Fatalf("%s took %s", f, d) } if cur, _, _, _ := p.Current(ctx); cur.SHA256 != good.SHA256 { t.Fatalf("%s replaced the pointer", f) } if !p.mu.TryLock() { t.Fatalf("%s left the slot held", f) } p.mu.Unlock() if entries, _ := os.ReadDir(p.tmpRoot); len(entries) != 0 { t.Fatalf("%s left staging: %v", f, entries) } } } // junkRecordMMDB writes an IPv4 database whose lower half answers GB and // whose upper half carries a 6M-element ignored array beside its country // (code review R11-1): generic verification walks the array, but the // production schema's skip recurses once per element. func junkRecordMMDB(t *testing.T) string { t.Helper() var buf bytes.Buffer str := func(s string) { buf.WriteByte(0x40 | byte(len(s))); buf.WriteString(s) } u16 := func(v uint16) { buf.WriteByte(0xA2); buf.WriteByte(byte(v >> 8)); buf.WriteByte(byte(v)) } // One node: left -> data at 0 (A), right -> data at offsetB (B). recA := []byte{0xE1, 0x47, 'c', 'o', 'u', 'n', 't', 'r', 'y', 0xE1, 0x48, 'i', 's', 'o', '_', 'c', 'o', 'd', 'e', 0x42, 'G', 'B'} offsetB := len(recA) ptr := func(dataOff int) []byte { v := 1 + 16 + dataOff; return []byte{byte(v >> 16), byte(v >> 8), byte(v)} } buf.Write(ptr(0)) buf.Write(ptr(offsetB)) buf.Write(make([]byte, 16)) buf.Write(recA) // B: {country: {iso_code: FR}, junk: [16,000,000 x false]} buf.Write([]byte{0xE2, 0x47, 'c', 'o', 'u', 'n', 't', 'r', 'y', 0xE1, 0x48, 'i', 's', 'o', '_', 'c', 'o', 'd', 'e', 0x42, 'F', 'R', 0x44, 'j', 'u', 'n', 'k'}) const n = 6_000_000 v := n - 65821 buf.Write([]byte{0x1F, 0x04, byte(v >> 16), byte(v >> 8), byte(v)}) // array, size 31 form falseBool := []byte{0x00, 0x07} for i := 0; i < n; i++ { buf.Write(falseBool) } buf.WriteString("\xAB\xCD\xEFMaxMind.com") buf.WriteByte(0xE0 | 9) str("node_count") buf.Write([]byte{0xC1, 1}) str("record_size") u16(24) str("ip_version") u16(4) str("database_type") str("Junk-Record") str("languages") buf.Write([]byte{0x01, 0x04}) str("en") str("binary_format_major_version") u16(2) str("binary_format_minor_version") u16(0) str("build_epoch") buf.Write([]byte{0x04, 0x02, 0x6a, 0xbc, 0x00, 0x00}) str("description") buf.WriteByte(0xE1) str("en") str("junk") p := filepath.Join(t.TempDir(), "junk.mmdb") if err := os.WriteFile(p, buf.Bytes(), 0o644); err != nil { t.Fatal(err) } return p } // Code review R11-1: a record that would overflow a serving lookup's stack // is refused by the verifier child, even when every probe address is fine. func TestPublishRefusesARecordThatWouldCrashALookup(t *testing.T) { prevProbes := geoipProbeAddrs geoipProbeAddrs = []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"} // all in the clean half t.Cleanup(func() { geoipProbeAddrs = prevProbes }) prev := geoipVerifyTimeout geoipVerifyTimeout = 90 * time.Second t.Cleanup(func() { geoipVerifyTimeout = prev }) ctx := context.Background() p, _ := newTestPublisher(t) f, err := os.Open(junkRecordMMDB(t)) if err != nil { t.Fatal(err) } defer f.Close() if _, err := p.Publish(ctx, f, geoipPublishOpts{Origin: "manual"}); !errors.Is(err, errGeoipRefused) { t.Fatalf("a crash-on-lookup database = %v, want refused", err) } if _, _, present, _ := p.Current(ctx); present { t.Fatal("it was published") } }