This commit is contained in:
Gabe Farrell 2023-12-09 07:04:17 +00:00
commit a9cc5a8aad
15 changed files with 1640 additions and 0 deletions

50
server/get_guess.go Normal file
View file

@ -0,0 +1,50 @@
package server
import (
"strings"
"github.com/labstack/echo/v4"
)
type GetGuessRequest struct {
QuestionId string `json:"question_id" query:"question_id"`
Guess string `json:"guess" query:"guess"`
}
type GetGuessResponse struct {
QuestionId string `json:"question_id"`
Correct bool `json:"correct"`
}
func (s *Server) GetGuess(e echo.Context) error {
req := new(GetGuessRequest)
e.Bind(req)
// ensure required parameters exist
errs := make(map[string]string, 0)
if req.Guess == "" {
errs["guess"] = "required parameter missing"
}
if req.QuestionId == "" {
errs["question_id"] = "required parameter missing"
}
if len(errs) > 0 {
return e.JSON(400, &ErrorResponse{
Error: true,
Data: errs,
})
}
question := s.Q.GetQuestionById(req.QuestionId)
if question == nil {
errs["question_id"] = "invalid or malformed"
return e.JSON(404, &ErrorResponse{
Error: true,
Data: errs,
})
}
// validate answer with case insensitive string compare
correct := strings.EqualFold(question.Answer, req.Guess)
return e.JSONPretty(200, &GetGuessResponse{req.QuestionId, correct}, " ")
}

127
server/get_guess_test.go Normal file
View file

@ -0,0 +1,127 @@
package server_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gabehf/trivia-api/server"
"github.com/labstack/echo/v4"
)
func TestGuessHandler(t *testing.T) {
jsonBody := []byte("{\"question_id\":\"World History|0\",\"guess\":\"seven\"}")
// OK path: json body
e := echo.New()
req := httptest.NewRequest("GET", "/guess", bytes.NewReader(jsonBody))
req.Header["Content-Type"] = []string{"application/json"}
res := httptest.NewRecorder()
c := e.NewContext(req, res)
err := S.GetGuess(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusOK {
t.Errorf("expected status 200 OK, got %d", res.Code)
}
result := new(server.GetGuessResponse)
err = json.Unmarshal(res.Body.Bytes(), result)
if err != nil {
t.Error("malformed json response")
}
if result.QuestionId != "World History|0" {
t.Errorf("expected question_id 'World History|0', got '%s'", result.QuestionId)
}
if result.Correct != true {
t.Errorf("expected correct to be true, got false")
}
// OK path: urlencoded body
e = echo.New()
req = httptest.NewRequest("GET", "/guess?question_id=World+History%7C0&guess=Seven", nil)
req.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetGuess(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusOK {
t.Errorf("expected status 200 OK, got %d", res.Code)
}
result = new(server.GetGuessResponse)
err = json.Unmarshal(res.Body.Bytes(), result)
if err != nil {
t.Error("malformed json response")
}
if result.QuestionId != "World History|0" {
t.Errorf("expected question_id 'World History|0', got '%s'", result.QuestionId)
}
if result.Correct != true {
t.Errorf("expected correct to be true, got false")
}
// FAIL path: invalid question id
e = echo.New()
req = httptest.NewRequest("GET", "/guess?question_id=hey&guess=Seven", nil)
req.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetGuess(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusNotFound {
t.Errorf("expected status 400 Bad Request, got %d", res.Code)
}
errResult := struct {
Error bool
Data map[string]string
}{}
err = json.Unmarshal(res.Body.Bytes(), &errResult)
if err != nil {
t.Error("malformed json response")
}
if !errResult.Error {
t.Error("expected error to be true, got false")
}
if errResult.Data["question_id"] == "" {
t.Errorf("expected error information in data[question_id], got \"\"")
}
// FAIL path: missing params
e = echo.New()
req = httptest.NewRequest("GET", "/guess", nil)
req.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetGuess(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusBadRequest {
t.Errorf("expected status 400 Bad Request, got %d", res.Code)
}
errResult = struct {
Error bool
Data map[string]string
}{}
err = json.Unmarshal(res.Body.Bytes(), &errResult)
if err != nil {
t.Error("malformed json response")
}
if !errResult.Error {
t.Error("expected error to be true, got false")
}
if errResult.Data["question_id"] == "" {
t.Errorf("expected error information in data[question_id], got \"\"")
}
if errResult.Data["guess"] == "" {
t.Errorf("expected error information in data[guess], got \"\"")
}
}

71
server/get_trivia.go Normal file
View file

@ -0,0 +1,71 @@
package server
import (
"math/rand"
"strconv"
"github.com/labstack/echo/v4"
)
type GetTriviaRequest struct {
Category string `json:"category" query:"category"`
}
type GetTriviaResponse struct {
QuestionId string `json:"question_id"`
Question string `json:"question"`
Category string `json:"category"`
Format string `json:"format"`
Choices map[string]string `json:"choices,omitempty"`
}
type ErrorResponse struct {
Error bool `json:"error"`
Data map[string]string `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
func (s *Server) GetTrivia(e echo.Context) error {
req := new(GetTriviaRequest)
e.Bind(req)
question, qIndex := s.Q.GetRandomQuestion(req.Category)
if question == nil {
return e.JSON(404, &ErrorResponse{
Error: true,
Data: map[string]string{
"category": "category is invalid",
},
})
}
// randomly order answer choices if the format is multiple choice
if question.Format == "MultipleChoice" && question.Choices != nil {
rand.Shuffle(len(question.Choices), func(i, j int) {
question.Choices[i], question.Choices[j] = question.Choices[j], question.Choices[i]
})
// enforce that multiple choice questions must have four choices
// if not, there must be an error in our data somewhere that we need
// to fix
if len(question.Choices) != 4 {
return e.JSON(500, &ErrorResponse{
Error: true,
Message: "internal server error",
})
}
}
// build and return response
tq := new(GetTriviaResponse)
tq.QuestionId = question.Category + "|" + strconv.Itoa(qIndex)
tq.Category = question.Category
tq.Format = question.Format
tq.Question = question.Question
if tq.Format == "MultipleChoice" {
tq.Choices = map[string]string{
"A": question.Choices[0],
"B": question.Choices[1],
"C": question.Choices[2],
"D": question.Choices[3],
}
}
return e.JSONPretty(200, tq, " ")
}

145
server/get_trivia_test.go Normal file
View file

@ -0,0 +1,145 @@
package server_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gabehf/trivia-api/server"
"github.com/labstack/echo/v4"
)
func TestTriviaHandler(t *testing.T) {
jsonBody := []byte("{\"category\":\"World History\"}")
expect := server.GetTriviaResponse{
Question: "The ancient city of Rome was built on how many hills?",
Format: "MultipleChoice",
Category: "World History",
}
// OK path: json body
e := echo.New()
req := httptest.NewRequest("GET", "/trivia", bytes.NewReader(jsonBody))
req.Header["Content-Type"] = []string{"application/json"}
res := httptest.NewRecorder()
c := e.NewContext(req, res)
err := S.GetTrivia(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusOK {
t.Errorf("expected status 200 OK, got %d", res.Code)
}
result := new(server.GetTriviaResponse)
err = json.Unmarshal(res.Body.Bytes(), result)
if err != nil {
t.Error("malformed json response")
}
if result.Question != expect.Question {
t.Errorf("expected question '%s', got '%s'", expect.Question, result.Question)
}
if result.Format != expect.Format {
t.Errorf("expected format %s, got %s", expect.Format, result.Format)
}
if !strings.EqualFold(expect.Category, result.Category) {
t.Errorf("expected category %s, got %s", expect.Category, result.Category)
}
// OK path: urlencoded body
e = echo.New()
req = httptest.NewRequest("GET", "/trivia?category=World+History", nil)
req.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetTrivia(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusOK {
t.Errorf("expected status 200 OK, got %d", res.Code)
}
expect = server.GetTriviaResponse{
Question: "The ancient city of Rome was built on how many hills?",
Format: "MultipleChoice",
Category: "World History",
}
result = new(server.GetTriviaResponse)
err = json.Unmarshal(res.Body.Bytes(), result)
if err != nil {
t.Error("malformed json response")
}
if result.Question != expect.Question {
t.Errorf("expected question '%s', got '%s'", expect.Question, result.Question)
}
if result.Format != expect.Format {
t.Errorf("expected format %s, got %s", expect.Format, result.Format)
}
if !strings.EqualFold(expect.Category, result.Category) {
t.Errorf("expected category %s, got %s", expect.Category, result.Category)
}
// OK path: no body (random category)
e = echo.New()
req = httptest.NewRequest("GET", "/trivia", nil)
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetTrivia(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusOK {
t.Errorf("expected status 200 OK, got %d", res.Code)
}
expect = server.GetTriviaResponse{
Question: "The ancient city of Rome was built on how many hills?",
Format: "MultipleChoice",
Category: "World History",
}
result = new(server.GetTriviaResponse)
err = json.Unmarshal(res.Body.Bytes(), result)
if err != nil {
t.Error("malformed json response")
}
if result.Question != expect.Question {
t.Errorf("expected question '%s', got '%s'", expect.Question, result.Question)
}
if result.Format != expect.Format {
t.Errorf("expected format %s, got %s", expect.Format, result.Format)
}
if !strings.EqualFold(expect.Category, result.Category) {
t.Errorf("expected category %s, got %s", expect.Category, result.Category)
}
// FAIL path: invalid category
e = echo.New()
req = httptest.NewRequest("GET", "/trivia?category=70s+Music", nil)
req.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
res = httptest.NewRecorder()
c = e.NewContext(req, res)
err = S.GetTrivia(c)
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
if res.Code != http.StatusNotFound {
t.Errorf("expected status 404 Not Found, got %d", res.Code)
}
errResult := struct {
Error bool
Data map[string]string
}{}
err = json.Unmarshal(res.Body.Bytes(), &errResult)
if err != nil {
t.Error("malformed json response")
}
if !errResult.Error {
t.Error("expected error to be true, got false")
}
if errResult.Data["category"] == "" {
t.Errorf("expected error information in data[category], got \"\"")
}
}

44
server/server.go Normal file
View file

@ -0,0 +1,44 @@
package server
import (
"os"
"github.com/gabehf/trivia-api/trivia"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
Q *trivia.Questions
}
func (s *Server) Init() {
s.Q = new(trivia.Questions)
s.Q.Init()
}
func Run() error {
// init server struct
s := new(Server)
s.Init()
// load trivia data
file, err := os.Open("trivia.json")
if err != nil {
panic(err)
}
err = s.Q.Load(file)
if err != nil {
panic(err)
}
// create router and mount handlers
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/trivia", s.GetTrivia)
e.GET("/guess", s.GetGuess)
// start listening
return e.Start(":3000")
}

View file

@ -0,0 +1,34 @@
package server_test
import (
"testing"
"github.com/gabehf/trivia-api/server"
"github.com/gabehf/trivia-api/trivia"
)
var S *server.Server
func TestMain(m *testing.M) {
S = new(server.Server)
S.Init()
S.Q.Init()
S.Q.Categories = []string{"world history"}
S.Q.M = map[string][]trivia.Question{
"world history": {
{
Question: "The ancient city of Rome was built on how many hills?",
Format: "MultipleChoice",
Category: "World History",
Choices: []string{
"Eight",
"Four",
"Nine",
"Seven",
},
Answer: "Seven",
},
},
}
m.Run()
}