Antonio Mika
·
17 Nov 24
web_test.go
1package pgs
2
3import (
4 "fmt"
5 "io"
6 "log/slog"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/picosh/pico/db"
14 "github.com/picosh/pico/db/stub"
15 "github.com/picosh/pico/shared"
16 "github.com/picosh/pico/shared/storage"
17 sst "github.com/picosh/pobj/storage"
18)
19
20var testUserID = "user-1"
21var testUsername = "user"
22
23type ApiExample struct {
24 name string
25 path string
26 want string
27 status int
28 contentType string
29
30 dbpool db.DB
31 storage map[string]map[string]string
32}
33
34type PgsDb struct {
35 *stub.StubDB
36}
37
38func NewPgsDb(logger *slog.Logger) *PgsDb {
39 sb := stub.NewStubDB(logger)
40 return &PgsDb{
41 StubDB: sb,
42 }
43}
44
45func (p *PgsDb) FindUserForName(name string) (*db.User, error) {
46 return &db.User{
47 ID: testUserID,
48 Name: testUsername,
49 }, nil
50}
51
52func (p *PgsDb) FindProjectByName(userID, name string) (*db.Project, error) {
53 return &db.Project{
54 ID: "project-1",
55 UserID: userID,
56 Name: name,
57 ProjectDir: name,
58 Username: testUsername,
59 Acl: db.ProjectAcl{
60 Type: "public",
61 },
62 }, nil
63}
64
65type PgsAnalyticsDb struct {
66 *PgsDb
67}
68
69func NewPgsAnalticsDb(logger *slog.Logger) *PgsAnalyticsDb {
70 return &PgsAnalyticsDb{
71 PgsDb: NewPgsDb(logger),
72 }
73}
74
75func (p *PgsAnalyticsDb) HasFeatureForUser(userID, feature string) bool {
76 return true
77}
78
79func mkpath(path string) string {
80 return fmt.Sprintf("https://%s-test.pgs.test%s", testUsername, path)
81}
82
83func TestApiBasic(t *testing.T) {
84 bucketName := shared.GetAssetBucketName(testUserID)
85 cfg := NewConfigSite()
86 cfg.Domain = "pgs.test"
87 tt := []*ApiExample{
88 {
89 name: "basic",
90 path: "/",
91 want: "hello world!",
92 status: http.StatusOK,
93 contentType: "text/html",
94
95 dbpool: NewPgsDb(cfg.Logger),
96 storage: map[string]map[string]string{
97 bucketName: {
98 "test/index.html": "hello world!",
99 },
100 },
101 },
102 {
103 name: "direct-file",
104 path: "/test.html",
105 want: "hello world!",
106 status: http.StatusOK,
107 contentType: "text/html",
108
109 dbpool: NewPgsDb(cfg.Logger),
110 storage: map[string]map[string]string{
111 bucketName: {
112 "test/test.html": "hello world!",
113 },
114 },
115 },
116 {
117 name: "subdir-301-redirect",
118 path: "/subdir",
119 want: `<a href="/subdir/">Moved Permanently</a>.`,
120 status: http.StatusMovedPermanently,
121 contentType: "text/html; charset=utf-8",
122
123 dbpool: NewPgsDb(cfg.Logger),
124 storage: map[string]map[string]string{
125 bucketName: {
126 "test/subdir/index.html": "hello world!",
127 },
128 },
129 },
130 {
131 name: "redirects-file-301",
132 path: "/anything",
133 want: `<a href="/about.html">Moved Permanently</a>.`,
134 status: http.StatusMovedPermanently,
135 contentType: "text/html; charset=utf-8",
136
137 dbpool: NewPgsDb(cfg.Logger),
138 storage: map[string]map[string]string{
139 bucketName: {
140 "test/_redirects": "/anything /about.html 301",
141 "test/about.html": "hello world!",
142 },
143 },
144 },
145 {
146 name: "subdir-direct",
147 path: "/subdir/index.html",
148 want: "hello world!",
149 status: http.StatusOK,
150 contentType: "text/html",
151
152 dbpool: NewPgsDb(cfg.Logger),
153 storage: map[string]map[string]string{
154 bucketName: {
155 "test/subdir/index.html": "hello world!",
156 },
157 },
158 },
159 {
160 name: "spa",
161 path: "/anything",
162 want: "hello world!",
163 status: http.StatusOK,
164 contentType: "text/html",
165
166 dbpool: NewPgsDb(cfg.Logger),
167 storage: map[string]map[string]string{
168 bucketName: {
169 "test/_redirects": "/* /index.html 200",
170 "test/index.html": "hello world!",
171 },
172 },
173 },
174 {
175 name: "not-found",
176 path: "/anything",
177 want: "404 not found",
178 status: http.StatusNotFound,
179 contentType: "text/plain; charset=utf-8",
180
181 dbpool: NewPgsDb(cfg.Logger),
182 storage: map[string]map[string]string{
183 bucketName: {},
184 },
185 },
186 {
187 name: "not-found-custom",
188 path: "/anything",
189 want: "boom!",
190 status: http.StatusNotFound,
191 contentType: "text/html",
192
193 dbpool: NewPgsDb(cfg.Logger),
194 storage: map[string]map[string]string{
195 bucketName: {
196 "test/404.html": "boom!",
197 },
198 },
199 },
200 {
201 name: "images",
202 path: "/profile.jpg",
203 want: "image",
204 status: http.StatusOK,
205 contentType: "image/jpeg",
206
207 dbpool: NewPgsDb(cfg.Logger),
208 storage: map[string]map[string]string{
209 bucketName: {
210 "test/profile.jpg": "image",
211 },
212 },
213 },
214 }
215
216 for _, tc := range tt {
217 t.Run(tc.name, func(t *testing.T) {
218 request := httptest.NewRequest("GET", mkpath(tc.path), strings.NewReader(""))
219 responseRecorder := httptest.NewRecorder()
220
221 st, _ := storage.NewStorageMemory(tc.storage)
222 ch := make(chan *db.AnalyticsVisits, 100)
223 router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch)
224 router.ServeHTTP(responseRecorder, request)
225
226 if responseRecorder.Code != tc.status {
227 t.Errorf("Want status '%d', got '%d'", tc.status, responseRecorder.Code)
228 }
229
230 ct := responseRecorder.Header().Get("content-type")
231 if ct != tc.contentType {
232 t.Errorf("Want status '%s', got '%s'", tc.contentType, ct)
233 }
234
235 body := strings.TrimSpace(responseRecorder.Body.String())
236 if body != tc.want {
237 t.Errorf("Want '%s', got '%s'", tc.want, body)
238 }
239 })
240 }
241}
242
243func TestAnalytics(t *testing.T) {
244 bucketName := shared.GetAssetBucketName(testUserID)
245 cfg := NewConfigSite()
246 cfg.Domain = "pgs.test"
247 expectedPath := "/app"
248 request := httptest.NewRequest("GET", mkpath(expectedPath), strings.NewReader(""))
249 responseRecorder := httptest.NewRecorder()
250
251 sto := map[string]map[string]string{
252 bucketName: {
253 "test/app.html": "hello world!",
254 },
255 }
256 st, _ := storage.NewStorageMemory(sto)
257 ch := make(chan *db.AnalyticsVisits, 100)
258 dbpool := NewPgsAnalticsDb(cfg.Logger)
259 router := NewWebRouter(cfg, cfg.Logger, dbpool, st, ch)
260
261 go func() {
262 for analytics := range ch {
263 if analytics.Path != expectedPath {
264 t.Errorf("Want path '%s', got '%s'", expectedPath, analytics.Path)
265 }
266 close(ch)
267 }
268 }()
269
270 router.ServeHTTP(responseRecorder, request)
271
272 select {
273 case <-ch:
274 return
275 case <-time.After(time.Second * 1):
276 t.Error("didnt receive analytics event within time limit")
277 }
278}
279
280type ImageStorageMemory struct {
281 *storage.StorageMemory
282 Opts *storage.ImgProcessOpts
283 Fpath string
284}
285
286func (s *ImageStorageMemory) ServeObject(bucket sst.Bucket, fpath string, opts *storage.ImgProcessOpts) (io.ReadCloser, string, error) {
287 s.Opts = opts
288 s.Fpath = fpath
289 return io.NopCloser(strings.NewReader("hello world!")), "image/jpeg", nil
290}
291
292func TestImageManipulation(t *testing.T) {
293 bucketName := shared.GetAssetBucketName(testUserID)
294 cfg := NewConfigSite()
295 cfg.Domain = "pgs.test"
296
297 tt := []ApiExample{
298 {
299 name: "root-img",
300 path: "/app.jpg/s:500/rt:90",
301 want: "hello world!",
302 status: http.StatusOK,
303 contentType: "image/jpeg",
304
305 dbpool: NewPgsDb(cfg.Logger),
306 storage: map[string]map[string]string{
307 bucketName: {
308 "test/app.jpg": "hello world!",
309 },
310 },
311 },
312 {
313 name: "root-subdir-img",
314 path: "/subdir/app.jpg/rt:90/s:500",
315 want: "hello world!",
316 status: http.StatusOK,
317 contentType: "image/jpeg",
318
319 dbpool: NewPgsDb(cfg.Logger),
320 storage: map[string]map[string]string{
321 bucketName: {
322 "test/subdir/app.jpg": "hello world!",
323 },
324 },
325 },
326 }
327
328 for _, tc := range tt {
329 t.Run(tc.name, func(t *testing.T) {
330 request := httptest.NewRequest("GET", mkpath(tc.path), strings.NewReader(""))
331 responseRecorder := httptest.NewRecorder()
332
333 memst, _ := storage.NewStorageMemory(tc.storage)
334 st := &ImageStorageMemory{
335 StorageMemory: memst,
336 Opts: &storage.ImgProcessOpts{
337 Ratio: &storage.Ratio{},
338 },
339 }
340 ch := make(chan *db.AnalyticsVisits, 100)
341 router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch)
342 router.ServeHTTP(responseRecorder, request)
343
344 if responseRecorder.Code != tc.status {
345 t.Errorf("Want status '%d', got '%d'", tc.status, responseRecorder.Code)
346 }
347
348 ct := responseRecorder.Header().Get("content-type")
349 if ct != tc.contentType {
350 t.Errorf("Want status '%s', got '%s'", tc.contentType, ct)
351 }
352
353 body := strings.TrimSpace(responseRecorder.Body.String())
354 if body != tc.want {
355 t.Errorf("Want '%s', got '%s'", tc.want, body)
356 }
357
358 if st.Opts.Ratio.Width != 500 {
359 t.Errorf("Want ratio width '500', got '%d'", st.Opts.Ratio.Width)
360 return
361 }
362
363 if st.Opts.Rotate != 90 {
364 t.Errorf("Want rotate '90', got '%d'", st.Opts.Rotate)
365 return
366 }
367 })
368 }
369}