Implementing first hashing
This commit is contained in:
parent
cd7e27504a
commit
bcdd00d506
14 changed files with 415 additions and 136 deletions
|
@ -1,17 +1,22 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import "git.andreafazzi.eu/andrea/testhub/models"
|
import "git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Content interface{} `json:"content"`
|
Content interface{} `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuizReadAllResponse struct {
|
type ReadAllQuizResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Content []*models.Quiz `json:"content"`
|
Content []*models.Quiz `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateQuizResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Content *models.Quiz `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateQuestionRequest struct {
|
type CreateQuestionRequest struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -1,13 +1,13 @@
|
||||||
module git.andreafazzi.eu/andrea/testhub
|
module git.andreafazzi.eu/andrea/probo
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
require github.com/sirupsen/logrus v1.8.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid 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
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,6 +1,4 @@
|
||||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
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=
|
||||||
|
|
26
hasher/hasher.go
Normal file
26
hasher/hasher.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package hasher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HashFunc func(string) string
|
||||||
|
|
||||||
|
type Hasher interface {
|
||||||
|
// Hash returns a slice of hashes. The first one is an
|
||||||
|
// hash calculated from the question using
|
||||||
|
// QuestionHash. Following hashes are calculated from the
|
||||||
|
// answers using AnswerHash.
|
||||||
|
QuizHashes(quiz *client.CreateQuizRequest) []string
|
||||||
|
|
||||||
|
// QuestionHash returns an hash calculated from a field of
|
||||||
|
// Question struct.
|
||||||
|
QuestionHash(question *client.CreateQuestionRequest) string
|
||||||
|
|
||||||
|
// AnswerHash returns an hash calculated from a field of
|
||||||
|
// Answer struct.
|
||||||
|
AnswerHash(answer *client.CreateAnswerRequest) string
|
||||||
|
|
||||||
|
// Calculate calculates a checksum from all the given hashes.
|
||||||
|
Calculate(hashes []string) string
|
||||||
|
}
|
64
hasher/hasher_test.go
Normal file
64
hasher/hasher_test.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package hasher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunner(t *testing.T) {
|
||||||
|
prettytest.Run(
|
||||||
|
t,
|
||||||
|
new(testSuite),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testSuite) TestQuizHashes() {
|
||||||
|
h := NewDefaultHash(DefaultSHA256HashingFn)
|
||||||
|
|
||||||
|
firstQuizRequest := &client.CreateQuizRequest{
|
||||||
|
Question: &client.CreateQuestionRequest{
|
||||||
|
Text: "Question 1"},
|
||||||
|
Answers: []*client.CreateAnswerRequest{
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
secondQuizRequest := &client.CreateQuizRequest{
|
||||||
|
Question: &client.CreateQuestionRequest{
|
||||||
|
Text: "Question 1"},
|
||||||
|
Answers: []*client.CreateAnswerRequest{
|
||||||
|
{Text: "Answer 1", Correct: false},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thirdQuizRequest := &client.CreateQuizRequest{
|
||||||
|
Question: &client.CreateQuestionRequest{
|
||||||
|
Text: "Question 2"},
|
||||||
|
Answers: []*client.CreateAnswerRequest{
|
||||||
|
{Text: "Answer 1", Correct: false},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hashesFromFirstRequest := h.QuizHashes(firstQuizRequest)
|
||||||
|
hashesFromSecondRequest := h.QuizHashes(secondQuizRequest)
|
||||||
|
hashesFromThirdRequest := h.QuizHashes(thirdQuizRequest)
|
||||||
|
|
||||||
|
t.Equal(5, len(hashesFromFirstRequest))
|
||||||
|
|
||||||
|
t.True(hashesFromFirstRequest[1] == hashesFromSecondRequest[2], "Answers' hashes should maintain original request's order")
|
||||||
|
t.True(hashesFromFirstRequest[4] == hashesFromSecondRequest[4], "Quiz hash should be the same because quizzes are duplicated")
|
||||||
|
t.True(hashesFromFirstRequest[1] != hashesFromThirdRequest[1], "Questions' hashes should not be the same because texts are different")
|
||||||
|
t.True(hashesFromFirstRequest[4] != hashesFromThirdRequest[4], "Quiz hash should not be the same because quizzes are not duplicated")
|
||||||
|
}
|
54
hasher/sha256/sha256.go
Normal file
54
hasher/sha256/sha256.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package sha256
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/hasher"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultSHA256HashingFn = func(text string) string {
|
||||||
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(text)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Default256Hasher struct {
|
||||||
|
hashFn hasher.HashFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefault256Hasher(hashFn hasher.HashFunc) *Default256Hasher {
|
||||||
|
return &Default256Hasher{hashFn}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Default256Hasher) QuizHashes(quiz *client.CreateQuizRequest) []string {
|
||||||
|
result := make([]string, 0)
|
||||||
|
|
||||||
|
result = append(result, h.QuestionHash(quiz.Question))
|
||||||
|
|
||||||
|
for _, a := range quiz.Answers {
|
||||||
|
result = append(result, h.AnswerHash(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, h.Calculate(result))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Default256Hasher) QuestionHash(question *client.CreateQuestionRequest) string {
|
||||||
|
return h.hashFn(question.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Default256Hasher) AnswerHash(answer *client.CreateAnswerRequest) string {
|
||||||
|
return h.hashFn(answer.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Default256Hasher) Calculate(hashes []string) string {
|
||||||
|
orderedHashes := make([]string, len(hashes))
|
||||||
|
|
||||||
|
copy(orderedHashes, hashes)
|
||||||
|
sort.Strings(orderedHashes)
|
||||||
|
|
||||||
|
return h.hashFn(strings.Join(orderedHashes, ""))
|
||||||
|
}
|
15
main.go
15
main.go
|
@ -4,8 +4,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/logger"
|
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
||||||
"git.andreafazzi.eu/andrea/testhub/store"
|
"git.andreafazzi.eu/andrea/probo/logger"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/store/memory"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,10 +15,14 @@ const port = "3000"
|
||||||
func main() {
|
func main() {
|
||||||
logger.SetLevel(logger.DebugLevel)
|
logger.SetLevel(logger.DebugLevel)
|
||||||
|
|
||||||
server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore())
|
server := NewQuizHubCollectorServer(
|
||||||
|
memory.NewMemoryQuizHubCollectorStore(
|
||||||
|
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
addr := "localhost:" + port
|
addr := "http://localhost:" + port
|
||||||
logrus.WithField("addr", addr).Info("TestHub Collector server is listening.")
|
logrus.WithField("address", addr).Info("Probo Collector is up&running...")
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":"+port, server))
|
log.Fatal(http.ListenAndServe(":"+port, server))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
type Quiz struct {
|
type Quiz struct {
|
||||||
ID uint
|
ID string
|
||||||
|
|
||||||
Question *Question
|
Question *Question
|
||||||
Answers []*Answer
|
Answers []*Answer
|
||||||
|
|
34
server.go
34
server.go
|
@ -5,10 +5,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/client"
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
"git.andreafazzi.eu/andrea/testhub/logger"
|
"git.andreafazzi.eu/andrea/probo/logger"
|
||||||
"git.andreafazzi.eu/andrea/testhub/models"
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
"git.andreafazzi.eu/andrea/testhub/store"
|
"git.andreafazzi.eu/andrea/probo/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const jsonContentType = "application/json"
|
const jsonContentType = "application/json"
|
||||||
|
@ -38,8 +38,17 @@ func (ps *QuizHubCollectorServer) testHandler(w http.ResponseWriter, r *http.Req
|
||||||
json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r))
|
json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r))
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
|
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)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
json.NewEncoder(w).Encode(ps.createQuiz(w, r))
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,18 +60,23 @@ 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) createQuiz(w http.ResponseWriter, r *http.Request) *models.Quiz {
|
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 {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
createQuizReq := new(client.CreateQuizRequest)
|
createQuizReq := new(client.CreateQuizRequest)
|
||||||
|
|
||||||
err = json.Unmarshal(body, &createQuizReq)
|
err = json.Unmarshal(body, &createQuizReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
createdQuiz := ps.store.CreateQuiz(createQuizReq)
|
createdQuiz, err := ps.store.CreateQuiz(createQuizReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return createdQuiz
|
return createdQuiz, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/client"
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
"git.andreafazzi.eu/andrea/testhub/models"
|
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
||||||
"git.andreafazzi.eu/andrea/testhub/store"
|
"git.andreafazzi.eu/andrea/probo/store/memory"
|
||||||
"github.com/remogatto/prettytest"
|
"github.com/remogatto/prettytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +18,11 @@ type integrationTestSuite struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *integrationTestSuite) TestQuizCreateAndReadAll() {
|
func (t *integrationTestSuite) TestQuizCreateAndReadAll() {
|
||||||
server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore())
|
server := NewQuizHubCollectorServer(
|
||||||
|
memory.NewMemoryQuizHubCollectorStore(
|
||||||
|
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// POST a new question using a JSON payload
|
// POST a new question using a JSON payload
|
||||||
|
|
||||||
|
@ -32,38 +37,80 @@ func (t *integrationTestSuite) TestQuizCreateAndReadAll() {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
createQuizResponse, err := t.createQuiz(server, payload)
|
||||||
|
t.True(err == nil)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
readAllQuizResponse, err := t.readAllQuiz(server)
|
||||||
|
t.True(err == nil)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
server := NewQuizHubCollectorServer(
|
||||||
|
memory.NewMemoryQuizHubCollectorStore(
|
||||||
|
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// POST a new question using a JSON payload
|
||||||
|
|
||||||
|
payload := `
|
||||||
|
{
|
||||||
|
"question": {"text": "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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
quiz1, err := t.createQuiz(server, payload)
|
||||||
|
t.True(err == nil)
|
||||||
|
quiz2, err := t.createQuiz(server, payload)
|
||||||
|
t.True(err == nil, "Quiz are duplicated, but the API should not return an error")
|
||||||
|
t.True(reflect.DeepEqual(quiz1, quiz2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *integrationTestSuite) createQuiz(server *QuizHubCollectorServer, payload string) (*client.CreateQuizResponse, error) {
|
||||||
request, _ := http.NewRequest(http.MethodPost, "/quizzes", strings.NewReader(payload))
|
request, _ := http.NewRequest(http.MethodPost, "/quizzes", strings.NewReader(payload))
|
||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
server.ServeHTTP(response, request)
|
server.ServeHTTP(response, request)
|
||||||
|
|
||||||
returnedTest := new(models.Quiz)
|
decodedResponse := new(client.CreateQuizResponse)
|
||||||
|
|
||||||
err := json.Unmarshal(response.Body.Bytes(), returnedTest)
|
err := json.Unmarshal(response.Body.Bytes(), decodedResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.True(err == nil, err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Equal("Question 1", returnedTest.Question.Text)
|
return decodedResponse, err
|
||||||
t.Equal("Text of the answer 1", returnedTest.Answers[0].Text)
|
}
|
||||||
t.Equal("Text of the answer 1", returnedTest.Correct.Text)
|
|
||||||
|
|
||||||
t.True(returnedTest.ID != 0, "Test ID should not be 0")
|
func (t *integrationTestSuite) readAllQuiz(server *QuizHubCollectorServer) (*client.ReadAllQuizResponse, error) {
|
||||||
t.True(returnedTest.Question.ID != "", "Question ID should not be empty")
|
request, _ := http.NewRequest(http.MethodGet, "/quizzes", nil)
|
||||||
t.True(returnedTest.Answers[0].ID != "", "Answer ID should not be empty")
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
request, _ = http.NewRequest(http.MethodGet, "/quizzes", nil)
|
|
||||||
response = httptest.NewRecorder()
|
|
||||||
|
|
||||||
server.ServeHTTP(response, request)
|
server.ServeHTTP(response, request)
|
||||||
|
|
||||||
decodedResponse := new(client.QuizReadAllResponse)
|
decodedResponse := new(client.ReadAllQuizResponse)
|
||||||
|
|
||||||
err = json.Unmarshal(response.Body.Bytes(), &decodedResponse)
|
err := json.Unmarshal(response.Body.Bytes(), decodedResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.True(err == nil, err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.True(len(decodedResponse.Content) == 1, "Length of returned tests should be 1")
|
return decodedResponse, err
|
||||||
t.Equal("Question 1", decodedResponse.Content[0].Question.Text)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/client"
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
"git.andreafazzi.eu/andrea/testhub/logger"
|
"git.andreafazzi.eu/andrea/probo/logger"
|
||||||
"git.andreafazzi.eu/andrea/testhub/models"
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
"github.com/remogatto/prettytest"
|
"github.com/remogatto/prettytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ type StubTestHubCollectorStore struct {
|
||||||
tests []*models.Quiz
|
tests []*models.Quiz
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *StubTestHubCollectorStore) CreateQuiz(test *client.CreateQuizRequest) *models.Quiz {
|
func (store *StubTestHubCollectorStore) CreateQuiz(test *client.CreateQuizRequest) (*models.Quiz, error) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *StubTestHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
func (store *StubTestHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
||||||
|
@ -44,7 +44,7 @@ func (t *testSuite) BeforeAll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestGETQuestions() {
|
func (t *testSuite) TestGETQuestions() {
|
||||||
expectedResult := &client.QuizReadAllResponse{
|
expectedResult := &client.ReadAllQuizResponse{
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Content: []*models.Quiz{
|
Content: []*models.Quiz{
|
||||||
{
|
{
|
||||||
|
@ -76,7 +76,7 @@ func (t *testSuite) TestGETQuestions() {
|
||||||
t.Equal(http.StatusOK, response.Code)
|
t.Equal(http.StatusOK, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResponse(body io.Reader) (response *client.QuizReadAllResponse) {
|
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 {
|
||||||
panic(fmt.Errorf("Unable to parse response from server %q into slice of Test, '%v'", body, err))
|
panic(fmt.Errorf("Unable to parse response from server %q into slice of Test, '%v'", body, err))
|
||||||
|
@ -85,6 +85,6 @@ func getResponse(body io.Reader) (response *client.QuizReadAllResponse) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func testsAreEqual(got, want *client.QuizReadAllResponse) bool {
|
func testsAreEqual(got, want *client.ReadAllQuizResponse) bool {
|
||||||
return reflect.DeepEqual(got, want)
|
return reflect.DeepEqual(got, want)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/client"
|
|
||||||
"git.andreafazzi.eu/andrea/testhub/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MemoryQuizHubCollectorStore struct {
|
|
||||||
questions map[string]*models.Question
|
|
||||||
answers map[string]*models.Answer
|
|
||||||
tests map[uint]*models.Quiz
|
|
||||||
lastQuizID uint
|
|
||||||
|
|
||||||
questionAnswer map[string][]string
|
|
||||||
testQuestion map[string]uint
|
|
||||||
|
|
||||||
// A mutex is used to synchronize read/write access to the map
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMemoryQuizHubCollectorStore() *MemoryQuizHubCollectorStore {
|
|
||||||
s := new(MemoryQuizHubCollectorStore)
|
|
||||||
|
|
||||||
s.questions = make(map[string]*models.Question)
|
|
||||||
s.answers = make(map[string]*models.Answer)
|
|
||||||
s.tests = make(map[uint]*models.Quiz)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryQuizHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
|
||||||
result := make([]*models.Quiz, 0)
|
|
||||||
for _, t := range s.tests {
|
|
||||||
result = append(result, t)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryQuizHubCollectorStore) CreateQuiz(r *client.CreateQuizRequest) *models.Quiz {
|
|
||||||
questionID := hash(r.Question.Text)
|
|
||||||
test := new(models.Quiz)
|
|
||||||
q, ok := s.questions[questionID]
|
|
||||||
if !ok { // if the question is not in the store add it
|
|
||||||
s.questions[questionID] = &models.Question{
|
|
||||||
ID: questionID,
|
|
||||||
Text: r.Question.Text,
|
|
||||||
}
|
|
||||||
q = s.questions[questionID]
|
|
||||||
}
|
|
||||||
// Populate Question field
|
|
||||||
test.Question = q
|
|
||||||
for _, answer := range r.Answers {
|
|
||||||
// Calculate the hash from text
|
|
||||||
answerID := hash(answer.Text)
|
|
||||||
_, ok := s.answers[answerID]
|
|
||||||
if !ok { // if the answer is not in the store add it
|
|
||||||
s.answers[answerID] = &models.Answer{
|
|
||||||
ID: answerID,
|
|
||||||
Text: answer.Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if answer.Correct {
|
|
||||||
test.Correct = s.answers[answerID]
|
|
||||||
}
|
|
||||||
test.Answers = append(test.Answers, s.answers[answerID])
|
|
||||||
}
|
|
||||||
|
|
||||||
s.lastQuizID++
|
|
||||||
test.ID = s.lastQuizID
|
|
||||||
s.tests[s.lastQuizID] = test
|
|
||||||
|
|
||||||
return test
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(text string) string {
|
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(text)))
|
|
||||||
}
|
|
147
store/memory/memory.go
Normal file
147
store/memory/memory.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/hasher"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemoryQuizHubCollectorStore struct {
|
||||||
|
questions map[string]*models.Question
|
||||||
|
answers map[string]*models.Answer
|
||||||
|
quizzes map[string]*models.Quiz
|
||||||
|
|
||||||
|
questionAnswer map[string][]string
|
||||||
|
testQuestion map[string]uint
|
||||||
|
|
||||||
|
// A mutex is used to synchronize read/write access to the map
|
||||||
|
lock sync.RWMutex
|
||||||
|
|
||||||
|
hasher hasher.Hasher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryQuizHubCollectorStore(hasher hasher.Hasher) *MemoryQuizHubCollectorStore {
|
||||||
|
s := new(MemoryQuizHubCollectorStore)
|
||||||
|
|
||||||
|
s.hasher = hasher
|
||||||
|
s.questions = make(map[string]*models.Question)
|
||||||
|
s.answers = make(map[string]*models.Answer)
|
||||||
|
s.quizzes = make(map[string]*models.Quiz)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) readQuiz(id string) *models.Quiz {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
quiz, ok := s.quizzes[id]
|
||||||
|
if ok {
|
||||||
|
return quiz
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) readQuestion(id string) *models.Question {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
question, ok := s.questions[id]
|
||||||
|
if ok {
|
||||||
|
return question
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) readAnswer(id string) *models.Answer {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
answer, ok := s.answers[id]
|
||||||
|
if ok {
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) createQuiz(id string, quiz *models.Quiz) *models.Quiz {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
quiz.ID = id
|
||||||
|
s.quizzes[id] = quiz
|
||||||
|
|
||||||
|
return quiz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) createQuestion(id string, question *models.Question) *models.Question {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
s.questions[id] = question
|
||||||
|
|
||||||
|
return question
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) createAnswer(id string, answer *models.Answer) *models.Answer {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
s.answers[id] = answer
|
||||||
|
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
||||||
|
result := make([]*models.Quiz, 0)
|
||||||
|
for id, _ := range s.quizzes {
|
||||||
|
result = append(result, s.readQuiz(id))
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error) {
|
||||||
|
hashes := s.hasher.QuizHashes(r)
|
||||||
|
|
||||||
|
quizID := hashes[len(hashes)-1]
|
||||||
|
|
||||||
|
quiz := s.readQuiz(quizID)
|
||||||
|
if quiz != nil { // Quiz is already present in the store
|
||||||
|
return quiz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
quiz = new(models.Quiz)
|
||||||
|
|
||||||
|
questionID := hashes[0]
|
||||||
|
q := s.readQuestion(questionID)
|
||||||
|
if q == nil { // if the question is not in the store add it
|
||||||
|
q = s.createQuestion(questionID, &models.Question{
|
||||||
|
ID: questionID,
|
||||||
|
Text: r.Question.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate Question field
|
||||||
|
quiz.Question = q
|
||||||
|
for i, answer := range r.Answers {
|
||||||
|
answerID := hashes[i+1]
|
||||||
|
a := s.readAnswer(answerID)
|
||||||
|
if a == nil { // if the answer is not in the store add it
|
||||||
|
a = s.createAnswer(answerID, &models.Answer{
|
||||||
|
ID: answerID,
|
||||||
|
Text: answer.Text,
|
||||||
|
})
|
||||||
|
if answer.Correct {
|
||||||
|
quiz.Correct = a // s.readAnswer(answerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quiz.Answers = append(quiz.Answers, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.createQuiz(quizID, quiz), nil
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.andreafazzi.eu/andrea/testhub/client"
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
"git.andreafazzi.eu/andrea/testhub/models"
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QuizHubCollectorStore interface {
|
type QuizHubCollectorStore interface {
|
||||||
ReadAllQuizzes() ([]*models.Quiz, error)
|
ReadAllQuizzes() ([]*models.Quiz, error)
|
||||||
CreateQuiz(r *client.CreateQuizRequest) *models.Quiz
|
CreateQuiz(r *client.CreateQuizRequest) (*models.Quiz, error)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue