Files

183 lines
7.0 KiB
Go

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)
}
}