JSON dicts for back-end API

This commit is contained in:
Neale Pickett 2020-10-16 14:18:44 -06:00
parent 682a6a7f86
commit bb2565b06d
11 changed files with 75 additions and 34 deletions

View File

@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Clear Debug.summary field when making mothballs - Clear Debug.summary field when making mothballs
### Changed
- Regulated category/puzzle provider API: now everything returns a JSON dictionary (or octet stream for files)
### Added ### Added
- More log events - More log events
- [Log channels document](docs/logs.md) - [Log channels document](docs/logs.md)

View File

@ -4,6 +4,7 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -13,6 +14,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/dirtbags/moth/pkg/transpile"
) )
// ProviderCommand specifies a command to run for the puzzle API // ProviderCommand specifies a command to run for the puzzle API
@ -111,14 +114,13 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo
} else if err != nil { } else if err != nil {
return false, err return false, err
} }
result := strings.TrimSpace(string(stdout))
if result != "correct" { ans := transpile.AnswerResponse{}
log.Printf("WARNING: %s: Nothing written to stdout", pc.Path) if err := json.Unmarshal(stdout, &ans); err != nil {
return false, nil return false, err
} }
return true, nil return ans.Correct, nil
} }
// Mothball just returns an error // Mothball just returns an error

View File

@ -185,7 +185,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor
export.Config = mh.Config export.Config = mh.Config
teamName, err := mh.State.TeamName(mh.teamID) teamName, err := mh.State.TeamName(mh.teamID)
registered := override || (err == nil) registered := override || mh.Config.Devel || (err == nil)
export.Messages = mh.State.Messages() export.Messages = mh.State.Messages()
export.TeamNames = make(map[string]string) export.TeamNames = make(map[string]string)

View File

@ -27,7 +27,23 @@ func NewTestServer() *MothServer {
return NewMothServer(Configuration{}, theme, state, puzzles) return NewMothServer(Configuration{}, theme, state, puzzles)
} }
func TestServer(t *testing.T) { func TestDevelServer(t *testing.T) {
server := NewTestServer()
server.Config.Devel = true
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
{
es := anonHandler.ExportState()
if !es.Config.Devel {
t.Error("Not marked as development server")
}
if len(es.Puzzles) != 1 {
t.Error("Wrong puzzles for anonymous state on devel server:", es.Puzzles)
}
}
}
func TestProdServer(t *testing.T) {
teamName := "OurTeam" teamName := "OurTeam"
participantID := "participantID" participantID := "participantID"
teamID := TestTeamID teamID := TestTeamID

View File

@ -26,9 +26,9 @@ EOT
;; ;;
answer:pategory:1) answer:pategory:1)
if [ "$ANSWER" = "answer" ]; then if [ "$ANSWER" = "answer" ]; then
echo "correct" echo '{"Correct":true}'
else else
echo "Sorry, wrong answer." echo '{"Correct":false}'
fi fi
;; ;;
answer:pategory:2) answer:pategory:2)

View File

@ -45,12 +45,14 @@ def open_file(filename):
shutil.copyfileobj(f, sys.stdout.buffer) shutil.copyfileobj(f, sys.stdout.buffer)
def check_answer(check): def check_answer(check):
if answer == check: obj = {
print("correct") "Correct": (answer == check)
else: }
print("incorrect") json.dump(obj, sys.stdout)
if len(sys.argv) == 1: if len(sys.argv) == 1:
raise RuntimeError("Command not provided")
elif sys.argv[1] == "puzzle":
puzzle() puzzle()
elif sys.argv[1] == "file": elif sys.argv[1] == "file":
open_file(sys.argv[2]) open_file(sys.argv[2])

View File

@ -9,12 +9,16 @@ import (
"os/exec" "os/exec"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
// InventoryResponse is what's handed back when we ask for an inventory.
type InventoryResponse struct {
Puzzles []int
}
// Category defines the functionality required to be a puzzle category. // Category defines the functionality required to be a puzzle category.
type Category interface { type Category interface {
// Inventory lists every puzzle in the category. // Inventory lists every puzzle in the category.
@ -141,12 +145,12 @@ func (c FsCommandCategory) Inventory() ([]int, error) {
return nil, err return nil, err
} }
ret := make([]int, 0) inv := InventoryResponse{}
if err := json.Unmarshal(stdout, &ret); err != nil { if err := json.Unmarshal(stdout, &inv); err != nil {
return nil, err return nil, err
} }
return ret, nil return inv.Puzzles, nil
} }
// Puzzle returns a Puzzle structure for the given point value. // Puzzle returns a Puzzle structure for the given point value.
@ -181,10 +185,11 @@ func (c FsCommandCategory) Answer(points int, answer string) bool {
return false return false
} }
switch strings.TrimSpace(string(stdout)) { ans := AnswerResponse{}
case "correct": if err := json.Unmarshal(stdout, &ans); err != nil {
return true log.Printf("ERROR: Answering %d points: %s", points, err)
return false
} }
return false return ans.Correct
} }

View File

@ -22,6 +22,11 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// AnswerResponse is handed back when we ask for an answer to be checked.
type AnswerResponse struct {
Correct bool
}
// Puzzle contains everything about a puzzle that a client would see. // Puzzle contains everything about a puzzle that a client would see.
type Puzzle struct { type Puzzle struct {
Pre struct { Pre struct {
@ -425,9 +430,11 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
return false return false
} }
switch strings.TrimSpace(string(stdout)) { ans := AnswerResponse{}
case "correct": if err := json.Unmarshal(stdout, &ans); err != nil {
return true log.Printf("ERROR: checking answer: %s", err)
return false
} }
return false
return ans.Correct
} }

View File

@ -7,8 +7,12 @@ fail () {
case $1:$2:$3 in case $1:$2:$3 in
inventory::) inventory::)
echo "[1,2,3," cat <<EOT
echo "4,5]" {
"Puzzles": [1, 2, 3,
4, 5]
}
EOT
;; ;;
puzzle:1:) puzzle:1:)
cat <<EOT cat <<EOT
@ -31,10 +35,10 @@ EOT
fail "No such file: $2" fail "No such file: $2"
;; ;;
answer:1:answer1.0) answer:1:answer1.0)
echo "correct" echo -n '{"Correct":true}'
;; ;;
answer:1:*) answer:1:*)
echo "incorrect" echo '{"Correct":false}'
;; ;;
answer:*:*) answer:*:*)
fail "Fail answer" fail "Fail answer"

View File

@ -24,16 +24,15 @@ EOT
fail "no such file: $1" fail "no such file: $1"
;; ;;
answer:moo) answer:moo)
echo "correct" echo '{"Correct":true}'
;; ;;
answer:error) answer:error)
fail "you requested an error" fail "you requested an error"
;; ;;
answer:*) answer:*)
echo "incorrect" echo '{"Correct":false}'
;; ;;
*) *)
fail "What is $1" fail "What is $1"
;; ;;
esac esac

View File

@ -30,6 +30,9 @@ function renderPuzzles(obj) {
// Create a sorted list of category names // Create a sorted list of category names
let cats = Object.keys(obj) let cats = Object.keys(obj)
cats.sort() cats.sort()
if (cats.length == 0) {
toast("No categories to serve!")
}
for (let cat of cats) { for (let cat of cats) {
if (cat.startsWith("__")) { if (cat.startsWith("__")) {
// Skip metadata // Skip metadata
@ -103,8 +106,8 @@ function renderState(obj) {
let params = new URLSearchParams(window.location.search) let params = new URLSearchParams(window.location.search)
sessionStorage.id = "1" sessionStorage.id = "1"
sessionStorage.pid = "rodney" sessionStorage.pid = "rodney"
} renderPuzzles(obj.Puzzles)
if (Object.keys(obj.Puzzles).length > 0) { } else if (Object.keys(obj.Puzzles).length > 0) {
renderPuzzles(obj.Puzzles) renderPuzzles(obj.Puzzles)
if (obj.Config.Detachable) { if (obj.Config.Detachable) {
fetchAll(obj.Puzzles) fetchAll(obj.Puzzles)