Update quiz

This commit is contained in:
Andrea Fazzi 2022-06-28 13:49:35 +02:00
parent 405113e639
commit ee45c1ba9c
10 changed files with 205 additions and 42 deletions

View file

@ -7,6 +7,20 @@ type Response struct {
Content interface{} `json:"content"` Content interface{} `json:"content"`
} }
type Question struct {
Text string `json:"text"`
}
type Answer struct {
Text string
Correct bool
}
type Quiz struct {
Question *Question `json:"question"`
Answers []*Answer `json:"answers"`
}
type ReadAllQuizResponse struct { type ReadAllQuizResponse struct {
Status string `json:"status"` Status string `json:"status"`
Content []*models.Quiz `json:"content"` Content []*models.Quiz `json:"content"`
@ -17,16 +31,24 @@ type CreateQuizResponse struct {
Content *models.Quiz `json:"content"` Content *models.Quiz `json:"content"`
} }
type UpdateQuizResponse struct {
Status string `json:"status"`
Content *models.Quiz `json:"content"`
}
type CreateQuestionRequest struct { type CreateQuestionRequest struct {
Text string `json:"text"` *Question
} }
type CreateAnswerRequest struct { type CreateAnswerRequest struct {
Text string *Answer
Correct bool
} }
type CreateQuizRequest struct { type CreateQuizRequest struct {
Question *CreateQuestionRequest `json:"question"` *Quiz
Answers []*CreateAnswerRequest `json:"answers"` }
type UpdateQuizRequest struct {
ID string
*Quiz
} }

1
go.mod
View file

@ -6,6 +6,7 @@ require github.com/sirupsen/logrus v1.8.1
require ( require (
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.1.0 // indirect github.com/kr/text v0.1.0 // indirect
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect

2
go.sum
View file

@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

View file

@ -11,15 +11,15 @@ type Hasher interface {
// hash calculated from the question using // hash calculated from the question using
// QuestionHash. Following hashes are calculated from the // QuestionHash. Following hashes are calculated from the
// answers using AnswerHash. // answers using AnswerHash.
QuizHashes(quiz *client.CreateQuizRequest) []string QuizHashes(quiz *client.Quiz) []string
// QuestionHash returns an hash calculated from a field of // QuestionHash returns an hash calculated from a field of
// Question struct. // Question struct.
QuestionHash(question *client.CreateQuestionRequest) string QuestionHash(question *client.Question) string
// AnswerHash returns an hash calculated from a field of // AnswerHash returns an hash calculated from a field of
// Answer struct. // Answer struct.
AnswerHash(answer *client.CreateAnswerRequest) string AnswerHash(answer *client.Answer) string
// Calculate calculates a checksum from all the given hashes. // Calculate calculates a checksum from all the given hashes.
Calculate(hashes []string) string Calculate(hashes []string) string

View file

@ -22,7 +22,7 @@ func NewDefault256Hasher(hashFn hasher.HashFunc) *Default256Hasher {
return &Default256Hasher{hashFn} return &Default256Hasher{hashFn}
} }
func (h *Default256Hasher) QuizHashes(quiz *client.CreateQuizRequest) []string { func (h *Default256Hasher) QuizHashes(quiz *client.Quiz) []string {
result := make([]string, 0) result := make([]string, 0)
result = append(result, h.QuestionHash(quiz.Question)) result = append(result, h.QuestionHash(quiz.Question))
@ -36,11 +36,11 @@ func (h *Default256Hasher) QuizHashes(quiz *client.CreateQuizRequest) []string {
return result return result
} }
func (h *Default256Hasher) QuestionHash(question *client.CreateQuestionRequest) string { func (h *Default256Hasher) QuestionHash(question *client.Question) string {
return h.hashFn(question.Text) return h.hashFn(question.Text)
} }
func (h *Default256Hasher) AnswerHash(answer *client.CreateAnswerRequest) string { func (h *Default256Hasher) AnswerHash(answer *client.Answer) string {
return h.hashFn(answer.Text) return h.hashFn(answer.Text)
} }

View file

@ -9,6 +9,7 @@ import (
"git.andreafazzi.eu/andrea/probo/logger" "git.andreafazzi.eu/andrea/probo/logger"
"git.andreafazzi.eu/andrea/probo/models" "git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store" "git.andreafazzi.eu/andrea/probo/store"
"github.com/julienschmidt/httprouter"
) )
const jsonContentType = "application/json" const jsonContentType = "application/json"
@ -22,34 +23,49 @@ func NewQuizHubCollectorServer(store store.QuizHubCollectorStore) *QuizHubCollec
ps := new(QuizHubCollectorServer) ps := new(QuizHubCollectorServer)
ps.store = store ps.store = store
router := http.NewServeMux() router := httprouter.New()
router.Handle("/quizzes", logger.WithLogging(http.HandlerFunc(ps.testHandler))) router.GET("/quizzes", httprouter.Handle(ps.readAllQuizzesHandler))
router.POST("/quizzes/create", httprouter.Handle(ps.createQuizHandler))
router.POST("/quizzes/:id/update", httprouter.Handle(ps.updateQuizHandler))
// router.Handle("/quizzes", logger.WithLogging(http.HandlerFunc(ps.testHandler)))
ps.Handler = router ps.Handler = logger.WithLogging(router)
return ps return ps
} }
func (ps *QuizHubCollectorServer) testHandler(w http.ResponseWriter, r *http.Request) { func (ps *QuizHubCollectorServer) readAllQuizzesHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
switch r.Method { w.Header().Set("content-type", jsonContentType)
case http.MethodGet: json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r))
w.Header().Set("content-type", jsonContentType) }
json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r))
case http.MethodPost: func (ps *QuizHubCollectorServer) createQuizHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
response := new(client.Response) response := new(client.Response)
quiz, err := ps.createQuiz(w, r) quiz, err := ps.createQuiz(w, r)
if err != nil { if err != nil {
response = &client.Response{Status: "error", Content: err.Error()} response = &client.Response{Status: "error", Content: err.Error()}
}
response = &client.Response{Status: "success", Content: quiz}
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(response)
} }
response = &client.Response{Status: "success", Content: quiz}
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(response)
}
func (ps *QuizHubCollectorServer) updateQuizHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
response := new(client.Response)
quiz, err := ps.updateQuiz(w, r, params.ByName("id"))
if err != nil {
response = &client.Response{Status: "error", Content: err.Error()}
}
response = &client.Response{Status: "success", Content: quiz}
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(response)
} }
func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http.Request) *client.Response { func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http.Request) *client.Response {
@ -60,6 +76,27 @@ func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http.
return &client.Response{Status: "success", Content: tests} return &client.Response{Status: "success", Content: tests}
} }
func (ps *QuizHubCollectorServer) updateQuiz(w http.ResponseWriter, r *http.Request, id string) (*models.Quiz, error) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
updateQuizReq := new(client.UpdateQuizRequest)
err = json.Unmarshal(body, &updateQuizReq)
if err != nil {
return nil, err
}
updatedQuiz, err := ps.store.UpdateQuiz(updateQuizReq)
if err != nil {
return nil, err
}
return updatedQuiz, nil
}
func (ps *QuizHubCollectorServer) createQuiz(w http.ResponseWriter, r *http.Request) (*models.Quiz, error) { func (ps *QuizHubCollectorServer) createQuiz(w http.ResponseWriter, r *http.Request) (*models.Quiz, error) {
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {

View file

@ -38,22 +38,26 @@ func (t *integrationTestSuite) TestQuizCreateAndReadAll() {
} }
` `
createQuizResponse, err := t.createQuiz(server, payload) createQuizResponse, err := t.createQuiz(server, payload)
t.True(err == nil) t.True(err == nil, "Response should be decoded properly")
t.Equal("success", createQuizResponse.Status) if !t.Failed() {
t.Equal("Question 1", createQuizResponse.Content.Question.Text) t.Equal("success", createQuizResponse.Status)
t.Equal("Text of the answer 1", createQuizResponse.Content.Answers[0].Text) t.Equal("Question 1", createQuizResponse.Content.Question.Text)
t.Equal("Text of the answer 1", createQuizResponse.Content.Correct.Text) t.Equal("Text of the answer 1", createQuizResponse.Content.Answers[0].Text)
t.Equal("Text of the answer 1", createQuizResponse.Content.Correct.Text)
t.True(createQuizResponse.Content.ID != "", "Test ID should not be empty") t.True(createQuizResponse.Content.ID != "", "Test ID should not be empty")
t.True(createQuizResponse.Content.Question.ID != "", "Question ID should not be empty") t.True(createQuizResponse.Content.Question.ID != "", "Question ID should not be empty")
t.True(createQuizResponse.Content.Answers[0].ID != "", "Answer ID should not be empty") t.True(createQuizResponse.Content.Answers[0].ID != "", "Answer ID should not be empty")
}
readAllQuizResponse, err := t.readAllQuiz(server) readAllQuizResponse, err := t.readAllQuiz(server)
t.True(err == nil) t.True(err == nil, "Response should be decoded properly")
t.True(len(readAllQuizResponse.Content) == 1, "Length of returned tests should be 1") if !t.Failed() {
t.Equal("Question 1", readAllQuizResponse.Content[0].Question.Text) t.True(len(readAllQuizResponse.Content) == 1, "Length of returned tests should be 1")
t.Equal("Question 1", readAllQuizResponse.Content[0].Question.Text)
}
} }
func (t *integrationTestSuite) TestCatchDuplicateQuiz() { func (t *integrationTestSuite) TestCatchDuplicateQuiz() {

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"strings"
"testing" "testing"
"git.andreafazzi.eu/andrea/probo/client" "git.andreafazzi.eu/andrea/probo/client"
@ -76,6 +77,51 @@ func (t *testSuite) TestGETQuestions() {
t.Equal(http.StatusOK, response.Code) t.Equal(http.StatusOK, response.Code)
} }
func (t *testSuite) TestUpdateQuiz() {
expectedResponse := &client.UpdateQuizResponse{
Status: "success",
Content: &models.Quiz{
Question: &models.Question{ID: "1", Text: "Question 1"},
Answers: []*models.Answer{{}, {}, {}},
},
}
store := &StubTestHubCollectorStore{[]*models.Quiz{
{
Question: &models.Question{ID: "1", Text: "Question 1"},
Answers: []*models.Answer{{}, {}, {}},
},
}}
server := NewQuizHubCollectorServer(store)
payload := `
{
"question": {"text": "Update Question 1"},
"answers": [
{"text": "Text of the answer 1", "correct": true},
{"text": "Text of the answer 2"},
{"text": "Text of the answer 3"},
{"text": "Text of the answer 4"}
]
}
`
request, _ := http.NewRequest(http.MethodPut, "/quizzes/1/update", strings.NewReader(payload))
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
result, err := decodeUpdateQuizResponse(response.Body)
t.True(err == nil, fmt.Sprintf("Response should be decoded without errors: %v", err))
if !t.Failed() {
t.True(result.Status == expectedResponse.Status)
t.True(reflect.DeepEqual(result, expectedResponse))
t.Equal(http.StatusOK, response.Code)
}
}
func getResponse(body io.Reader) (response *client.ReadAllQuizResponse) { func getResponse(body io.Reader) (response *client.ReadAllQuizResponse) {
err := json.NewDecoder(body).Decode(&response) err := json.NewDecoder(body).Decode(&response)
if err != nil { if err != nil {
@ -85,6 +131,14 @@ func getResponse(body io.Reader) (response *client.ReadAllQuizResponse) {
return return
} }
func decodeUpdateQuizResponse(body io.Reader) (response *client.UpdateQuizResponse, err error) {
err = json.NewDecoder(body).Decode(&response)
if err != nil {
return nil, fmt.Errorf("Unable to parse the response %q from the server: %v", body, err)
}
return
}
func testsAreEqual(got, want *client.ReadAllQuizResponse) bool { func testsAreEqual(got, want *client.ReadAllQuizResponse) bool {
return reflect.DeepEqual(got, want) return reflect.DeepEqual(got, want)
} }

View file

@ -107,7 +107,49 @@ func (s *MemoryQuizHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
} }
func (s *MemoryQuizHubCollectorStore) CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error) { func (s *MemoryQuizHubCollectorStore) CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error) {
hashes := s.hasher.QuizHashes(r) hashes := s.hasher.QuizHashes(r.Quiz)
quizID := uuid.New().String()
quizHash := hashes[len(hashes)-1]
quiz := s.readQuiz(quizHash)
if quiz != nil { // Quiz is already present in the store
return quiz, nil
}
quiz = new(models.Quiz)
questionHash := hashes[0]
q := s.readQuestion(questionHash)
if q == nil { // if the question is not in the store then we should add it
q = s.createQuestion(questionHash, &models.Question{
ID: uuid.New().String(),
Text: r.Question.Text,
})
}
// Populate Question field
quiz.Question = q
for i, answer := range r.Answers {
answerHash := hashes[i+1]
a := s.readAnswer(answerHash)
if a == nil { // if the answer is not in the store add it
a = s.createAnswer(answerHash, &models.Answer{
ID: uuid.New().String(),
Text: answer.Text,
})
if answer.Correct {
quiz.Correct = a // s.readAnswer(answerID)
}
}
quiz.Answers = append(quiz.Answers, a)
}
return s.createQuiz(quizID, quizHash, quiz), nil
}
func (s *MemoryQuizHubCollectorStore) UpdateQuiz(r *client.UpdateQuizRequest) (*models.Quiz, error) {
hashes := s.hasher.QuizHashes(r.Quiz)
quizID := uuid.New().String() quizID := uuid.New().String()
quizHash := hashes[len(hashes)-1] quizHash := hashes[len(hashes)-1]

View file

@ -8,4 +8,5 @@ import (
type QuizHubCollectorStore interface { type QuizHubCollectorStore interface {
ReadAllQuizzes() ([]*models.Quiz, error) ReadAllQuizzes() ([]*models.Quiz, error)
CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error) CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error)
UpdateQuiz(r *client.UpdateQuizRequest) (*models.Quiz, error)
} }