carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package seeds
2
3import (
4 "errors"
5 "strings"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/registry"
9)
10
11func 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
36type failingReader struct{}
37
38func (failingReader) Read([]byte) (int, error) { return 0, errors.New("no entropy") }
39
40func 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.
76func 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.
104func 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
117func 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
151func 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).
178func TestClosedGateIsTheRegistrysClosedGate(t *testing.T) {
179 if ClosedGate != registry.ImportGateClosed {
180 t.Fatalf("seeds.ClosedGate = %q, registry.ImportGateClosed = %q", ClosedGate, registry.ImportGateClosed)
181 }
182}
183