diff --git a/client/client.go b/client/client.go index fca676c..afd856e 100644 --- a/client/client.go +++ b/client/client.go @@ -7,6 +7,20 @@ type Response struct { 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 { Status string `json:"status"` Content []*models.Quiz `json:"content"` @@ -17,16 +31,24 @@ type CreateQuizResponse struct { Content *models.Quiz `json:"content"` } +type UpdateQuizResponse struct { + Status string `json:"status"` + Content *models.Quiz `json:"content"` +} + type CreateQuestionRequest struct { - Text string `json:"text"` + *Question } type CreateAnswerRequest struct { - Text string - Correct bool + *Answer } type CreateQuizRequest struct { - Question *CreateQuestionRequest `json:"question"` - Answers []*CreateAnswerRequest `json:"answers"` + *Quiz +} + +type UpdateQuizRequest struct { + ID string + *Quiz } diff --git a/go.mod b/go.mod index 9bf7911..54b7830 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require github.com/sirupsen/logrus v1.8.1 require ( 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/text v0.1.0 // indirect github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect diff --git a/go.sum b/go.sum index fe2dcf6..1796bf2 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ 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/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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/hasher/hasher.go b/hasher/hasher.go index 9dbaba3..64b036a 100644 --- a/hasher/hasher.go +++ b/hasher/hasher.go @@ -11,15 +11,15 @@ type Hasher interface { // hash calculated from the question using // QuestionHash. Following hashes are calculated from the // answers using AnswerHash. - QuizHashes(quiz *client.CreateQuizRequest) []string + QuizHashes(quiz *client.Quiz) []string // QuestionHash returns an hash calculated from a field of // Question struct. - QuestionHash(question *client.CreateQuestionRequest) string + QuestionHash(question *client.Question) string // AnswerHash returns an hash calculated from a field of // Answer struct. - AnswerHash(answer *client.CreateAnswerRequest) string + AnswerHash(answer *client.Answer) string // Calculate calculates a checksum from all the given hashes. Calculate(hashes []string) string diff --git a/hasher/sha256/sha256.go b/hasher/sha256/sha256.go index 8a9310f..169c639 100644 --- a/hasher/sha256/sha256.go +++ b/hasher/sha256/sha256.go @@ -22,7 +22,7 @@ func NewDefault256Hasher(hashFn hasher.HashFunc) *Default256Hasher { return &Default256Hasher{hashFn} } -func (h *Default256Hasher) QuizHashes(quiz *client.CreateQuizRequest) []string { +func (h *Default256Hasher) QuizHashes(quiz *client.Quiz) []string { result := make([]string, 0) result = append(result, h.QuestionHash(quiz.Question)) @@ -36,11 +36,11 @@ func (h *Default256Hasher) QuizHashes(quiz *client.CreateQuizRequest) []string { return result } -func (h *Default256Hasher) QuestionHash(question *client.CreateQuestionRequest) string { +func (h *Default256Hasher) QuestionHash(question *client.Question) string { 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) } diff --git a/server.go b/server.go index c68645e..19b6462 100644 --- a/server.go +++ b/server.go @@ -9,6 +9,7 @@ import ( "git.andreafazzi.eu/andrea/probo/logger" "git.andreafazzi.eu/andrea/probo/models" "git.andreafazzi.eu/andrea/probo/store" + "github.com/julienschmidt/httprouter" ) const jsonContentType = "application/json" @@ -22,34 +23,49 @@ func NewQuizHubCollectorServer(store store.QuizHubCollectorStore) *QuizHubCollec ps := new(QuizHubCollectorServer) 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 } -func (ps *QuizHubCollectorServer) testHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - w.Header().Set("content-type", jsonContentType) - json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r)) +func (ps *QuizHubCollectorServer) readAllQuizzesHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + w.Header().Set("content-type", jsonContentType) + json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r)) +} - case http.MethodPost: - response := new(client.Response) +func (ps *QuizHubCollectorServer) createQuizHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + response := new(client.Response) - quiz, err := ps.createQuiz(w, r) - 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) + quiz, err := ps.createQuiz(w, r) + 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) 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 { @@ -60,6 +76,27 @@ func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http. 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) { body, err := ioutil.ReadAll(r.Body) if err != nil { diff --git a/server_integration_test.go b/server_integration_test.go index e93f551..db2f63f 100644 --- a/server_integration_test.go +++ b/server_integration_test.go @@ -38,22 +38,26 @@ func (t *integrationTestSuite) TestQuizCreateAndReadAll() { } ` createQuizResponse, err := t.createQuiz(server, payload) - t.True(err == nil) + t.True(err == nil, "Response should be decoded properly") - t.Equal("success", createQuizResponse.Status) - t.Equal("Question 1", createQuizResponse.Content.Question.Text) - t.Equal("Text of the answer 1", createQuizResponse.Content.Answers[0].Text) - t.Equal("Text of the answer 1", createQuizResponse.Content.Correct.Text) + if !t.Failed() { + t.Equal("success", createQuizResponse.Status) + t.Equal("Question 1", createQuizResponse.Content.Question.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.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.ID != "", "Test 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") + } 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") - t.Equal("Question 1", readAllQuizResponse.Content[0].Question.Text) + if !t.Failed() { + 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() { diff --git a/server_test.go b/server_test.go index ac838de..87186fe 100644 --- a/server_test.go +++ b/server_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "git.andreafazzi.eu/andrea/probo/client" @@ -76,6 +77,51 @@ func (t *testSuite) TestGETQuestions() { 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) { err := json.NewDecoder(body).Decode(&response) if err != nil { @@ -85,6 +131,14 @@ func getResponse(body io.Reader) (response *client.ReadAllQuizResponse) { 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 { return reflect.DeepEqual(got, want) } diff --git a/store/memory/memory.go b/store/memory/memory.go index b08a2f7..fe67ed8 100644 --- a/store/memory/memory.go +++ b/store/memory/memory.go @@ -107,7 +107,49 @@ func (s *MemoryQuizHubCollectorStore) ReadAllQuizzes() ([]*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() quizHash := hashes[len(hashes)-1] diff --git a/store/store.go b/store/store.go index 1fc4c72..941138f 100644 --- a/store/store.go +++ b/store/store.go @@ -8,4 +8,5 @@ import ( type QuizHubCollectorStore interface { ReadAllQuizzes() ([]*models.Quiz, error) CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error) + UpdateQuiz(r *client.UpdateQuizRequest) (*models.Quiz, error) }