carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package domains
2
3import (
4 "context"
5 "errors"
6 "strconv"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12// TestSetClaimPathsRoundTrip: the map written is the map read back.
13func TestSetClaimPathsRoundTrip(t *testing.T) {
14 ctx := context.Background()
15 s := release.NewDirStore(t.TempDir())
16 if _, _, err := AttachClaim(ctx, s, Claim{
17 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
18 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
19 }); err != nil {
20 t.Fatal(err)
21 }
22 changed, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
23 map[string]string{"paulca": "go.tito.io"})
24 if err != nil {
25 t.Fatal(err)
26 }
27 if !changed {
28 t.Fatal("changed = false on the first write of a paths map")
29 }
30 c, err := GetClaim(ctx, s, "ti.to")
31 if err != nil {
32 t.Fatal(err)
33 }
34 if c.Paths["paulca"] != "go.tito.io" {
35 t.Fatalf("paths = %v, want paulca -> go.tito.io", c.Paths)
36 }
37}
38
39// TestSetClaimPathsIsWholesale: publishing is a REPLACE, not a merge. Tito
40// regenerates the whole map from its own database, so an account it no longer
41// lists must disappear — that is how a rollback reaches the edge.
42func TestSetClaimPathsIsWholesale(t *testing.T) {
43 ctx := context.Background()
44 s := release.NewDirStore(t.TempDir())
45 if _, _, err := AttachClaim(ctx, s, Claim{
46 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
47 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
48 }); err != nil {
49 t.Fatal(err)
50 }
51 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
52 map[string]string{"paulca": "go.tito.io", "bigcorp": "go.tito.io"}); err != nil {
53 t.Fatal(err)
54 }
55 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
56 map[string]string{"paulca": "go.tito.io"}); err != nil {
57 t.Fatal(err)
58 }
59 c, err := GetClaim(ctx, s, "ti.to")
60 if err != nil {
61 t.Fatal(err)
62 }
63 if _, still := c.Paths["bigcorp"]; still {
64 t.Fatal("bigcorp survived a republish that omitted it — publishing must replace, not merge")
65 }
66}
67
68// TestSetClaimPathsEmptyClearsTheMap: the final cutover. When every account
69// is across, Tito repoints the host's own route and publishes an empty map.
70func TestSetClaimPathsEmptyClearsTheMap(t *testing.T) {
71 ctx := context.Background()
72 s := release.NewDirStore(t.TempDir())
73 if _, _, err := AttachClaim(ctx, s, Claim{
74 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
75 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
76 }); err != nil {
77 t.Fatal(err)
78 }
79 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
80 map[string]string{"paulca": "go.tito.io"}); err != nil {
81 t.Fatal(err)
82 }
83 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic", nil); err != nil {
84 t.Fatal(err)
85 }
86 c, err := GetClaim(ctx, s, "ti.to")
87 if err != nil {
88 t.Fatal(err)
89 }
90 if len(c.Paths) != 0 {
91 t.Fatalf("paths = %v, want empty after publishing an empty map", c.Paths)
92 }
93}
94
95// TestSetClaimPathsIsOwnerGated: a caller who does not hold the claim gets
96// the same neutral ErrHostUnavailable the attach door uses — no existence
97// oracle, byte-identical to the answer for a claim that does not exist.
98func TestSetClaimPathsIsOwnerGated(t *testing.T) {
99 ctx := context.Background()
100 s := release.NewDirStore(t.TempDir())
101 if _, _, err := AttachClaim(ctx, s, Claim{
102 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
103 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
104 }); err != nil {
105 t.Fatal(err)
106 }
107 if _, err := SetClaimPaths(ctx, s, "ti.to", "someone-else", "classic",
108 map[string]string{"paulca": "evil.example"}); !errors.Is(err, ErrHostUnavailable) {
109 t.Fatalf("err = %v, want ErrHostUnavailable for a non-owner", err)
110 }
111 if _, err := SetClaimPaths(ctx, s, "never-claimed.example", "tito", "classic",
112 map[string]string{"a": "b.example.com"}); !errors.Is(err, ErrHostUnavailable) {
113 t.Fatalf("err = %v, want the identical ErrHostUnavailable for an absent claim", err)
114 }
115}
116
117// TestSetClaimPathsRejectsUnnormalisedTarget: a target that is not a
118// normalised hostname (no interior dot, wrong case, trailing dot, ...) is
119// refused with ErrBadPathTarget — this is an owned, existing claim, so the
120// failure can only be about the target shape, not about who is asking or
121// whether the host exists.
122//
123// Asserted through the SENTINEL, not merely as "non-nil and not
124// ErrHostUnavailable". The API door answers this class as the CALLER's error
125// (422, naming the entry) rather than as a store failure (502, "try again"),
126// and errors.Is is the only thing that tells it which it has — so a lost %w
127// wrap would silently start telling the publishing platform to retry a body
128// that will fail identically forever. A non-nil check passes either way and
129// would not catch it.
130func TestSetClaimPathsRejectsUnnormalisedTarget(t *testing.T) {
131 ctx := context.Background()
132 s := release.NewDirStore(t.TempDir())
133 if _, _, err := AttachClaim(ctx, s, Claim{
134 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
135 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
136 }); err != nil {
137 t.Fatal(err)
138 }
139 // Every shape NormalizeAliasHost turns away, and each one is something a
140 // generated map could plausibly carry.
141 for _, target := range []string{"notahostname", "GO.tito.io", "go.tito.io.", "", "go tito io"} {
142 _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
143 map[string]string{"paulca": target})
144 if !errors.Is(err, ErrBadPathTarget) {
145 t.Fatalf("target %q: err = %v, want ErrBadPathTarget", target, err)
146 }
147 if errors.Is(err, ErrHostUnavailable) {
148 t.Fatalf("target %q: err = %v, want the target-shape error, not ErrHostUnavailable", target, err)
149 }
150 // Refused WHOLE: a map with one bad entry stores none of it.
151 if c, err := GetClaim(ctx, s, "ti.to"); err != nil {
152 t.Fatal(err)
153 } else if len(c.Paths) != 0 {
154 t.Fatalf("target %q: claim paths = %v, want none", target, c.Paths)
155 }
156 }
157}
158
159// TestSetClaimPathsUnchangedIsNoWrite: republishing an identical map must not
160// burn a bucket write. Tito republishes wholesale as a repair, so the no-op
161// case is the common one.
162func TestSetClaimPathsUnchangedIsNoWrite(t *testing.T) {
163 ctx := context.Background()
164 s := release.NewDirStore(t.TempDir())
165 if _, _, err := AttachClaim(ctx, s, Claim{
166 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
167 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
168 }); err != nil {
169 t.Fatal(err)
170 }
171 m := map[string]string{"paulca": "go.tito.io"}
172 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic", m); err != nil {
173 t.Fatal(err)
174 }
175 changed, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic", m)
176 if err != nil {
177 t.Fatal(err)
178 }
179 if changed {
180 t.Fatal("changed = true for an identical republish — a no-op must not write")
181 }
182}
183
184// TestSetClaimPathsRefusesOversizeMap: the cap is enforced at the door, so an
185// oversize map is refused whole rather than silently truncated into the
186// bucket. (The reconcile pass caps again, defensively, for hand-written
187// claims — see Task 3.)
188func TestSetClaimPathsRefusesOversizeMap(t *testing.T) {
189 ctx := context.Background()
190 s := release.NewDirStore(t.TempDir())
191 if _, _, err := AttachClaim(ctx, s, Claim{
192 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
193 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
194 }); err != nil {
195 t.Fatal(err)
196 }
197 big := make(map[string]string, MaxPathsPerClaim+1)
198 for i := 0; i <= MaxPathsPerClaim; i++ {
199 big["acct"+strconv.Itoa(i)] = "go.tito.io"
200 }
201 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic", big); !errors.Is(err, ErrTooManyPaths) {
202 t.Fatalf("err = %v, want ErrTooManyPaths", err)
203 }
204}
205
206// TestSetClaimPathsRejectsMalformedPrefixes: a prefix is a bare segment. A
207// leading slash, an embedded slash, or an empty key would each produce a row
208// the edge could never match, so they are refused at the door rather than
209// stored as dead weight.
210func TestSetClaimPathsRejectsMalformedPrefixes(t *testing.T) {
211 ctx := context.Background()
212 s := release.NewDirStore(t.TempDir())
213 if _, _, err := AttachClaim(ctx, s, Claim{
214 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
215 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
216 }); err != nil {
217 t.Fatal(err)
218 }
219 for _, bad := range []string{"/paulca", "paulca/awesomeconf", "", "with space"} {
220 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
221 map[string]string{bad: "go.tito.io"}); !errors.Is(err, ErrBadPathPrefix) {
222 t.Fatalf("prefix %q: err = %v, want ErrBadPathPrefix", bad, err)
223 }
224 }
225}
226
227// TestAttachConvergePreservesPaths: a repoint must not strip the prefix map.
228func TestAttachConvergePreservesPaths(t *testing.T) {
229 ctx := context.Background()
230 s := release.NewDirStore(t.TempDir())
231 if _, _, err := AttachClaim(ctx, s, Claim{
232 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
233 Route: "ti.to", CreatedAt: "2026-09-03T00:00:00Z",
234 }); err != nil {
235 t.Fatal(err)
236 }
237 if _, err := SetClaimPaths(ctx, s, "ti.to", "tito", "classic",
238 map[string]string{"paulca": "go.tito.io"}); err != nil {
239 t.Fatal(err)
240 }
241 if _, _, err := AttachClaim(ctx, s, Claim{
242 Schema: 1, Host: "ti.to", Account: "tito", App: "classic",
243 Route: "ti-to-v2.tito.io", CreatedAt: "2026-09-03T00:00:00Z",
244 }); err != nil {
245 t.Fatal(err)
246 }
247 c, err := GetClaim(ctx, s, "ti.to")
248 if err != nil {
249 t.Fatal(err)
250 }
251 if c.Paths["paulca"] != "go.tito.io" {
252 t.Fatalf("paths = %v, want the map preserved across a converge repoint", c.Paths)
253 }
254}
255