From 74889f9d59e3f95b744cf3d4cd35e6094fe09dda Mon Sep 17 00:00:00 2001 From: meskio <meskio@sindominio.net> Date: Wed, 23 Sep 2020 13:26:52 +0200 Subject: [PATCH] Add purchases to API --- api/api.go | 9 ++- api/api_test.go | 2 +- api/auth.go | 33 +++++++--- api/member_test.go | 53 ++++++--------- api/product_test.go | 61 +++++++----------- api/purchase.go | 149 +++++++++++++++++++++++++++++++++++++++++++ api/purchase_test.go | 54 ++++++++++++++++ go.mod | 1 + go.sum | 2 + 9 files changed, 283 insertions(+), 81 deletions(-) create mode 100644 api/purchase.go create mode 100644 api/purchase_test.go diff --git a/api/api.go b/api/api.go index 211a457..21006aa 100644 --- a/api/api.go +++ b/api/api.go @@ -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 } diff --git a/api/api_test.go b/api/api_test.go index 83e4599..97378a6 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -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(), }) diff --git a/api/auth.go b/api/auth.go index 11cdf58..7f8e996 100644 --- a/api/auth.go +++ b/api/auth.go @@ -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) { diff --git a/api/member_test.go b/api/member_test.go index 330d373..c000462 100644 --- a/api/member_test.go +++ b/api/member_test.go @@ -5,22 +5,20 @@ import ( "testing" ) +var testMember = Member{ + Num: 10, + Name: "foo", + Email: "foo@example.com", + Balance: 2000, +} + func TestMemberAddList(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() 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) + } +} diff --git a/api/product_test.go b/api/product_test.go index 66b7ddc..dbf7411 100644 --- a/api/product_test.go +++ b/api/product_test.go @@ -5,22 +5,20 @@ import ( "testing" ) +var testProduct = Product{ + Code: 234, + Name: "Aceite", + Price: 1700, + Stock: 10, +} + func TestProductAddList(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) - } 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) + } +} diff --git a/api/purchase.go b/api/purchase.go new file mode 100644 index 0000000..5b982bb --- /dev/null +++ b/api/purchase.go @@ -0,0 +1,149 @@ +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) + } +} diff --git a/api/purchase_test.go b/api/purchase_test.go new file mode 100644 index 0000000..622a0d0 --- /dev/null +++ b/api/purchase_test.go @@ -0,0 +1,54 @@ +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) + } +} diff --git a/go.mod b/go.mod index a2d2f81..5bc0cfc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2708a9b..98aa088 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jstemmer/gotags v1.4.1 h1:aWIyXsU3lTDqhsEC49MP85p2cUUWr2ptvdGNqqGA3r4= +github.com/jstemmer/gotags v1.4.1/go.mod h1:b6J3X0bsLbR4C5SgSx3V3KjuWTtmRzcmWPbTkWZ49PA= github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA= github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/olivere/env v1.1.0 h1:owp/uwMwhru5668JjMDp8UTG3JGT27GTCk4ufYQfaTw= -- GitLab