Eric Bower
·
03 Dec 24
api_test.go
1package auth
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "log/slog"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/gkampitakis/go-snaps/snaps"
15 "github.com/picosh/pico/db"
16 "github.com/picosh/pico/db/stub"
17 "github.com/picosh/pico/shared"
18)
19
20var testUserID = "user-1"
21var testUsername = "user-a"
22
23func TestPaymentWebhook(t *testing.T) {
24 apiConfig := setupTest()
25
26 event := OrderEvent{
27 Meta: &OrderEventMeta{
28 EventName: "order_created",
29 CustomData: &CustomDataMeta{
30 PicoUsername: testUsername,
31 },
32 },
33 Data: &OrderEventData{
34 Attr: &OrderEventDataAttr{
35 UserEmail: "auth@pico.test",
36 CreatedAt: time.Now(),
37 Status: "paid",
38 OrderNumber: 1337,
39 },
40 },
41 }
42 jso, err := json.Marshal(event)
43 bail(err)
44 hash := shared.HmacString(apiConfig.Cfg.SecretWebhook, string(jso))
45 body := bytes.NewReader(jso)
46
47 request := httptest.NewRequest("POST", mkpath("/webhook"), body)
48 request.Header.Add("X-signature", hash)
49 responseRecorder := httptest.NewRecorder()
50
51 mux := authMux(apiConfig)
52 mux.ServeHTTP(responseRecorder, request)
53
54 testResponse(t, responseRecorder, 200, "text/plain")
55}
56
57func TestUser(t *testing.T) {
58 apiConfig := setupTest()
59
60 data := sishData{
61 Username: testUsername,
62 }
63 jso, err := json.Marshal(data)
64 bail(err)
65 body := bytes.NewReader(jso)
66
67 request := httptest.NewRequest("POST", mkpath("/user"), body)
68 request.Header.Add("Authorization", "Bearer 123")
69 responseRecorder := httptest.NewRecorder()
70
71 mux := authMux(apiConfig)
72 mux.ServeHTTP(responseRecorder, request)
73
74 testResponse(t, responseRecorder, 200, "application/json")
75}
76
77func TestKey(t *testing.T) {
78 apiConfig := setupTest()
79
80 data := sishData{
81 Username: testUsername,
82 PublicKey: "zzz",
83 }
84 jso, err := json.Marshal(data)
85 bail(err)
86 body := bytes.NewReader(jso)
87
88 request := httptest.NewRequest("POST", mkpath("/key"), body)
89 request.Header.Add("Authorization", "Bearer 123")
90 responseRecorder := httptest.NewRecorder()
91
92 mux := authMux(apiConfig)
93 mux.ServeHTTP(responseRecorder, request)
94
95 testResponse(t, responseRecorder, 200, "application/json")
96}
97
98func TestCheckout(t *testing.T) {
99 apiConfig := setupTest()
100
101 request := httptest.NewRequest("GET", mkpath("/checkout/"+testUsername), strings.NewReader(""))
102 request.Header.Add("Authorization", "Bearer 123")
103 responseRecorder := httptest.NewRecorder()
104
105 mux := authMux(apiConfig)
106 mux.ServeHTTP(responseRecorder, request)
107
108 loc := responseRecorder.Header().Get("Location")
109 if loc != "https://checkout.pico.sh/buy/73c26cf9-3fac-44c3-b744-298b3032a96b?discount=0&checkout[custom][username]=user-a" {
110 t.Errorf("Have Location %s, want checkout", loc)
111 }
112 if responseRecorder.Code != http.StatusMovedPermanently {
113 t.Errorf("Want status '%d', got '%d'", http.StatusMovedPermanently, responseRecorder.Code)
114 return
115 }
116}
117
118func TestIntrospect(t *testing.T) {
119 apiConfig := setupTest()
120
121 request := httptest.NewRequest("POST", mkpath("/introspect?token=123"), strings.NewReader(""))
122 responseRecorder := httptest.NewRecorder()
123
124 mux := authMux(apiConfig)
125 mux.ServeHTTP(responseRecorder, request)
126
127 testResponse(t, responseRecorder, 200, "application/json")
128}
129
130func TestToken(t *testing.T) {
131 apiConfig := setupTest()
132
133 request := httptest.NewRequest("POST", mkpath("/token?code=123"), strings.NewReader(""))
134 responseRecorder := httptest.NewRecorder()
135
136 mux := authMux(apiConfig)
137 mux.ServeHTTP(responseRecorder, request)
138
139 testResponse(t, responseRecorder, 200, "application/json")
140}
141
142func TestAuthApi(t *testing.T) {
143 apiConfig := setupTest()
144 tt := []*ApiExample{
145 {
146 name: "authorize",
147 path: "/authorize?response_type=json&client_id=333&redirect_uri=pico.test&scope=admin",
148 status: http.StatusOK,
149 contentType: "text/html; charset=utf-8",
150 dbpool: apiConfig.Dbpool,
151 },
152 {
153 name: "rss",
154 path: "/rss/123",
155 status: http.StatusOK,
156 contentType: "application/atom+xml",
157 dbpool: apiConfig.Dbpool,
158 },
159 {
160 name: "fileserver",
161 path: "/robots.txt",
162 status: http.StatusOK,
163 contentType: "text/plain; charset=utf-8",
164 dbpool: apiConfig.Dbpool,
165 },
166 {
167 name: "well-known",
168 path: "/.well-known/oauth-authorization-server",
169 status: http.StatusOK,
170 contentType: "application/json",
171 dbpool: apiConfig.Dbpool,
172 },
173 }
174
175 for _, tc := range tt {
176 t.Run(tc.name, func(t *testing.T) {
177 request := httptest.NewRequest("GET", mkpath(tc.path), strings.NewReader(""))
178 responseRecorder := httptest.NewRecorder()
179
180 mux := authMux(apiConfig)
181 mux.ServeHTTP(responseRecorder, request)
182
183 testResponse(t, responseRecorder, tc.status, tc.contentType)
184 })
185 }
186}
187
188type ApiExample struct {
189 name string
190 path string
191 status int
192 contentType string
193 dbpool db.DB
194}
195
196type AuthDb struct {
197 *stub.StubDB
198}
199
200func (a *AuthDb) AddPicoPlusUser(username, email, from, txid string) error {
201 return nil
202}
203
204func (a *AuthDb) FindUserForName(username string) (*db.User, error) {
205 return &db.User{ID: testUserID, Name: username}, nil
206}
207
208func (a *AuthDb) FindUserForKey(username string, pubkey string) (*db.User, error) {
209 return &db.User{ID: testUserID, Name: username}, nil
210}
211
212func (a *AuthDb) FindUserForToken(token string) (*db.User, error) {
213 if token != "123" {
214 return nil, fmt.Errorf("invalid token")
215 }
216 return &db.User{ID: testUserID, Name: testUsername}, nil
217}
218
219func (a *AuthDb) HasFeatureForUser(userID string, feature string) bool {
220 return true
221}
222
223func (a *AuthDb) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
224 return []*db.PublicKey{{ID: "1", UserID: user.ID, Name: "my-key", Key: "nice-pubkey", CreatedAt: &time.Time{}}}, nil
225}
226
227func (a *AuthDb) FindFeatureForUser(userID string, feature string) (*db.FeatureFlag, error) {
228 now := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
229 oneDayWarning := now.AddDate(0, 0, 1)
230 return &db.FeatureFlag{ID: "2", UserID: userID, Name: "plus", ExpiresAt: &oneDayWarning, CreatedAt: &now}, nil
231}
232
233func NewAuthDb(logger *slog.Logger) *AuthDb {
234 sb := stub.NewStubDB(logger)
235 return &AuthDb{
236 StubDB: sb,
237 }
238}
239
240func mkpath(path string) string {
241 return fmt.Sprintf("https://auth.pico.test%s", path)
242}
243
244func setupTest() *shared.ApiConfig {
245 logger := shared.CreateLogger("auth")
246 cfg := &shared.ConfigSite{
247 Issuer: "auth.pico.test",
248 Domain: "http://0.0.0.0:3000",
249 Port: "3000",
250 Secret: "",
251 SecretWebhook: "my-secret",
252 }
253 cfg.Logger = logger
254 db := NewAuthDb(cfg.Logger)
255 apiConfig := &shared.ApiConfig{
256 Cfg: cfg,
257 Dbpool: db,
258 }
259
260 return apiConfig
261}
262
263func testResponse(t *testing.T, responseRecorder *httptest.ResponseRecorder, status int, contentType string) {
264 if responseRecorder.Code != status {
265 t.Errorf("Want status '%d', got '%d'", status, responseRecorder.Code)
266 return
267 }
268
269 ct := responseRecorder.Header().Get("content-type")
270 if ct != contentType {
271 t.Errorf("Want content type '%s', got '%s'", contentType, ct)
272 return
273 }
274
275 body := strings.TrimSpace(responseRecorder.Body.String())
276 snaps.MatchSnapshot(t, body)
277}
278
279func bail(err error) {
280 if err != nil {
281 panic(bail)
282 }
283}