Skip to content
Snippets Groups Projects
Commit 3cc665ee authored by Mimmi Cromsjö's avatar Mimmi Cromsjö
Browse files

hej

parent 7a88392e
Branches
No related tags found
No related merge requests found
Pipeline #10327 passed
# To-Do API
This is a backend for managing a simple task management application.
The backend consists of three different parts:
- Go application
- SQLite (default)
- PostgreSQL (optional)
- NGINX (optional)
An overview of the system can be seen here (dotted boxes indicates optional parts):
![](architecture.png)
## Go application
In order to run the application, you need one dependency: "github.com/lib/pq","github.com/mattn/go-sqlite3".
This can be retrieved by running `go get`.
If you are using SQLite you may want to change the database file.
Update the following line in todo.go:
sql.Open("sqlite3", "./foo.db")
If you are using PostgreSQL you may want to update the database information to match your system.
Update the following constants in todo.go:
- databaseUser
- databaseHost
- databaseName
## SQLite database
If SQLite is used to store all the list and todo information.
In order to setup the database schema, run `sqlite3 foo.db < schema.sql`.
## PostgreSQL database
If PostgreSQL is used to store all the list and todo information.
In order to setup the database schema, run `psql -f schema.sql db_name`.
## NGINX (Optional)
There is a configuration ready for using NGINX as a reverse proxy (in `nginx.conf`).
However, the url to the application may need to be updated.
## Run
To build to application, simply use `go build` in the project directory.
You can also use `go run todo.go` to run the server directly.
## Test
Test the system to make sure everything works.
This is done by running `go test` in the application folder.
architecture.png

23.5 KiB

user nginx;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events{}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
keepalive_timeout 65;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server {
listen 80;
proxy_pass_header Server;
location / {
# TODO: Update the url
proxy_pass http://app:8080;
}
}
}
-- A table that stores all the lists in the system.
drop table if exists list;
create table list(
id integer primary key autoincrement,
name text not null
);
-- A table that stores all the tasks in the system.
drop table if exists task;
create table task(
id integer primary key autoincrement,
name text not null,
done boolean default false,
list int references list(id)
);
todo.go 0 → 100644
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
)
const (
databaseUser = "postgres"
databaseHost = "db"
databaseName = "postgres"
)
type Task struct {
Id int
Name string
Done bool
ListId int
}
type CreateTaskRequest struct {
Name string `json:"name"`
ListId int `json:"list_id"`
}
type List struct {
Id int
Name string
}
type ListCreateRequest struct {
Name string
}
type ListCreateResponse struct {
Id int
}
func CheckFatal(err error, w http.ResponseWriter) {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
type Database struct {
Db *sql.DB
}
// getURLParameter takes out the first URL parameter from the path
// path should be formated as /type/param.
// It returns a parameter representing a string.
func getURLParameter(path string) *string {
// TODO: Handle multiple parameters
param := strings.Split(path, "/")
if len(param) == 3 {
return &param[2]
} else {
return nil
}
}
// getLists retrieves all the lists from the database.
// It returns a slice of List structs.
func getLists(db *sql.DB, w http.ResponseWriter) []List {
rows, err := db.Query("select * from list")
CheckFatal(err, w)
// Retrieve all lists from the query
res := make([]List, 0)
for rows.Next() {
list := List{}
err := rows.Scan(&list.Id, &list.Name)
CheckFatal(err, w)
res = append(res, list)
}
return res
}
// getTass retrieves all the tasks from the database.
// It returns a slice of Task structs.
func getTasks(db *sql.DB, listId int, w http.ResponseWriter) []Task {
// Query the database for all tasks that references the specified list
rows, err := db.Query("select * from task where list=$1", listId)
CheckFatal(err, w)
// Retrieve all tasks from the query
res := make([]Task, 0)
for rows.Next() {
var name string
var id, list int
var done bool
err := rows.Scan(&id, &name, &done, &list)
CheckFatal(err, w)
res = append(res, Task{Id: id, Name: name, Done: done, ListId: list})
}
return res
}
// insertList adds a list to the database with listName as its name.
// It returns the Id of the list.
func insertList(db *sql.DB, listName string, w http.ResponseWriter) int {
var listId int
//err := db.QueryRow("insert into list (name) values ($1) returning id", listName).Scan(&listId)
_, err := db.Exec("insert into list (name) values ($1) ", listName)
db.QueryRow("select last_insert_rowid()").Scan(&listId) // SQLite specific
CheckFatal(err, w)
return 0
}
// insertTask adds a task to the database.
// taskName specifies the name of the task, and listId the list that it belongs to.
func insertTask(db *sql.DB, taskName string, listId int, w http.ResponseWriter) {
_, err := db.Exec("insert into task (name, list) values ($1, $2)", taskName, listId)
// Handle non-existing list id
CheckFatal(err, w)
}
// listHandler manages requests with regards to the lists.
// A GET request to /list will retrieve all the lists.
// A GET request to /list/<id> will retrieve all the tasks of the list with id <id>.
func (db *Database) listHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// Handle GET Request
param := getURLParameter(r.URL.Path)
// If no parameter exists, retrieve all lists
if param == nil || *param == "" {
// Retrieve lists
list := getLists(db.Db, w)
json.NewEncoder(w).Encode(&list)
} else {
// Get the list id from the parameter
listId, err := strconv.Atoi(*param)
CheckFatal(err, w)
// Retrieve tasks and send them back
tasks := getTasks(db.Db, listId, w)
json.NewEncoder(w).Encode(&tasks)
}
} else if r.Method == "POST" {
// Parse the request and create a new list
body, err := ioutil.ReadAll(r.Body)
CheckFatal(err, w)
listRequest := ListCreateRequest{}
err = json.Unmarshal(body, &listRequest)
CheckFatal(err, w)
listResponse := ListCreateResponse{}
listResponse.Id = insertList(db.Db, listRequest.Name, w)
json.NewEncoder(w).Encode(&listResponse)
}
}
// taskHandler manages requests with regards to the tasks.
// A POST request to /task will create a new task with the name and list provided
// in the Post Body. The Body should be in the format
// {"name": "taskName", "list_id": 123}
func (db *Database) taskHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
body, err := ioutil.ReadAll(r.Body)
CheckFatal(err, w)
taskRequest := CreateTaskRequest{}
err = json.Unmarshal(body, &taskRequest)
CheckFatal(err, w)
insertTask(db.Db, taskRequest.Name, taskRequest.ListId, w)
fmt.Fprintf(w, "OK")
}
}
// ConnextDB connects to a postgres database.
// it returns a database handle.
func ConnectDb() *sql.DB {
// TODO: Refactor the database config
//for postgres database
//db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s@%s/%s?sslmode=disable", databaseUser, databaseHost, databaseName))
//for sqlite3 database
db, err := sql.Open("sqlite3", "./foo.db")
if err != nil {
log.Fatal(err)
}
return db
}
// Handlers retrieves all handlers for the server.
func Handlers() *http.ServeMux {
db := Database{Db: ConnectDb()}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.listHandler))
mux.Handle("/list/", http.HandlerFunc(db.listHandler))
mux.Handle("/task", http.HandlerFunc(db.taskHandler))
return mux
}
func main() {
// Listen on port 5050
err := http.ListenAndServe(":8080", Handlers())
if err != nil {
log.Fatal(err)
}
}
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"strings"
"strconv"
"encoding/json"
"bytes"
)
// checkFail fails the tests if err is an error (not nil)
func checkFail(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}
// setup recreates the database according to schema.sql.
// This is so that all tests occurs from a clean slate
func setup(t *testing.T) {
db := ConnectDb()
defer db.Close()
sqlData, err := ioutil.ReadFile("schema.sql")
checkFail(t, err)
sqlStmts := string(sqlData)
for _, stmt := range strings.Split(sqlStmts, ";") {
_, err := db.Exec(stmt)
checkFail(t, err)
}
}
// TestTask tests all functionality with regards to tasks
func TestTask(test *testing.T) {
// Setup the database and create a test server
setup(test)
testServer := httptest.NewServer(Handlers())
defer testServer.Close()
// Create a new list for the different tasks
b, err := json.Marshal(ListCreateRequest{Name: "Work"})
checkFail(test, err)
res, err := http.Post(testServer.URL + "/list", "application/json", bytes.NewReader(b))
checkFail(test, err)
// Read the response and retrieve the id for the newly created list
listResp := ListCreateResponse{}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
err = json.Unmarshal(body, &listResp)
// Get all the tasks from the new list
res, err = http.Get(testServer.URL + "/list/" + strconv.Itoa(listResp.Id))
checkFail(test, err)
if res.StatusCode != 200 {
test.Errorf("Expected 200 got %d", res.StatusCode)
}
body, err = ioutil.ReadAll(res.Body)
defer res.Body.Close()
var tasks []Task
err = json.Unmarshal(body, &tasks)
checkFail(test, err)
// There shouldn't be any tasks in the list yet
if len(tasks) != 0 {
test.Errorf("Expected len(tasks) == 0 got %d", len(tasks))
}
// Test the creation of a task in the list
b, err = json.Marshal(CreateTaskRequest{Name: "Work", ListId: listResp.Id})
checkFail(test, err)
res, err = http.Post(testServer.URL + "/task", "application/json", bytes.NewReader(b))
checkFail(test, err)
if res.StatusCode != 200 {
test.Errorf("Expected 200, got %d",res.StatusCode)
}
// Test the if the task got stored in the list
res, err = http.Get(testServer.URL + "/list/" + strconv.Itoa(listResp.Id))
checkFail(test, err)
if res.StatusCode != 200 {
test.Errorf("Expected 200 got %d", res.StatusCode)
}
// Check so that the list contains 1 task
body, err = ioutil.ReadAll(res.Body)
defer res.Body.Close()
err = json.Unmarshal(body, &tasks)
checkFail(test, err)
if len(tasks) != 1 {
test.Errorf("Expected len(tasks) == 1 got %d", len(tasks))
}
}
// TestList manages the testing of lists
func TestList(test *testing.T) {
// Setup a clean database and a new test server
setup(test)
testServer := httptest.NewServer(Handlers())
defer testServer.Close()
// Retrieve the lists
res, err := http.Get(testServer.URL + "/list")
checkFail(test, err)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode != 200 {
test.Errorf("Expected status code 200 got %d", res.StatusCode)
}
// Check that it didn't contain any lists
var lists []List
err = json.Unmarshal(body, &lists)
checkFail(test, err)
if len(lists) != 0 {
test.Errorf("Expected [] got %d", len(lists))
}
// Test creation of a list
b, err := json.Marshal(ListCreateRequest{Name: "Work"})
checkFail(test, err)
res, err = http.Post(testServer.URL + "/list", "application/json", bytes.NewReader(b))
checkFail(test, err)
if res.StatusCode != 200 {
test.Errorf("Expected status code 200 got %d", res.StatusCode)
}
// Make sure that the list is stored
res, err = http.Get(testServer.URL + "/list")
checkFail(test, err)
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode != 200 {
test.Errorf("Expected status code 200 got %d", res.StatusCode)
}
err = json.Unmarshal(body, &lists)
checkFail(test, err)
if len(lists) != 1 {
test.Errorf("Expected [] got len(%d)", len(lists))
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment