diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 7540cd7..a31c2da 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -140,7 +140,7 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter filename := parts[2] if filename == "" { - filename = "puzzles.json" + filename = "puzzle.json" } points, _ := strconv.Atoi(pointsStr) diff --git a/cmd/mothd/httpd_test.go b/cmd/mothd/httpd_test.go index fe08ad4..30f7396 100644 --- a/cmd/mothd/httpd_test.go +++ b/cmd/mothd/httpd_test.go @@ -1,10 +1,125 @@ package main import ( + "bytes" + "encoding/json" + "fmt" + "net/http/httptest" + "net/url" "testing" + "time" ) -func TestHttpd(t *testing.T) { - //emptyBody := bytes.NewReader([]byte{}) - //request := httptest.NewRequest("GET", "/", emptyBody) +const TestParticipantID = "shipox" + +func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest.ResponseRecorder { + vals := url.Values{} + vals.Set("pid", TestParticipantID) + vals.Set("id", TestTeamID) + if args != nil { + for k, v := range args { + vals.Set(k, v) + } + } + + recorder := httptest.NewRecorder() + request := httptest.NewRequest( + "GET", + fmt.Sprintf("%s?%s", path, vals.Encode()), + bytes.NewReader([]byte{}), + ) + hs.ServeHTTP(recorder, request) + return recorder +} + +func TestHttpd(t *testing.T) { + hs := NewHTTPServer("/", NewTestServer()) + + if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/index.html", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } + if r := hs.TestRequest("/rolodex.html", nil); r.Result().StatusCode != 404 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{"pategory":[1]}}` { + t.Error("Unexpected state") + } + + if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"fail","data":{"short":"not registered","description":"Team ID not found in list of valid Team IDs"}}` { + t.Error("Register bad team ID failed") + } + + if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"Team ID registered"}}` { + t.Error("Register failed") + } + + if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` { + t.Error("Unexpected state", r.Body.String()) + } + + if r := hs.TestRequest("/content/pategory", nil); r.Result().StatusCode != 404 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/content/pategory/1/not-here", nil); r.Result().StatusCode != 404 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/content/pategory/2/moo.txt", nil); r.Result().StatusCode != 404 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/content/pategory/1/", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } + + if r := hs.TestRequest("/content/pategory/1/moo.txt", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `moo` { + t.Error("Unexpected body", r.Body.String()) + } + + if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Invalid answer"}}` { + t.Error("Unexpected body", r.Body.String()) + } + + if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"success","data":{"short":"accepted","description":"1 points awarded in pategory"}}` { + t.Error("Unexpected body", r.Body.String()) + } + + time.Sleep(TestMaintenanceInterval) + + state := StateExport{} + if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil { + t.Error(err) + } else if len(state.PointsLog) != 1 { + t.Error("Points log wrong length") + } else if len(state.Puzzles["pategory"]) != 2 { + t.Error("Didn't unlock next puzzle") + } + + if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { + t.Error(r.Result()) + } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Points already awarded to this team in this category"}}` { + t.Error("Unexpected body", r.Body.String()) + } } diff --git a/cmd/mothd/mothballs_test.go b/cmd/mothd/mothballs_test.go index d27dd83..bc56cd3 100644 --- a/cmd/mothd/mothballs_test.go +++ b/cmd/mothd/mothballs_test.go @@ -11,10 +11,12 @@ import ( var testFiles = []struct { Name, Body string }{ - {"puzzles.txt", "1"}, - {"answers.txt", "1 answer123\n1 answer456\n"}, + {"puzzles.txt", "1\n2\n"}, + {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"}, {"content/1/puzzle.json", `{"name": "moo"}`}, {"content/1/moo.txt", `moo`}, + {"content/2/puzzle.json", `{}`}, + {"content/2/moo.txt", `moo`}, } func (m *Mothballs) createMothball(cat string) { @@ -58,6 +60,16 @@ func TestMothballs(t *testing.T) { } } + if f, _, err := m.Open("nealegory", 1, "puzzle.json"); err == nil { + f.Close() + t.Error("You can't open a puzzle in a nealegory, that doesn't even rhyme!") + } + + if f, _, err := m.Open("pategory", 1, "bozo"); err == nil { + f.Close() + t.Error("This file shouldn't exist") + } + if err := m.CheckAnswer("pategory", 1, "answer"); err == nil { t.Error("Wrong answer marked right") } @@ -67,6 +79,11 @@ func TestMothballs(t *testing.T) { if err := m.CheckAnswer("pategory", 1, "answer456"); err != nil { t.Error("Right answer marked wrong", err) } + if err := m.CheckAnswer("nealegory", 1, "moo"); err == nil { + t.Error("Checking answer in non-existent category should fail") + } else if err.Error() != "No such category: nealegory" { + t.Error("Wrong error message") + } m.createMothball("test2") m.Fs.Remove("pategory.mb") diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 395445b..e50eb0a 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -101,9 +101,7 @@ type MothRequestHandler struct { // PuzzlesOpen opens a file associated with a puzzle. func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) { export := mh.ExportState() - fmt.Println(export.Puzzles) for _, p := range export.Puzzles[cat] { - fmt.Println(points, p) if p == points { return mh.Puzzles.Open(cat, points, path) } diff --git a/cmd/mothd/server_test.go b/cmd/mothd/server_test.go index 4d21e80..fc57708 100644 --- a/cmd/mothd/server_test.go +++ b/cmd/mothd/server_test.go @@ -21,7 +21,7 @@ func NewTestServer() *MothServer { go state.Maintain(TestMaintenanceInterval) theme := NewTestTheme() - afero.WriteFile(theme.Fs, "index.html", []byte("index.html"), 0644) + afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644) go theme.Maintain(TestMaintenanceInterval) return NewMothServer(puzzles, theme, state) @@ -37,7 +37,7 @@ func TestServer(t *testing.T) { if err := handler.Register(teamName); err != nil { t.Error(err) } - if r, _, err := handler.ThemeOpen("index.html"); err != nil { + if r, _, err := handler.ThemeOpen("/index.html"); err != nil { t.Error(err) } else if contents, err := ioutil.ReadAll(r); err != nil { t.Error(err) @@ -68,9 +68,23 @@ func TestServer(t *testing.T) { if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil { t.Error(err) } else if contents, err := ioutil.ReadAll(r); err != nil { + r.Close() t.Error(err) } else if string(contents) != "moo" { + r.Close() t.Error("moo.txt has wrong contents", contents) + } else { + r.Close() + } + + if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzles.json"); err == nil { + t.Error("Opening locked puzzle shouldn't work") + r.Close() + } + + if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzles.json"); err == nil { + t.Error("Opening non-existent puzzle shouldn't work") + r.Close() } if err := handler.CheckAnswer("pategory", 1, "answer123"); err != nil { @@ -83,4 +97,6 @@ func TestServer(t *testing.T) { if len(es.PointsLog) != 1 { t.Error("I didn't get my points!") } + + // BUG(neale): We aren't currently testing the various ways to disable the server } diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index dd7f48f..0f00d18 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -49,52 +49,59 @@ func NewState(fs afero.Fs) *State { // updateEnabled checks a few things to see if this state directory is "enabled". func (s *State) updateEnabled() { - if _, err := s.Stat("enabled"); os.IsNotExist(err) { - s.Enabled = false - log.Println("Suspended: enabled file missing") - return - } - nextEnabled := true - untilFile, err := s.Open("hours") - if err != nil { - return - } - defer untilFile.Close() + why := "`state/enabled` present, `state/hours` missing" - scanner := bufio.NewScanner(untilFile) - for scanner.Scan() { - line := scanner.Text() - if len(line) < 1 { - continue - } + if untilFile, err := s.Open("hours"); err == nil { + defer untilFile.Close() + why = "`state/hours` present" - thisEnabled := true - switch line[0] { - case '+': - thisEnabled = true - line = line[1:] - case '-': - thisEnabled = false - line = line[1:] - case '#': - continue - default: - log.Println("Misformatted line in hours file") - } - line = strings.TrimSpace(line) - until, err := time.Parse(time.RFC3339, line) - if err != nil { - log.Println("Suspended: Unparseable until date:", line) - continue - } - if until.Before(time.Now()) { - nextEnabled = thisEnabled + scanner := bufio.NewScanner(untilFile) + for scanner.Scan() { + line := scanner.Text() + if len(line) < 1 { + continue + } + + thisEnabled := true + switch line[0] { + case '+': + thisEnabled = true + line = line[1:] + case '-': + thisEnabled = false + line = line[1:] + case '#': + continue + default: + log.Println("Misformatted line in hours file") + } + line = strings.TrimSpace(line) + until, err := time.Parse(time.RFC3339, line) + if err != nil { + log.Println("Suspended: Unparseable until date:", line) + continue + } + if until.Before(time.Now()) { + nextEnabled = thisEnabled + } } } + + if _, err := s.Stat("enabled"); os.IsNotExist(err) { + dirs, _ := afero.ReadDir(s, ".") + for _, dir := range dirs { + log.Println(dir.Name()) + } + + log.Print(s, err) + nextEnabled = false + why = "`state/enabled` missing" + } + if nextEnabled != s.Enabled { s.Enabled = nextEnabled - log.Println("Setting enabled to", s.Enabled, "based on hours file") + log.Printf("Setting enabled=%v: %s", s.Enabled, why) } } @@ -115,28 +122,33 @@ func (s *State) TeamName(teamID string) (string, error) { // SetTeamName writes out team name. // This can only be done once. func (s *State) SetTeamName(teamID, teamName string) error { - f, err := s.Open("teamids.txt") + idsFile, err := s.Open("teamids.txt") if err != nil { return fmt.Errorf("Team IDs file does not exist") } + defer idsFile.Close() found := false - scanner := bufio.NewScanner(f) + scanner := bufio.NewScanner(idsFile) for scanner.Scan() { if scanner.Text() == teamID { found = true break } } - f.Close() if !found { return fmt.Errorf("Team ID not found in list of valid Team IDs") } - teamFile := filepath.Join("teams", teamID) - if err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644); os.IsExist(err) { + teamFilename := filepath.Join("teams", teamID) + teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_EXCL, 0644) + if os.IsExist(err) { return fmt.Errorf("Team ID is already registered") + } else if err != nil { + return err } - return err + defer teamFile.Close() + fmt.Fprintln(teamFile, teamName) + return nil } // PointsLog retrieves the current points log. @@ -152,6 +164,7 @@ func (s *State) PointsLog() award.List { scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() + log.Println(line) cur, err := award.Parse(line) if err != nil { log.Printf("Skipping malformed award line %s: %s", line, err) diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 0785c51..9e024de 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "os" "strings" "testing" @@ -46,6 +47,10 @@ func TestState(t *testing.T) { } teamID := string(teamIDs[0]) + if _, err := s.TeamName(teamID); err == nil { + t.Errorf("Bad team ID lookup didn't return error") + } + if err := s.SetTeamName("bad team ID", "bad team name"); err == nil { t.Errorf("Setting bad team ID didn't raise an error") } @@ -53,12 +58,31 @@ func TestState(t *testing.T) { if err := s.SetTeamName(teamID, "My Team"); err != nil { t.Errorf("Setting team name: %v", err) } + if err := s.SetTeamName(teamID, "wat"); err == nil { + t.Errorf("Registering team a second time didn't fail") + } category := "poot" points := 3928 - s.AwardPoints(teamID, category, points) + if err := s.AwardPoints(teamID, category, points); err != nil { + t.Error(err) + } + if err := s.AwardPoints(teamID, category, points); err != nil { + t.Error("Two awards before refresh:", err) + } + // Flex duplicate detection with different timestamp + if f, err := s.Create("points.new/moo"); err != nil { + t.Error("Creating duplicate points file:", err) + } else { + fmt.Fprintln(f, time.Now().Unix()+1, teamID, category, points) + f.Close() + } s.refresh() + if err := s.AwardPoints(teamID, category, points); err == nil { + t.Error("Duplicate points award didn't fail") + } + pl = s.PointsLog() if len(pl) != 1 { t.Errorf("After awarding points, points log has length %d", len(pl)) @@ -66,6 +90,18 @@ func TestState(t *testing.T) { t.Errorf("Incorrect logged award %v", pl) } + afero.WriteFile(s, "points.log", []byte("intentional parse error\n"), 0644) + if len(s.PointsLog()) != 0 { + t.Errorf("Intentional parse error breaks pointslog") + } + if err := s.AwardPoints(teamID, category, points); err != nil { + t.Error(err) + } + s.refresh() + if len(s.PointsLog()) != 1 { + t.Error("Intentional parse error screws up all parsing") + } + s.Fs.Remove("initialized") s.refresh() @@ -73,6 +109,7 @@ func TestState(t *testing.T) { if len(pl) != 0 { t.Errorf("After reinitialization, points log has length %d", len(pl)) } + } func TestStateEvents(t *testing.T) { @@ -88,6 +125,79 @@ func TestStateEvents(t *testing.T) { } } +func TestStateDisabled(t *testing.T) { + s := NewTestState() + s.refresh() + + if !s.Enabled { + t.Error("Brand new state is disabled") + } + + hoursFile, err := s.Create("hours") + if err != nil { + t.Error(err) + } + defer hoursFile.Close() + + fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z") + hoursFile.Sync() + s.refresh() + if s.Enabled { + t.Error("Disabling 1970-01-01") + } + + fmt.Fprintln(hoursFile, "+ 1970-01-01T01:01:01+05:00") + hoursFile.Sync() + s.refresh() + if !s.Enabled { + t.Error("Enabling 1970-01-02") + } + + fmt.Fprintln(hoursFile, "") + fmt.Fprintln(hoursFile, "# Comment") + hoursFile.Sync() + s.refresh() + if !s.Enabled { + t.Error("Comments") + } + + fmt.Fprintln(hoursFile, "intentional parse error") + hoursFile.Sync() + s.refresh() + if !s.Enabled { + t.Error("intentional parse error") + } + + fmt.Fprintln(hoursFile, "- 1980-01-01T01:01:01Z") + hoursFile.Sync() + s.refresh() + if s.Enabled { + t.Error("Disabling 1980-01-01") + } + + if err := s.Remove("hours"); err != nil { + t.Error(err) + } + s.refresh() + if !s.Enabled { + t.Error("Removing `hours` disabled event") + } + + if err := s.Remove("enabled"); err != nil { + t.Error(err) + } + s.refresh() + if s.Enabled { + t.Error("Removing `enabled` didn't disable") + } + + s.Remove("initialized") + s.refresh() + if !s.Enabled { + t.Error("Re-initalizing didn't start event") + } +} + func TestStateMaintainer(t *testing.T) { updateInterval := 10 * time.Millisecond diff --git a/cmd/mothd/theme_test.go b/cmd/mothd/theme_test.go index ad09ee9..c042264 100644 --- a/cmd/mothd/theme_test.go +++ b/cmd/mothd/theme_test.go @@ -31,4 +31,9 @@ func TestTheme(t *testing.T) { } else if !timestamp.Equal(fileInfo.ModTime()) { t.Error("Timestamp compared wrong") } + + if f, _, err := s.Open("nofile"); err == nil { + f.Close() + t.Error("Opening non-existent file didn't return an error") + } }