forked from
tangled.org/core
Monorepo for Tangled
6.2 kB
223 lines
1package knotserver
2
3import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "sync"
8 "testing"
9
10 jsmodels "github.com/bluesky-social/jetstream/pkg/models"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/knotserver/config"
13 "tangled.org/core/knotserver/db"
14 "tangled.org/core/log"
15)
16
17type logRecord struct {
18 Level slog.Level
19 Msg string
20 Attrs map[string]any
21}
22
23type capturingHandler struct {
24 mu *sync.Mutex
25 records *[]logRecord
26 attrs []slog.Attr
27}
28
29func newCapturingHandler() *capturingHandler {
30 return &capturingHandler{
31 mu: &sync.Mutex{},
32 records: &[]logRecord{},
33 }
34}
35
36func (h *capturingHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
37
38func (h *capturingHandler) Handle(_ context.Context, r slog.Record) error {
39 rec := logRecord{Level: r.Level, Msg: r.Message, Attrs: map[string]any{}}
40 for _, a := range h.attrs {
41 rec.Attrs[a.Key] = a.Value.Any()
42 }
43 r.Attrs(func(a slog.Attr) bool {
44 rec.Attrs[a.Key] = a.Value.Any()
45 return true
46 })
47 h.mu.Lock()
48 *h.records = append(*h.records, rec)
49 h.mu.Unlock()
50 return nil
51}
52
53func (h *capturingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
54 merged := make([]slog.Attr, 0, len(h.attrs)+len(attrs))
55 merged = append(merged, h.attrs...)
56 merged = append(merged, attrs...)
57 return &capturingHandler{mu: h.mu, records: h.records, attrs: merged}
58}
59
60func (h *capturingHandler) WithGroup(string) slog.Handler {
61 panic("capturingHandler: WithGroup not supported")
62}
63
64func (h *capturingHandler) snapshot() []logRecord {
65 h.mu.Lock()
66 defer h.mu.Unlock()
67 out := make([]logRecord, len(*h.records))
68 copy(out, *h.records)
69 return out
70}
71
72func newProcessRepoFixture(t *testing.T) (*Knot, context.Context, *capturingHandler) {
73 t.Helper()
74 d := newTestKnotDB(t)
75 cap := newCapturingHandler()
76 l := slog.New(cap)
77 ctx := log.IntoContext(context.Background(), l)
78
79 c := &config.Config{
80 Server: config.Server{Hostname: "knot.example"},
81 }
82 return &Knot{
83 c: c,
84 db: d,
85 l: l,
86 }, ctx, cap
87}
88
89func repoEvent(t *testing.T, authorDid, rkey, rev string, record tangled.Repo, op string) *jsmodels.Event {
90 t.Helper()
91 raw, err := json.Marshal(record)
92 if err != nil {
93 t.Fatalf("marshal record: %v", err)
94 }
95 return &jsmodels.Event{
96 Did: authorDid,
97 Kind: jsmodels.EventKindCommit,
98 Commit: &jsmodels.Commit{
99 Operation: op,
100 Collection: tangled.RepoNSID,
101 RKey: rkey,
102 Rev: rev,
103 Record: raw,
104 },
105 }
106}
107
108func ptr(s string) *string { return &s }
109
110func TestProcessRepo_CreateRegistersAlias(t *testing.T) {
111 h, ctx, _ := newProcessRepoFixture(t)
112 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil {
113 t.Fatalf("StoreRepoKey: %v", err)
114 }
115
116 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaab", tangled.Repo{
117 Knot: "knot.example",
118 RepoDid: ptr("did:plc:repo1"),
119 }, jsmodels.CommitOperationCreate)
120 if err := h.processRepo(ctx, ev); err != nil {
121 t.Fatalf("processRepo: %v", err)
122 }
123
124 _, current, err := h.db.CurrentRkey("did:plc:repo1")
125 if err != nil {
126 t.Fatalf("CurrentRkey: %v", err)
127 }
128 if current != "bar" {
129 t.Errorf("current rkey = %q, want bar (highest rev alias)", current)
130 }
131
132 oldDid, err := h.db.GetRepoDid("did:plc:akshay", "foo")
133 if err != nil || oldDid != "did:plc:repo1" {
134 t.Errorf("old rkey foo should still resolve: got (%q, %v)", oldDid, err)
135 }
136}
137
138func TestProcessRepo_DeleteRemovesAlias(t *testing.T) {
139 h, ctx, _ := newProcessRepoFixture(t)
140 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil {
141 t.Fatalf("StoreRepoKey: %v", err)
142 }
143 if err := h.db.UpsertRepoAlias(db.RepoAlias{
144 OwnerDid: "did:plc:akshay", Rkey: "bar", RepoDid: "did:plc:repo1", Rev: "3laaaaaaaaaab",
145 }); err != nil {
146 t.Fatalf("UpsertRepoAlias: %v", err)
147 }
148
149 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaac", tangled.Repo{}, jsmodels.CommitOperationDelete)
150 if err := h.processRepo(ctx, ev); err != nil {
151 t.Fatalf("processRepo: %v", err)
152 }
153
154 if _, err := h.db.GetRepoDid("did:plc:akshay", "bar"); err == nil {
155 t.Errorf("bar alias should have been deleted")
156 }
157
158 _, current, _ := h.db.CurrentRkey("did:plc:repo1")
159 if current != "foo" {
160 t.Errorf("current rkey after delete = %q, want foo", current)
161 }
162}
163
164func TestProcessRepo_MalformedJSONReturnsError(t *testing.T) {
165 h, ctx, _ := newProcessRepoFixture(t)
166
167 ev := &jsmodels.Event{
168 Did: "did:plc:akshay",
169 Kind: jsmodels.EventKindCommit,
170 Commit: &jsmodels.Commit{
171 Operation: jsmodels.CommitOperationCreate,
172 Collection: tangled.RepoNSID,
173 RKey: "rkey1",
174 Record: []byte("{not valid json"),
175 },
176 }
177 if err := h.processRepo(ctx, ev); err == nil {
178 t.Fatalf("processRepo returned nil, want unmarshal error")
179 }
180}
181
182func TestProcessRepo_NotOwnedRejected(t *testing.T) {
183 h, ctx, _ := newProcessRepoFixture(t)
184 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil {
185 t.Fatalf("StoreRepoKey: %v", err)
186 }
187
188 ev := repoEvent(t, "did:plc:mallory", "pwned", "3laaaaaaaaaab", tangled.Repo{
189 Knot: "knot.example",
190 RepoDid: ptr("did:plc:repo1"),
191 }, jsmodels.CommitOperationCreate)
192 if err := h.processRepo(ctx, ev); err != nil {
193 t.Fatalf("processRepo: %v", err)
194 }
195
196 _, current, _ := h.db.CurrentRkey("did:plc:repo1")
197 if current != "foo" {
198 t.Errorf("current rkey = %q, want foo (mallory's event must be rejected)", current)
199 }
200 if _, err := h.db.GetRepoDid("did:plc:mallory", "pwned"); err == nil {
201 t.Errorf("mallory should not be able to register an alias on alice's repo")
202 }
203}
204
205func TestProcessRepo_WrongKnotIgnored(t *testing.T) {
206 h, ctx, _ := newProcessRepoFixture(t)
207 if err := h.db.StoreRepoKey("did:plc:repo1", []byte("k"), "did:plc:akshay", "foo"); err != nil {
208 t.Fatalf("StoreRepoKey: %v", err)
209 }
210
211 ev := repoEvent(t, "did:plc:akshay", "bar", "3laaaaaaaaaab", tangled.Repo{
212 Knot: "other.example",
213 RepoDid: ptr("did:plc:repo1"),
214 }, jsmodels.CommitOperationCreate)
215 if err := h.processRepo(ctx, ev); err != nil {
216 t.Fatalf("processRepo: %v", err)
217 }
218
219 _, current, _ := h.db.CurrentRkey("did:plc:repo1")
220 if current != "foo" {
221 t.Errorf("current rkey = %q, want foo (foreign-knot event must be ignored)", current)
222 }
223}