Check for new mothballs with the same name

Also updated some error messages to pass newer linter.

Fixes #172
This commit is contained in:
Neale Pickett 2021-10-13 18:25:27 -06:00
parent dd8ca81186
commit f68201ab53
7 changed files with 63 additions and 39 deletions

View File

@ -117,11 +117,11 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite
} }
if err := mh.Register(teamName); err == ErrAlreadyRegistered { if err := mh.Register(teamName); err == ErrAlreadyRegistered {
jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered") jsend.Sendf(w, jsend.Success, "already registered", "team ID has already been registered")
} else if err != nil { } else if err != nil {
jsend.Sendf(w, jsend.Fail, "not registered", err.Error()) jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
} else { } else {
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered") jsend.Sendf(w, jsend.Success, "registered", "team ID registered")
} }
} }

View File

@ -18,10 +18,8 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest
vals := url.Values{} vals := url.Values{}
vals.Set("pid", TestParticipantID) vals.Set("pid", TestParticipantID)
vals.Set("id", TestTeamID) vals.Set("id", TestTeamID)
if args != nil { for k, v := range args {
for k, v := range args { vals.Set(k, v)
vals.Set(k, v)
}
} }
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -56,19 +54,19 @@ func TestHttpd(t *testing.T) {
if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) 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"}}` { } 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") t.Error("Register bad team ID failed")
} }
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"Team ID registered"}}` { } else if r.Body.String() != `{"status":"success","data":{"short":"registered","description":"team ID registered"}}` {
t.Error("Register failed") t.Error("Register failed")
} }
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` { } else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"team ID has already been registered"}}` {
t.Error("Register failed", r.Body.String()) t.Error("Register failed", r.Body.String())
} }
@ -102,7 +100,7 @@ func TestHttpd(t *testing.T) {
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Incorrect answer"}}` { } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"incorrect answer"}}` {
t.Error("Unexpected body", r.Body.String()) t.Error("Unexpected body", r.Body.String())
} }
@ -131,7 +129,7 @@ func TestHttpd(t *testing.T) {
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Error awarding points: Points already awarded to this team in this category"}}` { } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` {
t.Error("Unexpected body", r.Body.String()) t.Error("Unexpected body", r.Body.String())
} }
} }

View File

@ -19,6 +19,7 @@ import (
type zipCategory struct { type zipCategory struct {
afero.Fs afero.Fs
io.Closer io.Closer
mtime time.Time
} }
// Mothballs provides a collection of active mothball files (puzzle categories) // Mothballs provides a collection of active mothball files (puzzle categories)
@ -48,7 +49,7 @@ func (m *Mothballs) getCat(cat string) (zipCategory, bool) {
func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) { func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
zc, ok := m.getCat(cat) zc, ok := m.getCat(cat)
if !ok { if !ok {
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat) return nil, time.Time{}, fmt.Errorf("no such category: %s", cat)
} }
f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename)) f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
@ -91,12 +92,12 @@ func (m *Mothballs) Inventory() []Category {
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) { func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
zfs, ok := m.getCat(cat) zfs, ok := m.getCat(cat)
if !ok { if !ok {
return false, fmt.Errorf("No such category: %s", cat) return false, fmt.Errorf("no such category: %s", cat)
} }
af, err := zfs.Open("answers.txt") af, err := zfs.Open("answers.txt")
if err != nil { if err != nil {
return false, fmt.Errorf("No answers.txt file") return false, fmt.Errorf("no answers.txt file")
} }
defer af.Close() defer af.Close()
@ -132,7 +133,18 @@ func (m *Mothballs) refresh() {
categoryName := strings.TrimSuffix(filename, ".mb") categoryName := strings.TrimSuffix(filename, ".mb")
found[categoryName] = true found[categoryName] = true
if _, ok := m.categories[categoryName]; !ok { reopen := false
if existingMothball, ok := m.categories[categoryName]; !ok {
reopen = true
} else if si, err := m.Fs.Stat(filename); err != nil {
log.Println(err)
} else if si.ModTime().After(existingMothball.mtime) {
existingMothball.Close()
delete(m.categories, categoryName)
reopen = true
}
if reopen {
f, err := m.Fs.Open(filename) f, err := m.Fs.Open(filename)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -174,7 +186,7 @@ func (m *Mothballs) refresh() {
// Mothball just returns an error // Mothball just returns an error
func (m *Mothballs) Mothball(cat string, w io.Writer) error { func (m *Mothballs) Mothball(cat string, w io.Writer) error {
return fmt.Errorf("Refusing to repackage a compiled mothball") return fmt.Errorf("refusing to repackage a compiled mothball")
} }
// Maintain performs housekeeping for Mothballs. // Maintain performs housekeeping for Mothballs.

View File

@ -3,6 +3,8 @@ package main
import ( import (
"archive/zip" "archive/zip"
"fmt" "fmt"
"io/ioutil"
"log"
"testing" "testing"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -14,14 +16,13 @@ var testFiles = []struct {
{"puzzles.txt", "1\n3\n2\n"}, {"puzzles.txt", "1\n3\n2\n"},
{"answers.txt", "1 answer123\n1 answer456\n2 wat\n"}, {"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
{"1/puzzle.json", `{"name": "moo"}`}, {"1/puzzle.json", `{"name": "moo"}`},
{"1/moo.txt", `moo`},
{"2/puzzle.json", `{}`}, {"2/puzzle.json", `{}`},
{"2/moo.txt", `moo`}, {"2/moo.txt", `moo`},
{"3/puzzle.json", `{}`}, {"3/puzzle.json", `{}`},
{"3/moo.txt", `moo`}, {"3/moo.txt", `moo`},
} }
func (m *Mothballs) createMothball(cat string) { func (m *Mothballs) createMothballWithMoo1(cat string, moo1 string) {
f, _ := m.Create(fmt.Sprintf("%s.mb", cat)) f, _ := m.Create(fmt.Sprintf("%s.mb", cat))
defer f.Close() defer f.Close()
@ -32,6 +33,13 @@ func (m *Mothballs) createMothball(cat string) {
of, _ := w.Create(file.Name) of, _ := w.Create(file.Name)
of.Write([]byte(file.Body)) of.Write([]byte(file.Body))
} }
of, _ := w.Create("1/moo.txt")
of.Write([]byte(moo1))
}
func (m *Mothballs) createMothball(cat string) {
m.createMothballWithMoo1(cat, "moo")
} }
func NewTestMothballs() *Mothballs { func NewTestMothballs() *Mothballs {
@ -92,10 +100,23 @@ func TestMothballs(t *testing.T) {
} }
if ok, err := m.CheckAnswer("nealegory", 1, "moo"); ok { if ok, err := m.CheckAnswer("nealegory", 1, "moo"); ok {
t.Error("Checking answer in non-existent category should fail") t.Error("Checking answer in non-existent category should fail")
} else if err.Error() != "No such category: nealegory" { } else if err.Error() != "no such category: nealegory" {
t.Error("Wrong error message") t.Error("Wrong error message")
} }
goofyText := "bozonics"
log.Print("Hey Bozo")
//time.Sleep(1 * time.Second) // I don't love this, but we need the mtime to increase, and it's only accurate to 1s
m.createMothballWithMoo1("pategory", goofyText)
m.refresh()
if f, _, err := m.Open("pategory", 1, "moo.txt"); err != nil {
t.Error("pategory/1/moo.txt", err)
} else if contents, err := ioutil.ReadAll(f); err != nil {
t.Error("read all pategory/1/moo.txt", err)
} else if string(contents) != goofyText {
t.Error("read all replacement pategory/1/moo.txt contents wrong, got", string(contents))
}
m.createMothball("test2") m.createMothball("test2")
m.Fs.Remove("pategory.mb") m.Fs.Remove("pategory.mb")
m.refresh() m.refresh()

View File

@ -125,7 +125,7 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo
// Mothball just returns an error // Mothball just returns an error
func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) { func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) {
return nil, fmt.Errorf("Can't package a command-generated category") return nil, fmt.Errorf("can't package a command-generated category")
} }
// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping // Maintain does nothing: a command puzzle ProviderCommand has no housekeeping

View File

@ -115,7 +115,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
} }
} }
if !found { if !found {
return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked") return nil, time.Time{}, fmt.Errorf("puzzle does not exist or is locked")
} }
// Try every provider until someone doesn't return an error // Try every provider until someone doesn't return an error
@ -146,16 +146,16 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string)
} }
if !correct { if !correct {
mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points) mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points)
return fmt.Errorf("Incorrect answer") return fmt.Errorf("incorrect answer")
} }
mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points) mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points)
if _, err := mh.State.TeamName(mh.teamID); err != nil { if _, err := mh.State.TeamName(mh.teamID); err != nil {
return fmt.Errorf("Invalid team ID") return fmt.Errorf("invalid team ID")
} }
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil { if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
return fmt.Errorf("Error awarding points: %s", err) return fmt.Errorf("error awarding points: %s", err)
} }
return nil return nil
@ -170,7 +170,7 @@ func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time,
func (mh *MothRequestHandler) Register(teamName string) error { func (mh *MothRequestHandler) Register(teamName string) error {
// BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success // BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success
if teamName == "" { if teamName == "" {
return fmt.Errorf("Empty team name") return fmt.Errorf("empty team name")
} }
mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0) mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0)
return mh.State.SetTeamName(mh.teamID, teamName) return mh.State.SetTeamName(mh.teamID, teamName)
@ -254,7 +254,7 @@ func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error {
var err error var err error
if !mh.Config.Devel { if !mh.Config.Devel {
return fmt.Errorf("Cannot mothball in production mode") return fmt.Errorf("cannot mothball in production mode")
} }
for _, provider := range mh.PuzzleProviders { for _, provider := range mh.PuzzleProviders {
if err = provider.Mothball(cat, w); err == nil { if err = provider.Mothball(cat, w); err == nil {

View File

@ -27,7 +27,7 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy="
const RFC3339Space = "2006-01-02 15:04:05Z07:00" const RFC3339Space = "2006-01-02 15:04:05Z07:00"
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously. // ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
var ErrAlreadyRegistered = errors.New("Team ID has already been registered") var ErrAlreadyRegistered = errors.New("team ID has already been registered")
// State defines the current state of a MOTH instance. // State defines the current state of a MOTH instance.
// We use the filesystem for synchronization between threads. // We use the filesystem for synchronization between threads.
@ -123,9 +123,9 @@ func (s *State) TeamName(teamID string) (string, error) {
teamFs := afero.NewBasePathFs(s.Fs, "teams") teamFs := afero.NewBasePathFs(s.Fs, "teams")
teamNameBytes, err := afero.ReadFile(teamFs, teamID) teamNameBytes, err := afero.ReadFile(teamFs, teamID)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return "", fmt.Errorf("Unregistered team ID: %s", teamID) return "", fmt.Errorf("unregistered team ID: %s", teamID)
} else if err != nil { } else if err != nil {
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err) return "", fmt.Errorf("unregistered team ID: %s (%s)", teamID, err)
} }
teamName := strings.TrimSpace(string(teamNameBytes)) teamName := strings.TrimSpace(string(teamNameBytes))
@ -137,7 +137,7 @@ func (s *State) TeamName(teamID string) (string, error) {
func (s *State) SetTeamName(teamID, teamName string) error { func (s *State) SetTeamName(teamID, teamName string) error {
idsFile, 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() defer idsFile.Close()
found := false found := false
@ -149,7 +149,7 @@ func (s *State) SetTeamName(teamID, teamName string) error {
} }
} }
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")
} }
teamFilename := filepath.Join("teams", teamID) teamFilename := filepath.Join("teams", teamID)
@ -211,7 +211,7 @@ func (s *State) AwardPoints(teamID, category string, points int) error {
for _, e := range s.PointsLog() { for _, e := range s.PointsLog() {
if a.Equal(e) { if a.Equal(e) {
return fmt.Errorf("Points already awarded to this team in this category") return fmt.Errorf("points already awarded to this team in this category")
} }
} }
@ -363,13 +363,6 @@ func (s *State) maybeInitialize() {
} }
} }
func logstr(s string) string {
if s == "" {
return "-"
}
return s
}
// LogEvent writes to the event log // LogEvent writes to the event log
func (s *State) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) { func (s *State) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
s.eventStream <- append( s.eventStream <- append(