Loads more tests!

This commit is contained in:
Neale Pickett 2020-08-21 17:02:38 -06:00
parent b1b7fec6d1
commit 8f85b02935
8 changed files with 330 additions and 56 deletions

View File

@ -140,7 +140,7 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter
filename := parts[2] filename := parts[2]
if filename == "" { if filename == "" {
filename = "puzzles.json" filename = "puzzle.json"
} }
points, _ := strconv.Atoi(pointsStr) points, _ := strconv.Atoi(pointsStr)

View File

@ -1,10 +1,125 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"testing" "testing"
"time"
) )
func TestHttpd(t *testing.T) { const TestParticipantID = "shipox"
//emptyBody := bytes.NewReader([]byte{})
//request := httptest.NewRequest("GET", "/", emptyBody) 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())
}
} }

View File

@ -11,10 +11,12 @@ import (
var testFiles = []struct { var testFiles = []struct {
Name, Body string Name, Body string
}{ }{
{"puzzles.txt", "1"}, {"puzzles.txt", "1\n2\n"},
{"answers.txt", "1 answer123\n1 answer456\n"}, {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
{"content/1/puzzle.json", `{"name": "moo"}`}, {"content/1/puzzle.json", `{"name": "moo"}`},
{"content/1/moo.txt", `moo`}, {"content/1/moo.txt", `moo`},
{"content/2/puzzle.json", `{}`},
{"content/2/moo.txt", `moo`},
} }
func (m *Mothballs) createMothball(cat string) { 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 { if err := m.CheckAnswer("pategory", 1, "answer"); err == nil {
t.Error("Wrong answer marked right") t.Error("Wrong answer marked right")
} }
@ -67,6 +79,11 @@ func TestMothballs(t *testing.T) {
if err := m.CheckAnswer("pategory", 1, "answer456"); err != nil { if err := m.CheckAnswer("pategory", 1, "answer456"); err != nil {
t.Error("Right answer marked wrong", err) 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.createMothball("test2")
m.Fs.Remove("pategory.mb") m.Fs.Remove("pategory.mb")

View File

@ -101,9 +101,7 @@ type MothRequestHandler struct {
// PuzzlesOpen opens a file associated with a puzzle. // PuzzlesOpen opens a file associated with a puzzle.
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) { func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
export := mh.ExportState() export := mh.ExportState()
fmt.Println(export.Puzzles)
for _, p := range export.Puzzles[cat] { for _, p := range export.Puzzles[cat] {
fmt.Println(points, p)
if p == points { if p == points {
return mh.Puzzles.Open(cat, points, path) return mh.Puzzles.Open(cat, points, path)
} }

View File

@ -21,7 +21,7 @@ func NewTestServer() *MothServer {
go state.Maintain(TestMaintenanceInterval) go state.Maintain(TestMaintenanceInterval)
theme := NewTestTheme() 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) go theme.Maintain(TestMaintenanceInterval)
return NewMothServer(puzzles, theme, state) return NewMothServer(puzzles, theme, state)
@ -37,7 +37,7 @@ func TestServer(t *testing.T) {
if err := handler.Register(teamName); err != nil { if err := handler.Register(teamName); err != nil {
t.Error(err) t.Error(err)
} }
if r, _, err := handler.ThemeOpen("index.html"); err != nil { if r, _, err := handler.ThemeOpen("/index.html"); err != nil {
t.Error(err) t.Error(err)
} else if contents, err := ioutil.ReadAll(r); err != nil { } else if contents, err := ioutil.ReadAll(r); err != nil {
t.Error(err) t.Error(err)
@ -68,9 +68,23 @@ func TestServer(t *testing.T) {
if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil { if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil {
t.Error(err) t.Error(err)
} else if contents, err := ioutil.ReadAll(r); err != nil { } else if contents, err := ioutil.ReadAll(r); err != nil {
r.Close()
t.Error(err) t.Error(err)
} else if string(contents) != "moo" { } else if string(contents) != "moo" {
r.Close()
t.Error("moo.txt has wrong contents", contents) 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 { if err := handler.CheckAnswer("pategory", 1, "answer123"); err != nil {
@ -83,4 +97,6 @@ func TestServer(t *testing.T) {
if len(es.PointsLog) != 1 { if len(es.PointsLog) != 1 {
t.Error("I didn't get my points!") t.Error("I didn't get my points!")
} }
// BUG(neale): We aren't currently testing the various ways to disable the server
} }

View File

@ -49,52 +49,59 @@ func NewState(fs afero.Fs) *State {
// updateEnabled checks a few things to see if this state directory is "enabled". // updateEnabled checks a few things to see if this state directory is "enabled".
func (s *State) updateEnabled() { func (s *State) updateEnabled() {
if _, err := s.Stat("enabled"); os.IsNotExist(err) {
s.Enabled = false
log.Println("Suspended: enabled file missing")
return
}
nextEnabled := true nextEnabled := true
untilFile, err := s.Open("hours") why := "`state/enabled` present, `state/hours` missing"
if err != nil {
return
}
defer untilFile.Close()
scanner := bufio.NewScanner(untilFile) if untilFile, err := s.Open("hours"); err == nil {
for scanner.Scan() { defer untilFile.Close()
line := scanner.Text() why = "`state/hours` present"
if len(line) < 1 {
continue
}
thisEnabled := true scanner := bufio.NewScanner(untilFile)
switch line[0] { for scanner.Scan() {
case '+': line := scanner.Text()
thisEnabled = true if len(line) < 1 {
line = line[1:] continue
case '-': }
thisEnabled = false
line = line[1:] thisEnabled := true
case '#': switch line[0] {
continue case '+':
default: thisEnabled = true
log.Println("Misformatted line in hours file") line = line[1:]
} case '-':
line = strings.TrimSpace(line) thisEnabled = false
until, err := time.Parse(time.RFC3339, line) line = line[1:]
if err != nil { case '#':
log.Println("Suspended: Unparseable until date:", line) continue
continue default:
} log.Println("Misformatted line in hours file")
if until.Before(time.Now()) { }
nextEnabled = thisEnabled 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 { if nextEnabled != s.Enabled {
s.Enabled = nextEnabled 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. // SetTeamName writes out team name.
// This can only be done once. // This can only be done once.
func (s *State) SetTeamName(teamID, teamName string) error { func (s *State) SetTeamName(teamID, teamName string) error {
f, err := s.Open("teamids.txt") idsFile, err := s.Open("teamids.txt")
if err != nil { if err != nil {
return fmt.Errorf("Team IDs file does not exist") return fmt.Errorf("Team IDs file does not exist")
} }
defer idsFile.Close()
found := false found := false
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(idsFile)
for scanner.Scan() { for scanner.Scan() {
if scanner.Text() == teamID { if scanner.Text() == teamID {
found = true found = true
break break
} }
} }
f.Close()
if !found { if !found {
return fmt.Errorf("Team ID not found in list of valid Team IDs") return fmt.Errorf("Team ID not found in list of valid Team IDs")
} }
teamFile := filepath.Join("teams", teamID) teamFilename := filepath.Join("teams", teamID)
if err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644); os.IsExist(err) { 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") 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. // PointsLog retrieves the current points log.
@ -152,6 +164,7 @@ func (s *State) PointsLog() award.List {
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
log.Println(line)
cur, err := award.Parse(line) cur, err := award.Parse(line)
if err != nil { if err != nil {
log.Printf("Skipping malformed award line %s: %s", line, err) log.Printf("Skipping malformed award line %s: %s", line, err)

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -46,6 +47,10 @@ func TestState(t *testing.T) {
} }
teamID := string(teamIDs[0]) 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 { if err := s.SetTeamName("bad team ID", "bad team name"); err == nil {
t.Errorf("Setting bad team ID didn't raise an error") 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 { if err := s.SetTeamName(teamID, "My Team"); err != nil {
t.Errorf("Setting team name: %v", err) 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" category := "poot"
points := 3928 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() s.refresh()
if err := s.AwardPoints(teamID, category, points); err == nil {
t.Error("Duplicate points award didn't fail")
}
pl = s.PointsLog() pl = s.PointsLog()
if len(pl) != 1 { if len(pl) != 1 {
t.Errorf("After awarding points, points log has length %d", len(pl)) 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) 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.Fs.Remove("initialized")
s.refresh() s.refresh()
@ -73,6 +109,7 @@ func TestState(t *testing.T) {
if len(pl) != 0 { if len(pl) != 0 {
t.Errorf("After reinitialization, points log has length %d", len(pl)) t.Errorf("After reinitialization, points log has length %d", len(pl))
} }
} }
func TestStateEvents(t *testing.T) { 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) { func TestStateMaintainer(t *testing.T) {
updateInterval := 10 * time.Millisecond updateInterval := 10 * time.Millisecond

View File

@ -31,4 +31,9 @@ func TestTheme(t *testing.T) {
} else if !timestamp.Equal(fileInfo.ModTime()) { } else if !timestamp.Equal(fileInfo.ModTime()) {
t.Error("Timestamp compared wrong") 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")
}
} }