Monorepo for Tangled
tangled.org
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "path/filepath"
8 "testing"
9
10 "tangled.org/core/appview/models"
11 "tangled.org/core/orm"
12)
13
14func newTestDB(t *testing.T) *DB {
15 t.Helper()
16 path := filepath.Join(t.TempDir(), "test.db")
17 d, err := Make(context.Background(), path)
18 if err != nil {
19 t.Fatalf("Make: %v", err)
20 }
21 t.Cleanup(func() { d.Close() })
22 return d
23}
24
25func seedRepo(t *testing.T, d *DB, did, knot, name, rkey, repoDid string) *models.Repo {
26 t.Helper()
27 tx, err := d.Begin()
28 if err != nil {
29 t.Fatalf("Begin: %v", err)
30 }
31 repo := &models.Repo{
32 Did: did,
33 Name: name,
34 Knot: knot,
35 Rkey: rkey,
36 RepoDid: repoDid,
37 }
38 if err := AddRepo(tx, repo); err != nil {
39 t.Fatalf("AddRepo: %v", err)
40 }
41 if err := tx.Commit(); err != nil {
42 t.Fatalf("Commit: %v", err)
43 }
44 return repo
45}
46
47func TestRenameRepo_HappyPath(t *testing.T) {
48 d := newTestDB(t)
49 seedRepo(t, d, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
50
51 tx, err := d.Begin()
52 if err != nil {
53 t.Fatalf("Begin: %v", err)
54 }
55 defer tx.Rollback()
56
57 if err := RenameRepo(tx, "did:plc:akshay", "foo", "bar", "Bar"); err != nil {
58 t.Fatalf("RenameRepo: %v", err)
59 }
60 if err := tx.Commit(); err != nil {
61 t.Fatalf("Commit: %v", err)
62 }
63
64 got, err := GetRepoByDid(d, "did:plc:repo1")
65 if err != nil {
66 t.Fatalf("GetRepoByDid: %v", err)
67 }
68 if got.Rkey != "bar" {
69 t.Errorf("rkey = %q, want %q", got.Rkey, "bar")
70 }
71 if got.Name != "Bar" {
72 t.Errorf("name = %q, want %q", got.Name, "Bar")
73 }
74}
75
76func TestUpdateRepoDisplayName_HappyPath(t *testing.T) {
77 d := newTestDB(t)
78 seedRepo(t, d, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1")
79
80 if err := UpdateRepoDisplayName(d, "did:plc:akshay", "foo", "Foo"); err != nil {
81 t.Fatalf("UpdateRepoDisplayName: %v", err)
82 }
83
84 got, err := GetRepoByDid(d, "did:plc:repo1")
85 if err != nil {
86 t.Fatalf("GetRepoByDid: %v", err)
87 }
88 if got.Name != "Foo" {
89 t.Errorf("name = %q, want %q", got.Name, "Foo")
90 }
91 if got.Rkey != "foo" {
92 t.Errorf("rkey should be unchanged but got %q, want %q", got.Rkey, "foo")
93 }
94}
95
96func TestRecordAndLookupRepoRename(t *testing.T) {
97 d := newTestDB(t)
98 seedRepo(t, d, "did:plc:akshay", "knot.example", "bar", "rkey1", "did:plc:repo1")
99
100 if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:repo1"); err != nil {
101 t.Fatalf("RecordRepoRename: %v", err)
102 }
103
104 repo, err := LookupRepoRename(d, "did:plc:akshay", "foo")
105 if err != nil {
106 t.Fatalf("LookupRepoRename: %v", err)
107 }
108 if repo.RepoDid != "did:plc:repo1" {
109 t.Errorf("repoDid = %q, want %q", repo.RepoDid, "did:plc:repo1")
110 }
111 if repo.Name != "bar" {
112 t.Errorf("name = %q, want %q", repo.Name, "bar")
113 }
114}
115
116func TestLookupRepoRename_MultipleOldNamesResolveToCurrent(t *testing.T) {
117 d := newTestDB(t)
118 seedRepo(t, d, "did:plc:akshay", "knot.example", "baz", "baz", "did:plc:repo1")
119
120 if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:repo1"); err != nil {
121 t.Fatalf("record foo: %v", err)
122 }
123 if err := RecordRepoRename(d, "did:plc:akshay", "bar", "did:plc:repo1"); err != nil {
124 t.Fatalf("record bar: %v", err)
125 }
126
127 for _, oldName := range []string{"foo", "bar"} {
128 repo, err := LookupRepoRename(d, "did:plc:akshay", oldName)
129 if err != nil {
130 t.Fatalf("lookup %q: %v", oldName, err)
131 }
132 if repo.Name != "baz" {
133 t.Errorf("lookup %q: name = %q, want %q", oldName, repo.Name, "baz")
134 }
135 }
136}
137
138func TestRecordRepoRename_UpsertRefreshesTarget(t *testing.T) {
139 d := newTestDB(t)
140 seedRepo(t, d, "did:plc:akshay", "knot.example", "current", "rkey1", "did:plc:repo1")
141 seedRepo(t, d, "did:plc:akshay", "knot.example", "other", "rkey2", "did:plc:repo2")
142
143 if err := RecordRepoRename(d, "did:plc:akshay", "shared", "did:plc:repo1"); err != nil {
144 t.Fatalf("first record: %v", err)
145 }
146 if err := RecordRepoRename(d, "did:plc:akshay", "shared", "did:plc:repo2"); err != nil {
147 t.Fatalf("second record: %v", err)
148 }
149
150 repo, err := LookupRepoRename(d, "did:plc:akshay", "shared")
151 if err != nil {
152 t.Fatalf("LookupRepoRename: %v", err)
153 }
154 if repo.RepoDid != "did:plc:repo2" {
155 t.Errorf("latest record should win: repoDid = %q, want %q", repo.RepoDid, "did:plc:repo2")
156 }
157}
158
159func TestLookupRepoRename_StaleSelfHeal(t *testing.T) {
160 d := newTestDB(t)
161
162 if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:ghost"); err != nil {
163 t.Fatalf("RecordRepoRename: %v", err)
164 }
165
166 _, err := LookupRepoRename(d, "did:plc:akshay", "foo")
167 if !errors.Is(err, sql.ErrNoRows) {
168 t.Errorf("target should be gone and fall through to 404: err = %v, want sql.ErrNoRows", err)
169 }
170}
171
172func TestLookupRepoRename_NoRow(t *testing.T) {
173 d := newTestDB(t)
174
175 _, err := LookupRepoRename(d, "did:plc:akshay", "nothing")
176 if !errors.Is(err, sql.ErrNoRows) {
177 t.Errorf("err = %v, want sql.ErrNoRows", err)
178 }
179}
180
181func TestDuplicateRkeyUnderSameDID_Rejected(t *testing.T) {
182 d := newTestDB(t)
183 seedRepo(t, d, "did:plc:akshay", "knot.example", "myrepo", "myrepo", "did:plc:repo1")
184
185 tx, err := d.Begin()
186 if err != nil {
187 t.Fatalf("Begin: %v", err)
188 }
189 defer tx.Rollback()
190
191 err = AddRepo(tx, &models.Repo{
192 Did: "did:plc:akshay",
193 Name: "myrepo",
194 Knot: "knot.example",
195 Rkey: "myrepo",
196 RepoDid: "did:plc:repo2",
197 })
198 if err == nil {
199 t.Fatal("expected unique violation for duplicate (did, rkey), got nil")
200 }
201 if !orm.IsUniqueViolation(err) {
202 t.Errorf("err = %v, want unique violation", err)
203 }
204}
205
206func TestRenameRepo_OldRkeyRowGone(t *testing.T) {
207 d := newTestDB(t)
208 seedRepo(t, d, "did:plc:akshay", "knot.example", "old", "old", "did:plc:repo1")
209
210 tx, err := d.Begin()
211 if err != nil {
212 t.Fatalf("Begin: %v", err)
213 }
214 defer tx.Rollback()
215
216 if err := RenameRepo(tx, "did:plc:akshay", "old", "new", "New"); err != nil {
217 t.Fatalf("RenameRepo: %v", err)
218 }
219 if err := tx.Commit(); err != nil {
220 t.Fatalf("Commit: %v", err)
221 }
222
223 got, err := GetRepoByDid(d, "did:plc:repo1")
224 if err != nil {
225 t.Fatalf("GetRepoByDid: %v", err)
226 }
227 if got.Rkey != "new" {
228 t.Errorf("rkey = %q, want %q", got.Rkey, "new")
229 }
230
231 var dummy int
232 err = d.QueryRow(`select 1 from repos where did = ? and rkey = ?`, "did:plc:akshay", "old").Scan(&dummy)
233 if !errors.Is(err, sql.ErrNoRows) {
234 t.Errorf("old rkey row should be gone, got err = %v", err)
235 }
236}
237
238func TestRenameRepo_PipelineRenamed(t *testing.T) {
239 d := newTestDB(t)
240 seedRepo(t, d, "did:plc:akshay", "knot.example", "old", "old", "did:plc:repo1")
241
242 if _, err := d.Exec(
243 `insert into triggers (kind) values (?)`, "push",
244 ); err != nil {
245 t.Fatalf("seed trigger: %v", err)
246 }
247 if _, err := d.Exec(
248 `insert into pipelines (rkey, knot, repo_owner, repo_name, sha, trigger_id, repo_did)
249 values (?, ?, ?, ?, ?, ?, ?)`,
250 "pipe1", "knot.example", "did:plc:akshay", "old",
251 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1, "did:plc:repo1",
252 ); err != nil {
253 t.Fatalf("seed pipeline: %v", err)
254 }
255
256 tx, err := d.Begin()
257 if err != nil {
258 t.Fatalf("Begin: %v", err)
259 }
260 defer tx.Rollback()
261
262 if err := RenameRepo(tx, "did:plc:akshay", "old", "new", "New"); err != nil {
263 t.Fatalf("RenameRepo: %v", err)
264 }
265 if err := tx.Commit(); err != nil {
266 t.Fatalf("Commit: %v", err)
267 }
268
269 var repoName string
270 if err := d.QueryRow(
271 `select repo_name from pipelines where repo_owner = ? and rkey = ?`,
272 "did:plc:akshay", "pipe1",
273 ).Scan(&repoName); err != nil {
274 t.Fatalf("query pipeline: %v", err)
275 }
276 if repoName != "new" {
277 t.Errorf("pipeline repo_name = %q, want %q", repoName, "new")
278 }
279}