v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/metazla/meta-core/internal/claim"
|
||||
"github.com/metazla/meta-core/internal/storage"
|
||||
)
|
||||
|
||||
func withVars(r *http.Request, vars map[string]string) *http.Request { return mux.SetURLVars(r, vars) }
|
||||
|
||||
// routedServer wires the real route table, so the test sees what clients see.
|
||||
func routedServer(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
s := &Server{storage: newTestStorage(t), router: mux.NewRouter()}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
}
|
||||
|
||||
func call(t *testing.T, s *Server, method, path, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
rr := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
// Every unsigned write route of v1.x answers 410 with a pointer to the claims.
|
||||
func TestUnsignedWritesAreGone(t *testing.T) {
|
||||
s := routedServer(t)
|
||||
for _, c := range []struct{ method, path string }{
|
||||
{"PUT", "/meta/bafy"}, {"PATCH", "/meta/bafy"}, {"DELETE", "/meta/bafy"},
|
||||
{"POST", "/meta/bafy/_add/genres"}, {"PUT", "/meta/bafy/title"}, {"DELETE", "/meta/bafy/title"},
|
||||
{"PUT", "/api/metadata/bafy"}, {"DELETE", "/api/metadata/bafy"}, {"PUT", "/api/metadata/bafy/property"},
|
||||
{"POST", "/api/metadata/batch"},
|
||||
} {
|
||||
rr := call(t, s, c.method, c.path, `{"title":"x"}`)
|
||||
if rr.Code != http.StatusGone || !strings.Contains(rr.Body.String(), `"use":"POST /api/claims"`) {
|
||||
t.Errorf("%s %s = %d %s", c.method, c.path, rr.Code, rr.Body)
|
||||
}
|
||||
}
|
||||
if rr := call(t, s, "PUT", "/api/kv/value", `{"key":"file:R/title","value":"x"}`); rr.Code != http.StatusConflict {
|
||||
t.Errorf("raw record write via /api/kv/value = %d", rr.Code)
|
||||
}
|
||||
if rr := call(t, s, "PUT", "/api/kv/value", `{"key":"btmagnet:abc","value":"magnet:"}`); rr.Code != http.StatusOK {
|
||||
t.Errorf("service key write = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostClaimsAndReadBack(t *testing.T) {
|
||||
s := routedServer(t)
|
||||
a, b := testSigner(t, 1), testSigner(t, 2)
|
||||
bad := signed(t, b, "bafyA", "year", "1999", 5)
|
||||
bad.Value = claim.Text("2001")
|
||||
body, _ := json.Marshal([]claim.Claim{
|
||||
signed(t, a, "bafyA", "title", "Matrix", 5),
|
||||
signed(t, b, "bafyA", "title", "Matrix", 6),
|
||||
bad,
|
||||
})
|
||||
rr := call(t, s, "POST", "/api/claims", string(body))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("POST /api/claims = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
var res []storage.ClaimResult
|
||||
_ = json.Unmarshal(rr.Body.Bytes(), &res)
|
||||
if len(res) != 3 || res[0].Status != "accepted" || res[1].Status != "accepted" || res[2].Status != "bad_signature" {
|
||||
t.Fatalf("statuses %+v", res)
|
||||
}
|
||||
|
||||
rr = call(t, s, "GET", "/api/meta/bafyA", "")
|
||||
if !strings.Contains(rr.Body.String(), `"title":"Matrix"`) {
|
||||
t.Fatalf("merged read = %s", rr.Body)
|
||||
}
|
||||
rr = call(t, s, "GET", "/api/meta/bafyA/claims", "")
|
||||
var cr ClaimsResponse
|
||||
_ = json.Unmarshal(rr.Body.Bytes(), &cr)
|
||||
if rr.Code != http.StatusOK || len(cr.Claims) != 2 {
|
||||
t.Fatalf("claims by cid = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
if cr.RID == "" || len(cr.CIDs) != 1 || cr.CIDs[0] != "bafyA" {
|
||||
t.Fatalf("claims envelope rid/cids = %q %v", cr.RID, cr.CIDs)
|
||||
}
|
||||
rr = call(t, s, "GET", "/api/metadata/"+cr.RID+"/claims?key=title&uid="+a.UID, "")
|
||||
var byRid ClaimsResponse
|
||||
_ = json.Unmarshal(rr.Body.Bytes(), &byRid)
|
||||
if rr.Code != http.StatusOK || len(byRid.Claims) != 1 || byRid.Claims[0].UID != a.UID || byRid.RID != cr.RID {
|
||||
t.Fatalf("claims by rid with key+uid = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
if rr := call(t, s, "GET", "/api/meta/bafyA/claims?key=nope", ""); !strings.Contains(rr.Body.String(), `"claims":[]`) {
|
||||
t.Fatalf("key filter = %s", rr.Body)
|
||||
}
|
||||
if rr := call(t, s, "GET", "/api/meta/bafyUnknown/claims", ""); rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("unknown cid claims = %d", rr.Code)
|
||||
}
|
||||
if rr := call(t, s, "GET", "/api/metadata/01NOSUCHRID/claims", ""); rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("unknown rid claims = %d", rr.Code)
|
||||
}
|
||||
|
||||
// ?as= overlays the viewer's own vote.
|
||||
me := testSigner(t, 9)
|
||||
storeClaims(t, s.storage, signed(t, me, "bafyA", "title", "My title", 7))
|
||||
if rr := call(t, s, "GET", "/api/meta/bafyA?as="+me.UID, ""); !strings.Contains(rr.Body.String(), `"title":"My title"`) {
|
||||
t.Fatalf("?as overlay = %s", rr.Body)
|
||||
}
|
||||
if rr := call(t, s, "GET", "/api/meta/bafyA", ""); !strings.Contains(rr.Body.String(), `"title":"Matrix"`) {
|
||||
t.Fatalf("shared view changed by a single viewer: %s", rr.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeEndpoint(t *testing.T) {
|
||||
s := routedServer(t)
|
||||
a, b, c := testSigner(t, 1), testSigner(t, 2), testSigner(t, 3)
|
||||
bad := signed(t, c, "bafyX", "title", "Forged", 1)
|
||||
bad.UID = a.UID
|
||||
body, _ := json.Marshal(MergeRequest{Claims: []claim.Claim{
|
||||
signed(t, a, "bafyX", "title", "Dune", 2),
|
||||
signed(t, b, "bafyX", "title", "Dune", 3),
|
||||
signed(t, c, "bafyX", "title", "Dune Part One", 1),
|
||||
bad,
|
||||
}})
|
||||
rr := call(t, s, "POST", "/api/merge", string(body))
|
||||
var mr MergeResponse
|
||||
_ = json.Unmarshal(rr.Body.Bytes(), &mr)
|
||||
if rr.Code != http.StatusOK || mr.Fields["title"] != "Dune" || mr.Nsrc["title"] != 2 || mr.Rejected != 1 {
|
||||
t.Fatalf("merge = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), `"rejected":1`) || !strings.Contains(rr.Body.String(), `"nsrc":{"title":2}`) {
|
||||
t.Fatalf("merge wire shape = %s", rr.Body)
|
||||
}
|
||||
|
||||
// `as`: the viewer's own vote in the request wins…
|
||||
me := testSigner(t, 9)
|
||||
body, _ = json.Marshal(MergeRequest{As: me.UID, Claims: []claim.Claim{
|
||||
signed(t, a, "bafyX", "title", "Dune", 2),
|
||||
signed(t, b, "bafyX", "title", "Dune", 3),
|
||||
signed(t, me, "bafyX", "title", "Dune (my cut)", 4),
|
||||
}})
|
||||
_ = json.Unmarshal(call(t, s, "POST", "/api/merge", string(body)).Body.Bytes(), &mr)
|
||||
if mr.Fields["title"] != "Dune (my cut)" || mr.Nsrc["title"] != 1 {
|
||||
t.Fatalf("request overlay = %+v", mr)
|
||||
}
|
||||
// …and so do its votes held by this core for the same cids.
|
||||
storeClaims(t, s.storage, signed(t, me, "bafyX", "year", "2021", 5))
|
||||
body, _ = json.Marshal(MergeRequest{As: me.UID, Claims: []claim.Claim{
|
||||
signed(t, a, "bafyX", "title", "Dune", 2),
|
||||
signed(t, a, "bafyX", "year", "1984", 2),
|
||||
}})
|
||||
_ = json.Unmarshal(call(t, s, "POST", "/api/merge", string(body)).Body.Bytes(), &mr)
|
||||
if mr.Fields["year"] != "2021" || mr.Fields["title"] != "Dune" {
|
||||
t.Fatalf("local overlay = %+v", mr)
|
||||
}
|
||||
// An all-invalid batch is still 200 with everything rejected.
|
||||
bad2 := signed(t, a, "bafyX", "title", "x", 9)
|
||||
bad2.Sig = bad2.Sig[:10]
|
||||
body, _ = json.Marshal(MergeRequest{Claims: []claim.Claim{bad2}})
|
||||
rr = call(t, s, "POST", "/api/merge", string(body))
|
||||
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"rejected":1`) {
|
||||
t.Fatalf("all-invalid merge = %d %s", rr.Code, rr.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTooManyClaimsIs413(t *testing.T) {
|
||||
s := routedServer(t)
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[")
|
||||
for i := 0; i <= storage.MaxClaimsPerBatch; i++ {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(`{"cid":"c","uid":"u","key":"k","value":null,"ts":1,"sig":""}`)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
if rr := call(t, s, "POST", "/api/claims", sb.String()); rr.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("oversized batch = %d", rr.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user