Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
7.9 kB · 218 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219package appview
import ( "context" "encoding/json" "errors" "math" "net/http" "net/http/httptest" "net/url" "path/filepath" "reflect" "strings" "testing" "time"
"provenance/internal/domain" "provenance/internal/storage" "provenance/internal/trust" "provenance/internal/trust/algorithms")
const carol = "did:plc:carol"const dan = "did:plc:dan"const eve = "did:plc:eve"
type accountRepository struct { repository calls map[string]int}
func (r *accountRepository) Resolve(ctx context.Context, handle string) (string, error) { if _, ok := ctx.Deadline(); !ok { return "", errors.New("missing identity deadline") } r.calls[handle]++ switch handle { case "carol.example.com", "alias.example.com": return carol, nil case "bad.example.com": return "not-a-DID", nil } return r.repository.Resolve(ctx, handle)}
func algorithmService(t *testing.T, algorithm trust.Algorithm) (*Service, *accountRepository) { t.Helper() store, err := storage.Open(filepath.Join(t.TempDir(), "appview.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { store.Close() }) repo := &accountRepository{calls: map[string]int{}} service := NewWithAlgorithm(store, repo, trust.Limits{States: 100, Edges: 100, Penalty: .1}, algorithm) for _, account := range []struct { did string edges []domain.Vouch }{ {alice, []domain.Vouch{{Author: alice, Subject: carol, Score: -1}, {Author: alice, Subject: dan, Score: 2}}}, {carol, []domain.Vouch{{Author: carol, Subject: bob, Score: 2}}}, {dan, []domain.Vouch{{Author: dan, Subject: eve, Score: 2}}}, {eve, []domain.Vouch{{Author: eve, Subject: bob, Score: 2}}}, } { if err := service.Join(context.Background(), account.did); err != nil { t.Fatal(err) } if err := store.Replace(context.Background(), account.did, account.edges, time.Now()); err != nil { t.Fatal(err) } } return service, repo}
func TestAlgorithmAPIShape(t *testing.T) { for _, algorithm := range []trust.Algorithm{algorithms.Optimistic{}, algorithms.ShortestPath{}} { t.Run(algorithm.Describe().Name, func(t *testing.T) { service, _ := algorithmService(t, algorithm) handler := Handler(service, serviceKey) call := func(path string, params url.Values, authenticated bool) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, path+"?"+params.Encode(), nil) if authenticated { r.Header.Set("Authorization", "Bearer "+serviceKey) r.Header.Set(IdentityHeader, alice) } w := httptest.NewRecorder() handler.ServeHTTP(w, r) return w } w := call("/v1/openapi.json", nil, false) var doc struct { OpenAPI string `json:"openapi"` Algorithm string `json:"x-provenance-algorithm"` Paths map[string]map[string]struct { Parameters []parameter `json:"parameters"` Security []map[string][]string `json:"security"` Responses map[string]any `json:"responses"` } `json:"paths"` } if err := json.Unmarshal(w.Body.Bytes(), &doc); err != nil { t.Fatal(err) } if w.Code != http.StatusOK || doc.OpenAPI != openAPIVersion || doc.Algorithm != algorithm.Describe().Name { t.Fatalf("schema: %s", w.Body) } if len(doc.Paths) != 16 { t.Fatalf("undocumented routes: %d", len(doc.Paths)) } supportsIgnored := len(algorithm.Describe().Accounts) > 0 for _, path := range []string{"/v1/trust", "/v1/me/trust", "/v1/trusted"} { authenticated := path != "/v1/trust" operation := doc.Paths[path]["get"] found := false for _, p := range operation.Parameters { if p.Name == algorithms.IgnoredParameter { found = true if p.Schema["type"] != "array" || p.Style != "form" || !p.Explode { t.Fatal("ignored parameter has wrong serialization") } } } if found != supportsIgnored || (len(operation.Security) > 0) != authenticated { t.Fatalf("schema capability mismatch: %s", path) } params := url.Values{"target": {"bob.example.com"}} if !authenticated { params.Set("source", "alice.example.com") } w = call(path, params, authenticated) if w.Code != http.StatusOK { t.Fatalf("base query: %d %s", w.Code, w.Body) } if path == "/v1/trusted" { want := `{"trusted":true}` if supportsIgnored { want = `{"trusted":false}` } if strings.TrimSpace(w.Body.String()) != want { t.Fatalf("base trusted: %s", w.Body) } } params[algorithms.IgnoredParameter] = []string{" @CAROL.EXAMPLE.COM ", carol} w = call(path, params, authenticated) if !supportsIgnored { if w.Code != http.StatusBadRequest { t.Fatalf("optimistic accepted ignored accounts: %d %s", w.Code, w.Body) } continue } if w.Code != http.StatusOK { t.Fatalf("ignored query: %d %s", w.Code, w.Body) } if path == "/v1/trusted" { if strings.TrimSpace(w.Body.String()) != `{"trusted":true}` { t.Fatalf("ignored trusted: %s", w.Body) } continue } var result trust.Result if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatal(err) } if result.Score == nil || math.Abs(*result.Score-1.8) > 1e-9 { t.Fatalf("ignored score: %s", w.Body) } if !authenticated && (strings.Contains(w.Body.String(), "trusted") || strings.Contains(w.Body.String(), "threshold") || result.Path != nil) { t.Fatal("public response exposed private or unrequested data") } } for _, value := range []string{"", "missing.example.com", "bad.example.com", alice, "bob.example.com"} { params := url.Values{"source": {alice}, "target": {bob}, algorithms.IgnoredParameter: {value}} if w := call("/v1/trust", params, false); w.Code != http.StatusBadRequest { t.Fatalf("invalid ignored account %q: %d %s", value, w.Code, w.Body) } } params := url.Values{"source": {alice}, "target": {bob}, algorithms.IgnoredParameter: make([]string, algorithms.MaxIgnoredAccounts+1)} if w := call("/v1/trust", params, false); w.Code != http.StatusBadRequest { t.Fatal("unbounded account list accepted") } params = url.Values{"source": {alice}, "target": {bob}, "include_path": {"true"}} if supportsIgnored { params[algorithms.IgnoredParameter] = []string{carol} } w = call("/v1/trust", params, false) var result trust.Result if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatal(err) } if !reflect.DeepEqual(result.Path, []string{alice, dan, eve, bob}) || len(result.PathScores) != 4 { t.Fatalf("path: %s", w.Body) } for _, suffix := range []string{"&extra=1", "&target=" + bob, "&include_path=maybe", "&mode=unknown", "&ignored=%ZZ"} { r := httptest.NewRequest(http.MethodGet, "/v1/trust?source="+alice+"&target="+bob+suffix, nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) if w.Code != http.StatusBadRequest { t.Fatalf("invalid query %q: %d %s", suffix, w.Code, w.Body) } } }) }}
func TestIdentityNormalization(t *testing.T) { service, repo := algorithmService(t, algorithms.ShortestPath{}) input, err := service.resolveInput(context.Background(), " @ALICE.EXAMPLE.COM ", "bob.example.com", []trust.Accounts{{algorithms.IgnoredParameter: {" @CAROL.EXAMPLE.COM ", "carol.example.com", carol, "alias.example.com"}}}) if err != nil { t.Fatal(err) } if input.Source != alice || input.Target != bob || !reflect.DeepEqual(input.Accounts[algorithms.IgnoredParameter], []string{carol}) { t.Fatalf("normalized input: %+v", input) } if repo.calls["carol.example.com"] != 1 || repo.calls["alice.example.com"] != 1 || repo.calls["bob.example.com"] != 1 { t.Fatalf("identity cache: %+v", repo.calls) } if _, err := service.resolveInput(context.Background(), alice, bob, []trust.Accounts{{"unsupported": {carol}}}); err == nil { t.Fatal("unsupported service option accepted") }}