website-feeds

Make RSS feeds of your favorite "vote on posts" websites

git clone https://code.pdelong.com/website-feeds.git

  1package server
  2
  3// TODO: Write test for /feed/<SITE>.  This will require the fake fetcher.
  4
  5import (
  6	"fmt"
  7	"net/http"
  8	"net/http/httptest"
  9	"net/url"
 10	"strings"
 11	"testing"
 12	"time"
 13
 14	"website-feeds/model"
 15	"website-feeds/model/modeltest"
 16	"website-feeds/store"
 17)
 18
 19func TestHandleGetRootSuccess(t *testing.T) {
 20	fakeStore := store.NewFake()
 21	handler := newHandler(&fakeStore)
 22
 23	req := httptest.NewRequest("GET", "/", nil)
 24	w := httptest.NewRecorder()
 25
 26	handler.ServeHTTP(w, req)
 27
 28	if w.Code != http.StatusOK {
 29		t.Errorf("response was not OK: %d", w.Code)
 30	}
 31}
 32
 33func TestHandleGetRootInvalidHost(t *testing.T) {
 34	fakeStore := store.NewFake()
 35	handler := newHandler(&fakeStore)
 36
 37	req := httptest.NewRequest("GET", "/", nil)
 38	req.RemoteAddr = "!!!!!!!"
 39	w := httptest.NewRecorder()
 40
 41	handler.ServeHTTP(w, req)
 42
 43	if w.Code != http.StatusInternalServerError {
 44		t.Errorf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)
 45	}
 46}
 47
 48func TestHandleGetStaticSuccess(t *testing.T) {
 49	fakeStore := store.NewFake()
 50	handler := newHandler(&fakeStore)
 51
 52	req := httptest.NewRequest("GET", "/static/favicon.ico", nil)
 53	w := httptest.NewRecorder()
 54
 55	handler.ServeHTTP(w, req)
 56
 57	if w.Code != http.StatusOK {
 58		t.Errorf("response was not OK: %d", w.Code)
 59	}
 60}
 61
 62func TestHandleGetStaticNotFound(t *testing.T) {
 63	fakeStore := store.NewFake()
 64	handler := newHandler(&fakeStore)
 65
 66	req := httptest.NewRequest("GET", "/static/i-definitely-do-not-exist", nil)
 67	w := httptest.NewRecorder()
 68
 69	handler.ServeHTTP(w, req)
 70
 71	if w.Code != http.StatusNotFound {
 72		t.Errorf("response was not NOT_FOUND: %d", w.Code)
 73	}
 74}
 75
 76func TestHandleGetSettingsSuccessDefault(t *testing.T) {
 77	fakeStore := store.NewFake()
 78	handler := newHandler(&fakeStore)
 79
 80	req := httptest.NewRequest("GET", "/settings", nil)
 81	w := httptest.NewRecorder()
 82
 83	handler.ServeHTTP(w, req)
 84
 85	if w.Code != http.StatusOK {
 86		t.Errorf("response was not OK: %d", w.Code)
 87	}
 88}
 89
 90func TestHandleGetSettingsSuccess(t *testing.T) {
 91	settings := model.Settings{
 92		Reddit: &model.RedditSettings{
 93			ClientID:     "THIS_IS_THE_CLIENT_ID",
 94			ClientSecret: "THIS_IS_THE_CLIENT_SECRET",
 95		},
 96		DefaultNumPosts: 111111,
 97		MaxNumPosts:     222222,
 98		SiteFetchWait:   333333 * time.Hour,
 99	}
100
101	fakeStore := store.NewFake()
102	handler := newHandler(&fakeStore)
103	err := fakeStore.UpdateSettings(t.Context(), &settings)
104	if err != nil {
105		t.Fatalf("failed to update settings: %s", err)
106	}
107
108	req := httptest.NewRequest("GET", "/settings", nil)
109	w := httptest.NewRecorder()
110
111	handler.ServeHTTP(w, req)
112
113	if w.Code != http.StatusOK {
114		t.Errorf("response was not OK: %d", w.Code)
115	}
116
117	body := w.Body.String()
118
119	if !strings.Contains(body, "111111") {
120		t.Error("body didn't contain DefaultNumPosts")
121	}
122
123	if !strings.Contains(body, "222222") {
124		t.Error("body didn't contain MaxNumPosts")
125	}
126
127	if !strings.Contains(body, "333333") {
128		t.Error("body didn't contain SiteFetchWait")
129	}
130
131	if !strings.Contains(body, "THIS_IS_THE_CLIENT_ID") {
132		t.Error("body didn't contain Reddit.ClientID")
133	}
134
135	if !strings.Contains(body, "THIS_IS_THE_CLIENT_SECRET") {
136		t.Error("body didn't contain Reddit.ClientSecret")
137	}
138}
139
140func TestHandleGetSettingsFailure(t *testing.T) {
141	fakeStore := store.NewFake()
142	injectedErr := fmt.Errorf("oopsie")
143	fakeStore.InjectSettingsError = &injectedErr
144	handler := newHandler(&fakeStore)
145
146	req := httptest.NewRequest("GET", "/settings", nil)
147	w := httptest.NewRecorder()
148
149	handler.ServeHTTP(w, req)
150
151	if w.Code != http.StatusInternalServerError {
152		t.Errorf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)
153	}
154}
155
156func TestHandlePostSettingsSuccess(t *testing.T) {
157	tests := []struct {
158		name             string
159		values           url.Values
160		expectedSettings *model.Settings
161	}{
162		{
163			name: "all populated",
164			values: url.Values{
165				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
166				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
167				"default-num-posts":    {"111111"},
168				"max-num-posts":        {"222222"},
169				"site-fetch-wait":      {"333333"},
170			},
171			expectedSettings: &model.Settings{
172				Reddit: &model.RedditSettings{
173					ClientID:     "THIS_IS_THE_CLIENT_ID",
174					ClientSecret: "THIS_IS_THE_CLIENT_SECRET",
175				},
176				DefaultNumPosts: 111111,
177				MaxNumPosts:     222222,
178				SiteFetchWait:   333333 * time.Hour,
179			},
180		},
181		{
182			name: "no reddit",
183			values: url.Values{
184				"default-num-posts": {"111111"},
185				"max-num-posts":     {"222222"},
186				"site-fetch-wait":   {"333333"},
187			},
188			expectedSettings: &model.Settings{
189				Reddit:          nil,
190				DefaultNumPosts: 111111,
191				MaxNumPosts:     222222,
192				SiteFetchWait:   333333 * time.Hour,
193			},
194		},
195		{
196			name: "empty reddit",
197			values: url.Values{
198				"reddit-client-id":     {""},
199				"reddit-client-secret": {""},
200				"default-num-posts":    {"111111"},
201				"max-num-posts":        {"222222"},
202				"site-fetch-wait":      {"333333"},
203			},
204			expectedSettings: &model.Settings{
205				Reddit:          nil,
206				DefaultNumPosts: 111111,
207				MaxNumPosts:     222222,
208				SiteFetchWait:   333333 * time.Hour,
209			},
210		},
211	}
212
213	for _, test := range tests {
214		t.Run(test.name, func(t *testing.T) {
215			fakeStore := store.NewFake()
216			handler := newHandler(&fakeStore)
217
218			req := httptest.NewRequest(
219				"POST",
220				"/settings",
221				strings.NewReader(test.values.Encode()),
222			)
223			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
224			w := httptest.NewRecorder()
225
226			handler.ServeHTTP(w, req)
227
228			if w.Code != http.StatusSeeOther {
229				t.Fatalf("response was not SEE_OTHER: %d", w.Code)
230			}
231
232			settings, err := fakeStore.Settings(t.Context())
233			if err != nil {
234				t.Fatalf("failed to get settings: %s", err)
235			}
236
237			modeltest.CompareSettings(t, settings, test.expectedSettings)
238		})
239	}
240}
241
242func TestHandlePostSettingsInvalid(t *testing.T) {
243	tests := []struct {
244		name   string
245		values url.Values
246	}{
247		{
248			name: "empty client id",
249			values: url.Values{
250				"reddit-client-id":     {""},
251				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
252				"default-num-posts":    {"111111"},
253				"max-num-posts":        {"222222"},
254				"site-fetch-wait":      {"333333"},
255			},
256		},
257		{
258			name: "missing client id",
259			values: url.Values{
260				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
261				"default-num-posts":    {"111111"},
262				"max-num-posts":        {"222222"},
263				"site-fetch-wait":      {"333333"},
264			},
265		},
266		{
267			name: "empty client secret",
268			values: url.Values{
269				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
270				"default-num-posts":    {"111111"},
271				"max-num-posts":        {"222222"},
272				"site-fetch-wait":      {"333333"},
273			},
274		},
275		{
276			name: "missing client secret",
277			values: url.Values{
278				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
279				"reddit-client-secret": {""},
280				"default-num-posts":    {"111111"},
281				"max-num-posts":        {"222222"},
282				"site-fetch-wait":      {"333333"},
283			},
284		},
285		{
286			name: "empty default num posts",
287			values: url.Values{
288				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
289				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
290				"default-num-posts":    {""},
291				"max-num-posts":        {"222222"},
292				"site-fetch-wait":      {"333333"},
293			},
294		},
295		{
296			name: "missing default num posts",
297			values: url.Values{
298				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
299				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
300				"max-num-posts":        {"222222"},
301				"site-fetch-wait":      {"333333"},
302			},
303		},
304		{
305			name: "non-integer default num posts",
306			values: url.Values{
307				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
308				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
309				"default-num-posts":    {"NOPE"},
310				"max-num-posts":        {"22222"},
311				"site-fetch-wait":      {"33333"},
312			},
313		},
314		{
315			name: "empty max num posts",
316			values: url.Values{
317				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
318				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
319				"default-num-posts":    {"111111"},
320				"max-num-posts":        {""},
321				"site-fetch-wait":      {"333333"},
322			},
323		},
324		{
325			name: "missing max num posts",
326			values: url.Values{
327				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
328				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
329				"default-num-posts":    {"111111"},
330				"site-fetch-wait":      {"333333"},
331			},
332		},
333		{
334			name: "non-integer max num posts",
335			values: url.Values{
336				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
337				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
338				"default-num-posts":    {"11111"},
339				"max-num-posts":        {"22222"},
340				"site-fetch-wait":      {"NOPE"},
341			},
342		},
343		{
344			name: "empty site fetch wait",
345			values: url.Values{
346				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
347				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
348				"default-num-posts":    {"111111"},
349				"max-num-posts":        {"222222"},
350				"site-fetch-wait":      {""},
351			},
352		},
353		{
354			name: "missing site fetch wait",
355			values: url.Values{
356				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
357				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
358				"default-num-posts":    {"111111"},
359				"max-num-posts":        {"222222"},
360			},
361		},
362		{
363			name: "non-integer site fetch wait",
364			values: url.Values{
365				"reddit-client-id":     {"THIS_IS_THE_CLIENT_ID"},
366				"reddit-client-secret": {"THIS_IS_THE_CLIENT_SECRET"},
367				"default-num-posts":    {"11111"},
368				"max-num-posts":        {"22222"},
369				"site-fetch-wait":      {"NOPE"},
370			},
371		},
372	}
373
374	defaultSettings := model.Settings{
375		Reddit:          nil,
376		DefaultNumPosts: 21,
377		MaxNumPosts:     70,
378		SiteFetchWait:   3 * time.Hour,
379	}
380
381	for _, test := range tests {
382		t.Run(test.name, func(t *testing.T) {
383			fakeStore := store.NewFake()
384			handler := newHandler(&fakeStore)
385
386			req := httptest.NewRequest(
387				"POST",
388				"/settings",
389				strings.NewReader(test.values.Encode()),
390			)
391			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
392			w := httptest.NewRecorder()
393
394			handler.ServeHTTP(w, req)
395
396			if w.Code != http.StatusBadRequest {
397				t.Fatalf("response was not BAD_REQUEST: %d", w.Code)
398			}
399
400			settings, err := fakeStore.Settings(t.Context())
401			if err != nil {
402				t.Fatalf("failed to get settings: %s", err)
403			}
404
405			modeltest.CompareSettings(t, settings, &defaultSettings)
406		})
407	}
408}
409
410func TestHandlePostSettingsError(t *testing.T) {
411	v := url.Values{}
412	v.Set("reddit-client-id", "THIS_IS_THE_CLIENT_ID")
413	v.Set("reddit-client-secret", "THIS_IS_THE_CLIENT_SECRET")
414	v.Set("default-num-posts", "111111")
415	v.Set("max-num-posts", "222222")
416	v.Set("site-fetch-wait", "333333")
417
418	defaultSettings := model.Settings{
419		Reddit:          nil,
420		DefaultNumPosts: 21,
421		MaxNumPosts:     70,
422		SiteFetchWait:   3 * time.Hour,
423	}
424
425	fakeStore := store.NewFake()
426	injectedErr := fmt.Errorf("INJECTED ERROR")
427	fakeStore.InjectUpdateSettingsError = &injectedErr
428	handler := newHandler(&fakeStore)
429
430	req := httptest.NewRequest(
431		"POST",
432		"/settings",
433		strings.NewReader(v.Encode()),
434	)
435	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
436	w := httptest.NewRecorder()
437
438	handler.ServeHTTP(w, req)
439
440	if w.Code != http.StatusInternalServerError {
441		t.Fatalf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)
442	}
443
444	settings, err := fakeStore.Settings(t.Context())
445	if err != nil {
446		t.Fatalf("failed to get settings: %s", err)
447	}
448
449	modeltest.CompareSettings(t, settings, &defaultSettings)
450}