tinyrelayfile
tinyrelay/internal/catalog/catalog_test.go/

internal/catalog/catalog_test.go raw
1package catalog
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "strings"
8 "sync"
9 "testing"
10)
11
12const testOwner = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
13
14func TestCreateAndLifecycle(t *testing.T) {
15 cat := openTestCatalog(t)
16 tenant, err := cat.Create(context.Background(), CreateOptions{Name: "Example", Owner: testOwner, Template: "public"})
17 if err != nil {
18 t.Fatal(err)
19 }
20 if tenant.Status != StatusCreating || tenant.Owner != testOwner {
21 t.Fatalf("unexpected tenant: %+v", tenant)
22 }
23 if tenant.Paths.Database != filepath.Join(tenant.Paths.Root, "relay.db") {
24 t.Fatalf("database path is not stable: %+v", tenant.Paths)
25 }
26 if tenant.Paths.Blobs == tenant.Paths.Git || !strings.HasSuffix(tenant.Paths.Blobs, "blobs") {
27 t.Fatalf("unexpected tenant paths: %+v", tenant.Paths)
28 }
29 if err := cat.MarkReady(context.Background(), tenant.ID); err != nil {
30 t.Fatal(err)
31 }
32 ready, err := cat.GetByID(context.Background(), tenant.ID)
33 if err != nil {
34 t.Fatal(err)
35 }
36 if ready.Status != StatusReady {
37 t.Fatalf("status = %q, want ready", ready.Status)
38 }
39 if err := cat.Disable(context.Background(), tenant.ID); err != nil {
40 t.Fatal(err)
41 }
42 if err := cat.Enable(context.Background(), tenant.ID); err != nil {
43 t.Fatal(err)
44 }
45 newOwner := "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"
46 if err := cat.TransferOwner(context.Background(), tenant.ID, testOwner, newOwner); err != nil {
47 t.Fatal(err)
48 }
49 updated, err := cat.GetByID(context.Background(), tenant.ID)
50 if err != nil || updated.Owner != newOwner {
51 t.Fatalf("owner after transfer = %q, err=%v", updated.Owner, err)
52 }
53}
54
55func TestSQLiteDurabilityPragmas(t *testing.T) {
56 cat := openTestCatalog(t)
57 var synchronous string
58 if err := cat.db.QueryRow("PRAGMA synchronous").Scan(&synchronous); err != nil {
59 t.Fatal(err)
60 }
61 if synchronous != "2" {
62 t.Fatalf("synchronous = %q, want FULL (2)", synchronous)
63 }
64}
65
66func TestCreateRejectsInvalidOwnerAndUnsafeName(t *testing.T) {
67 cat := openTestCatalog(t)
68 for _, tc := range []struct {
69 name string
70 owner string
71 }{
72 {"ok", "bad"},
73 {"../escape", testOwner},
74 {"", testOwner},
75 } {
76 _, err := cat.Create(context.Background(), CreateOptions{Name: tc.name, Owner: tc.owner})
77 if err == nil {
78 t.Errorf("Create(%q, %q) succeeded", tc.name, tc.owner)
79 }
80 }
81}
82
83func TestNameAndHostUniquenessAndNormalization(t *testing.T) {
84 cat := openTestCatalog(t)
85 one, err := cat.Create(context.Background(), CreateOptions{Name: "First", Owner: testOwner})
86 if err != nil {
87 t.Fatal(err)
88 }
89 if _, err := cat.Create(context.Background(), CreateOptions{Name: " first ", Owner: testOwner}); !errors.Is(err, ErrNameTaken) {
90 t.Fatalf("duplicate name error = %v, want ErrNameTaken", err)
91 }
92 if err := cat.SetHost(context.Background(), one.ID, "Relay.Example.COM."); err != nil {
93 t.Fatal(err)
94 }
95 resolved, err := cat.ResolveHost(context.Background(), "relay.example.com")
96 if err != nil {
97 t.Fatal(err)
98 }
99 if resolved.ID != one.ID {
100 t.Fatalf("resolved %q, want %q", resolved.ID, one.ID)
101 }
102 two, err := cat.Create(context.Background(), CreateOptions{Name: "Second", Owner: testOwner})
103 if err != nil {
104 t.Fatal(err)
105 }
106 if err := cat.SetHost(context.Background(), two.ID, "relay.example.com"); !errors.Is(err, ErrHostTaken) {
107 t.Fatalf("duplicate host error = %v, want ErrHostTaken", err)
108 }
109 if err := cat.ClearHost(context.Background(), one.ID); err != nil {
110 t.Fatal(err)
111 }
112 if _, err := cat.ResolveHost(context.Background(), "relay.example.com"); !errors.Is(err, ErrNotFound) {
113 t.Fatalf("cleared host error = %v, want ErrNotFound", err)
114 }
115}
116
117func TestHostMappingsResolveAndRemainUnique(t *testing.T) {
118 cat := openTestCatalog(t)
119 one, err := cat.Create(context.Background(), CreateOptions{Name: "First", Owner: testOwner})
120 if err != nil {
121 t.Fatal(err)
122 }
123 two, err := cat.Create(context.Background(), CreateOptions{Name: "Second", Owner: testOwner})
124 if err != nil {
125 t.Fatal(err)
126 }
127 mapping, err := cat.AddHost(context.Background(), one.ID, "Relay.Alice.Example.", "site-label")
128 if err != nil {
129 t.Fatal(err)
130 }
131 if mapping.Host != "relay.alice.example" || mapping.Site != "site-label" {
132 t.Fatalf("mapping = %+v", mapping)
133 }
134 resolved, err := cat.ResolveHost(context.Background(), "relay.alice.example")
135 if err != nil || resolved.ID != one.ID {
136 t.Fatalf("resolved = %+v, err=%v", resolved, err)
137 }
138 if _, err := cat.AddHost(context.Background(), two.ID, "relay.alice.example", ""); !errors.Is(err, ErrHostTaken) {
139 t.Fatalf("duplicate alias error = %v", err)
140 }
141 if err := cat.SetHost(context.Background(), two.ID, "relay.alice.example"); !errors.Is(err, ErrHostTaken) {
142 t.Fatalf("primary/alias collision = %v", err)
143 }
144 updated, err := cat.SetHostSite(context.Background(), one.ID, "relay.alice.example", "other")
145 if err != nil || updated.Site != "other" {
146 t.Fatalf("updated = %+v, err=%v", updated, err)
147 }
148 if err := cat.RemoveHost(context.Background(), one.ID, "relay.alice.example"); err != nil {
149 t.Fatal(err)
150 }
151 if _, err := cat.ResolveHostMapping(context.Background(), "relay.alice.example"); !errors.Is(err, ErrHostMappingNotFound) {
152 t.Fatalf("removed alias error = %v", err)
153 }
154}
155
156func TestConcurrentCreatesDoNotDuplicateNames(t *testing.T) {
157 cat := openTestCatalog(t)
158 const workers = 12
159 var wg sync.WaitGroup
160 results := make(chan error, workers)
161 for range workers {
162 wg.Add(1)
163 go func() {
164 defer wg.Done()
165 _, err := cat.Create(context.Background(), CreateOptions{Name: "same", Owner: testOwner})
166 results <- err
167 }()
168 }
169 wg.Wait()
170 close(results)
171 created := 0
172 for err := range results {
173 if err == nil {
174 created++
175 } else if !errors.Is(err, ErrNameTaken) {
176 t.Errorf("unexpected concurrent create error: %v", err)
177 }
178 }
179 if created != 1 {
180 t.Fatalf("created %d tenants, want 1", created)
181 }
182}
183
184func TestRestartKeepsCreatingTenant(t *testing.T) {
185 root := t.TempDir()
186 one, err := Open(context.Background(), root)
187 if err != nil {
188 t.Fatal(err)
189 }
190 tenant, err := one.Create(context.Background(), CreateOptions{Name: "recover", Owner: testOwner})
191 if err != nil {
192 t.Fatal(err)
193 }
194 if err := one.Close(); err != nil {
195 t.Fatal(err)
196 }
197 two, err := Open(context.Background(), root)
198 if err != nil {
199 t.Fatal(err)
200 }
201 recovered, err := two.GetByID(context.Background(), tenant.ID)
202 if err != nil {
203 t.Fatal(err)
204 }
205 if recovered.Status != StatusCreating {
206 t.Fatalf("status after restart = %q", recovered.Status)
207 }
208}
209
210func openTestCatalog(t *testing.T) *Catalog {
211 t.Helper()
212 cat, err := Open(context.Background(), t.TempDir())
213 if err != nil {
214 t.Fatal(err)
215 }
216 t.Cleanup(func() { _ = cat.Close() })
217 return cat
218}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs