This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / xrpc / xrpc_test.go
4.5 kB 158 lines
1package xrpc 2 3import ( 4 "context" 5 "encoding/json" 6 "io" 7 "log/slog" 8 "net/http" 9 "net/http/httptest" 10 "path/filepath" 11 "testing" 12 "time" 13 14 "github.com/bluesky-social/indigo/atproto/atcrypto" 15 "github.com/bluesky-social/indigo/atproto/auth" 16 "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 "tangled.org/core/appview/config" 19 "tangled.org/core/appview/db" 20 "tangled.org/core/appview/models" 21 "tangled.org/core/xrpc/serviceauth" 22) 23 24const ( 25 testActor = "did:plc:tester" 26 testAudience = "did:web:test.example" 27) 28 29// newTestXrpc builds an Xrpc backed by a fresh temp DB, with service auth wired 30// to a mock directory holding testActor's key. It returns the router, the DB, 31// and a function that signs a service-auth token for a given lexicon method. 32func newTestXrpc(t *testing.T) (http.Handler, *db.DB, func(nsid string) string) { 33 t.Helper() 34 35 d, err := db.Make(context.Background(), filepath.Join(t.TempDir(), "test.db")) 36 if err != nil { 37 t.Fatalf("db.Make: %v", err) 38 } 39 t.Cleanup(func() { d.Close() }) 40 41 priv, err := atcrypto.GeneratePrivateKeyP256() 42 if err != nil { 43 t.Fatalf("generate key: %v", err) 44 } 45 pub, err := priv.PublicKey() 46 if err != nil { 47 t.Fatalf("derive pubkey: %v", err) 48 } 49 50 dir := identity.NewMockDirectory() 51 dir.Insert(identity.Identity{ 52 DID: syntax.DID(testActor), 53 Keys: map[string]identity.VerificationMethod{ 54 "atproto": {Type: "Multikey", PublicKeyMultibase: pub.Multibase()}, 55 }, 56 }) 57 58 logger := slog.New(slog.NewTextHandler(io.Discard, nil)) 59 x := &Xrpc{ 60 DB: d, 61 Config: &config.Config{}, 62 Logger: logger, 63 ServiceAuth: serviceauth.NewServiceAuth(logger, dir, testAudience), 64 } 65 66 sign := func(nsid string) string { 67 lxm := syntax.NSID(nsid) 68 token, err := auth.SignServiceAuth(syntax.DID(testActor), testAudience, time.Minute, &lxm, priv) 69 if err != nil { 70 t.Fatalf("sign service auth: %v", err) 71 } 72 return token 73 } 74 75 return x.Router(), d, sign 76} 77 78func TestHealth(t *testing.T) { 79 router, _, _ := newTestXrpc(t) 80 81 rec := httptest.NewRecorder() 82 router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/_health", nil)) 83 84 if rec.Code != http.StatusOK { 85 t.Fatalf("status = %d, want 200", rec.Code) 86 } 87 var body map[string]string 88 if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { 89 t.Fatalf("decode: %v; body=%s", err, rec.Body.String()) 90 } 91 if body["version"] == "" { 92 t.Fatalf("missing version in %s", rec.Body.String()) 93 } 94} 95 96func TestServiceAuthRequired(t *testing.T) { 97 router, _, _ := newTestXrpc(t) 98 99 rec := httptest.NewRecorder() 100 router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/org.tangled.temp.notification.getUnreadCount", nil)) 101 102 if rec.Code != http.StatusForbidden { 103 t.Fatalf("status = %d, want 403 without a service-auth token", rec.Code) 104 } 105} 106 107func TestNotificationGetUnreadCount(t *testing.T) { 108 router, d, sign := newTestXrpc(t) 109 110 nsid := "org.tangled.temp.notification.getUnreadCount" 111 call := func() int { 112 req := httptest.NewRequest(http.MethodGet, "/"+nsid, nil) 113 req.Header.Set("Authorization", "Bearer "+sign(nsid)) 114 rec := httptest.NewRecorder() 115 router.ServeHTTP(rec, req) 116 if rec.Code != http.StatusOK { 117 t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) 118 } 119 var out struct { 120 Count int `json:"count"` 121 } 122 if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { 123 t.Fatalf("decode: %v; body=%s", err, rec.Body.String()) 124 } 125 return out.Count 126 } 127 128 if got := call(); got != 0 { 129 t.Fatalf("empty db count = %d, want 0", got) 130 } 131 132 if err := db.CreateNotification(d, &models.Notification{ 133 RecipientDid: testActor, 134 ActorDid: "did:plc:someone", 135 Type: models.NotificationTypeRepoStarred, 136 Read: false, 137 }); err != nil { 138 t.Fatalf("CreateNotification: %v", err) 139 } 140 141 if got := call(); got != 1 { 142 t.Fatalf("count after one unread = %d, want 1", got) 143 } 144} 145 146func TestWrongLexiconTokenRejected(t *testing.T) { 147 router, _, sign := newTestXrpc(t) 148 149 // a token minted for a different method must not authorize this call 150 req := httptest.NewRequest(http.MethodGet, "/org.tangled.temp.notification.getUnreadCount", nil) 151 req.Header.Set("Authorization", "Bearer "+sign("org.tangled.temp.notification.listNotifications")) 152 rec := httptest.NewRecorder() 153 router.ServeHTTP(rec, req) 154 155 if rec.Code != http.StatusForbidden { 156 t.Fatalf("status = %d, want 403 for a token bound to a different method", rec.Code) 157 } 158}