1package server23// TODO: Write test for /feed/<SITE>. This will require the fake fetcher.45import (6 "fmt"7 "net/http"8 "net/http/httptest"9 "net/url"10 "strings"11 "testing"12 "time"1314 "website-feeds/model"15 "website-feeds/model/modeltest"16 "website-feeds/store"17)1819func TestHandleGetRootSuccess(t *testing.T) {20 fakeStore := store.NewFake()21 handler := newHandler(&fakeStore)2223 req := httptest.NewRequest("GET", "/", nil)24 w := httptest.NewRecorder()2526 handler.ServeHTTP(w, req)2728 if w.Code != http.StatusOK {29 t.Errorf("response was not OK: %d", w.Code)30 }31}3233func TestHandleGetRootInvalidHost(t *testing.T) {34 fakeStore := store.NewFake()35 handler := newHandler(&fakeStore)3637 req := httptest.NewRequest("GET", "/", nil)38 req.RemoteAddr = "!!!!!!!"39 w := httptest.NewRecorder()4041 handler.ServeHTTP(w, req)4243 if w.Code != http.StatusInternalServerError {44 t.Errorf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)45 }46}4748func TestHandleGetStaticSuccess(t *testing.T) {49 fakeStore := store.NewFake()50 handler := newHandler(&fakeStore)5152 req := httptest.NewRequest("GET", "/static/favicon.ico", nil)53 w := httptest.NewRecorder()5455 handler.ServeHTTP(w, req)5657 if w.Code != http.StatusOK {58 t.Errorf("response was not OK: %d", w.Code)59 }60}6162func TestHandleGetStaticNotFound(t *testing.T) {63 fakeStore := store.NewFake()64 handler := newHandler(&fakeStore)6566 req := httptest.NewRequest("GET", "/static/i-definitely-do-not-exist", nil)67 w := httptest.NewRecorder()6869 handler.ServeHTTP(w, req)7071 if w.Code != http.StatusNotFound {72 t.Errorf("response was not NOT_FOUND: %d", w.Code)73 }74}7576func TestHandleGetSettingsSuccessDefault(t *testing.T) {77 fakeStore := store.NewFake()78 handler := newHandler(&fakeStore)7980 req := httptest.NewRequest("GET", "/settings", nil)81 w := httptest.NewRecorder()8283 handler.ServeHTTP(w, req)8485 if w.Code != http.StatusOK {86 t.Errorf("response was not OK: %d", w.Code)87 }88}8990func 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 }100101 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 }107108 req := httptest.NewRequest("GET", "/settings", nil)109 w := httptest.NewRecorder()110111 handler.ServeHTTP(w, req)112113 if w.Code != http.StatusOK {114 t.Errorf("response was not OK: %d", w.Code)115 }116117 body := w.Body.String()118119 if !strings.Contains(body, "111111") {120 t.Error("body didn't contain DefaultNumPosts")121 }122123 if !strings.Contains(body, "222222") {124 t.Error("body didn't contain MaxNumPosts")125 }126127 if !strings.Contains(body, "333333") {128 t.Error("body didn't contain SiteFetchWait")129 }130131 if !strings.Contains(body, "THIS_IS_THE_CLIENT_ID") {132 t.Error("body didn't contain Reddit.ClientID")133 }134135 if !strings.Contains(body, "THIS_IS_THE_CLIENT_SECRET") {136 t.Error("body didn't contain Reddit.ClientSecret")137 }138}139140func TestHandleGetSettingsFailure(t *testing.T) {141 fakeStore := store.NewFake()142 injectedErr := fmt.Errorf("oopsie")143 fakeStore.InjectSettingsError = &injectedErr144 handler := newHandler(&fakeStore)145146 req := httptest.NewRequest("GET", "/settings", nil)147 w := httptest.NewRecorder()148149 handler.ServeHTTP(w, req)150151 if w.Code != http.StatusInternalServerError {152 t.Errorf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)153 }154}155156func TestHandlePostSettingsSuccess(t *testing.T) {157 tests := []struct {158 name string159 values url.Values160 expectedSettings *model.Settings161 }{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 }212213 for _, test := range tests {214 t.Run(test.name, func(t *testing.T) {215 fakeStore := store.NewFake()216 handler := newHandler(&fakeStore)217218 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()225226 handler.ServeHTTP(w, req)227228 if w.Code != http.StatusSeeOther {229 t.Fatalf("response was not SEE_OTHER: %d", w.Code)230 }231232 settings, err := fakeStore.Settings(t.Context())233 if err != nil {234 t.Fatalf("failed to get settings: %s", err)235 }236237 modeltest.CompareSettings(t, settings, test.expectedSettings)238 })239 }240}241242func TestHandlePostSettingsInvalid(t *testing.T) {243 tests := []struct {244 name string245 values url.Values246 }{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 }373374 defaultSettings := model.Settings{375 Reddit: nil,376 DefaultNumPosts: 21,377 MaxNumPosts: 70,378 SiteFetchWait: 3 * time.Hour,379 }380381 for _, test := range tests {382 t.Run(test.name, func(t *testing.T) {383 fakeStore := store.NewFake()384 handler := newHandler(&fakeStore)385386 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()393394 handler.ServeHTTP(w, req)395396 if w.Code != http.StatusBadRequest {397 t.Fatalf("response was not BAD_REQUEST: %d", w.Code)398 }399400 settings, err := fakeStore.Settings(t.Context())401 if err != nil {402 t.Fatalf("failed to get settings: %s", err)403 }404405 modeltest.CompareSettings(t, settings, &defaultSettings)406 })407 }408}409410func 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")417418 defaultSettings := model.Settings{419 Reddit: nil,420 DefaultNumPosts: 21,421 MaxNumPosts: 70,422 SiteFetchWait: 3 * time.Hour,423 }424425 fakeStore := store.NewFake()426 injectedErr := fmt.Errorf("INJECTED ERROR")427 fakeStore.InjectUpdateSettingsError = &injectedErr428 handler := newHandler(&fakeStore)429430 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()437438 handler.ServeHTTP(w, req)439440 if w.Code != http.StatusInternalServerError {441 t.Fatalf("response was not INTERNAL_SERVER_ERROR: %d", w.Code)442 }443444 settings, err := fakeStore.Settings(t.Context())445 if err != nil {446 t.Fatalf("failed to get settings: %s", err)447 }448449 modeltest.CompareSettings(t, settings, &defaultSettings)450}