This repository has no description
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}