package console import ( "context" "errors" "net/http" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/dbdoor" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/provision" ) func TestResolveDBSource(t *testing.T) { fx, _ := instanceFixture(t, "code-db-source") if _, err := fx.st.CreateAppIn(OpsAccountID, "shop", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } m := provision.Manifest{Schema: 1, App: "shop", Domain: "shop.example.com", Channel: "stable", Regions: []string{"ie"}, Seed: []string{"seed"}} enableInstances(t, fx, "shop", m) fx.srv.cfg.Region = "ie" fx.srv.cfg.HibernatePrefix = "instances" acct, err := fx.st.AccountBySqid("bab") if err != nil { t.Fatal(err) } ctx := context.Background() put := func(host string, mut func(*provision.Record)) { rec := provision.Record{Schema: 1, Host: host, Slug: strings.Split(host, ".")[0], App: "shop", Account: "bab", Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC()} if mut != nil { mut(&rec) } if err := provision.CreateRecord(ctx, fx.rel, rec); err != nil { t.Fatal(err) } } put("acme.shop.example.com", nil) put("pooled.shop.example.com", func(r *provision.Record) { r.Pool = "shared" }) put("far.shop.example.com", func(r *provision.Record) { r.Region = "uk" }) put("addr.shop.example.com", func(r *provision.Record) { r.Addr = "127.0.0.1:9000" }) src, err := fx.srv.resolveDBSource(ctx, acct, "alice@keymail.dev", "shop", "acme.shop.example.com") if err != nil || src.prefix != "instances/acme.shop.example.com" || src.rec.Host != "acme.shop.example.com" { t.Fatalf("acme = %+v, %v", src, err) } for host, want := range map[string]error{ "nope.shop.example.com": os.ErrNotExist, "pooled.shop.example.com": ErrPooled, "far.shop.example.com": ErrResidency, "addr.shop.example.com": errSourceClass, "not a host": ErrBadHost, } { if _, err := fx.srv.resolveDBSource(ctx, acct, "alice@keymail.dev", "shop", host); !errors.Is(err, want) { t.Errorf("%s: err = %v, want %v", host, err, want) } } } // A claim-read failure is wrapped in errSourceClaim and also kept whole on // dbSource.claimErr: errors.Unwrap of a two-%w wrap is nil, and // dbClaimFailure needs the original to pick its answer. func TestResolveDBSourceClaimReadError(t *testing.T) { fx := dbFixture(t) fx.srv.cfg.HomeRegion = "ie" fx.srv.cfg.HostClaim = true fx.srv.cfg.DBVersions = leasetest.NewVersionedBucket() acct, err := fx.st.AccountBySqid("bab") if err != nil { t.Fatal(err) } app := titoManifest().App src, err := fx.srv.resolveDBSource(t.Context(), acct, "alice@keymail.dev", app, dbTestHost) if !errors.Is(err, errSourceClaim) { t.Fatalf("err = %v, want errSourceClaim", err) } if src.claimErr == nil || !errors.Is(src.claimErr, errDBNotAdopted) { t.Fatalf("claimErr = %v, want errDBNotAdopted", src.claimErr) } if errors.Unwrap(err) != nil { t.Fatalf("errors.Unwrap(err) = %v, expected nil for a two-%%w wrap", errors.Unwrap(err)) } // Through the door: the original error reaches dbClaimFailure, which // answers a missing adoption with a 409. A nil error would be a 502. status, out := getRaw(t, fx.url(dbdoor.ListPath("bab", app, dbTestHost, 0)), fx.member) if status != http.StatusConflict || !strings.Contains(string(out), errDBNotAdopted.Error()) { t.Fatalf("door answered %d %q, want 409 with %q", status, out, errDBNotAdopted) } }