package console import ( "bytes" "context" "database/sql" "encoding/json" "io" "net/http" "os" "path/filepath" "strings" "testing" "time" lsfile "github.com/benbjohnson/litestream/file" "github.com/carlosframework/platform/internal/dbdoor" "github.com/carlosframework/platform/internal/dbdoor/dbdoortest" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" "github.com/superfly/ltx" ) func TestDoorPlanUnnamedHomeRegion(t *testing.T) { for _, unfinished := range []bool{false, true} { t.Run(map[bool]string{false: "origin", true: "unfinished"}[unfinished], func(t *testing.T) { fx := dbFixture(t) model := enableDoorPlan(t, fx) fx.srv.cfg.HomeRegion = "" fx.srv.cfg.HibernateRegion = "eu-west-1" if fx.srv.cfg.Region != "" { t.Fatal("fixture has a placement") } if unfinished { obs, err := hostclaim.Read(t.Context(), fx.rel, dbTestHost) if err != nil { t.Fatal(err) } c := cloneDoorClaim(obs.Claim) c.Holder.Generation = "next" c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "next", Predecessor: hostclaim.RootGeneration, Region: "eu-west-1", PredecessorRegion: "eu-west-1", Mode: "cold"}} writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, c) } model.Put("data-test", dbVersionKey("instances/"+dbTestHost, 0, 1, 1), doorLTXHeader(t, 1, 1)) status, body := getRaw(t, fx.url(dbdoor.ListPath("bab", "titogo", dbTestHost, 0)), fx.member) if status != http.StatusOK { t.Fatalf("unnamed home listing: %d %s", status, body) } }) } } func TestDoorPlanRegionStartup(t *testing.T) { fx := dbFixture(t) for _, mode := range []string{"unresolved", "bucket-region", "named-home", "placement", "flag-off", "door-off"} { t.Run(mode, func(t *testing.T) { cfg := fx.srv.cfg cfg.HostClaim = true cfg.Region, cfg.HomeRegion, cfg.HibernateRegion = "", "", "" switch mode { case "bucket-region": cfg.HibernateRegion = "eu-west-1" case "named-home": cfg.HomeRegion = "ie" case "placement": cfg.Region = "uk" case "flag-off": cfg.HostClaim = false case "door-off": cfg.HibernateBucket = "" } st := fx.st if cfg.HostClaim { var err error st, err = OpenGated(filepath.Join(t.TempDir(), "gated.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) } _, err := NewServer(cfg, st) if mode == "unresolved" { if err == nil || !strings.Contains(err.Error(), "region") { t.Fatalf("startup without a region: %v", err) } } else if err != nil { t.Fatal(err) } }) } } func TestDoorPlanPinnedSnapshotCRC(t *testing.T) { for _, mode := range []string{"match", "mismatch", "zero", "historical", "successor"} { t.Run(mode, func(t *testing.T) { fx := dbFixture(t) fx.replicate(t, dbTestHost, 2) replica := lsfile.NewReplicaClient(filepath.Join(fx.root, "instances", dbTestHost)) var readers []io.Reader for _, f := range listFiles(t, replica, 0) { b, err := os.ReadFile(replica.LTXFilePath(0, f.MinTXID, f.MaxTXID)) if err != nil { t.Fatal(err) } readers = append(readers, bytes.NewReader(b)) } var snapshot bytes.Buffer compactor, err := ltx.NewCompactor(&snapshot, readers) if err != nil { t.Fatal(err) } compactor.HeaderFlags = ltx.HeaderFlagNoChecksum if err := compactor.Compact(t.Context()); err != nil { t.Fatal(err) } dec := ltx.NewDecoder(bytes.NewReader(snapshot.Bytes())) if err := dec.Verify(); err != nil { t.Fatal(err) } crc := uint64(dec.Trailer().FileChecksum) maxTXID := uint64(dec.Header().MaxTXID) model := enableDoorPlan(t, fx) obs, err := hostclaim.Read(t.Context(), fx.rel, dbTestHost) if err != nil { t.Fatal(err) } c := cloneDoorClaim(obs.Claim) c.Holder.Generation = "live" loc := c.Holder.Location key := dbVersionKey(loc+"/g-live", 9, 1, maxTXID) id := model.Put("data-test", key, snapshot.Bytes()) pin := genselect.Pin{SnapshotKey: key, SnapshotVersion: id, SnapshotTXID: maxTXID, SnapshotCRC64: crc} switch mode { case "mismatch": pin.SnapshotCRC64 ^= 1 case "zero": pin.SnapshotCRC64 = 0 case "historical": model.Put("data-test", key, []byte("late replacement")) case "successor": pin.SnapshotKey = dbVersionKey(loc+"/g-live", 9, 1, 1) pin.SnapshotTXID = 1 pin.SnapshotVersion = "retained-marker-only" pin.SnapshotCRC64 ^= 1 } marker := genselect.Marker{Op: "live", Host: dbTestHost, Incarnation: c.Holder.Incarnation, Box: "box", InstalledAt: time.Unix(1, 0).UTC(), Pin: pin} raw, err := json.Marshal(marker) if err != nil { t.Fatal(err) } model.Put("data-test", loc+"/generation.json", raw) writeDoorClaim(t, fx.srv, dbRequest{host: dbTestHost}, c) do := func(ctx context.Context, tail string, h http.Header) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, "GET", fx.url(dbdoor.Base("bab", "titogo", dbTestHost)+tail), nil) if err != nil { return nil, err } req.Header = h.Clone() req.Header.Set("Authorization", "Bearer "+fx.member) return http.DefaultClient.Do(req) } parent := t.TempDir() got, err := dbdoor.RestoreTo(t.Context(), dbdoor.NewClient(do), parent) if mode == "mismatch" || mode == "zero" { if got != nil { os.RemoveAll(got.Dir) t.Fatal("restored a valid snapshot whose CRC differs from the marker") } if err == nil || !strings.Contains(err.Error(), dbdoor.ErrCheckFailed.Error()) { t.Fatalf("want member-safe check refusal: %v", err) } entries, readErr := os.ReadDir(parent) if readErr != nil || len(entries) != 0 { t.Fatalf("failed restore left files: %v %v", entries, readErr) } } else { if err != nil { t.Fatal(err) } defer os.RemoveAll(got.Dir) db, err := sql.Open("sqlite", "file:"+got.Path+"?mode=ro") if err != nil { t.Fatal(err) } defer db.Close() var rows int if err := db.QueryRow("SELECT count(*) FROM t").Scan(&rows); err != nil || rows != 2*dbdoortest.Rows { t.Fatalf("rows %d: %v", rows, err) } } res, err := do(t.Context(), dbdoor.ListTail(9), http.Header{}) if err != nil { t.Fatal(err) } defer res.Body.Close() var list struct { Files []struct { Handle string SnapshotCRC64 *uint64 `json:"snapshot_crc64"` } } if err := json.NewDecoder(res.Body).Decode(&list); err != nil || len(list.Files) != 1 { t.Fatalf("listing: %+v %v", list, err) } h, err := verifyDBHandle(fx.srv.dbHandleKey, list.Files[0].Handle, time.Now()) if err != nil { t.Fatal(err) } raw, err = json.Marshal(h) if err != nil { t.Fatal(err) } var signed struct { SnapshotCRC64 *uint64 `json:"snapshot_crc64"` } if err := json.Unmarshal(raw, &signed); err != nil { t.Fatal(err) } for _, value := range []*uint64{list.Files[0].SnapshotCRC64, signed.SnapshotCRC64} { if mode == "successor" { if value != nil { t.Fatal("successor inherited old pin CRC") } } else if value == nil || *value != pin.SnapshotCRC64 { t.Fatalf("lost expected CRC: %v, want %d", value, pin.SnapshotCRC64) } } }) } }