| 1 | package seeds |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | ) |
| 10 | |
| 11 | func TestNewIDShape(t *testing.T) { |
| 12 | a, err := NewID() |
| 13 | if err != nil { |
| 14 | t.Fatal(err) |
| 15 | } |
| 16 | b, _ := NewID() |
| 17 | if !ValidID(a) || !ValidID(b) || a == b { |
| 18 | t.Fatalf("ids %q and %q: want two distinct valid ids", a, b) |
| 19 | } |
| 20 | if !strings.HasPrefix(a, "sd_") || len(a) != 29 { |
| 21 | t.Fatalf("id %q: want sd_ and 26 characters", a) |
| 22 | } |
| 23 | for _, bad := range []string{"", "sd_", "mv_aaaaaaaaaaaaaaaaaaaaaaaaaa", "sd_AAAAAAAAAAAAAAAAAAAAAAAAAA", "sd_aaaaaaaaaaaaaaaaaaaaaaaaa1", "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa/x"} { |
| 24 | if ValidID(bad) { |
| 25 | t.Errorf("ValidID(%q) = true", bad) |
| 26 | } |
| 27 | } |
| 28 | old := randReader |
| 29 | randReader = failingReader{} |
| 30 | defer func() { randReader = old }() |
| 31 | if _, err := NewID(); err == nil { |
| 32 | t.Error("NewID with a failing random source: want an error") |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | type failingReader struct{} |
| 37 | |
| 38 | func (failingReader) Read([]byte) (int, error) { return 0, errors.New("no entropy") } |
| 39 | |
| 40 | func TestSnapshotKeysRoundTrip(t *testing.T) { |
| 41 | id, _ := NewID() |
| 42 | key := SnapshotKey("canary.shop.example.com", id) |
| 43 | if key != "seeds/canary.shop.example.com/"+id+"/snapshot.db" { |
| 44 | t.Fatalf("key = %q", key) |
| 45 | } |
| 46 | if SnapshotPrefix("canary.shop.example.com", id) != "seeds/canary.shop.example.com/"+id+"/" { |
| 47 | t.Fatalf("prefix = %q", SnapshotPrefix("canary.shop.example.com", id)) |
| 48 | } |
| 49 | if HostPrefix("canary.shop.example.com") != "seeds/canary.shop.example.com/" { |
| 50 | t.Fatalf("host prefix = %q", HostPrefix("canary.shop.example.com")) |
| 51 | } |
| 52 | host, got, ok := ParseSnapshotKey(key) |
| 53 | if !ok || host != "canary.shop.example.com" || got != id { |
| 54 | t.Fatalf("ParseSnapshotKey(%q) = %q, %q, %v", key, host, got, ok) |
| 55 | } |
| 56 | for _, bad := range []string{ |
| 57 | "seeds/canary.shop.example.com/" + id + "/other.db", |
| 58 | "seeds/canary.shop.example.com/" + id + "/x/snapshot.db", |
| 59 | "seeds/canary.shop.example.com/not-an-id/snapshot.db", |
| 60 | "instances/canary.shop.example.com/" + id + "/snapshot.db", |
| 61 | "seeds//" + id + "/snapshot.db", |
| 62 | "seeds/../" + id + "/snapshot.db", |
| 63 | "seeds/Shop.Example.com/" + id + "/snapshot.db", |
| 64 | "seeds/" + id + "/snapshot.db", |
| 65 | "", |
| 66 | } { |
| 67 | if _, _, ok := ParseSnapshotKey(bad); ok { |
| 68 | t.Errorf("ParseSnapshotKey(%q) parsed", bad) |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // A key builder is handed a host or id from a request or a record. An |
| 74 | // invalid one must never produce a key that leaves seeds/<host>/<id>/, or a |
| 75 | // prefix that names more than one seed. |
| 76 | func TestKeyBuildersContainHostileInput(t *testing.T) { |
| 77 | id, _ := NewID() |
| 78 | const host = "shop.example.com" |
| 79 | if err := Check(host, id); err != nil { |
| 80 | t.Fatalf("Check of a valid pair: %v", err) |
| 81 | } |
| 82 | hosts := []string{"", "..", "../x", "a/b", "a/../b", "a\\b", "Shop.example.com", "shop.example.com/", ".", "a..b", "shop.example.com\x00", "*", "a b"} |
| 83 | ids := []string{"", ".", "..", "../" + id, id + "/..", id + "/", "sd_x", strings.ToUpper(id)} |
| 84 | for _, h := range hosts { |
| 85 | if !errors.Is(Check(h, id), ErrBadHost) { |
| 86 | t.Errorf("Check(%q, id) = %v; want ErrBadHost", h, Check(h, id)) |
| 87 | } |
| 88 | for _, k := range []string{HostPrefix(h), SnapshotPrefix(h, id), SnapshotKey(h, id)} { |
| 89 | assertContained(t, k) |
| 90 | } |
| 91 | } |
| 92 | for _, i := range ids { |
| 93 | if !errors.Is(Check(host, i), ErrBadID) { |
| 94 | t.Errorf("Check(host, %q) = %v; want ErrBadID", i, Check(host, i)) |
| 95 | } |
| 96 | for _, k := range []string{SnapshotPrefix(host, i), SnapshotKey(host, i)} { |
| 97 | assertContained(t, k) |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // assertContained: the key is under seeds/, carries no dot-dot or backslash, |
| 103 | // names no real host or seed, and does not parse as a snapshot. |
| 104 | func assertContained(t *testing.T, k string) { |
| 105 | t.Helper() |
| 106 | if !strings.HasPrefix(k, Prefix+invalidHost+"/") { |
| 107 | t.Errorf("key %q: want a fail-closed key under %s%s/", k, Prefix, invalidHost) |
| 108 | } |
| 109 | if strings.Contains(k, "..") || strings.Contains(k, "\\") || strings.Contains(k, "\x00") { |
| 110 | t.Errorf("key %q: has a traversal or control character", k) |
| 111 | } |
| 112 | if _, _, ok := ParseSnapshotKey(k); ok { |
| 113 | t.Errorf("key %q parsed as a snapshot", k) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestVerifyToken(t *testing.T) { |
| 118 | tok, hash, err := NewVerifyToken() |
| 119 | if err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | if len(tok) != 43 || len(hash) != 64 { |
| 123 | t.Fatalf("token %d chars, hash %d chars: want 43 and 64", len(tok), len(hash)) |
| 124 | } |
| 125 | if HashToken(tok) != hash || !TokenMatches(hash, tok) { |
| 126 | t.Fatal("the token does not match its own hash") |
| 127 | } |
| 128 | if TokenMatches(hash, tok+"x") || TokenMatches(hash, "") { |
| 129 | t.Error("a wrong or empty token matched") |
| 130 | } |
| 131 | if TokenMatches(ClosedGate, tok) || TokenMatches(ClosedGate, ClosedGate) || TokenMatches("", "") { |
| 132 | t.Error("the closed gate or an empty hash matched a token") |
| 133 | } |
| 134 | // An uppercase spelling of the hash is not the hash. |
| 135 | if TokenMatches(strings.ToUpper(hash), tok) { |
| 136 | t.Error("an uppercase hash matched") |
| 137 | } |
| 138 | // Two tokens differ. |
| 139 | tok2, hash2, _ := NewVerifyToken() |
| 140 | if tok == tok2 || hash == hash2 || TokenMatches(hash, tok2) { |
| 141 | t.Error("two minted tokens collided or cross-matched") |
| 142 | } |
| 143 | old := randReader |
| 144 | randReader = failingReader{} |
| 145 | defer func() { randReader = old }() |
| 146 | if _, _, err := NewVerifyToken(); err == nil { |
| 147 | t.Error("NewVerifyToken with a failing random source: want an error") |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | func TestRowSeedEncoding(t *testing.T) { |
| 152 | id, _ := NewID() |
| 153 | r := RowSeed{ID: id, Kind: KindReplica, SHA256: strings.Repeat("a", 64), Bytes: 4096, State: StateReady} |
| 154 | got, err := ParseRowSeed(r.Encode()) |
| 155 | if err != nil || got == nil || *got != r { |
| 156 | t.Fatalf("round trip = %+v, %v", got, err) |
| 157 | } |
| 158 | if got, err := ParseRowSeed(""); got != nil || err != nil { |
| 159 | t.Fatalf("empty column = %+v, %v; want nil, nil", got, err) |
| 160 | } |
| 161 | if _, err := ParseRowSeed(`{"id":"nope","state":"ready"}`); err == nil { |
| 162 | t.Error("a malformed id parsed") |
| 163 | } |
| 164 | if _, err := ParseRowSeed(`not json`); err == nil { |
| 165 | t.Error("malformed json parsed") |
| 166 | } |
| 167 | for state, want := range map[string]bool{StateReady: true, StateSwapped: true, StatePlaced: true, |
| 168 | StateReserving: false, StateCommitted: false, StateAborted: false, StateRefused: false} { |
| 169 | if (RowSeed{State: state}).Placeable() != want { |
| 170 | t.Errorf("Placeable(%s) != %v", state, want) |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | // The edge refuses a row whose import gate is the registry's closed value, |
| 176 | // and the console and the box name it as seeds.ClosedGate: the two must |
| 177 | // be one value (seeds may import registry; registry does not import seeds). |
| 178 | func TestClosedGateIsTheRegistrysClosedGate(t *testing.T) { |
| 179 | if ClosedGate != registry.ImportGateClosed { |
| 180 | t.Fatalf("seeds.ClosedGate = %q, registry.ImportGateClosed = %q", ClosedGate, registry.ImportGateClosed) |
| 181 | } |
| 182 | } |
| 183 | |