Browse Source

First commit.

Andrea Fazzi 1 year ago
commit
cd7e27504a

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Probo Collector
+
+A backend to collect quizzes in a RESTful way.

+ 27 - 0
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"`
+}

+ 13 - 0
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
+)

+ 18 - 0
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=

+ 112 - 0
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...)
+	}
+}

+ 23 - 0
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))
+}

+ 121 - 0
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.*
+
+*~

+ 21 - 0
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.

+ 4 - 0
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.

+ 10 - 0
misc/logseq/icon.svg

@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hierarchy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="gray" fill="gray" stroke-linecap="round" stroke-linejoin="round">
+  <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
+  <circle cx="12" cy="5" r="2" />
+  <circle cx="5" cy="19" r="2" />
+  <circle cx="19" cy="19" r="2" />
+  <path d="M6.5 17.5l5.5 -4.5l5.5 4.5" />
+  <line x1="12" y1="7" x2="12" y2="13" />
+</svg>
+
+

+ 29 - 0
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"
+  }
+}

+ 13 - 0
misc/logseq/src/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="index.ts" type="module"></script>
+  </body>
+</html>

+ 101 - 0
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: `<button data-on-click="postQuiz" class="renderBtn">Save</button>`,
+		});
+	});
+
+};
+
+logseq.ready(main).catch(console.error);

+ 6 - 0
models/answer.go

@@ -0,0 +1,6 @@
+package models
+
+type Answer struct {
+	ID   string
+	Text string
+}

+ 6 - 0
models/player.go

@@ -0,0 +1,6 @@
+package models
+
+type Player struct {
+	Name string
+	Wins int
+}

+ 7 - 0
models/question.go

@@ -0,0 +1,7 @@
+package models
+
+type Question struct {
+	ID        string
+	Text      string
+	AnswerIDs []string
+}

+ 10 - 0
models/test.go

@@ -0,0 +1,10 @@
+package models
+
+type Quiz struct {
+	ID uint
+
+	Question *Question
+	Answers  []*Answer
+	Correct  *Answer
+	Type     int
+}

+ 68 - 0
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
+}

+ 69 - 0
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)
+}

+ 90 - 0
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)
+}

+ 81 - 0
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)))
+}

+ 11 - 0
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
+}

+ 10 - 0
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"}
+  ]
+}

+ 1 - 0
tests/hurl/read_all_quiz.hurl

@@ -0,0 +1 @@
+GET http://localhost:3000/quizzes