package console import ( "bytes" "context" "encoding/json" "io" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/dbdoor" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" "github.com/superfly/ltx" ) func doorLTXHeader(t *testing.T, lo, hi uint64) []byte { t.Helper() h := ltx.Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize: 4096, MinTXID: ltx.TXID(lo), MaxTXID: ltx.TXID(hi)} b, err := h.MarshalBinary() if err != nil { t.Fatal(err) } return b } func TestDoorPlanHTTPGenerationSelection(t *testing.T) { for _, mode := range []string{"origin", "published", "unfinished", "unfinished-origin", "gap", "missing-pin", "partial-list", "disagreement", "bad-header", "holder-change", "op-change"} { t.Run(mode, func(t *testing.T) { fx := dbFixture(t) model := enableDoorPlan(t, fx) fx.srv.cfg.HomeRegion = "ie" obs, err := hostclaim.Read(t.Context(), fx.rel, dbTestHost) if err != nil { t.Fatal(err) } c := cloneDoorClaim(obs.Claim) loc := c.Holder.Location gen := "live" if mode == "origin" || mode == "unfinished-origin" { gen = "" } c.Holder.Generation = gen base := loc if gen != "" { base += "/g-" + gen } put := func(level int, lo, hi uint64) genselect.File { key := dbVersionKey(base, level, lo, hi) b := doorLTXHeader(t, lo, hi) id := model.Put("data-test", key, b) return genselect.File{Key: key, VersionID: id, Level: level, MinTXID: lo, MaxTXID: hi, Size: int64(len(b))} } root := put(9, 1, 11) cont := put(1, 10, 13) put(0, 11, 11) put(0, 12, 12) if gen != "" { marker := genselect.Marker{Op: gen, Host: dbTestHost, Incarnation: c.Holder.Incarnation, Box: "box", InstalledAt: time.Unix(1, 0).UTC(), Pin: genselect.Pin{SnapshotKey: root.Key, SnapshotVersion: root.VersionID, SnapshotTXID: 11}} raw, err := json.Marshal(marker) if err != nil { t.Fatal(err) } model.Put("data-test", loc+"/generation.json", raw) model.Put("data-test", root.Key, []byte("late duplicate must never be selected")) model.Put("data-test", dbVersionKey(loc, 9, 1, 99), doorLTXHeader(t, 1, 99)) } switch mode { case "unfinished", "unfinished-origin": predecessor := gen if predecessor == "" { predecessor = hostclaim.RootGeneration } c.Holder.Generation = "next" c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "next", Predecessor: predecessor, Region: "ie", PredecessorRegion: "ie", Mode: "cold"}} case "gap": model.BareDelete("data-test", cont.Key) model.BareDelete("data-test", dbVersionKey(base, 0, 12, 12)) put(0, 13, 13) case "missing-pin": if err := model.DeleteVersion(t.Context(), "data-test", root.Key, root.VersionID); err != nil { t.Fatal(err) } case "partial-list": model.PageSize, model.FailListAfterPages = 1, 1 case "disagreement": c.Holder.Generation = "unknown" case "bad-header": model.Put("data-test", dbVersionKey(base, 9, 1, 20), []byte("broken successor")) } writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, c) if mode == "holder-change" || mode == "op-change" { fx.srv.cfg.DBVersions = doorOpenHook{VersionedStore: model, open: func(ctx context.Context, bucket, key, version string) (io.ReadCloser, error) { rc, err := model.OpenVersion(ctx, bucket, key, version) next := cloneDoorClaim(c) if mode == "holder-change" { next.Holder.Session++ } else { next.Op.Exec = "changed" } writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, next) return rc, err }} } status, h, body := rawReq(t, "GET", fx.url(dbdoor.ListPath("bab", "titogo", dbTestHost, 9)), fx.member, "") if mode != "origin" && mode != "published" && mode != "unfinished" && mode != "unfinished-origin" { if status != 409 || h.Get("Retry-After") != "1" || strings.Contains(body, "files") || strings.Contains(body, "instances/") || strings.Contains(body, "genselect:") { t.Fatalf("refusal: %d %v %s", status, h, body) } return } if status != 200 { t.Fatalf("list: %d %s", status, body) } var list dbdoor.ListResponse if err := json.Unmarshal([]byte(body), &list); err != nil { t.Fatal(err) } if list.Protocol != 2 || len(list.Files) != 2 { t.Fatalf("list = %+v", list) } for i, want := range []genselect.File{root, cont} { f := list.Files[i] handle, err := verifyDBHandle(fx.srv.dbHandleKey, f.Handle, time.Now()) if err != nil || handle.Key != want.Key || handle.VersionID != want.VersionID || handle.Size != want.Size || f.Size != want.Size || f.Level != want.Level || f.MinTXID != want.MinTXID || f.MaxTXID != want.MaxTXID { t.Fatalf("file %d: %+v %+v %v; want %+v", i, f, handle, err, want) } model.Put("data-test", want.Key, []byte("overwritten after listing")) req, err := http.NewRequest("GET", fx.url(dbdoor.GetPath("bab", "titogo", dbTestHost, f.Level, f.MinTXID, f.MaxTXID)), nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer "+fx.member) req.Header.Set(dbdoor.HandleHeader, f.Handle) req.Header.Set("Range", "bytes=2-7") res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } got, err := io.ReadAll(res.Body) res.Body.Close() if err != nil || res.StatusCode != 206 || !bytes.Equal(got, doorLTXHeader(t, want.MinTXID, want.MaxTXID)[2:8]) || res.Header.Get(dbdoor.HandleHeader) != f.Handle { t.Fatalf("get: %d %x %v", res.StatusCode, got, err) } if err := model.DeleteVersion(t.Context(), "data-test", want.Key, want.VersionID); err != nil { t.Fatal(err) } res, err = http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } res.Body.Close() if res.StatusCode != 404 { t.Fatalf("missing version fell back: %d", res.StatusCode) } } if fx.opens.Load() != 0 { t.Fatal("opened unversioned replica") } }) } }