mirror of https://github.com/gabehf/trivia-api.git
commit
a9cc5a8aad
@ -0,0 +1,11 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM golang:1.21
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY *.go ./
|
||||
COPY ./server/*.go ./server/
|
||||
COPY ./trivia/*.go ./trivia/
|
||||
COPY trivia.json ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /TriviaAPI
|
||||
CMD ["/TriviaAPI"]
|
||||
@ -0,0 +1,23 @@
|
||||
Stateless, Containerized Trivia API
|
||||
---
|
||||
|
||||
This project has a blog post going through the development process. Read the post here: https://mnrva.dev/posts/stateless-containerized-trivia-api-go
|
||||
|
||||
## Running the server
|
||||
Copy the repo
|
||||
```bash
|
||||
git clone git@github.com:gabehf/trivia-api.git
|
||||
```
|
||||
Then, run the server using Go
|
||||
```bash
|
||||
cd trivia-api
|
||||
go run .
|
||||
```
|
||||
Or, build the docker image and run that
|
||||
```bash
|
||||
docker build --tag trivia-api .
|
||||
```
|
||||
Then,
|
||||
```bash
|
||||
docker run -p 3000:3000 trivia-api
|
||||
```
|
||||
@ -0,0 +1,19 @@
|
||||
module github.com/gabehf/trivia-api
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require github.com/labstack/echo/v4 v4.11.3
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
)
|
||||
@ -0,0 +1,47 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gabehf/trivia-api/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Trivia API listening on http://127.0.0.1:3000")
|
||||
log.Fatal(server.Run())
|
||||
}
|
||||
@ -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}, " ")
|
||||
}
|
||||
@ -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 \"\"")
|
||||
}
|
||||
}
|
||||
@ -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, " ")
|
||||
}
|
||||
@ -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 \"\"")
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -0,0 +1,741 @@
|
||||
{
|
||||
"world history": [
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "How many years did the 100 years war last?",
|
||||
"answer": "116",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"116",
|
||||
"87",
|
||||
"12",
|
||||
"205"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "In which year was John F. Kennedy assassinated?",
|
||||
"answer": "1963",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"1963",
|
||||
"1967",
|
||||
"1958",
|
||||
"1978"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Which world leader is famous for his “Little Red Book”?",
|
||||
"answer": "Mao Zedong",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Mao Zedong",
|
||||
"Xi Xinping",
|
||||
"Ho Chi Minh",
|
||||
"Kim Jong-Un"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "The ancient city of Rome was built on how many hills?",
|
||||
"answer": "Seven",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Seven",
|
||||
"Eight",
|
||||
"Four",
|
||||
"Twelve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Before becoming the state of New Jersey, this US state was originally known as New __________.",
|
||||
"answer": "Netherlands",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Denmark",
|
||||
"Netherlands",
|
||||
"Spain",
|
||||
"England"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Which ancient city was famous for its Hanging Gardens?",
|
||||
"answer": "Babylon",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Mesopotamia",
|
||||
"Athens",
|
||||
"Babylon",
|
||||
"Rome"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "In which country was Adolf Hitler born?",
|
||||
"answer": "",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Australia",
|
||||
"Austria",
|
||||
"Germany",
|
||||
"Switzerland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "The Hundred Years War was a conflict between the two nations France and who?",
|
||||
"answer": "England",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"England",
|
||||
"Spain",
|
||||
"Italy",
|
||||
"China"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Who wrote the document known as the 95 Theses?",
|
||||
"answer": "Martin Luther",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Martin Luther",
|
||||
"Pope Francis",
|
||||
"The Dalai Lama",
|
||||
"Barack Obama"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Victory in Europe Day, or VE Day, celebrates the Allies' victory over Nazi Germany on which date?",
|
||||
"answer": "May 8th",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"May 8th",
|
||||
"May 9th",
|
||||
"July 4th",
|
||||
"January 6th"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "The Reign of Terror was a period during which major social and political event?",
|
||||
"answer": "French Revolution",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"French Revolution",
|
||||
"Nazi Occupation of Poland",
|
||||
"November 11th",
|
||||
"WWI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "Which Chinese destination was the site of protests in 1989?",
|
||||
"answer": "Tiananmen Square",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Tiananmen Square",
|
||||
"Taiwan",
|
||||
"Guangzhou",
|
||||
"Hangzhou"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "True or False: Civil rights leader Martin Luther King, Jr won the Nobel Peace prize in 1964.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "True or False: John Wilkes Booth assassinated Abraham Lincoln.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "True or False: Cleopatra, the last Queen of Egypt, was born in Rome",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
}
|
||||
],
|
||||
"art": [
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Banksy's artwork started appearing on walls in which decade?",
|
||||
"answer": "1990s",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"1990s",
|
||||
"2000s",
|
||||
"2010s",
|
||||
"1970s"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Which female Mexican artist is famous for self-portraits?",
|
||||
"answer": "Frida Kahlo",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Leonora Carrington",
|
||||
"Sofía Táboas",
|
||||
"Frida Kahlo",
|
||||
"Elizabeth Catlett"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Renoir was one of the leading artists in which artistic movement?",
|
||||
"answer": "Impressionism",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Impressionism",
|
||||
"Realism",
|
||||
"Surrealism",
|
||||
"New Wave"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Which rebel leader was featured in the Pop Art of Andy Warhol?",
|
||||
"answer": "Che Guevara",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Luke Skywalker",
|
||||
"Che Guevara",
|
||||
"Marilyn Monroe",
|
||||
"Fidel Castro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Katsushika Hokusai painted The Great _______ off Kanagawa circa 1830.",
|
||||
"answer": "Wave",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Wave",
|
||||
"Fleet",
|
||||
"Island",
|
||||
"Skies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "The Starry Night Over The Rhone was painted in 1888 by which artist?",
|
||||
"answer": "Van Gogh",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Monet",
|
||||
"Da Vinci",
|
||||
"Van Gogh",
|
||||
"Raphael"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Vincent Van Gogh is considered by many to be Post-Impressionist, or \"The Father of ________\".",
|
||||
"answer": "Expressionism",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Expressionism",
|
||||
"Impressionism",
|
||||
"Modernism",
|
||||
"Post-Modernism"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "In Botticelli's \"Birth of Venus\" (c. 1485), Venus is standing on a(n) what?",
|
||||
"answer": "Shell",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Shell",
|
||||
"Wave",
|
||||
"Rock",
|
||||
"Whale"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Johannes Vermeer's \"Girl with a Pearl ________\" (c. 1665) shares its name with a 2003 Romance film starring Scarlett Johansson.",
|
||||
"answer": "Earring",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Necklace",
|
||||
"Earring",
|
||||
"Hairclip",
|
||||
"Bracelet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Alexandros of Antioch's \"Venus de Milo\" (c. 150 BC) is a sculpture depicting which greek goddess?",
|
||||
"answer": "Aphrodite",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Aphrodite",
|
||||
"Artemis",
|
||||
"Persephone",
|
||||
"Hestia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "\"The Night Watch\" is one of the most famous works by which artist?",
|
||||
"answer": "Rembrandt",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Monet",
|
||||
"Van Gogh",
|
||||
"Rembrandt",
|
||||
"Renoir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "True or False: The Smithsonian American Art Museum is in Washington, D.C.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "Which artistic movement includes works like 'Starry Night' by Vincent van Gogh?",
|
||||
"answer": "Post-Impressionism",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Cubism", "Surrealism", "Impressionism", "Post-Impressionism"]
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "True or False: The painting 'The Persistence of Memory' features melting clocks.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Art",
|
||||
"question": "True or False: Claude Monet and Edgar Degas are both examples of Renaissance artists.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
}
|
||||
],
|
||||
"movies": [
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "In which film did Humphrey Bogart say the famous line, 'Here's looking at you, kid'?",
|
||||
"answer": "Casablanca",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["The Maltese Falcon", "Casablanca", "Key Largo", "To Have and Have Not"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Who directed the 1994 film 'Pulp Fiction'?",
|
||||
"answer": "Quentin Tarantino",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Martin Scorsese", "Quentin Tarantino", "Coen Brothers", "Steven Spielberg"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Which animated film features a character named Simba?",
|
||||
"answer": "The Lion King",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Aladdin", "Beauty and the Beast", "The Lion King", "Toy Story"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "In 'The Shawshank Redemption,' what does Andy Dufresne carve his way through to escape from prison?",
|
||||
"answer": "Wall",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Floor", "Wall", "Ceiling", "Cell Bars"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Which actor played the role of Tony Stark in the Marvel Cinematic Universe?",
|
||||
"answer": "Robert Downey Jr.",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Chris Evans", "Mark Ruffalo", "Chris Hemsworth", "Robert Downey Jr."]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "True or False: 'The Godfather' is based on a novel of the same name by Mario Puzo.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Who won the Academy Award for Best Actress for her role in 'La La Land'?",
|
||||
"answer": "Emma Stone",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Emma Stone", "Meryl Streep", "Natalie Portman", "Jennifer Lawrence"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Which film features a character named Travis Bickle who drives a taxi in New York City?",
|
||||
"answer": "Taxi Driver",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Collateral", "Nightcrawler", "Drive", "Taxi Driver"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "What is the name of the fictional African country in 'Black Panther'?",
|
||||
"answer": "Wakanda",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Zamunda", "Wakanda", "Genovia", "Elbonia"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "True or False: 'Forrest Gump' is based on a novel by Winston Groom.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Who directed the 2017 horror film 'Get Out'?",
|
||||
"answer": "Jordan Peele",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Jordan Peele", "Ari Aster", "John Krasinski", "James Wan"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Which James Cameron film features the line, 'I'll be back'?",
|
||||
"answer": "The Terminator",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Aliens", "The Terminator", "Avatar", "Titanic"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "Who played the lead role in the film 'The Matrix'?",
|
||||
"answer": "Keanu Reeves",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Keanu Reeves", "Tom Cruise", "Will Smith", "Brad Pitt"]
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "True or False: 'The Dark Knight' was directed by Christopher Nolan.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Movies",
|
||||
"question": "In the film 'Inception,' what is used to enter the dream world?",
|
||||
"answer": "Dream Machine",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Dreamcatcher", "Dream Weaver", "Dream Machine", "Dream Portal"]
|
||||
}
|
||||
],
|
||||
"music": [
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which band released the album 'The Dark Side of the Moon' in 1973?",
|
||||
"answer": "Pink Floyd",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Led Zeppelin", "The Beatles", "Pink Floyd", "The Rolling Stones"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Who is known as the 'King of Pop'?",
|
||||
"answer": "Michael Jackson",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Elvis Presley", "Michael Jackson", "Prince", "David Bowie"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which female artist released the album '21' in 2011?",
|
||||
"answer": "Adele",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Taylor Swift", "Adele", "Beyoncé", "Rihanna"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "In which year did The Beatles release their debut album 'Please Please Me'?",
|
||||
"answer": "1963",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["1962", "1963", "1964", "1965"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Who is the lead singer of the rock band U2?",
|
||||
"answer": "Bono",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Chris Martin", "Bono", "Mick Jagger", "Bruce Springsteen"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "True or False: Elvis Presley is also known as 'The Boss.'",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which artist won the Nobel Prize in Literature in 2016?",
|
||||
"answer": "Bob Dylan",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Bob Dylan", "Joni Mitchell", "Leonard Cohen", "Paul Simon"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "In which year did Nirvana release their iconic album 'Nevermind'?",
|
||||
"answer": "1991",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["1990", "1991", "1992", "1993"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which headline performers took the stage at the halftime show of Super Bowl LIV in 2020?",
|
||||
"answer": "Shakira and Jennifer Lopez",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Coldplay", "Shakira and Jennifer Lopez", "Maroon 5", "U2"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "True or False: Madonna's real name is Madonna.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which 1970's pop wrote the 1974 ballad 'Don't Let the Sun Go Down on Me'?",
|
||||
"answer": "Elton John",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["David Bowie", "Elton John", "Freddie Mercury", "Billy Joel"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which pop star is known as the 'Material Girl'?",
|
||||
"answer": "Madonna",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Britney Spears", "Lady Gaga", "Madonna", "Katy Perry"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Who wrote the song 'Bohemian Rhapsody'?",
|
||||
"answer": "Freddie Mercury",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Freddie Mercury", "John Lennon", "David Bowie", "Elton John"]
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "True or False: Adele's full name is Adele Laurie Blue Adkins.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Music",
|
||||
"question": "Which country is the birthplace of the band ABBA?",
|
||||
"answer": "Sweden",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Norway", "Denmark", "Sweden", "Finland"]
|
||||
}
|
||||
],
|
||||
"geography": [
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "What is the capital city of Japan?",
|
||||
"answer": "Tokyo",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Beijing", "Seoul", "Bangkok", "Tokyo"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which river is the longest in the world?",
|
||||
"answer": "Nile",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Amazon", "Nile", "Yangtze", "Mississippi"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "The __________ Desert is the largest desert in the world?",
|
||||
"answer": "Antarctic",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Sahara", "Gobi", "Arabian", "Antarctic"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which country is known as the 'Land of the Rising Sun'?",
|
||||
"answer": "Japan",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["China", "South Korea", "Japan", "Vietnam"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "What is the capital city of Australia?",
|
||||
"answer": "Canberra",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Sydney", "Melbourne", "Canberra", "Brisbane"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "True or False: The Amazon Rainforest is located in Africa.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which mountain is the tallest in North America?",
|
||||
"answer": "Denali",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Mount McKinley", "Mount Logan", "Denali", "Mount St. Elias"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "In which continent is the Sahara Desert located?",
|
||||
"answer": "Africa",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Asia", "Africa", "South America", "Australia"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which ocean is the largest on Earth?",
|
||||
"answer": "Pacific Ocean",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Atlantic Ocean", "Indian Ocean", "Southern Ocean", "Pacific Ocean"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "True or False: The Great Wall of China is visible from space without aid.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which river flows through Cairo?",
|
||||
"answer": "Nile",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Tigris", "Euphrates", "Nile", "Jordan"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "What is the smallest country in the world?",
|
||||
"answer": "Vatican City",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Monaco", "San Marino", "Liechtenstein", "Vatican City"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which African country is known as the 'Pearl of Africa'?",
|
||||
"answer": "Uganda",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Kenya", "Tanzania", "Uganda", "Rwanda"]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "True or False: Mount Everest is located in the Himalayas.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "Which strait separates Europe and Asia?",
|
||||
"answer": "Bosporus",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Strait of Gibraltar", "Bering Strait", "Bosporus", "Strait of Hormuz"]
|
||||
}
|
||||
],
|
||||
"science": [
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the chemical symbol for the element gold?",
|
||||
"answer": "Au",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Ag", "Fe", "Au", "Cu"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "Which planet is known as the 'Red Planet'?",
|
||||
"answer": "Mars",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Venus", "Mars", "Jupiter", "Saturn"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the powerhouse of the cell?",
|
||||
"answer": "Mitochondria",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Nucleus", "Mitochondria", "Endoplasmic Reticulum", "Golgi Apparatus"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the largest mammal in the world?",
|
||||
"answer": "Blue Whale",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Elephant", "Giraffe", "Blue Whale", "Hippopotamus"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "Which gas is primarily responsible for the Earth's greenhouse effect?",
|
||||
"answer": "Carbon Dioxide",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Oxygen", "Nitrogen", "Carbon Dioxide", "Methane"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "True or False: Water boils at 100 degrees Celsius at sea level.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the chemical symbol for the element with the atomic number 1?",
|
||||
"answer": "H",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["He", "H", "Li", "Be"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "Which scientist developed the theory of general relativity?",
|
||||
"answer": "Albert Einstein",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Isaac Newton", "Galileo Galilei", "Marie Curie", "Albert Einstein"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the chemical symbol for water?",
|
||||
"answer": "H2O",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["CO2", "O2", "H2O", "N2"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "True or False: An adult human skeleton has 206 bones.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "Which planet was referred to as the 'Pale Blue Dot'?",
|
||||
"answer": "Earth",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Mars", "Venus", "Earth", "Neptune"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the smallest unit of matter?",
|
||||
"answer": "Atom",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Molecule", "Cell", "Atom", "Proton"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "Which gas do plants absorb from the atmosphere during photosynthesis?",
|
||||
"answer": "Carbon Dioxide",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Oxygen", "Methane", "Carbon Monoxide", "Carbon Dioxide"]
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "True or False: Antibiotics are effective against viruses.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
},
|
||||
{
|
||||
"category": "Science",
|
||||
"question": "What is the largest planet in our solar system?",
|
||||
"answer": "Jupiter",
|
||||
"format": "MultipleChoice",
|
||||
"choices": ["Mars", "Saturn", "Jupiter", "Neptune"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package trivia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// represents the structure of trivia questions stored
|
||||
// in the trivia.json file
|
||||
type Question struct {
|
||||
Question string `json:"question"`
|
||||
Category string `json:"category"`
|
||||
Format string `json:"format"`
|
||||
Choices []string `json:"choices"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
type Questions struct {
|
||||
Categories []string
|
||||
M map[string][]Question
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (q *Questions) Init() {
|
||||
q.lock = &sync.RWMutex{}
|
||||
}
|
||||
|
||||
func (q *Questions) Load(r io.Reader) error {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
if q.M == nil {
|
||||
q.M = make(map[string][]Question, 0)
|
||||
}
|
||||
err := json.NewDecoder(r).Decode(&q.M)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if q.Categories == nil {
|
||||
q.Categories = make([]string, 0)
|
||||
}
|
||||
for key := range q.M {
|
||||
q.Categories = append(q.Categories, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Questions) categoryExists(cat string) bool {
|
||||
q.lock.RLock()
|
||||
defer q.lock.RUnlock()
|
||||
cat = strings.ToLower(cat)
|
||||
if q.M[cat] == nil || len(q.M[cat]) < 1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (q *Questions) getRandomCategory() string {
|
||||
q.lock.RLock()
|
||||
defer q.lock.RUnlock()
|
||||
return q.Categories[rand.Int()%len(q.Categories)]
|
||||
}
|
||||
|
||||
// Gets a random question from the category, if specified.
|
||||
func (q *Questions) GetRandomQuestion(category string) (*Question, int) {
|
||||
q.lock.RLock()
|
||||
defer q.lock.RUnlock()
|
||||
// NOTE: it is okay to call another function that locks the RWMutex here,
|
||||
// as it will not cause a deadlock since CategoryExists only locks the Read
|
||||
if category == "" {
|
||||
category = q.getRandomCategory()
|
||||
} else if !q.categoryExists(category) {
|
||||
return nil, 0
|
||||
}
|
||||
category = strings.ToLower(category)
|
||||
qIndex := rand.Int() % len(q.M[category])
|
||||
return &q.M[category][qIndex], qIndex
|
||||
}
|
||||
|
||||
func (q *Questions) GetQuestionById(id string) *Question {
|
||||
// get values from question_id
|
||||
questionSlice := strings.Split(id, "|")
|
||||
if len(questionSlice) != 2 {
|
||||
return nil
|
||||
}
|
||||
category, indexS := questionSlice[0], questionSlice[1]
|
||||
category = strings.ToLower(category)
|
||||
index, err := strconv.Atoi(indexS)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.lock.RLock()
|
||||
defer q.lock.RUnlock()
|
||||
// ensure category exists
|
||||
if !q.categoryExists(category) {
|
||||
return nil
|
||||
}
|
||||
// ensure question index is valid
|
||||
if len(q.M[category]) <= index {
|
||||
return nil
|
||||
}
|
||||
|
||||
// retrieve question
|
||||
return &q.M[category][index]
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package trivia_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/gabehf/trivia-api/trivia"
|
||||
)
|
||||
|
||||
func TestGetRandomQuestion(t *testing.T) {
|
||||
// on OK path, GetTrivia must return the question in our test data
|
||||
tq, _ := Q.GetRandomQuestion("world history")
|
||||
if tq == nil {
|
||||
t.Fatal("trivia question must not be nil")
|
||||
}
|
||||
if !reflect.DeepEqual(tq, expect) {
|
||||
t.Errorf("returned question does not match expectation, got %v", tq)
|
||||
}
|
||||
|
||||
// with no category specified, GetTrivia must pick a random category and fetch a question
|
||||
// with only one question in our test data, it is the same question from before
|
||||
tq, _ = Q.GetRandomQuestion("")
|
||||
if tq == nil {
|
||||
t.Fatal("trivia question must not be nil")
|
||||
}
|
||||
if !reflect.DeepEqual(tq, expect) {
|
||||
t.Errorf("returned question does not match expectation, got %v", tq)
|
||||
}
|
||||
|
||||
// on FAIL path, GetTrivia must return nil to indicate no questions are found
|
||||
tq, _ = Q.GetRandomQuestion("Geography")
|
||||
if tq != nil {
|
||||
t.Errorf("expected nil, got %v", tq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQuestionById(t *testing.T) {
|
||||
// on OK path, GetTrivia must return the question in our test data
|
||||
tq := Q.GetQuestionById("world History|0")
|
||||
if tq == nil {
|
||||
t.Fatal("trivia question must not be nil")
|
||||
}
|
||||
if !reflect.DeepEqual(tq, expect) {
|
||||
t.Errorf("returned question does not match expectation, got %v", tq)
|
||||
}
|
||||
|
||||
// FAIL path: malformed id
|
||||
tq = Q.GetQuestionById("hey")
|
||||
if tq != nil {
|
||||
t.Errorf("expected nil, got %v", tq)
|
||||
}
|
||||
// FAIL path: invalid category
|
||||
tq = Q.GetQuestionById("hey|0")
|
||||
if tq != nil {
|
||||
t.Errorf("expected nil, got %v", tq)
|
||||
}
|
||||
// FAIL path: invalid index
|
||||
tq = Q.GetQuestionById("world history|9")
|
||||
if tq != nil {
|
||||
t.Errorf("expected nil, got %v", tq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
json := []byte(`
|
||||
{
|
||||
"world history": [
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "How many years did the 100 years war last?",
|
||||
"answer": "116",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"116",
|
||||
"87",
|
||||
"12",
|
||||
"205"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "World History",
|
||||
"question": "True or False: John Wilkes Booth assassinated Abraham Lincoln.",
|
||||
"answer": "True",
|
||||
"format": "TrueFalse"
|
||||
}
|
||||
],
|
||||
"geography": [
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "What is the capital city of Japan?",
|
||||
"answer": "Tokyo",
|
||||
"format": "MultipleChoice",
|
||||
"choices": [
|
||||
"Beijing",
|
||||
"Seoul",
|
||||
"Bangkok",
|
||||
"Tokyo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Geography",
|
||||
"question": "True or False: The Amazon Rainforest is located in Africa.",
|
||||
"answer": "False",
|
||||
"format": "TrueFalse"
|
||||
}
|
||||
]
|
||||
}
|
||||
`)
|
||||
expectCategories := []string{"world history", "geography"}
|
||||
expectQuestions := map[string][]trivia.Question{
|
||||
"world history": {
|
||||
{
|
||||
Question: "How many years did the 100 years war last?",
|
||||
Format: "MultipleChoice",
|
||||
Answer: "116",
|
||||
Category: "World History",
|
||||
Choices: []string{
|
||||
"116",
|
||||
"87",
|
||||
"12",
|
||||
"205",
|
||||
},
|
||||
},
|
||||
{
|
||||
Question: "True or False: John Wilkes Booth assassinated Abraham Lincoln.",
|
||||
Format: "TrueFalse",
|
||||
Answer: "True",
|
||||
Category: "World History",
|
||||
},
|
||||
},
|
||||
"geography": {
|
||||
{
|
||||
Question: "What is the capital city of Japan?",
|
||||
Format: "MultipleChoice",
|
||||
Answer: "Tokyo",
|
||||
Category: "Geography",
|
||||
Choices: []string{"Beijing", "Seoul", "Bangkok", "Tokyo"},
|
||||
},
|
||||
{
|
||||
Question: "True or False: The Amazon Rainforest is located in Africa.",
|
||||
Format: "TrueFalse",
|
||||
Answer: "False",
|
||||
Category: "Geography",
|
||||
},
|
||||
},
|
||||
}
|
||||
qq := new(trivia.Questions)
|
||||
qq.Init()
|
||||
err := qq.Load(bytes.NewReader(json))
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
}
|
||||
for _, cat := range expectCategories {
|
||||
if !slices.Contains(qq.Categories, cat) {
|
||||
t.Errorf("expected category %s not present", cat)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(qq.M, expectQuestions) {
|
||||
t.Errorf("unexpected question map, got %v", qq.M)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package trivia_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gabehf/trivia-api/trivia"
|
||||
)
|
||||
|
||||
var Q trivia.Questions
|
||||
var expect *trivia.Question
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
Q.Init()
|
||||
Q.Categories = []string{"world history"}
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
expect = &trivia.Question{
|
||||
Question: "The ancient city of Rome was built on how many hills?",
|
||||
Category: "World History",
|
||||
Format: "MultipleChoice",
|
||||
Choices: []string{
|
||||
"Eight",
|
||||
"Four",
|
||||
"Nine",
|
||||
"Seven",
|
||||
},
|
||||
Answer: "Seven",
|
||||
}
|
||||
m.Run()
|
||||
}
|
||||
Loading…
Reference in new issue