package seeds import ( "errors" "strings" "testing" "github.com/carlosframework/platform/internal/registry" ) func TestNewIDShape(t *testing.T) { a, err := NewID() if err != nil { t.Fatal(err) } b, _ := NewID() if !ValidID(a) || !ValidID(b) || a == b { t.Fatalf("ids %q and %q: want two distinct valid ids", a, b) } if !strings.HasPrefix(a, "sd_") || len(a) != 29 { t.Fatalf("id %q: want sd_ and 26 characters", a) } for _, bad := range []string{"", "sd_", "mv_aaaaaaaaaaaaaaaaaaaaaaaaaa", "sd_AAAAAAAAAAAAAAAAAAAAAAAAAA", "sd_aaaaaaaaaaaaaaaaaaaaaaaaa1", "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa/x"} { if ValidID(bad) { t.Errorf("ValidID(%q) = true", bad) } } old := randReader randReader = failingReader{} defer func() { randReader = old }() if _, err := NewID(); err == nil { t.Error("NewID with a failing random source: want an error") } } type failingReader struct{} func (failingReader) Read([]byte) (int, error) { return 0, errors.New("no entropy") } func TestSnapshotKeysRoundTrip(t *testing.T) { id, _ := NewID() key := SnapshotKey("canary.shop.example.com", id) if key != "seeds/canary.shop.example.com/"+id+"/snapshot.db" { t.Fatalf("key = %q", key) } if SnapshotPrefix("canary.shop.example.com", id) != "seeds/canary.shop.example.com/"+id+"/" { t.Fatalf("prefix = %q", SnapshotPrefix("canary.shop.example.com", id)) } if HostPrefix("canary.shop.example.com") != "seeds/canary.shop.example.com/" { t.Fatalf("host prefix = %q", HostPrefix("canary.shop.example.com")) } host, got, ok := ParseSnapshotKey(key) if !ok || host != "canary.shop.example.com" || got != id { t.Fatalf("ParseSnapshotKey(%q) = %q, %q, %v", key, host, got, ok) } for _, bad := range []string{ "seeds/canary.shop.example.com/" + id + "/other.db", "seeds/canary.shop.example.com/" + id + "/x/snapshot.db", "seeds/canary.shop.example.com/not-an-id/snapshot.db", "instances/canary.shop.example.com/" + id + "/snapshot.db", "seeds//" + id + "/snapshot.db", "seeds/../" + id + "/snapshot.db", "seeds/Shop.Example.com/" + id + "/snapshot.db", "seeds/" + id + "/snapshot.db", "", } { if _, _, ok := ParseSnapshotKey(bad); ok { t.Errorf("ParseSnapshotKey(%q) parsed", bad) } } } // A key builder is handed a host or id from a request or a record. An // invalid one must never produce a key that leaves seeds///, or a // prefix that names more than one seed. func TestKeyBuildersContainHostileInput(t *testing.T) { id, _ := NewID() const host = "shop.example.com" if err := Check(host, id); err != nil { t.Fatalf("Check of a valid pair: %v", err) } hosts := []string{"", "..", "../x", "a/b", "a/../b", "a\\b", "Shop.example.com", "shop.example.com/", ".", "a..b", "shop.example.com\x00", "*", "a b"} ids := []string{"", ".", "..", "../" + id, id + "/..", id + "/", "sd_x", strings.ToUpper(id)} for _, h := range hosts { if !errors.Is(Check(h, id), ErrBadHost) { t.Errorf("Check(%q, id) = %v; want ErrBadHost", h, Check(h, id)) } for _, k := range []string{HostPrefix(h), SnapshotPrefix(h, id), SnapshotKey(h, id)} { assertContained(t, k) } } for _, i := range ids { if !errors.Is(Check(host, i), ErrBadID) { t.Errorf("Check(host, %q) = %v; want ErrBadID", i, Check(host, i)) } for _, k := range []string{SnapshotPrefix(host, i), SnapshotKey(host, i)} { assertContained(t, k) } } } // assertContained: the key is under seeds/, carries no dot-dot or backslash, // names no real host or seed, and does not parse as a snapshot. func assertContained(t *testing.T, k string) { t.Helper() if !strings.HasPrefix(k, Prefix+invalidHost+"/") { t.Errorf("key %q: want a fail-closed key under %s%s/", k, Prefix, invalidHost) } if strings.Contains(k, "..") || strings.Contains(k, "\\") || strings.Contains(k, "\x00") { t.Errorf("key %q: has a traversal or control character", k) } if _, _, ok := ParseSnapshotKey(k); ok { t.Errorf("key %q parsed as a snapshot", k) } } func TestVerifyToken(t *testing.T) { tok, hash, err := NewVerifyToken() if err != nil { t.Fatal(err) } if len(tok) != 43 || len(hash) != 64 { t.Fatalf("token %d chars, hash %d chars: want 43 and 64", len(tok), len(hash)) } if HashToken(tok) != hash || !TokenMatches(hash, tok) { t.Fatal("the token does not match its own hash") } if TokenMatches(hash, tok+"x") || TokenMatches(hash, "") { t.Error("a wrong or empty token matched") } if TokenMatches(ClosedGate, tok) || TokenMatches(ClosedGate, ClosedGate) || TokenMatches("", "") { t.Error("the closed gate or an empty hash matched a token") } // An uppercase spelling of the hash is not the hash. if TokenMatches(strings.ToUpper(hash), tok) { t.Error("an uppercase hash matched") } // Two tokens differ. tok2, hash2, _ := NewVerifyToken() if tok == tok2 || hash == hash2 || TokenMatches(hash, tok2) { t.Error("two minted tokens collided or cross-matched") } old := randReader randReader = failingReader{} defer func() { randReader = old }() if _, _, err := NewVerifyToken(); err == nil { t.Error("NewVerifyToken with a failing random source: want an error") } } func TestRowSeedEncoding(t *testing.T) { id, _ := NewID() r := RowSeed{ID: id, Kind: KindReplica, SHA256: strings.Repeat("a", 64), Bytes: 4096, State: StateReady} got, err := ParseRowSeed(r.Encode()) if err != nil || got == nil || *got != r { t.Fatalf("round trip = %+v, %v", got, err) } if got, err := ParseRowSeed(""); got != nil || err != nil { t.Fatalf("empty column = %+v, %v; want nil, nil", got, err) } if _, err := ParseRowSeed(`{"id":"nope","state":"ready"}`); err == nil { t.Error("a malformed id parsed") } if _, err := ParseRowSeed(`not json`); err == nil { t.Error("malformed json parsed") } for state, want := range map[string]bool{StateReady: true, StateSwapped: true, StatePlaced: true, StateReserving: false, StateCommitted: false, StateAborted: false, StateRefused: false} { if (RowSeed{State: state}).Placeable() != want { t.Errorf("Placeable(%s) != %v", state, want) } } } // The edge refuses a row whose import gate is the registry's closed value, // and the console and the box name it as seeds.ClosedGate: the two must // be one value (seeds may import registry; registry does not import seeds). func TestClosedGateIsTheRegistrysClosedGate(t *testing.T) { if ClosedGate != registry.ImportGateClosed { t.Fatalf("seeds.ClosedGate = %q, registry.ImportGateClosed = %q", ClosedGate, registry.ImportGateClosed) } }