repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / auth
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}