commit cd7e27504ae5fe9efa77f7186005bec8c2cc56da Author: Andrea Fazzi Date: Thu Jun 23 11:25:35 2022 +0200 First commit. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c435bac --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Probo Collector + +A backend to collect quizzes in a RESTful way. diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..2263024 --- /dev/null +++ b/client/client.go @@ -0,0 +1,27 @@ +package client + +import "git.andreafazzi.eu/andrea/testhub/models" + +type Response struct { + Status string `json:"status"` + Content interface{} `json:"content"` +} + +type QuizReadAllResponse struct { + Status string `json:"status"` + Content []*models.Quiz `json:"content"` +} + +type CreateQuestionRequest struct { + Text string `json:"text"` +} + +type CreateAnswerRequest struct { + Text string + Correct bool +} + +type CreateQuizRequest struct { + Question *CreateQuestionRequest `json:"question"` + Answers []*CreateAnswerRequest `json:"answers"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..50eb730 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.andreafazzi.eu/andrea/testhub + +go 1.17 + +require ( + github.com/google/uuid 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 + github.com/sirupsen/logrus v1.8.1 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fe2dcf6 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U= +github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..130107b --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,112 @@ +package logger + +import ( + "log" + "net/http" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + Disabled = iota + InfoLevel + WarningLevel + DebugLevel +) + +type ( + // struct for holding response details + responseData struct { + status int + size int + } + + // our http.ResponseWriter implementation + loggingResponseWriter struct { + http.ResponseWriter // compose original http.ResponseWriter + responseData *responseData + } +) + +var loggerLevel int = InfoLevel + +func (r *loggingResponseWriter) Write(b []byte) (int, error) { + size, err := r.ResponseWriter.Write(b) // write response using original http.ResponseWriter + r.responseData.size += size // capture size + return size, err +} + +func (r *loggingResponseWriter) WriteHeader(statusCode int) { + r.ResponseWriter.WriteHeader(statusCode) // write status code using original http.ResponseWriter + r.responseData.status = statusCode // capture status code +} + +func WithLogging(h http.Handler) http.Handler { + loggingFn := func(rw http.ResponseWriter, req *http.Request) { + start := time.Now() + + responseData := &responseData{ + status: 0, + size: 0, + } + lrw := loggingResponseWriter{ + ResponseWriter: rw, // compose original http.ResponseWriter + responseData: responseData, + } + h.ServeHTTP(&lrw, req) // inject our implementation of http.ResponseWriter + + duration := time.Since(start) + + if loggerLevel >= InfoLevel { + logrus.WithFields(logrus.Fields{ + "URI": req.RequestURI, + "Method": req.Method, + "Status": responseData.status, + "Duration": duration, + "Size": responseData.size, + }).Info("Completed.") + } + } + return http.HandlerFunc(loggingFn) +} + +func SetLevel(level int) { + loggerLevel = level +} + +func Info(v ...interface{}) { + if loggerLevel >= InfoLevel { + log.Println(v...) + } +} + +func Infof(format string, v ...interface{}) { + if loggerLevel >= InfoLevel { + log.Printf(format, v...) + } +} + +func Warning(v ...interface{}) { + if loggerLevel >= WarningLevel { + log.Println(v...) + } +} + +func Warningf(format string, v ...interface{}) { + if loggerLevel >= WarningLevel { + log.Printf(format, v...) + } +} + +func Debug(v ...interface{}) { + if loggerLevel >= DebugLevel { + log.Println(v...) + } +} + +func Debugf(format string, v ...interface{}) { + if loggerLevel >= DebugLevel { + log.Printf(format, v...) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e92f090 --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "net/http" + + "git.andreafazzi.eu/andrea/testhub/logger" + "git.andreafazzi.eu/andrea/testhub/store" + "github.com/sirupsen/logrus" +) + +const port = "3000" + +func main() { + logger.SetLevel(logger.DebugLevel) + + server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore()) + + addr := "localhost:" + port + logrus.WithField("addr", addr).Info("TestHub Collector server is listening.") + + log.Fatal(http.ListenAndServe(":"+port, server)) +} diff --git a/misc/logseq/.gitignore b/misc/logseq/.gitignore new file mode 100644 index 0000000..3b89403 --- /dev/null +++ b/misc/logseq/.gitignore @@ -0,0 +1,121 @@ +.DS_Store +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +*~ diff --git a/misc/logseq/LICENSE.md b/misc/logseq/LICENSE.md new file mode 100644 index 0000000..6623b47 --- /dev/null +++ b/misc/logseq/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) 2022 Andrea Fazzi + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/misc/logseq/README.md b/misc/logseq/README.md new file mode 100644 index 0000000..49f6771 --- /dev/null +++ b/misc/logseq/README.md @@ -0,0 +1,4 @@ +# What's that? + +A very basic boilerplate useful to start devoloping a +[Logseq](https://logseq.com/) plugin. diff --git a/misc/logseq/icon.svg b/misc/logseq/icon.svg new file mode 100644 index 0000000..9225b08 --- /dev/null +++ b/misc/logseq/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/misc/logseq/package.json b/misc/logseq/package.json new file mode 100644 index 0000000..8558724 --- /dev/null +++ b/misc/logseq/package.json @@ -0,0 +1,29 @@ +{ + "logseq": { + "id": "logseq-helloworld-plugin", + "title": "logseq-helloworld-plugin", + "icon": "./icon.svg" + }, + "name": "logseq-helloworld-plugin", + "version": "1.2.0", + "description": "", + "main": "dist/index.html", + "targets": { + "main": false + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "parcel build --no-source-maps src/index.html --public-url ./" + }, + "keywords": [], + "author": "Andrea Fazzi", + "license": "MIT", + "dependencies": { + "@logseq/libs": "^0.0.1-alpha.35", + "js-base64": "^3.7.2" + }, + "devDependencies": { + "buffer": "^6.0.3", + "parcel": "^2.2.0" + } +} diff --git a/misc/logseq/src/index.html b/misc/logseq/src/index.html new file mode 100644 index 0000000..8770ae7 --- /dev/null +++ b/misc/logseq/src/index.html @@ -0,0 +1,13 @@ + + + + + + + Document + + +
+ + + diff --git a/misc/logseq/src/index.ts b/misc/logseq/src/index.ts new file mode 100644 index 0000000..b3c57df --- /dev/null +++ b/misc/logseq/src/index.ts @@ -0,0 +1,101 @@ +import "@logseq/libs"; +import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user"; + +const endpoint = 'http://localhost:3000/quizzes'; + +const uniqueIdentifier = () => + Math.random() + .toString(36) + .replace(/[^a-z]+/g, ""); + +async function fetchQuizzes() { + const { status: status, content: quizzes } = await fetch(endpoint).then(res => res.json()) + const ret = quizzes || [] + + return ret.map((quiz, i) => { + return `${i + 1}. ${quiz.Question.Text}` + }) +} + +const main = () => { + console.log("logseq-quizhub-plugin LOADED!"); + + logseq.Editor.registerSlashCommand("Get All Quizzes", async () => { + const currBlock = await logseq.Editor.getCurrentBlock(); + + let blocks = await fetchQuizzes() + + blocks = blocks.map((it: BlockEntity) => ({ content: it })) + + await logseq.Editor.insertBatchBlock(currBlock.uuid, blocks, { + sibling: false + }) + }); + + logseq.Editor.registerSlashCommand("Insert a batch of quizzes", async () => { + await logseq.Editor.insertAtEditingCursor( + `{{renderer :quizhub_${uniqueIdentifier()}}}` + ); + + const currBlock = await logseq.Editor.getCurrentBlock(); + await logseq.Editor.insertBlock(currBlock.uuid, 'Text of the question here...', + { + sibling: false, + before: false, + } + ); + + await logseq.Editor.exitEditingMode(); + + }); + + logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => { + const [type] = payload.arguments; + const id = type.split("_")[1]?.trim(); + const quizhubId = `quizhub_${id}`; + + logseq.provideModel({ + async postQuiz() { + const parentBlock = await logseq.Editor.getBlock(payload.uuid, { includeChildren: true }); + const quizzes = parentBlock.children.map((child: BlockEntity) => { + const question = { text: child.content } + const answers = child.children.map((answer: BlockEntity, i: number) => { + return { text: answer.content, correct: (i == 0) ? true : false } + }) + return { question: question, answers: answers } + }); + quizzes.forEach(async quiz => { + const res = await fetch(endpoint, { method: 'POST', body: JSON.stringify(quiz) }) + const data = await res.json(); + console.log(data) + }) + + } + }); + + logseq.provideStyle(` + .renderBtn { + border: 1px solid black; + border-radius: 8px; + padding: 3px; + font-size: 80%; + background-color: white; + color: black; + } + .renderBtn:hover { + background-color: black; + color: white; + } +`); + + logseq.provideUI({ + key: `${quizhubId}`, + slot, + reset: true, + template: ``, + }); + }); + +}; + +logseq.ready(main).catch(console.error); diff --git a/models/answer.go b/models/answer.go new file mode 100644 index 0000000..55ee370 --- /dev/null +++ b/models/answer.go @@ -0,0 +1,6 @@ +package models + +type Answer struct { + ID string + Text string +} diff --git a/models/player.go b/models/player.go new file mode 100644 index 0000000..fffb3be --- /dev/null +++ b/models/player.go @@ -0,0 +1,6 @@ +package models + +type Player struct { + Name string + Wins int +} diff --git a/models/question.go b/models/question.go new file mode 100644 index 0000000..fb06704 --- /dev/null +++ b/models/question.go @@ -0,0 +1,7 @@ +package models + +type Question struct { + ID string + Text string + AnswerIDs []string +} diff --git a/models/test.go b/models/test.go new file mode 100644 index 0000000..077374f --- /dev/null +++ b/models/test.go @@ -0,0 +1,10 @@ +package models + +type Quiz struct { + ID uint + + Question *Question + Answers []*Answer + Correct *Answer + Type int +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..ee4b83e --- /dev/null +++ b/server.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "git.andreafazzi.eu/andrea/testhub/client" + "git.andreafazzi.eu/andrea/testhub/logger" + "git.andreafazzi.eu/andrea/testhub/models" + "git.andreafazzi.eu/andrea/testhub/store" +) + +const jsonContentType = "application/json" + +type QuizHubCollectorServer struct { + store store.QuizHubCollectorStore + http.Handler +} + +func NewQuizHubCollectorServer(store store.QuizHubCollectorStore) *QuizHubCollectorServer { + ps := new(QuizHubCollectorServer) + ps.store = store + + router := http.NewServeMux() + + router.Handle("/quizzes", logger.WithLogging(http.HandlerFunc(ps.testHandler))) + + ps.Handler = 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)) + + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(ps.createQuiz(w, r)) + } +} + +func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http.Request) *client.Response { + tests, err := ps.store.ReadAllQuizzes() + if err != nil { + return &client.Response{Status: "error", Content: err.Error()} + } + return &client.Response{Status: "success", Content: tests} +} + +func (ps *QuizHubCollectorServer) createQuiz(w http.ResponseWriter, r *http.Request) *models.Quiz { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + createQuizReq := new(client.CreateQuizRequest) + err = json.Unmarshal(body, &createQuizReq) + if err != nil { + panic(err) + } + + createdQuiz := ps.store.CreateQuiz(createQuizReq) + + return createdQuiz +} diff --git a/server_integration_test.go b/server_integration_test.go new file mode 100644 index 0000000..102d92e --- /dev/null +++ b/server_integration_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + + "git.andreafazzi.eu/andrea/testhub/client" + "git.andreafazzi.eu/andrea/testhub/models" + "git.andreafazzi.eu/andrea/testhub/store" + "github.com/remogatto/prettytest" +) + +type integrationTestSuite struct { + prettytest.Suite +} + +func (t *integrationTestSuite) TestQuizCreateAndReadAll() { + server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore()) + + // 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"} + ] +} +` + request, _ := http.NewRequest(http.MethodPost, "/quizzes", strings.NewReader(payload)) + response := httptest.NewRecorder() + + server.ServeHTTP(response, request) + + returnedTest := new(models.Quiz) + + err := json.Unmarshal(response.Body.Bytes(), returnedTest) + if err != nil { + t.True(err == nil, err.Error()) + } + + t.Equal("Question 1", returnedTest.Question.Text) + 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") + t.True(returnedTest.Question.ID != "", "Question ID should not be empty") + t.True(returnedTest.Answers[0].ID != "", "Answer ID should not be empty") + + request, _ = http.NewRequest(http.MethodGet, "/quizzes", nil) + response = httptest.NewRecorder() + + server.ServeHTTP(response, request) + + decodedResponse := new(client.QuizReadAllResponse) + + err = json.Unmarshal(response.Body.Bytes(), &decodedResponse) + if err != nil { + t.True(err == nil, err.Error()) + } + + t.True(len(decodedResponse.Content) == 1, "Length of returned tests should be 1") + t.Equal("Question 1", decodedResponse.Content[0].Question.Text) +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..d2d0a39 --- /dev/null +++ b/server_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "git.andreafazzi.eu/andrea/testhub/client" + "git.andreafazzi.eu/andrea/testhub/logger" + "git.andreafazzi.eu/andrea/testhub/models" + "github.com/remogatto/prettytest" +) + +type testSuite struct { + prettytest.Suite +} + +type StubTestHubCollectorStore struct { + tests []*models.Quiz +} + +func (store *StubTestHubCollectorStore) CreateQuiz(test *client.CreateQuizRequest) *models.Quiz { + return nil +} + +func (store *StubTestHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) { + return store.tests, nil +} + +func TestRunner(t *testing.T) { + prettytest.Run( + t, + new(testSuite), + new(integrationTestSuite), + ) +} + +func (t *testSuite) BeforeAll() { + logger.SetLevel(logger.Disabled) +} + +func (t *testSuite) TestGETQuestions() { + expectedResult := &client.QuizReadAllResponse{ + 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) + + request, _ := http.NewRequest(http.MethodGet, "/quizzes", nil) + response := httptest.NewRecorder() + + server.ServeHTTP(response, request) + + result := getResponse(response.Body) + + t.True(result.Status == expectedResult.Status) + t.True(testsAreEqual(result, expectedResult)) + + t.Equal(http.StatusOK, response.Code) +} + +func getResponse(body io.Reader) (response *client.QuizReadAllResponse) { + err := json.NewDecoder(body).Decode(&response) + if err != nil { + panic(fmt.Errorf("Unable to parse response from server %q into slice of Test, '%v'", body, err)) + } + + return +} + +func testsAreEqual(got, want *client.QuizReadAllResponse) bool { + return reflect.DeepEqual(got, want) +} diff --git a/store/memory.go b/store/memory.go new file mode 100644 index 0000000..5509898 --- /dev/null +++ b/store/memory.go @@ -0,0 +1,81 @@ +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))) +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..dfdbd96 --- /dev/null +++ b/store/store.go @@ -0,0 +1,11 @@ +package store + +import ( + "git.andreafazzi.eu/andrea/testhub/client" + "git.andreafazzi.eu/andrea/testhub/models" +) + +type QuizHubCollectorStore interface { + ReadAllQuizzes() ([]*models.Quiz, error) + CreateQuiz(r *client.CreateQuizRequest) *models.Quiz +} diff --git a/tests/hurl/create_new_quiz.hurl b/tests/hurl/create_new_quiz.hurl new file mode 100644 index 0000000..1f9cf67 --- /dev/null +++ b/tests/hurl/create_new_quiz.hurl @@ -0,0 +1,10 @@ +POST http://localhost:3000/quizzes +{ + "question": {"text": "Text of 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"} + ] +} diff --git a/tests/hurl/read_all_quiz.hurl b/tests/hurl/read_all_quiz.hurl new file mode 100644 index 0000000..5b36af9 --- /dev/null +++ b/tests/hurl/read_all_quiz.hurl @@ -0,0 +1 @@ +GET http://localhost:3000/quizzes