From faadbe0c627647d025217f96cf5e1c2fb8e68280 Mon Sep 17 00:00:00 2001 From: Alexis Poyen <apoyen@mail.apoyen.fr> Date: Tue, 24 Mar 2020 17:49:50 +0100 Subject: [PATCH] Feat : manage bank account without authent --- go.mod | 11 +- go.sum | 41 ++++-- internal/models/models.go | 228 +++++++++++++++++++++++++++++++++ internal/models/models_test.go | 105 +++++++++++++++ internal/rootmux/rootmux.go | 9 ++ pkg/glob/LICENSE | 21 +++ pkg/glob/glob.go | 56 ++++++++ pkg/glob/glob_test.go | 105 +++++++++++++++ pkg/tester/tester.go | 2 +- testdata/static/index.html | 30 ----- web/index.html | 2 +- 11 files changed, 562 insertions(+), 48 deletions(-) create mode 100644 internal/models/models.go create mode 100644 internal/models/models_test.go create mode 100644 pkg/glob/LICENSE create mode 100644 pkg/glob/glob.go create mode 100644 pkg/glob/glob_test.go delete mode 100644 testdata/static/index.html diff --git a/go.mod b/go.mod index 4a1a0da..df131be 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.14 require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang/protobuf v1.3.4 // indirect + github.com/golang/protobuf v1.3.5 // indirect + github.com/jinzhu/gorm v1.9.12 github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/oschwald/maxminddb-golang v1.6.0 - github.com/secure-io/sio-go v0.3.0 - golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d - golang.org/x/net v0.0.0-20200226121028-0de0cce0169b + golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 + golang.org/x/net v0.0.0-20200319234117-63522dbf7eec // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae + golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d // indirect google.golang.org/appengine v1.6.5 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index c978160..a32cd92 100644 --- a/go.sum +++ b/go.sum @@ -2,35 +2,54 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo= -github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww= +golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200319234117-63522dbf7eec h1:w0SItUiQ4sBiXBAwWNkyu8Fu2Qpn/dtDIcoPkPDqjRw= +golang.org/x/net v0.0.0-20200319234117-63522dbf7eec/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -38,8 +57,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44= +golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..272556b --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,228 @@ +package models + +import ( + "net/http" + "strconv" + "strings" + "time" + + "encoding/json" + + "github.com/jinzhu/gorm" + // Needed for sqlite + + _ "github.com/jinzhu/gorm/dialects/sqlite" +) + +// DataHandler init a gorm DB an presents API handlers +type DataHandler struct { + db *gorm.DB +} + +// NewDataHandler init a DataHandler and returns a pointer to it +func NewDataHandler() *DataHandler { + db, err := gorm.Open("sqlite3", "../../data/test.db") + if err != nil { + panic("failed to connect database") + } + db.LogMode(true) + + db.Model(&Operation{}).AddForeignKey("creditor", "bank_account(id)", "RESTRICT", "RESTRICT") + + // Migrate the schema + db.AutoMigrate(&UserClient{}) + db.AutoMigrate(&UserBanker{}) + db.AutoMigrate(&BankAccount{}) + db.AutoMigrate(&Operation{}) + return &DataHandler{db: db} +} + +// UserClient has many BankAccounts, UserClientID is the foreign key +type UserClient struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` + Name string + BankAccounts []BankAccount +} + +type UserBanker struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` + Name string + UserClients []UserClient +} + +// BankAccount belongs to an UserClient +type BankAccount struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` + Number string `gorm:"primary_key"` + UserClientID uint + Type string + Amount int + BankOverdraft int + Operations []Operation `gorm:"foreignkey:Debtor"` +} + +type Operation struct { + ID uint `gorm:"primary_key"` + Debtor uint + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt *time.Time `json:"-"` + Amount int + Date time.Time + Creditor uint +} + +// HandleClients expose the UserClients API +func (d *DataHandler) HandleClients(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/UserClients/")) + switch method := r.Method; method { + case "GET": + if id != 0 { + var o UserClient + if err := d.db.Preload("BankAccounts").First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(o) + } else { + var o []UserClient + d.db.Preload("BankAccounts").Find(&o) + json.NewEncoder(w).Encode(o) + } + case "POST": + var o UserClient + err := json.NewDecoder(r.Body).Decode(&o) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + d.db.Create(&o) + case "DELETE": + if id != 0 { + var o UserClient + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + d.db.Delete(&o) + } else { + http.Error(w, "id is missing", http.StatusNotFound) + } + default: + http.Error(w, "method not allowed", 400) + } +} + +func (d *DataHandler) HandleBankAccounts(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/BankAccounts/")) + switch method := r.Method; method { + case "GET": + if id != 0 { + var o BankAccount + if err := d.db.Preload("Operations").First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(o) + } else { + var o []BankAccount + d.db.Preload("Operations").Find(&o) + json.NewEncoder(w).Encode(o) + } + case "POST": + var o BankAccount + err := json.NewDecoder(r.Body).Decode(&o) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + d.db.Create(&o) + case "DELETE": + if id != 0 { + var o BankAccount + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + d.db.Delete(&o) + } else { + http.Error(w, "id is missing", http.StatusNotFound) + } + default: + http.Error(w, "method not allowed", 400) + } +} + +func (d *DataHandler) HandleOperations(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/Operations/")) + switch method := r.Method; method { + case "GET": + if id != 0 { + var o Operation + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(o) + } else { + var o []Operation + d.db.Find(&o) + json.NewEncoder(w).Encode(o) + } + case "POST": + var o Operation + err := json.NewDecoder(r.Body).Decode(&o) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + var debtor BankAccount + var creditor BankAccount + if err := d.db.First(&debtor, o.Debtor).Error; err == nil { + if (debtor.Amount + o.Amount) >= debtor.BankOverdraft { + if err := d.db.First(&creditor, o.Creditor).Error; err == nil { + // Update BankAccounts + debtor.Amount += o.Amount + creditor.Amount -= o.Amount + d.db.Save(&debtor) + d.db.Save(&creditor) + + now := time.Now() + o.Date = now + d.db.Create(&o) + + // Add the operation to creditor + op := Operation{ + Debtor: o.Creditor, + Amount: o.Amount, + Date: now, + Creditor: o.Debtor, + } + d.db.Create(&op) + } + } else { + http.Error(w, "Not enough money", http.StatusExpectationFailed) + } + } + case "DELETE": + if id != 0 { + var o Operation + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, "id does not exist", http.StatusNotFound) + return + } + d.db.Delete(&o) + } else { + http.Error(w, "id is missing", http.StatusNotFound) + } + default: + http.Error(w, "method not allowed", 400) + } +} diff --git a/internal/models/models_test.go b/internal/models/models_test.go new file mode 100644 index 0000000..e76f4c1 --- /dev/null +++ b/internal/models/models_test.go @@ -0,0 +1,105 @@ +package models + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/nicolaspernoud/vestibule/pkg/tester" +) + +func TestHandleUserClients(t *testing.T) { + // Remove the test db to start clean + os.Remove("../../data/test.db") + // Create the handler + dh := NewDataHandler() + ts := httptest.NewServer(http.HandlerFunc(dh.HandleClients)) + defer ts.Close() + url, _ := url.Parse(ts.URL) + port := url.Port() + // Wrap the testing function + do := tester.CreateServerTester(t, port, "sdk-go.io", nil) + noH := tester.Header{Key: "", Value: ""} + + // Try to create a client + do("POST", "/api/UserClients", noH, `{"Name":"Dupond"}`, 200, "") + // Try to create an other one + do("POST", "/api/UserClients", noH, `{"Name":"Boulangerie"}`, 200, "") + // Try to get the first client + do("GET", "/api/UserClients/1", noH, "", 200, `{"ID":1,"Name":"Dupond","BankAccounts":[]}`) + // // Try to get all the clients + do("GET", "/api/UserClients", noH, "", 200, `[{"ID":1,"Name":"Dupond","BankAccounts":[]},{"ID":2,"Name":"Boulangerie","BankAccounts":[]}]`) +} + +func TestHandleBankAccounts(t *testing.T) { + // Create the handler + dh := NewDataHandler() + ts := httptest.NewServer(http.HandlerFunc(dh.HandleBankAccounts)) + defer ts.Close() + url, _ := url.Parse(ts.URL) + port := url.Port() + // Wrap the testing function + do := tester.CreateServerTester(t, port, "sdk-go.io", nil) + noH := tester.Header{Key: "", Value: ""} + + // Add bank account to client + do("POST", "/api/BankAccounts", noH, `{"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100}`, 200, "") + // Add an other bank account to client + do("POST", "/api/BankAccounts", noH, `{"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0}`, 200, "") + // Get account where id=1 + do("GET", "/api/BankAccounts/1", noH, "", 200, `{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100,"Operations":[]}`) + // Get all Bank account + do("GET", "/api/BankAccounts/", noH, "", 200, `[{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100,"Operations":[]},{"ID":2,"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0,"Operations":[]}]`) + // Add a bank account to Bakery + do("POST", "/api/BankAccounts", noH, `{"Number":"02-01","UserClientID":2,"Type":"checking-account","Amount":4745,"BankOverdraft":-500}`, 200, "") + + // Try to delete both BankAccount + // do("DELETE", "/api/BankAccounts/1", noH, ``, 200, "") + // do("DELETE", "/api/BankAccounts/2", noH, ``, 200, "") +} + +func TestHandleOperations(t *testing.T) { + // Create the handler + dh := NewDataHandler() + ts := httptest.NewServer(http.HandlerFunc(dh.HandleOperations)) + defer ts.Close() + url, _ := url.Parse(ts.URL) + port := url.Port() + // Wrap the testing function + do := tester.CreateServerTester(t, port, "sdk-go.io", nil) + noH := tester.Header{Key: "", Value: ""} + + // Add operation between client and Bakery + do("POST", "/api/Operations", noH, `{"Debtor":1,"Amount":-100,"Creditor":3}`, 200, "") + // Get operation where id=1 + do("GET", "/api/Operations/1", noH, "", 200, `{"ID":1,"Debtor":1,"Amount":-100`) + // Get all Bank account + do("GET", "/api/Operations/", noH, "", 200, `[{"ID":1,"Debtor":1,"Amount":-100`) + // Add invalid operation between client and Bakery must be refused with 417 (Expectation failed) + do("POST", "/api/Operations", noH, `{"Debtor":1,"Amount":-1789,"Creditor":3}`, 417, "Not enough money") + +} + +func TestHandleUserClientWithAccounts(t *testing.T) { + // Create the handler + dh := NewDataHandler() + ts := httptest.NewServer(http.HandlerFunc(dh.HandleClients)) + defer ts.Close() + url, _ := url.Parse(ts.URL) + port := url.Port() + // Wrap the testing function + do := tester.CreateServerTester(t, port, "sdk-go.io", nil) + noH := tester.Header{Key: "", Value: ""} + + // Get client Dupond with his banks accounts and operations up to date (-100€ on checking-account) + do("GET", "/api/UserClients/1", noH, "", 200, `{"ID":1,"Name":"Dupond","BankAccounts":[{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":358,"BankOverdraft":-100,"Operations":null},{"ID":2,"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0,"Operations":null}]}`) + // Get client Bakery with his banks accounts and operations up to date (+100€ on checking-account) + do("GET", "/api/UserClients/2", noH, "", 200, `{"ID":2,"Name":"Boulangerie","BankAccounts":[{"ID":3,"Number":"02-01","UserClientID":2,"Type":"checking-account","Amount":4845,"BankOverdraft":-500,"Operations":null}]}`) + + // // Try to delete the above created user + // do("DELETE", "/api/UserBanks/1", noH, ``, 200, "") + // // Try to get the first user again + // do("GET", "/api/UserBanks/1", noH, "", 404, `id does not exist`) +} diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go index 92aa5e6..586d5b0 100644 --- a/internal/rootmux/rootmux.go +++ b/internal/rootmux/rootmux.go @@ -4,6 +4,7 @@ import ( "net/http" "os" + "github.com/nicolaspernoud/vestibule/internal/models" "github.com/nicolaspernoud/vestibule/pkg/auth" "github.com/nicolaspernoud/vestibule/pkg/middlewares" @@ -28,6 +29,14 @@ func CreateRootMux(port int, staticDir string) RootMux { mainMux.Handle("/OAuth2Callback", m.HandleOAuth2Callback()) mainMux.HandleFunc("/Logout", m.HandleLogout) mainMux.HandleFunc("/Login", m.HandleInMemoryLogin) + + // Bank API endpoints + dh := models.NewDataHandler() + mainMux.HandleFunc("/api/UserClients/", dh.HandleClients) + mainMux.HandleFunc("/api/BankAccounts/", dh.HandleBankAccounts) + mainMux.HandleFunc("/api/Operations/", dh.HandleOperations) + + // Common API endpoints commonMux := http.NewServeMux() mainMux.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false)) commonMux.HandleFunc("/Share", auth.GetShareToken) diff --git a/pkg/glob/LICENSE b/pkg/glob/LICENSE new file mode 100644 index 0000000..bdfbd95 --- /dev/null +++ b/pkg/glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +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. diff --git a/pkg/glob/glob.go b/pkg/glob/glob.go new file mode 100644 index 0000000..e67db3b --- /dev/null +++ b/pkg/glob/glob.go @@ -0,0 +1,56 @@ +package glob + +import "strings" + +// The character which is treated like a glob +const GLOB = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// subject string. The result is a simple true/false, determining whether or +// not the glob pattern matched the subject text. +func Glob(pattern, subj string) bool { + // Empty pattern can only match empty subject + if pattern == "" { + return subj == pattern + } + + // If the pattern _is_ a glob, it matches everything + if pattern == GLOB { + return true + } + + parts := strings.Split(pattern, GLOB) + + if len(parts) == 1 { + // No globs in pattern, so test for equality + return subj == pattern + } + + leadingGlob := strings.HasPrefix(pattern, GLOB) + trailingGlob := strings.HasSuffix(pattern, GLOB) + end := len(parts) - 1 + + // Go over the leading parts and ensure they match. + for i := 0; i < end; i++ { + idx := strings.Index(subj, parts[i]) + + switch i { + case 0: + // Check the first section. Requires special handling. + if !leadingGlob && idx != 0 { + return false + } + default: + // Check that the middle parts match. + if idx < 0 { + return false + } + } + + // Trim evaluated text from subj as we loop over the pattern. + subj = subj[idx+len(parts[i]):] + } + + // Reached the last section. Requires special handling. + return trailingGlob || strings.HasSuffix(subj, parts[end]) +} diff --git a/pkg/glob/glob_test.go b/pkg/glob/glob_test.go new file mode 100644 index 0000000..fa4edee --- /dev/null +++ b/pkg/glob/glob_test.go @@ -0,0 +1,105 @@ +package glob + +import ( + "strings" + "testing" +) + +func testGlobMatch(t *testing.T, pattern, subj string) { + if !Glob(pattern, subj) { + t.Fatalf("%s should match %s", pattern, subj) + } +} + +func testGlobNoMatch(t *testing.T, pattern, subj string) { + if Glob(pattern, subj) { + t.Fatalf("%s should not match %s", pattern, subj) + } +} + +func TestEmptyPattern(t *testing.T) { + testGlobMatch(t, "", "") + testGlobNoMatch(t, "", "test") +} + +func TestEmptySubject(t *testing.T) { + for _, pattern := range []string{ + "", + "*", + "**", + "***", + "****************", + strings.Repeat("*", 1000000), + } { + testGlobMatch(t, pattern, "") + } + + for _, pattern := range []string{ + // No globs/non-glob characters + "test", + "*test*", + + // Trailing characters + "*x", + "*****************x", + strings.Repeat("*", 1000000) + "x", + + // Leading characters + "x*", + "x*****************", + "x" + strings.Repeat("*", 1000000), + + // Mixed leading/trailing characters + "x*x", + "x****************x", + "x" + strings.Repeat("*", 1000000) + "x", + } { + testGlobNoMatch(t, pattern, "") + } +} + +func TestPatternWithoutGlobs(t *testing.T) { + testGlobMatch(t, "test", "test") +} + +func TestGlob(t *testing.T) { + // Matches + for _, pattern := range []string{ + "*test", // Leading glob + "this*", // Trailing glob + "this*test", // Middle glob + "*is *", // String in between two globs + "*is*a*", // Lots of globs + "**test**", // Double glob characters + "**is**a***test*", // Varying number of globs + "* *", // White space between globs + "*", // Lone glob + "**********", // Nothing but globs + "*Ѿ*", // Unicode with globs + "*is a ϗѾ *", // Mixed ASCII/unicode + } { + testGlobMatch(t, pattern, "this is a ϗѾ test") + } + + // Non-matches + for _, pattern := range []string{ + "test*", // Implicit substring match + "*is", // Partial match + "*no*", // Globs without a match between them + " ", // Plain white space + "* ", // Trailing white space + " *", // Leading white space + "*ʤ*", // Non-matching unicode + "this*this is a test", // Repeated prefix + } { + testGlobNoMatch(t, pattern, "this is a test") + } +} + +func BenchmarkGlob(b *testing.B) { + for i := 0; i < b.N; i++ { + if !Glob("*quick*fox*dog", "The quick brown fox jumped over the lazy dog") { + b.Fatalf("should match") + } + } +} diff --git a/pkg/tester/tester.go b/pkg/tester/tester.go index f6f25ef..11f7daa 100644 --- a/pkg/tester/tester.go +++ b/pkg/tester/tester.go @@ -49,7 +49,7 @@ func DoRequestOnServer(t *testing.T, hostname string, port string, jar *cookieja // or create your own transport, there's an example on godoc. http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { addrAndPort := strings.Split(addr, ":") - if strings.HasSuffix(addrAndPort[0], "vestibule.io") { + if strings.HasSuffix(addrAndPort[0], "sdk-go.io") { addr = "127.0.0.1:" + addrAndPort[1] } return dialer.DialContext(ctx, network, addr) diff --git a/testdata/static/index.html b/testdata/static/index.html deleted file mode 100644 index c30a977..0000000 --- a/testdata/static/index.html +++ /dev/null @@ -1,30 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta http-equiv="X-UA-Compatible" content="ie=edge"> - <title>Vestibule Test</title> -</head> -<script> - - async function getData() { - data = await fetch("https://api.vestibule.127.0.0.1.nip.io:1443/", { - "credentials": "include", - "method": "GET", - "mode": "cors" - }); - result = await data.json(); - document.getElementById('api-result').innerHTML = JSON.stringify(result); - } - -</script> - -<body onload="getData()"> - <h1>This is a test !</h1> - <h2>API fetch result :</h2> - <div id="api-result">NO RESULT (YET)</div> -</body> - -</html> \ No newline at end of file diff --git a/web/index.html b/web/index.html index f72817d..717b324 100644 --- a/web/index.html +++ b/web/index.html @@ -1,6 +1,6 @@ <!DOCTYPE html> <html lang="en" class="has-navbar-fixed-top"> - <title>Vestibule</title> + <title>SDK-GO</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="assets/brand/favicon.ico" /> -- GitLab