A very experimental PLC implementation which uses BFT consensus for decentralization
1package httpapi
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "testing"
11 "time"
12
13 cmtlog "github.com/cometbft/cometbft/libs/log"
14 "github.com/did-method-plc/go-didplc"
15 "github.com/stretchr/testify/require"
16 "tangled.org/gbl08ma.com/didplcbft/config"
17 "tangled.org/gbl08ma.com/didplcbft/plc"
18 "tangled.org/gbl08ma.com/didplcbft/testutil"
19 "tangled.org/gbl08ma.com/didplcbft/transaction"
20 "tangled.org/gbl08ma.com/didplcbft/types"
21)
22
23var testLogger = cmtlog.NewNopLogger()
24
25// MockReadPLC is a mock implementation of the ReadPLC interface for testing.
26type MockReadPLC struct {
27 shouldReturnError bool
28 errorType string
29}
30
31func (m *MockReadPLC) ValidateOperation(ctx context.Context, readTx transaction.Read, did string, opBytes []byte) error {
32 if m.shouldReturnError {
33 switch m.errorType {
34 case "notfound":
35 return plc.ErrDIDNotFound
36 case "gone":
37 return plc.ErrDIDGone
38 }
39 return fmt.Errorf("internal error")
40 }
41 return nil
42}
43
44func (m *MockReadPLC) Resolve(ctx context.Context, readTx transaction.Read, did string) (didplc.Doc, error) {
45 if m.shouldReturnError {
46 switch m.errorType {
47 case "notfound":
48 return didplc.Doc{}, plc.ErrDIDNotFound
49 case "gone":
50 return didplc.Doc{}, plc.ErrDIDGone
51 }
52 return didplc.Doc{}, fmt.Errorf("internal error")
53 }
54 return didplc.Doc{
55 ID: "did:plc:test",
56 }, nil
57}
58
59func (m *MockReadPLC) OperationLog(ctx context.Context, readTx transaction.Read, did string) ([]didplc.OpEnum, error) {
60 if m.shouldReturnError {
61 if m.errorType == "notfound" {
62 return []didplc.OpEnum{}, plc.ErrDIDNotFound
63 }
64 return []didplc.OpEnum{}, fmt.Errorf("internal error")
65 }
66 return []didplc.OpEnum{}, nil
67}
68
69func (m *MockReadPLC) AuditLog(ctx context.Context, readTx transaction.Read, did string) ([]didplc.LogEntry, error) {
70 if m.shouldReturnError {
71 if m.errorType == "notfound" {
72 return []didplc.LogEntry{}, plc.ErrDIDNotFound
73 }
74 return []didplc.LogEntry{}, fmt.Errorf("internal error")
75 }
76 return []didplc.LogEntry{}, nil
77}
78
79func (m *MockReadPLC) LastOperation(ctx context.Context, readTx transaction.Read, did string) (didplc.OpEnum, error) {
80 if m.shouldReturnError {
81 if m.errorType == "notfound" {
82 return didplc.OpEnum{}, plc.ErrDIDNotFound
83 }
84 return didplc.OpEnum{}, fmt.Errorf("internal error")
85 }
86 return didplc.OpEnum{}, nil
87}
88
89func (m *MockReadPLC) Data(ctx context.Context, readTx transaction.Read, did string) (didplc.RegularOp, error) {
90 if m.shouldReturnError {
91 switch m.errorType {
92 case "notfound":
93 return didplc.RegularOp{}, plc.ErrDIDNotFound
94 case "gone":
95 return didplc.RegularOp{}, plc.ErrDIDGone
96 }
97 return didplc.RegularOp{}, fmt.Errorf("internal error")
98 }
99 return didplc.RegularOp{}, nil
100}
101
102func (m *MockReadPLC) Export(ctx context.Context, readTx transaction.Read, after uint64, count int) ([]types.SequencedLogEntry, error) {
103 if m.shouldReturnError {
104 return []types.SequencedLogEntry{}, fmt.Errorf("internal error")
105 }
106 return []types.SequencedLogEntry{}, nil
107}
108
109func TestServer(t *testing.T) {
110 mockPLC := &MockReadPLC{}
111
112 txFactory, _, _ := testutil.NewTestTxFactory(t)
113
114 // newTestServer creates a new server instance for testing with the given MockReadPLC
115 newTestServer := func(t *testing.T, plc *MockReadPLC) *Server {
116 t.Helper()
117 cfg := &config.PLCConfig{
118 ListenAddress: "tcp://127.0.0.1:8080",
119 Pprof: false,
120 MaxStreamingExportCursorAge: 7 * 24 * time.Hour,
121 ResponseTimeout: 15 * time.Second,
122 }
123 server, err := NewServer(testLogger, txFactory, plc, nil, nil, cfg)
124 require.NoError(t, err)
125 return server
126 }
127
128 t.Run("Test Resolve DID", func(t *testing.T) {
129 server := newTestServer(t, mockPLC)
130
131 req, err := http.NewRequest("GET", "/did:plc:test", nil)
132 require.NoError(t, err)
133
134 rr := httptest.NewRecorder()
135 server.router.ServeHTTP(rr, req)
136
137 require.Equal(t, http.StatusOK, rr.Code)
138 require.Contains(t, rr.Body.String(), "did:plc:test")
139 })
140
141 t.Run("Test Resolve DID Not Found", func(t *testing.T) {
142 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"}
143 server := newTestServer(t, mockPLC)
144
145 req, err := http.NewRequest("GET", "/did:plc:test", nil)
146 require.NoError(t, err)
147
148 rr := httptest.NewRecorder()
149 server.router.ServeHTTP(rr, req)
150
151 require.Equal(t, http.StatusNotFound, rr.Code)
152 require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test")
153 })
154
155 t.Run("Test Resolve DID Gone", func(t *testing.T) {
156 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "gone"}
157 server := newTestServer(t, mockPLC)
158
159 req, err := http.NewRequest("GET", "/did:plc:test", nil)
160 require.NoError(t, err)
161
162 rr := httptest.NewRecorder()
163 server.router.ServeHTTP(rr, req)
164
165 require.Equal(t, http.StatusGone, rr.Code)
166 require.Contains(t, rr.Body.String(), "DID not available: did:plc:test")
167 })
168
169 t.Run("Test Resolve DID Internal Error", func(t *testing.T) {
170 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"}
171 server := newTestServer(t, mockPLC)
172
173 req, err := http.NewRequest("GET", "/did:plc:test", nil)
174 require.NoError(t, err)
175
176 rr := httptest.NewRecorder()
177 server.router.ServeHTTP(rr, req)
178
179 require.Equal(t, http.StatusInternalServerError, rr.Code)
180 require.Contains(t, rr.Body.String(), "Internal server error")
181 })
182
183 t.Run("Test Create PLC Operation", func(t *testing.T) {
184 server := newTestServer(t, mockPLC)
185
186 op := map[string]interface{}{
187 "type": "plc_operation",
188 "rotationKeys": []string{"did:key:test"},
189 "verificationMethods": map[string]string{"atproto": "did:key:test"},
190 "alsoKnownAs": []string{"at://test"},
191 "services": map[string]interface{}{"atproto_pds": map[string]string{"type": "AtprotoPersonalDataServer", "endpoint": "https://test.com"}},
192 "prev": nil,
193 "sig": "test",
194 }
195 opBytes, _ := json.Marshal(op)
196
197 req, err := http.NewRequest("POST", "/did:plc:test", bytes.NewBuffer(opBytes))
198 require.NoError(t, err)
199
200 rr := httptest.NewRecorder()
201 server.router.ServeHTTP(rr, req)
202
203 require.Equal(t, http.StatusOK, rr.Code)
204 })
205
206 t.Run("Test Get PLC Log", func(t *testing.T) {
207 server := newTestServer(t, mockPLC)
208
209 req, err := http.NewRequest("GET", "/did:plc:test/log", nil)
210 require.NoError(t, err)
211
212 rr := httptest.NewRecorder()
213 server.router.ServeHTTP(rr, req)
214
215 require.Equal(t, http.StatusOK, rr.Code)
216 })
217
218 t.Run("Test Get PLC Log Not Found", func(t *testing.T) {
219 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"}
220 server := newTestServer(t, mockPLC)
221
222 req, err := http.NewRequest("GET", "/did:plc:test/log", nil)
223 require.NoError(t, err)
224
225 rr := httptest.NewRecorder()
226 server.router.ServeHTTP(rr, req)
227
228 require.Equal(t, http.StatusNotFound, rr.Code)
229 require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test")
230 })
231
232 t.Run("Test Get PLC Audit Log", func(t *testing.T) {
233 server := newTestServer(t, mockPLC)
234
235 req, err := http.NewRequest("GET", "/did:plc:test/log/audit", nil)
236 require.NoError(t, err)
237
238 rr := httptest.NewRecorder()
239 server.router.ServeHTTP(rr, req)
240
241 require.Equal(t, http.StatusOK, rr.Code)
242 })
243
244 t.Run("Test Get Last Operation", func(t *testing.T) {
245 server := newTestServer(t, mockPLC)
246
247 req, err := http.NewRequest("GET", "/did:plc:test/log/last", nil)
248 require.NoError(t, err)
249
250 rr := httptest.NewRecorder()
251 server.router.ServeHTTP(rr, req)
252
253 require.Equal(t, http.StatusOK, rr.Code)
254 })
255
256 t.Run("Test Get Last Operation Internal Error", func(t *testing.T) {
257 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"}
258 server := newTestServer(t, mockPLC)
259
260 req, err := http.NewRequest("GET", "/did:plc:test/log/last", nil)
261 require.NoError(t, err)
262
263 rr := httptest.NewRecorder()
264 server.router.ServeHTTP(rr, req)
265
266 require.Equal(t, http.StatusInternalServerError, rr.Code)
267 require.Contains(t, rr.Body.String(), "Internal server error")
268 })
269
270 t.Run("Test Get PLC Data", func(t *testing.T) {
271 server := newTestServer(t, mockPLC)
272
273 req, err := http.NewRequest("GET", "/did:plc:test/data", nil)
274 require.NoError(t, err)
275
276 rr := httptest.NewRecorder()
277 server.router.ServeHTTP(rr, req)
278
279 require.Equal(t, http.StatusOK, rr.Code)
280 })
281
282 t.Run("Test Get PLC Data Not Found", func(t *testing.T) {
283 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"}
284 server := newTestServer(t, mockPLC)
285
286 req, err := http.NewRequest("GET", "/did:plc:test/data", nil)
287 require.NoError(t, err)
288
289 rr := httptest.NewRecorder()
290 server.router.ServeHTTP(rr, req)
291
292 require.Equal(t, http.StatusNotFound, rr.Code)
293 require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test")
294 })
295
296 t.Run("Test Export", func(t *testing.T) {
297 server := newTestServer(t, mockPLC)
298
299 req, err := http.NewRequest("GET", "/export?count=10", nil)
300 require.NoError(t, err)
301
302 rr := httptest.NewRecorder()
303 server.router.ServeHTTP(rr, req)
304
305 require.Equal(t, http.StatusOK, rr.Code)
306 })
307
308 t.Run("Test Export Internal Error", func(t *testing.T) {
309 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"}
310 server := newTestServer(t, mockPLC)
311
312 req, err := http.NewRequest("GET", "/export?count=10", nil)
313 require.NoError(t, err)
314
315 rr := httptest.NewRecorder()
316 server.router.ServeHTTP(rr, req)
317
318 require.Equal(t, http.StatusInternalServerError, rr.Code)
319 require.Contains(t, rr.Body.String(), "Internal server error")
320 })
321}