Reorganize project structure and fix SQLite usage
This commit is contained in:
parent
0e762841a3
commit
e2dda0ade7
20 changed files with 76 additions and 46 deletions
78
internal/auth/auth.go
Normal file
78
internal/auth/auth.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed login.html
|
||||
var loginPage string
|
||||
|
||||
//go:embed wrongPassword.html
|
||||
var wrongPassword string
|
||||
|
||||
type Authenticator struct {
|
||||
sessions map[string]token
|
||||
}
|
||||
|
||||
func New() *Authenticator {
|
||||
return &Authenticator{sessions: make(map[string]token)}
|
||||
}
|
||||
|
||||
func (a *Authenticator) login(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("content-type", "text/html")
|
||||
fmt.Fprint(w, loginPage)
|
||||
return
|
||||
}
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
w.Header().Set("content-type", "text/html")
|
||||
fmt.Fprint(w, loginPage)
|
||||
return
|
||||
}
|
||||
login := r.PostFormValue("login")
|
||||
password := r.PostFormValue("password")
|
||||
if login != "admin" || password != "secret" {
|
||||
w.Header().Set("content-type", "text/html")
|
||||
fmt.Fprint(w, wrongPassword)
|
||||
return
|
||||
}
|
||||
key := generateKey()
|
||||
a.sessions[key] = token{userName: login, created: time.Now(), lastAccess: time.Now()}
|
||||
c := http.Cookie{
|
||||
Name: "token",
|
||||
Value: key,
|
||||
HttpOnly: true,
|
||||
// #todo (in prod) uncomment this line
|
||||
//Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Path: "/",
|
||||
}
|
||||
http.SetCookie(w, &c)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *Authenticator) Middleware(in http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/login" {
|
||||
a.login(w, r)
|
||||
return
|
||||
}
|
||||
c, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
value, ok := a.sessions[c.Value]
|
||||
if !ok || time.Since(value.lastAccess) > time.Hour*24 || time.Since(value.lastAccess) > time.Hour*240 {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
value.lastAccess = time.Now()
|
||||
a.sessions[c.Value] = value
|
||||
in.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
12
internal/auth/login.html
Normal file
12
internal/auth/login.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<header>
|
||||
<h1>Login</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="post">
|
||||
<label>Login</label>
|
||||
<input type="text" name="login" />
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" />
|
||||
<button type="submit">login</button>
|
||||
</form>
|
||||
</main>
|
||||
19
internal/auth/token.go
Normal file
19
internal/auth/token.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
)
|
||||
|
||||
type token struct {
|
||||
userName string
|
||||
created time.Time
|
||||
lastAccess time.Time
|
||||
}
|
||||
|
||||
func generateKey() string {
|
||||
key := [32]byte{}
|
||||
rand.Read(key[:])
|
||||
return base64.StdEncoding.EncodeToString(key[:])
|
||||
}
|
||||
12
internal/auth/wrongPassword.html
Normal file
12
internal/auth/wrongPassword.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<header>
|
||||
<h1>Wrong Password</h1>
|
||||
</header>
|
||||
<main>
|
||||
<form method="post">
|
||||
<label>Login</label>
|
||||
<input type="text" name="login" />
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" />
|
||||
<button type="submit">login</button>
|
||||
</form>
|
||||
</main>
|
||||
16
internal/logging/logger.go
Normal file
16
internal/logging/logger.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Middleware(in http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
fmt.Println(r.Method, r.URL.String())
|
||||
in.ServeHTTP(w, r)
|
||||
fmt.Println(r.Method, "finished in", time.Since(start))
|
||||
})
|
||||
}
|
||||
13
internal/server/errors.go
Normal file
13
internal/server/errors.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func errorPage(err error, status int) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprint(w, err.Error())
|
||||
}
|
||||
}
|
||||
5
internal/server/fetcher.go
Normal file
5
internal/server/fetcher.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
type fetcher func(r *http.Request) (any, int, error)
|
||||
45
internal/server/get.go
Normal file
45
internal/server/get.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Bronku/iroon/internal/store"
|
||||
)
|
||||
|
||||
func (h *Server) index(r *http.Request) (any, int, error) {
|
||||
data, err := h.s.GetOrders()
|
||||
return data, http.StatusOK, err
|
||||
}
|
||||
|
||||
func (h *Server) getOrder(r *http.Request) (any, int, error) {
|
||||
type formData struct {
|
||||
Order store.Order
|
||||
Catalogue []store.Cake
|
||||
}
|
||||
var err error
|
||||
var data formData
|
||||
|
||||
data.Catalogue, err = h.s.GetCakes()
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
url := strings.Split(r.URL.String(), "/")
|
||||
if len(url) < 3 || url[2] == "" {
|
||||
return data, http.StatusOK, nil
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(url[2])
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
data.Order, err = h.s.GetOrder(id)
|
||||
if err != nil {
|
||||
return nil, http.StatusNotFound, err
|
||||
}
|
||||
|
||||
return data, http.StatusOK, nil
|
||||
}
|
||||
56
internal/server/post.go
Normal file
56
internal/server/post.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Bronku/iroon/internal/store"
|
||||
)
|
||||
|
||||
func (h *Server) postOrder(r *http.Request) (any, int, error) {
|
||||
cakes, err := h.s.GetCakes()
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
var n store.Order
|
||||
n.ID, err = strconv.Atoi(r.FormValue("id"))
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
n.Paid, err = strconv.Atoi(r.FormValue("paid"))
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
n.Date, err = time.Parse("2006-01-02", r.FormValue("date"))
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
n.Cakes = make([]store.Cake, 0)
|
||||
for _, e := range cakes {
|
||||
e.Amount, err = strconv.Atoi(r.FormValue(fmt.Sprintf("cake[%d]", e.ID)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
n.Cakes = append(n.Cakes, e)
|
||||
}
|
||||
|
||||
n.Accepted = time.Now()
|
||||
n.Name = strings.TrimSpace(r.FormValue("name"))
|
||||
n.Surname = strings.TrimSpace(r.FormValue("surname"))
|
||||
n.Phone = strings.TrimSpace(r.FormValue("phone"))
|
||||
n.Location = strings.TrimSpace(r.FormValue("location"))
|
||||
n.Status = strings.TrimSpace(r.FormValue("status"))
|
||||
|
||||
n.ID, err = h.s.SaveOrder(n)
|
||||
return n, http.StatusAccepted, err
|
||||
}
|
||||
58
internal/server/server.go
Normal file
58
internal/server/server.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/Bronku/iroon/internal/store"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
tmpl *template.Template
|
||||
s *store.Store
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func (h *Server) Close() {
|
||||
if h.s != nil {
|
||||
h.s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Server) openStore() error {
|
||||
var err error
|
||||
h.s, err = store.OpenStore("./foo.db")
|
||||
if err != nil {
|
||||
h.s.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Server) loadHandler() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /order/", h.render(h.getOrder, "order.html"))
|
||||
mux.HandleFunc("GET /", h.render(h.index, "index.html"))
|
||||
mux.HandleFunc("POST /order/", h.render(h.postOrder, "confirmation.html"))
|
||||
|
||||
h.Handler = mux
|
||||
}
|
||||
|
||||
func New() (*Server, error) {
|
||||
var server Server
|
||||
var err error
|
||||
|
||||
err = server.loadTemplates()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = server.openStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server.loadHandler()
|
||||
|
||||
return &server, nil
|
||||
}
|
||||
31
internal/server/template.go
Normal file
31
internal/server/template.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
func (h *Server) loadTemplates() error {
|
||||
var err error
|
||||
h.tmpl, err = template.ParseFS(templates, "templates/*")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) render(fetch fetcher, templateName string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "text/html")
|
||||
data, code, err := fetch(r)
|
||||
if err != nil {
|
||||
errorPage(err, code).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
err = s.tmpl.ExecuteTemplate(w, templateName, data)
|
||||
if err != nil {
|
||||
errorPage(err, http.StatusInternalServerError).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
internal/server/templates/confirmation.html
Normal file
9
internal/server/templates/confirmation.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<header>
|
||||
<h1>Confirmation</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">index</a>
|
||||
</nav>
|
||||
<main>
|
||||
<h2>Order {{.ID}} for {{.Name}} {{.Surname}} confirmed</h2>
|
||||
</main>
|
||||
36
internal/server/templates/index.html
Normal file
36
internal/server/templates/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<header>
|
||||
<h1>Kill me please</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="order">New</a>
|
||||
</nav>
|
||||
<main>
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Surname</th>
|
||||
<th>Phone</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
<th>Accepted</th>
|
||||
<th>Status</th>
|
||||
<th>Paid</th>
|
||||
<th>Cakes</th>
|
||||
</tr>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<th><a href="/order/{{.ID}}">edit</a></th>
|
||||
<th>{{.Name}}</th>
|
||||
<th>{{.Surname}}</th>
|
||||
<th>{{.Phone}}</th>
|
||||
<th>{{.Location}}</th>
|
||||
<th>{{.Date.Format "2006-01-02"}}</th>
|
||||
<th>{{.Accepted.Format "2006-01-02"}}</th>
|
||||
<th>{{.Status}}</th>
|
||||
<th>{{.Paid}}</th>
|
||||
<th>{{range .Cakes}} {{.Name}} {{.Amount}} {{end}}</th>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</main>
|
||||
91
internal/server/templates/order.html
Normal file
91
internal/server/templates/order.html
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<header>
|
||||
<h1>New Order</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">back</a>
|
||||
</nav>
|
||||
<main>
|
||||
<form method="post">
|
||||
{{with .Order}}
|
||||
<div>
|
||||
<h2>Info</h2>
|
||||
<input hidden="true" name="id" value="{{.ID}}">
|
||||
<label>name</label>
|
||||
<input type="text" name="name" value="{{.Name}}">
|
||||
<label>surname</label>
|
||||
<input type="text" name="surname" value="{{.Surname}}">
|
||||
<label>phone</label>
|
||||
<input type="tel" name="phone" value="{{.Phone}}">
|
||||
<label>location</label>
|
||||
<select name="location">
|
||||
<option {{ if eq .Location "Kartuzy" }}selected{{ end }} value="Kartuzy">Kartuzy</option>
|
||||
<option {{ if eq .Location "Somonino" }}selected{{ end }} value="Somonino">Somonino</option>
|
||||
</select>
|
||||
<label>date</label>
|
||||
<input type="date" name="date" value="{{.Date.Format "2006-01-02"}}">
|
||||
<label>status</label>
|
||||
<select name="status" id="status" >
|
||||
<option {{ if eq .Status "accepted" }}selected{{ end }} value="accepted">accepted</option>
|
||||
<option {{ if eq .Status "done" }}selected{{ end }} value="done">done</option>
|
||||
</select>
|
||||
<label>paid</label>
|
||||
<input type="number" name="paid" min="0" value="{{.Paid}}">
|
||||
</div>
|
||||
<div>
|
||||
<h2>Basket</h2>
|
||||
<ul id="basket">
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .Catalogue}}
|
||||
<div>
|
||||
<h2>Catalogue</h2>
|
||||
<ul>
|
||||
{{range .}}
|
||||
<li>
|
||||
{{.Name}} {{.Price}} <button type=button onClick="addCake({{.ID}},'{{.Name}} {{.Price}}', 1)">add</button>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
<button type="submit">submit</button>
|
||||
</form>
|
||||
</main>
|
||||
<script>
|
||||
function addCake(id, name, amount){
|
||||
var cake = document.getElementById("cake["+id+"]")
|
||||
if (cake != null){
|
||||
var value = Number(cake.getAttribute("value"))
|
||||
cake.setAttribute("value", value+1)
|
||||
return;
|
||||
}
|
||||
var label = document.createElement("label")
|
||||
label.innerText = name
|
||||
var input = document.createElement("input")
|
||||
input.setAttribute("id","cake["+id+"]")
|
||||
input.setAttribute("type", "number")
|
||||
input.setAttribute("min", "0")
|
||||
input.setAttribute("name", "cake["+id+"]")
|
||||
input.setAttribute("value", amount)
|
||||
var button = document.createElement("button")
|
||||
button.setAttribute("type", "button")
|
||||
button.setAttribute("onClick", "removeCake("+id+")")
|
||||
button.innerText= "delete"
|
||||
var li = document.createElement("li")
|
||||
li.appendChild(label)
|
||||
li.appendChild(input)
|
||||
li.appendChild(button)
|
||||
li.setAttribute("id", "li["+id+"]")
|
||||
var ul = document.getElementById("basket")
|
||||
ul.appendChild(li)
|
||||
}
|
||||
|
||||
function removeCake(id){
|
||||
document.getElementById("li["+id+"]").remove()
|
||||
}
|
||||
|
||||
{{range .Order.Cakes}}
|
||||
addCake({{.ID}}, '{{.Name}} {{.Price}}', {{.Amount}})
|
||||
{{end}}
|
||||
</script>
|
||||
23
internal/store/model.go
Normal file
23
internal/store/model.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
type Cake struct {
|
||||
Name string
|
||||
ID int
|
||||
Price int
|
||||
Amount int
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID int
|
||||
Name string
|
||||
Surname string
|
||||
Phone string
|
||||
Location string
|
||||
Date time.Time
|
||||
Accepted time.Time
|
||||
Status string
|
||||
Paid int // increments of 0.01
|
||||
Cakes []Cake
|
||||
}
|
||||
26
internal/store/schema.sql
Normal file
26
internal/store/schema.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
create table cake (
|
||||
id integer primary key autoincrement,
|
||||
name text not null unique,
|
||||
price integer not null
|
||||
);
|
||||
|
||||
create table customer_order (
|
||||
id integer primary key autoincrement,
|
||||
name text,
|
||||
surname text,
|
||||
phone text,
|
||||
location text,
|
||||
order_date string not null,
|
||||
delivery_date string not null,
|
||||
status text,
|
||||
paid integer
|
||||
);
|
||||
|
||||
create table ordered_cake (
|
||||
customer_order integer references customer_order (id) not null,
|
||||
cake integer references cake (id) not null,
|
||||
amount integer not null,
|
||||
primary key (customer_order, cake)
|
||||
);
|
||||
|
||||
PRAGMA user_version = 1;
|
||||
252
internal/store/store.go
Normal file
252
internal/store/store.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schema string
|
||||
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func OpenStore(filename string) (*Store, error) {
|
||||
var out Store
|
||||
var err error
|
||||
|
||||
out.db, err = sql.Open("sqlite3", filename)
|
||||
if err != nil {
|
||||
return &out, err
|
||||
}
|
||||
|
||||
err = out.loadSchema()
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
if s.db != nil {
|
||||
s.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) version() int {
|
||||
out := -1
|
||||
row, err := s.db.Query("PRAGMA user_version;")
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
_ = row.Scan(&out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) loadSchema() error {
|
||||
if s.db == nil {
|
||||
return errors.New("database doesn't exist")
|
||||
}
|
||||
|
||||
if s.version() == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetCake(id int) (Cake, error) {
|
||||
out := Cake{ID: id}
|
||||
row, err := s.db.Query("select name, price from cake where id = ?;", id)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer row.Close()
|
||||
row.Next()
|
||||
err = row.Scan(&out.Name, &out.Price)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *Store) GetCakes() ([]Cake, error) {
|
||||
rows, err := s.db.Query("select id, name, price from cake")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
cakes := make([]Cake, 0)
|
||||
for rows.Next() {
|
||||
var c Cake
|
||||
c.Amount = 0
|
||||
err = rows.Scan(&c.ID, &c.Name, &c.Price)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cakes = append(cakes, c)
|
||||
}
|
||||
return cakes, err
|
||||
}
|
||||
|
||||
func (s *Store) SaveCake(newCake Cake) (int, error) {
|
||||
query := "insert into cake(name, price) values (?, ?) returning id;"
|
||||
if newCake.ID != 0 {
|
||||
query = "update cake set name = ? , price = ? where id = "
|
||||
query += strconv.Itoa(newCake.ID) + " returning id;"
|
||||
}
|
||||
|
||||
row, err := s.db.Query(query, newCake.Name, newCake.Price)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return 0, errors.New("The database didn't respond with an id")
|
||||
}
|
||||
err = row.Scan(&newCake.ID)
|
||||
return newCake.ID, err
|
||||
}
|
||||
|
||||
func (s *Store) GetOrder(id int) (Order, error) {
|
||||
var out Order
|
||||
row, err := s.db.Query("select id, name, surname, phone, location, order_date, delivery_date, status, paid from customer_order where id = ?;", id)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
var order_date, delivery_date string
|
||||
row.Next()
|
||||
err = row.Scan(&out.ID, &out.Name, &out.Surname, &out.Phone, &out.Location, &order_date, &delivery_date, &out.Status, &out.Paid)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Accepted, _ = time.Parse("2006-01-02 15:04", order_date)
|
||||
out.Date, _ = time.Parse("2006-01-02 15:04", delivery_date)
|
||||
|
||||
out.Cakes = make([]Cake, 0)
|
||||
rows, err := s.db.Query("select cake, amount from ordered_cake where customer_order = ?;", id)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var newCake Cake
|
||||
err = rows.Scan(&newCake.ID, &newCake.Amount)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
cakeData, err := s.GetCake(newCake.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
newCake.Name = cakeData.Name
|
||||
newCake.Price = cakeData.Price
|
||||
out.Cakes = append(out.Cakes, newCake)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetOrders() ([]Order, error) {
|
||||
var out []Order
|
||||
rows, err := s.db.Query("select id, name, surname, phone, location, order_date, delivery_date, status, paid from customer_order;")
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var o Order
|
||||
var order_date, delivery_date string
|
||||
err = rows.Scan(&o.ID, &o.Name, &o.Surname, &o.Phone, &o.Location, &order_date, &delivery_date, &o.Status, &o.Paid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Accepted, _ = time.Parse("2006-01-02 15:04", order_date)
|
||||
o.Date, _ = time.Parse("2006-01-02 15:04", delivery_date)
|
||||
|
||||
o.Cakes = make([]Cake, 0)
|
||||
rows, err := s.db.Query("select cake, amount from ordered_cake where customer_order = ?;", o.ID)
|
||||
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var newCake Cake
|
||||
err = rows.Scan(&newCake.ID, &newCake.Amount)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
cakeData, err := s.GetCake(newCake.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
newCake.Name = cakeData.Name
|
||||
newCake.Price = cakeData.Price
|
||||
o.Cakes = append(o.Cakes, newCake)
|
||||
}
|
||||
|
||||
out = append(out, o)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) SaveOrder(newOrder Order) (int, error) {
|
||||
query := "insert into customer_order(name, surname, phone, location, order_date, delivery_date, status, paid) values (?, ?, ?, ?, ?, ?, ?, ?) returning id;"
|
||||
if newOrder.ID != 0 {
|
||||
query = "update customer_order set name = ?, surname = ?, phone = ?, location = ?, order_date = ?, delivery_date = ?, status = ?, paid = ? where id = "
|
||||
query += strconv.Itoa(newOrder.ID) + " returning id;"
|
||||
}
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
accepted := newOrder.Accepted.Format("2006-01-02 15:04")
|
||||
date := newOrder.Date.Format("2006-01-02 15:04")
|
||||
row, err := tx.Query(query, newOrder.Name, newOrder.Surname, newOrder.Phone, newOrder.Location, accepted, date, newOrder.Status, newOrder.Paid)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
_ = tx.Rollback()
|
||||
return 0, errors.New("The database didn't respond with an id")
|
||||
}
|
||||
err = row.Scan(&newOrder.ID)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return newOrder.ID, err
|
||||
}
|
||||
|
||||
// remove all ordered_cakes associated with this order before inserting
|
||||
query = "delete from ordered_cake where customer_order = ?;"
|
||||
_, err = tx.Exec(query, newOrder.ID)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return newOrder.ID, err
|
||||
}
|
||||
|
||||
// add all ordered_cakes for this order
|
||||
query = "insert into ordered_cake(customer_order, cake, amount) values (?,?,?);"
|
||||
for _, e := range newOrder.Cakes {
|
||||
_, err := tx.Exec(query, newOrder.ID, e.ID, e.Amount)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return newOrder.ID, err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return newOrder.ID, err
|
||||
}
|
||||
294
internal/store/store_test.go
Normal file
294
internal/store/store_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
s, err := OpenStore("file:memdb1?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
// ensure db is empty
|
||||
cakes, err := s.GetCakes()
|
||||
if err != nil {
|
||||
t.Errorf("Error getting cakes: %v", err)
|
||||
}
|
||||
if len(cakes) != 0 {
|
||||
t.Errorf("Expected empty cakes list, got: %v", cakes)
|
||||
}
|
||||
orders, err := s.GetOrders()
|
||||
if err != nil {
|
||||
t.Errorf("Error getting orders: %v", err)
|
||||
}
|
||||
if len(orders) != 0 {
|
||||
t.Errorf("Expected empty orders list, got: %v", orders)
|
||||
}
|
||||
|
||||
// create new cake
|
||||
newCake := Cake{Name: "Chocolate Cake", Price: 2500}
|
||||
newCake.ID, err = s.SaveCake(newCake)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save new cake: %v", err)
|
||||
}
|
||||
if newCake.ID <= 0 {
|
||||
t.Errorf("Expected positive cake ID, got: %d", newCake.ID)
|
||||
}
|
||||
|
||||
// create another cake
|
||||
anotherCake := Cake{Name: "Another Cake", Price: 3000}
|
||||
anotherCake.ID, err = s.SaveCake(anotherCake)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save new cake: %v", err)
|
||||
}
|
||||
if anotherCake.ID <= 0 {
|
||||
t.Errorf("Expected positive cake ID, got: %d", anotherCake.ID)
|
||||
}
|
||||
|
||||
// Update Existing Cake
|
||||
newCake.Name = "Updated Cake"
|
||||
newCake.Price = 100
|
||||
newID, err := s.SaveCake(newCake)
|
||||
if newID != newCake.ID {
|
||||
t.Error("wrong id")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("error updating cake", err)
|
||||
}
|
||||
|
||||
// Update Non-existing Cake
|
||||
var updatedCake Cake
|
||||
updatedCake.ID = 10
|
||||
_, err = s.SaveCake(updatedCake)
|
||||
if err == nil {
|
||||
t.Error("did not return an error when attempted to update non existant cake")
|
||||
}
|
||||
|
||||
// get cakes
|
||||
cakes, err = s.GetCakes()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get all cakes after creation: %v", err)
|
||||
}
|
||||
if len(cakes) != 2 {
|
||||
t.Error("Expected two cakes returned, got ", len(cakes))
|
||||
}
|
||||
for i, c := range cakes {
|
||||
if c.ID == newCake.ID {
|
||||
newCake.Amount = i
|
||||
}
|
||||
if c.ID == anotherCake.ID {
|
||||
anotherCake.Amount = i
|
||||
}
|
||||
}
|
||||
if newCake.Amount == -1 {
|
||||
t.Error("not found newCake")
|
||||
}
|
||||
if cakes[newCake.Amount].ID != newCake.ID || cakes[newCake.Amount].Name != newCake.Name || cakes[newCake.Amount].Price != newCake.Price {
|
||||
t.Errorf("Want %v\nGot %v", newCake, cakes)
|
||||
}
|
||||
if anotherCake.Amount == -1 {
|
||||
t.Error("not found anotherCake")
|
||||
}
|
||||
if cakes[anotherCake.Amount].ID != anotherCake.ID || cakes[anotherCake.Amount].Name != anotherCake.Name || cakes[anotherCake.Amount].Price != anotherCake.Price {
|
||||
t.Errorf("Want %v\nGot %v", anotherCake, cakes)
|
||||
}
|
||||
|
||||
selectedCake, err := s.GetCake(newCake.ID)
|
||||
if err != nil {
|
||||
t.Error("error getting a cake", err)
|
||||
}
|
||||
if selectedCake.ID != newCake.ID || selectedCake.Name != newCake.Name || selectedCake.Price != newCake.Price {
|
||||
t.Errorf("Want %v\nGot %v", newCake, selectedCake)
|
||||
}
|
||||
|
||||
selectedCake, err = s.GetCake(-1)
|
||||
if err == nil {
|
||||
t.Error("no error getting a non-existant cake")
|
||||
}
|
||||
|
||||
// create new order
|
||||
now := time.Now()
|
||||
newOrder := Order{
|
||||
Name: "John",
|
||||
Surname: "Doe",
|
||||
Phone: "123-456-7890",
|
||||
Location: "Some Location",
|
||||
Accepted: now,
|
||||
Date: now.Add(time.Hour * 24),
|
||||
Status: "Pending",
|
||||
Paid: 1000,
|
||||
Cakes: []Cake{{ID: newCake.ID, Amount: 2}, {ID: anotherCake.ID, Amount: 10}},
|
||||
}
|
||||
newOrder.ID, err = s.SaveOrder(newOrder)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save new order: %v", err)
|
||||
}
|
||||
if newOrder.ID <= 0 {
|
||||
t.Errorf("Expected positive order ID, got: %d", newOrder.ID)
|
||||
}
|
||||
|
||||
// create another order
|
||||
anotherOrder := Order{
|
||||
Name: "Jane",
|
||||
Surname: "Doe",
|
||||
Phone: "123-456-7890",
|
||||
Location: "New Location",
|
||||
Accepted: now,
|
||||
Date: now.Add(time.Hour * 192),
|
||||
Status: "Accepted",
|
||||
Paid: 1500,
|
||||
Cakes: []Cake{{ID: newCake.ID, Amount: 100}},
|
||||
}
|
||||
anotherOrder.ID, err = s.SaveOrder(anotherOrder)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save new order: %v", err)
|
||||
}
|
||||
if anotherOrder.ID <= 0 {
|
||||
t.Errorf("Expected positive order ID, got: %d", anotherOrder.ID)
|
||||
}
|
||||
|
||||
// update existing order
|
||||
newOrder.Name = "James"
|
||||
newOrder.Status = "Done"
|
||||
newOrder.Cakes = newOrder.Cakes[1:]
|
||||
newID, err = s.SaveOrder(newOrder)
|
||||
if newID != newOrder.ID {
|
||||
t.Error("wrong id")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("error updating order", err)
|
||||
}
|
||||
|
||||
// update non existing order
|
||||
var updatedOrder Order
|
||||
updatedOrder.ID = 10
|
||||
_, err = s.SaveOrder(updatedOrder)
|
||||
if err == nil {
|
||||
t.Error("did not return an error when attempted to update non existant cake")
|
||||
}
|
||||
|
||||
// get orders
|
||||
orders, err = s.GetOrders()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get all orders after creation: %v", err)
|
||||
}
|
||||
if len(orders) != 2 {
|
||||
t.Error("Expected two orders returned, got ", len(orders))
|
||||
}
|
||||
newOrderPos := -1
|
||||
anotherOrderPos := -1
|
||||
for i, o := range orders {
|
||||
if o.ID == newOrder.ID {
|
||||
newOrderPos = i
|
||||
}
|
||||
if o.ID == anotherOrder.ID {
|
||||
anotherOrderPos = i
|
||||
}
|
||||
}
|
||||
if newOrderPos == -1 {
|
||||
t.Error("not found newOrder")
|
||||
}
|
||||
if newOrder.Name != orders[newOrderPos].Name ||
|
||||
newOrder.Surname != orders[newOrderPos].Surname ||
|
||||
newOrder.Phone != orders[newOrderPos].Phone ||
|
||||
newOrder.Status != orders[newOrderPos].Status ||
|
||||
newOrder.Location != orders[newOrderPos].Location ||
|
||||
newOrder.Accepted.Format("2006-01-02 15:04") != orders[newOrderPos].Accepted.Format("2006-01-02 15:04") ||
|
||||
newOrder.Date.Format("2006-01-02 15:04") != orders[newOrderPos].Date.Format("2006-01-02 15:04") {
|
||||
t.Errorf("Want %v\nGot %v", newOrder, orders[newOrderPos])
|
||||
}
|
||||
if !areCakeSlicesEqual(newOrder.Cakes, orders[newOrderPos].Cakes) {
|
||||
t.Errorf("Want %v\nGot %v", newOrder.Cakes, orders[newOrderPos].Cakes)
|
||||
}
|
||||
|
||||
if anotherOrderPos == -1 {
|
||||
t.Error("not found anotherOrder")
|
||||
}
|
||||
if anotherOrder.Name != orders[anotherOrderPos].Name ||
|
||||
anotherOrder.Surname != orders[anotherOrderPos].Surname ||
|
||||
anotherOrder.Phone != orders[anotherOrderPos].Phone ||
|
||||
anotherOrder.Status != orders[anotherOrderPos].Status ||
|
||||
anotherOrder.Location != orders[anotherOrderPos].Location ||
|
||||
anotherOrder.Accepted.Format("2006-01-02 15:04") != orders[anotherOrderPos].Accepted.Format("2006-01-02 15:04") ||
|
||||
anotherOrder.Date.Format("2006-01-02 15:04") != orders[anotherOrderPos].Date.Format("2006-01-02 15:04") {
|
||||
t.Errorf("Want %v\nGot %v", anotherOrder, orders[anotherOrderPos])
|
||||
}
|
||||
|
||||
if !areCakeSlicesEqual(anotherOrder.Cakes, orders[anotherOrderPos].Cakes) {
|
||||
t.Errorf("Want %v\nGot %v", anotherOrder.Cakes, orders[anotherOrderPos].Cakes)
|
||||
}
|
||||
|
||||
selectedOrder, err := s.GetOrder(newOrder.ID)
|
||||
if err != nil {
|
||||
t.Error("error getting an order by id ", err)
|
||||
}
|
||||
if newOrder.Name != selectedOrder.Name ||
|
||||
newOrder.Surname != selectedOrder.Surname ||
|
||||
newOrder.Phone != selectedOrder.Phone ||
|
||||
newOrder.Status != selectedOrder.Status ||
|
||||
newOrder.Location != selectedOrder.Location ||
|
||||
newOrder.Accepted.Format("2006-01-02 15:04") != selectedOrder.Accepted.Format("2006-01-02 15:04") ||
|
||||
newOrder.Date.Format("2006-01-02 15:04") != selectedOrder.Date.Format("2006-01-02 15:04") {
|
||||
t.Errorf("Want %v\nGot %v", newOrder, selectedOrder)
|
||||
}
|
||||
|
||||
if !areCakeSlicesEqual(newOrder.Cakes, selectedOrder.Cakes) {
|
||||
t.Errorf("Want %v\nGot %v", newOrder.Cakes, selectedOrder.Cakes)
|
||||
}
|
||||
|
||||
_, err = s.GetOrder(-1)
|
||||
if err == nil {
|
||||
t.Error("no error getting invalid order")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceComparison(t *testing.T) {
|
||||
a := []Cake{
|
||||
{Name: "ok", Price: 100, ID: 12, Amount: 10},
|
||||
{Name: "ok", Price: 11, ID: 13, Amount: 1},
|
||||
{Name: "ok", Price: 100, ID: 10, Amount: 11},
|
||||
}
|
||||
|
||||
b := []Cake{
|
||||
{Name: "okas", Price: 110, ID: 12, Amount: 10},
|
||||
{Name: "oksa", Price: 11, ID: 13, Amount: 1},
|
||||
{Name: "oasdk", Price: 101, ID: 10, Amount: 11},
|
||||
}
|
||||
if !areCakeSlicesEqual(a, b) {
|
||||
t.Error("aren't equal")
|
||||
}
|
||||
}
|
||||
|
||||
func areCakesEqual(a, b Cake) bool {
|
||||
return a.ID == b.ID && a.Amount == b.Amount
|
||||
}
|
||||
|
||||
func areCakeSlicesEqual(a, b []Cake) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
matched := make([]bool, len(a))
|
||||
|
||||
mainLoop:
|
||||
for _, e1 := range a {
|
||||
for i, e2 := range b {
|
||||
if matched[i] {
|
||||
continue
|
||||
}
|
||||
if areCakesEqual(e1, e2) {
|
||||
matched[i] = true
|
||||
continue mainLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range matched {
|
||||
if e == false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue