Lewis: May this revision serve well! lewis@tangled.org
+411
-18
Diff
round #0
+279
appview/db/repos_rename_test.go
+279
appview/db/repos_rename_test.go
···
1
+
package db
2
+
3
+
import (
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
+
14
+
func 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
+
25
+
func 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
+
47
+
func 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
+
76
+
func 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
+
96
+
func 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
+
116
+
func 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
+
138
+
func 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
+
159
+
func 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
+
172
+
func 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
+
181
+
func 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
+
206
+
func 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
+
238
+
func 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
+
}
+50
appview/repo/rename_test.go
+50
appview/repo/rename_test.go
···
1
+
package repo
2
+
3
+
import (
4
+
"strings"
5
+
"testing"
6
+
)
7
+
8
+
func TestValidateRenameInput(t *testing.T) {
9
+
cases := []struct {
10
+
name string
11
+
currentName string
12
+
raw string
13
+
wantName string
14
+
wantErrSub string
15
+
}{
16
+
{"happy path", "foo", "bar", "bar", ""},
17
+
{"trims surrounding whitespace", "foo", " bar ", "bar", ""},
18
+
{"strips .git suffix", "foo", "bar.git", "bar", ""},
19
+
{"empty after trim", "foo", " ", "", "cannot be empty"},
20
+
{"raw empty", "foo", "", "", "cannot be empty"},
21
+
{"path traversal slash", "foo", "../bar", "", "invalid path"},
22
+
{"invalid character", "foo", "ba r", "", "alphanumeric"},
23
+
{"same name as current", "foo", "foo", "", "matches the current name"},
24
+
{"case-only diff is not a no-op", "foo", "Foo", "Foo", ""},
25
+
{"strip-git collides with current", "foo", "foo.git", "", "matches the current name"},
26
+
}
27
+
for _, tc := range cases {
28
+
t.Run(tc.name, func(t *testing.T) {
29
+
got, err := validateRenameInput(tc.currentName, tc.raw)
30
+
if tc.wantErrSub == "" {
31
+
if err != nil {
32
+
t.Fatalf("err = %v, want nil", err)
33
+
}
34
+
if got != tc.wantName {
35
+
t.Errorf("name = %q, want %q", got, tc.wantName)
36
+
}
37
+
return
38
+
}
39
+
if err == nil {
40
+
t.Fatalf("err = nil, want error containing %q", tc.wantErrSub)
41
+
}
42
+
if got != "" {
43
+
t.Errorf("name = %q, want empty on error", got)
44
+
}
45
+
if !strings.Contains(err.Error(), tc.wantErrSub) {
46
+
t.Errorf("err = %q, want substring %q", err.Error(), tc.wantErrSub)
47
+
}
48
+
})
49
+
}
50
+
}
+62
-10
spindle/ingester.go
+62
-10
spindle/ingester.go
···
5
5
"encoding/json"
6
6
"errors"
7
7
"fmt"
8
+
"strings"
8
9
"time"
9
10
10
11
"tangled.org/core/api/tangled"
···
154
155
}
155
156
156
157
domain := s.cfg.Server.Hostname
158
+
rkey := e.Commit.RKey
157
159
158
160
// no spindle configured for this repo
159
161
if record.Spindle == nil {
160
-
l.Info("no spindle configured", "name", record.Name)
162
+
l.Info("no spindle configured", "rkey", rkey)
161
163
return nil
162
164
}
163
165
164
166
// this repo did not want this spindle
165
167
if *record.Spindle != domain {
166
-
l.Info("different spindle configured", "name", record.Name, "spindle", *record.Spindle, "domain", domain)
168
+
l.Info("different spindle configured", "rkey", rkey, "spindle", *record.Spindle, "domain", domain)
167
169
return nil
168
170
}
169
171
170
172
// add this repo to the watch list
171
-
if err := s.db.AddRepo(record.Knot, did, record.Name); err != nil {
173
+
if err := s.db.AddRepo(record.Knot, did, rkey); err != nil {
172
174
l.Error("failed to add repo", "error", err)
173
175
return fmt.Errorf("failed to add repo: %w", err)
174
176
}
175
177
176
-
didSlashRepo, err := securejoin.SecureJoin(did, record.Name)
178
+
didSlashRepo, err := securejoin.SecureJoin(did, rkey)
177
179
if err != nil {
178
180
return err
179
181
}
···
228
230
var rbacResource string
229
231
var ownerDid string
230
232
switch {
231
-
case record.Repo != nil:
232
-
repoAt, parseErr := syntax.ParseATURI(*record.Repo)
233
+
case strings.HasPrefix(record.Repo, "did:"):
234
+
resolvedOwner, repoName, lookupErr := s.resolveRepoDid(ctx, e.Did, record.Repo)
235
+
if lookupErr != nil {
236
+
return fmt.Errorf("unknown repo DID %s: %w", record.Repo, lookupErr)
237
+
}
238
+
ownerDid = resolvedOwner
239
+
rbacResource, _ = securejoin.SecureJoin(ownerDid, repoName)
240
+
241
+
case strings.Contains(record.Repo, "/"):
242
+
repoAt, parseErr := syntax.ParseATURI(record.Repo)
233
243
if parseErr != nil {
234
-
l.Info("rejecting record, invalid repoAt", "repoAt", *record.Repo)
244
+
l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo)
235
245
return nil
236
246
}
237
247
···
249
259
return getErr
250
260
}
251
261
252
-
repo := resp.Value.Val.(*tangled.Repo)
253
-
rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repo.Name)
262
+
if _, ok := resp.Value.Val.(*tangled.Repo); !ok {
263
+
return fmt.Errorf("record at %s is not a tangled.Repo", repoAt)
264
+
}
265
+
rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repoAt.RecordKey().String())
254
266
ownerDid = owner.DID.String()
255
267
256
268
default:
257
-
l.Info("rejecting collaborator record without repo at-uri (spindle RBAC keyed by owner/name)")
269
+
l.Info("rejecting collaborator record with unrecognized repo format", "repo", record.Repo)
258
270
return nil
259
271
}
260
272
···
272
284
return nil
273
285
}
274
286
287
+
func (s *Spindle) resolveRepoDid(ctx context.Context, ownerDid string, repoDid string) (string, string, error) {
288
+
owner, resolveErr := s.res.ResolveIdent(ctx, ownerDid)
289
+
if resolveErr != nil || owner.Handle.IsInvalidHandle() {
290
+
return "", "", fmt.Errorf("failed to resolve owner %s: %w", ownerDid, resolveErr)
291
+
}
292
+
293
+
xrpcc := xrpc.Client{
294
+
Host: owner.PDSEndpoint(),
295
+
}
296
+
297
+
cursor := ""
298
+
for {
299
+
resp, listErr := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoNSID, cursor, 100, ownerDid, false)
300
+
if listErr != nil {
301
+
return "", "", fmt.Errorf("failed to list repo records for %s: %w", ownerDid, listErr)
302
+
}
303
+
304
+
for _, r := range resp.Records {
305
+
if r == nil {
306
+
continue
307
+
}
308
+
repo, ok := r.Value.Val.(*tangled.Repo)
309
+
if !ok {
310
+
continue
311
+
}
312
+
if repo.RepoDid != nil && *repo.RepoDid == repoDid {
313
+
rkey := r.Uri[strings.LastIndex(r.Uri, "/")+1:]
314
+
return ownerDid, rkey, nil
315
+
}
316
+
}
317
+
318
+
if resp.Cursor == nil || *resp.Cursor == "" {
319
+
break
320
+
}
321
+
cursor = *resp.Cursor
322
+
}
323
+
324
+
return "", "", fmt.Errorf("repo DID %s not found in records for %s", repoDid, ownerDid)
325
+
}
326
+
275
327
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error {
276
328
l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators")
277
329
+5
-2
spindle/xrpc/add_secret.go
+5
-2
spindle/xrpc/add_secret.go
···
61
61
return
62
62
}
63
63
64
-
repo := resp.Value.Val.(*tangled.Repo)
65
-
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
64
+
if _, ok := resp.Value.Val.(*tangled.Repo); !ok {
65
+
fail(xrpcerr.RepoNotFoundError)
66
+
return
67
+
}
68
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String())
66
69
if err != nil {
67
70
fail(xrpcerr.GenericError(err))
68
71
return
+5
-2
spindle/xrpc/list_secrets.go
+5
-2
spindle/xrpc/list_secrets.go
···
56
56
return
57
57
}
58
58
59
-
repo := resp.Value.Val.(*tangled.Repo)
60
-
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
59
+
if _, ok := resp.Value.Val.(*tangled.Repo); !ok {
60
+
fail(xrpcerr.RepoNotFoundError)
61
+
return
62
+
}
63
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String())
61
64
if err != nil {
62
65
fail(xrpcerr.GenericError(err))
63
66
return
+5
-2
spindle/xrpc/pipeline_cancel_pipeline.go
+5
-2
spindle/xrpc/pipeline_cancel_pipeline.go
···
66
66
return
67
67
}
68
68
69
-
repo := resp.Value.Val.(*tangled.Repo)
70
-
didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
69
+
if _, ok := resp.Value.Val.(*tangled.Repo); !ok {
70
+
fail(xrpcerr.RepoNotFoundError)
71
+
return
72
+
}
73
+
didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String())
71
74
if err != nil {
72
75
fail(xrpcerr.GenericError(err))
73
76
return
+5
-2
spindle/xrpc/remove_secret.go
+5
-2
spindle/xrpc/remove_secret.go
···
55
55
return
56
56
}
57
57
58
-
repo := resp.Value.Val.(*tangled.Repo)
59
-
didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
58
+
if _, ok := resp.Value.Val.(*tangled.Repo); !ok {
59
+
fail(xrpcerr.RepoNotFoundError)
60
+
return
61
+
}
62
+
didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String())
60
63
if err != nil {
61
64
fail(xrpcerr.GenericError(err))
62
65
return
History
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
knotmirror,spindle,appview: propagate repo rename across services
Lewis: May this revision serve well! <lewis@tangled.org>
failed to check merge status: invalid xrpc request