Skip to content
Snippets Groups Projects
Commit 74889f9d authored by meskio's avatar meskio :tent:
Browse files

Add purchases to API

parent b2e9dd50
Branches
No related tags found
No related merge requests found
......@@ -19,8 +19,7 @@ func initDB(dbPath string) (*gorm.DB, error) {
return nil, err
}
db.AutoMigrate(&Member{})
db.AutoMigrate(&Product{})
db.AutoMigrate(&Member{}, &Product{}, &PurchasedProduct{}, &Purchase{})
return db, err
}
......@@ -42,11 +41,17 @@ func Init(dbPath string, signKey string, r *mux.Router) error {
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.GetMember)).Methods("GET")
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.UpdateMember)).Methods("PUT")
r.HandleFunc("/member/{num:[0-9]+}", a.auth(a.DeleteMember)).Methods("DELETE")
r.HandleFunc("/member/{num:[0-9]+}/purchase", a.auth(a.GetMemberPurchases)).Methods("GET")
r.HandleFunc("/product", a.auth(a.ListProducts)).Methods("GET")
r.HandleFunc("/product", a.auth(a.AddProduct)).Methods("POST")
r.HandleFunc("/product/{code:[0-9]+}", a.auth(a.GetProduct)).Methods("GET")
r.HandleFunc("/product/{code:[0-9]+}", a.auth(a.UpdateProduct)).Methods("PUT")
r.HandleFunc("/product/{code:[0-9]+}", a.auth(a.DeleteProduct)).Methods("DELETE")
r.HandleFunc("/purchase", a.auth(a.ListPurchases)).Methods("GET")
r.HandleFunc("/purchase", a.authNum(a.AddPurchase)).Methods("POST")
r.HandleFunc("/purchase/{id:[0-9]+}", a.auth(a.GetPurchase)).Methods("GET")
r.HandleFunc("/purchase/mine", a.authNum(a.getPurchasesByMember)).Methods("GET")
return nil
}
......@@ -53,7 +53,7 @@ func newTestAPI(t *testing.T) *testAPI {
server := httptest.NewServer(r)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"num": 0,
"num": testMember.Num,
"role": "admin",
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
......
......@@ -62,7 +62,7 @@ func (a *api) SignIn(w http.ResponseWriter, req *http.Request) {
func (a *api) auth(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
token := req.Header.Get("x-authentication")
if !a.validToken(token) {
if ok, _ := a.validateToken(token); !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
......@@ -70,6 +70,24 @@ func (a *api) auth(fn func(http.ResponseWriter, *http.Request)) func(http.Respon
}
}
func (a *api) authNum(fn func(int, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
token := req.Header.Get("x-authentication")
ok, claims := a.validateToken(token)
if !ok {
log.Print("foo")
w.WriteHeader(http.StatusUnauthorized)
return
}
num, ok := claims["num"].(float64)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
return
}
fn(int(num), w, req)
}
}
func (a *api) newToken(num int, role string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"num": num,
......@@ -79,26 +97,25 @@ func (a *api) newToken(num int, role string) (string, error) {
return token.SignedString(a.signKey)
}
func (a *api) validToken(token string) bool {
func (a *api) validateToken(token string) (bool, jwt.MapClaims) {
t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return a.signKey, nil
})
if err != nil {
return false
return false, nil
}
if !t.Valid {
return false
return false, nil
}
claims, ok := t.Claims.(jwt.MapClaims)
if !ok {
return false
return false, nil
}
exp, ok := claims["exp"].(float64)
if !ok {
return false
return false, claims
}
// TODO: num, role
return time.Unix(int64(exp), 0).After(time.Now())
return time.Unix(int64(exp), 0).After(time.Now()), claims
}
func newHashPass(password string) (hash []byte, salt []byte, err error) {
......
......@@ -5,22 +5,20 @@ import (
"testing"
)
func TestMemberAddList(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
member := Member{
var testMember = Member{
Num: 10,
Name: "foo",
Email: "foo@example.com",
Balance: 2000,
}
resp := tapi.do("POST", "/member", member, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create member:", resp.Status)
}
func TestMemberAddList(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
tapi.addTestMember()
var members []Member
resp = tapi.do("GET", "/member", nil, &members)
resp := tapi.do("GET", "/member", nil, &members)
if resp.StatusCode != http.StatusOK {
t.Fatal("Can't get members:", resp.Status)
}
......@@ -44,16 +42,7 @@ func TestMemberGetDelete(t *testing.T) {
if resp.StatusCode != http.StatusNotFound {
t.Error("Expected not found:", resp.Status, resp.Body)
}
member := Member{
Num: 10,
Name: "foo",
Email: "foo@example.com",
Balance: 2000,
}
resp = tapi.do("POST", "/member", member, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create member:", resp.Status)
}
tapi.addTestMember()
var gotMember Member
resp = tapi.do("GET", "/member/10", nil, &gotMember)
......@@ -78,19 +67,10 @@ func TestMemberUpdate(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
member := Member{
Num: 10,
Name: "foo",
Email: "foo@example.com",
Balance: 2000,
}
resp := tapi.do("POST", "/member", member, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create member:", resp.Status)
}
tapi.addTestMember()
member := testMember
member.Balance = 1000
resp = tapi.do("PUT", "/member/10", member, nil)
resp := tapi.do("PUT", "/member/10", member, nil)
if resp.StatusCode != http.StatusAccepted {
t.Fatal("Can't update member:", resp.Status)
}
......@@ -104,3 +84,10 @@ func TestMemberUpdate(t *testing.T) {
t.Error("Wrong balance:", gotMember)
}
}
func (tapi *testAPI) addTestMember() {
resp := tapi.do("POST", "/member", testMember, nil)
if resp.StatusCode != http.StatusCreated {
tapi.t.Fatal("Can't create member:", resp.Status)
}
}
......@@ -5,22 +5,20 @@ import (
"testing"
)
func TestProductAddList(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
product := Product{
var testProduct = Product{
Code: 234,
Name: "Aceite",
Price: 1700,
Stock: 10,
}
resp := tapi.do("POST", "/product", product, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create product:", resp.Status)
}
func TestProductAddList(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
tapi.addTestProducts()
var products []Product
resp = tapi.do("GET", "/product", nil, &products)
resp := tapi.do("GET", "/product", nil, &products)
if resp.StatusCode != http.StatusOK {
t.Fatal("Can't get products:", resp.Status)
}
......@@ -28,10 +26,10 @@ func TestProductAddList(t *testing.T) {
if len(products) != 1 {
t.Fatal("Wrong number of products", len(products), products)
}
if products[0].Name != "Aceite" {
if products[0].Name != testProduct.Name {
t.Error("Wrong name:", products[0].Name)
}
if products[0].Price != 1700 {
if products[0].Price != testProduct.Price {
t.Error("Wrong price:", products[0].Price)
}
}
......@@ -44,23 +42,14 @@ func TestProductGetDelete(t *testing.T) {
if resp.StatusCode != http.StatusNotFound {
t.Error("Expected not found:", resp.Status, resp.Body)
}
product := Product{
Code: 234,
Name: "Aceite",
Price: 1700,
Stock: 10,
}
resp = tapi.do("POST", "/product", product, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create product:", resp.Status)
}
tapi.addTestProducts()
var gotProduct Product
resp = tapi.do("GET", "/product/234", nil, &gotProduct)
if resp.StatusCode != http.StatusOK {
t.Error("Can't find the product:", resp.Status)
}
if gotProduct.Code != 234 {
if gotProduct.Code != testProduct.Code {
t.Error("Wrong product:", gotProduct.Code)
}
resp = tapi.do("DELETE", "/product/234", nil, nil)
......@@ -77,20 +66,11 @@ func TestProductGetDelete(t *testing.T) {
func TestProductUpdate(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
tapi.addTestProducts()
product := Product{
Code: 234,
Name: "Aceite",
Price: 1700,
Stock: 10,
}
resp := tapi.do("POST", "/product", product, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create product:", resp.Status)
}
product.Stock = product.Stock - 5
resp = tapi.do("PUT", "/product/234", product, nil)
product := testProduct
product.Stock = testProduct.Stock - 5
resp := tapi.do("PUT", "/product/234", product, nil)
if resp.StatusCode != http.StatusAccepted {
t.Fatal("Can't update product:", resp.Status)
}
......@@ -104,3 +84,10 @@ func TestProductUpdate(t *testing.T) {
t.Error("Wrong sotck:", gotProduct)
}
}
func (tapi *testAPI) addTestProducts() {
resp := tapi.do("POST", "/product", testProduct, nil)
if resp.StatusCode != http.StatusCreated {
tapi.t.Fatal("Can't create product:", resp.Status)
}
}
package api
import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"gorm.io/gorm"
)
type Purchase struct {
gorm.Model `json:"-"`
MemberNum int `json:"member" gorm:"column:member"`
Member Member `json:"-" gorm:"foreignKey:MemberNum;references:Num"`
Date time.Time `json:"date"`
Total int `json:"total"`
Products []PurchasedProduct `json:"products"`
}
type PurchasedProduct struct {
gorm.Model `json:"-"`
PurchaseID int `json:"-" gorm:"column:purchase"`
ProductCode int `json:"product" gorm:"column:product"`
Product Product `gorm:"foreignKey:ProductCode;references:Code"`
Price int `json:"price"`
Ammount int `json:"ammount"`
}
func (a *api) ListPurchases(w http.ResponseWriter, req *http.Request) {
var purchases []Purchase
err := a.db.Preload("Products.Product").Find(&purchases).Error
if err != nil {
log.Printf("Can't list purchases: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(purchases)
if err != nil {
log.Printf("Can't encode purchases: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (a *api) AddPurchase(num int, w http.ResponseWriter, req *http.Request) {
var products []PurchasedProduct
err := json.NewDecoder(req.Body).Decode(&products)
if err != nil {
log.Printf("Can't create purchase: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
total := 0
for i, p := range products {
var product Product
err := a.db.Where("code = ?", p.ProductCode).First(&product).Error
if err != nil {
log.Printf("Can't get product %d: %v", p.ProductCode, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
total += product.Price * p.Ammount
products[i].Price = product.Price
}
purchase := Purchase{
MemberNum: num,
Date: time.Now(),
Products: products,
Total: total,
}
err = a.db.Create(&purchase).Error
if err != nil {
log.Printf("Can't create purchase: %v\n%v", err, purchase)
w.WriteHeader(http.StatusInternalServerError)
return
}
for _, p := range products {
err := a.db.Model(&Product{}).
Where("code = ?", p.ProductCode).
Update("stock", gorm.Expr("stock - ?", p.Ammount)).Error
if err != nil {
log.Printf("Can't update product stock %d: %v", p.ProductCode, err)
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(purchase)
if err != nil {
log.Printf("Can't encode added purchase: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (a *api) GetPurchase(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
var purchase Purchase
err := a.db.Where("id = ?", vars["id"]).First(&purchase).Error
if err != nil {
if err.Error() == "record not found" {
w.WriteHeader(http.StatusNotFound)
return
}
log.Printf("Can't get purchase %s: %v", vars["code"], err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(purchase)
if err != nil {
log.Printf("Can't encode purchase: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (a *api) GetMemberPurchases(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
num, _ := strconv.Atoi(vars["num"])
a.getPurchasesByMember(num, w, req)
}
func (a *api) getPurchasesByMember(num int, w http.ResponseWriter, req *http.Request) {
var purchases []Purchase
err := a.db.Where("member = ?", num).
Preload("Products.Product").
Find(&purchases).Error
if err != nil {
log.Printf("Can't list purchases: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(purchases)
if err != nil {
log.Printf("Can't encode purchases: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
package api
import (
"net/http"
"testing"
)
func TestPurchaseAddListMine(t *testing.T) {
tapi := newTestAPI(t)
defer tapi.close()
tapi.addTestMember()
tapi.addTestProducts()
products := []PurchasedProduct{
{
ProductCode: testProduct.Code,
Ammount: 5,
},
}
resp := tapi.do("POST", "/purchase", products, nil)
if resp.StatusCode != http.StatusCreated {
t.Fatal("Can't create purchase:", resp.Status)
}
var purchases []Purchase
resp = tapi.do("GET", "/purchase/mine", nil, &purchases)
if resp.StatusCode != http.StatusOK {
t.Fatal("Can't get purchases:", resp.Status)
}
if len(purchases) != 1 {
t.Fatal("Wrong number of purchases", len(purchases), purchases)
}
if purchases[0].Total != testProduct.Price*products[0].Ammount {
t.Error("Wrong total:", purchases[0].Total)
}
if len(purchases[0].Products) != 1 {
t.Fatal("Wrong number of products", len(purchases[0].Products), purchases[0].Products)
}
if purchases[0].Products[0].ProductCode != testProduct.Code {
t.Error("Wrong product code:", purchases[0].Products[0].ProductCode)
}
if purchases[0].Products[0].Price != testProduct.Price {
t.Error("Wrong product price:", purchases[0].Products[0].Price)
}
var product Product
resp = tapi.do("GET", "/product/234", nil, &product)
if resp.StatusCode != http.StatusOK {
t.Error("Can't find the product:", resp.Status)
}
if product.Stock != testProduct.Stock-products[0].Ammount {
t.Error("Wrong product stock:", product)
}
}
......@@ -5,6 +5,7 @@ go 1.14
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/jstemmer/gotags v1.4.1 // indirect
github.com/olivere/env v1.1.0
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
gorm.io/driver/sqlite v1.1.2
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment