diff --git a/README.md b/README.md index f84904e..cf4c811 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Prjctr Go course. Mock order service project. - confirms order payment from payment gateway - "/order/{uuid}/payment/{paymentUuid}" - DONE! - provides order status (as requested from user service, product service, others) - "/retrieve/{uuid}" - DONE! +## TODO +- implement DI with Wire (function type injection, ftw) +- investigage using GORM raw url for read model retrieveal (null values?) +- implement html ui with htmx +- use type receivers (read-only) for write model +- implement backend data admin (https://go-admin.com, https://github.com/LyricTian/gin-admin) + ## Technical implementation - REST API - uses repository pattern for entity persistence diff --git a/actions/createOrder.go b/actions/createOrder.go index 43d2756..8ed4e6a 100644 --- a/actions/createOrder.go +++ b/actions/createOrder.go @@ -9,15 +9,18 @@ import ( // NewOrderRequest. type NewOrderRequest struct{} +// Save order function. +type SaveOrder func(order *write.Order) error + // CreateOrder action. type CreateOrder struct { - repo write.IOrderSaver + saveOrder SaveOrder } // Constructor. -func NewCreateOrder(repo write.IOrderSaver) *CreateOrder { +func NewCreateOrder(saveOrder SaveOrder) *CreateOrder { return &CreateOrder{ - repo: repo, + saveOrder: saveOrder, } } @@ -28,7 +31,7 @@ func (action *CreateOrder) Create(r NewOrderRequest) (*write.Order, error) { CreatedAt: time.Now(), } - err := action.repo.Save(order) + err := action.saveOrder(order) if err != nil { return nil, err diff --git a/actions/createOrder_test.go b/actions/createOrder_test.go new file mode 100644 index 0000000..22bee11 --- /dev/null +++ b/actions/createOrder_test.go @@ -0,0 +1,37 @@ +package actions_test + +import ( + // "orders/actions" + "errors" + "orders/actions" + "orders/model/write" + "testing" +) + +func Test_CreateOrder_Success(t *testing.T) { + // SaveOrder function type mock. + saveOrder:=func(order *write.Order) error { + return nil + } + + action:=actions.NewCreateOrder(saveOrder) + _,err:=action.Create(actions.NewOrderRequest{}) + + if err!=nil{ + t.Logf("Cannot create new order") + } +} + +func Test_CreateOrder_Failure(t *testing.T) { + // SaveOrder function type mock. + saveOrder:=func(order *write.Order) error { + return errors.New("could not save order") + } + + action:=actions.NewCreateOrder(saveOrder) + _,err:=action.Create(actions.NewOrderRequest{}) + + if err==nil{ + t.Logf("save order error not handled") + } +} diff --git a/actions/retrieveOrder.go b/actions/retrieveOrder.go index 7879fa1..84abe5c 100644 --- a/actions/retrieveOrder.go +++ b/actions/retrieveOrder.go @@ -1,26 +1,30 @@ package actions import ( + "errors" "orders/model/read" ) // RetrieveOrder action. type RetrieveOrder struct { - finder read.OrderFinderById + findOrder read.FindOrder } // Constructor. -func NewRetrieveOrder(finder read.OrderFinderById) *RetrieveOrder { +func NewRetrieveOrder(findOrder read.FindOrder) *RetrieveOrder { return &RetrieveOrder{ - finder: finder, + findOrder: findOrder, } } func (action *RetrieveOrder) Retrieve(uuid string) (*read.Order, error) { - order, err := action.finder.Find(uuid) + order, err := action.findOrder(uuid) if err != nil { return nil, err } + if order == nil { + return nil, errors.New("order is nil") + } return order, nil } diff --git a/actions/retrieveOrder_test.go b/actions/retrieveOrder_test.go new file mode 100644 index 0000000..3e45d86 --- /dev/null +++ b/actions/retrieveOrder_test.go @@ -0,0 +1,53 @@ +package actions_test + +import ( + "errors" + "orders/actions" + "orders/model/read" + "reflect" + "testing" +) + +func Test_RetrieveOrder(t *testing.T) { + order := &read.Order{} + + tests := []struct { + testName string + findOrder read.FindOrder + orderResult *read.Order + errResult error + }{ + {"success", + func(uuid string) (*read.Order, error) { + return order, nil + }, + order, + nil, + }, + {"failure - order finder error", + func(uuid string) (*read.Order, error) { + return nil, errors.New("order not found") + }, + nil, + errors.New("order not found"), + }, + {"failure - order is nil", + func(uuid string) (*read.Order, error) { + return nil, nil + }, + nil, + errors.New("order is nil"), + }, + } + + // Loop through each test case + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + action := actions.NewRetrieveOrder(tt.findOrder) + order, err := action.Retrieve("someuuid") + if order != tt.orderResult || reflect.TypeOf(err) != reflect.TypeOf(tt.errResult) { + t.Errorf(tt.testName, order, err, tt.orderResult, tt.errResult) + } + }) + } +} diff --git a/actions/transferToCheckout_test.go b/actions/transferToCheckout_test.go new file mode 100644 index 0000000..8ee7ca1 --- /dev/null +++ b/actions/transferToCheckout_test.go @@ -0,0 +1,21 @@ +package actions_test + +import ( + "database/sql" + "orders/actions" + "orders/model/read" + "strconv" + "testing" +) + +func Test_TransferToCheckout(t *testing.T) { + order := &read.Order{Uuid: sql.NullString{String: "someuuid"}, Total: 100} + url := "http://checkout.url" + action := actions.NewCheckoutTransfer(url) + got := action.Url(order) + + wanted := url + "?cart=" + order.Uuid.String + "&total=" + strconv.Itoa(order.Total) + if got != wanted { + t.Fatalf("wanted %s, got %s", wanted, got) + } +} diff --git a/cmd/fixtures.go b/cmd/fixtures/fixtures.go similarity index 98% rename from cmd/fixtures.go rename to cmd/fixtures/fixtures.go index 9e363c9..da99b4a 100644 --- a/cmd/fixtures.go +++ b/cmd/fixtures/fixtures.go @@ -1,4 +1,4 @@ -package main +package fixtures import ( "github.com/joho/godotenv" diff --git a/cmd/migrations.go b/cmd/migrations/migrations.go similarity index 96% rename from cmd/migrations.go rename to cmd/migrations/migrations.go index 128045f..fb5995a 100644 --- a/cmd/migrations.go +++ b/cmd/migrations/migrations.go @@ -1,4 +1,4 @@ -package main +package migrations import ( "github.com/joho/godotenv" diff --git a/cmd/webserver.go b/cmd/webserver/webserver.go similarity index 95% rename from cmd/webserver.go rename to cmd/webserver/webserver.go index fa4b349..86a5908 100644 --- a/cmd/webserver.go +++ b/cmd/webserver/webserver.go @@ -1,4 +1,4 @@ -package main +package webserver import ( "database/sql" @@ -50,11 +50,11 @@ func main() { orderActiveFinder := read.NewOrderFinderActiveById(mysqlDb) // Actions. - retrieveOrderAction := actions.NewRetrieveOrder(read.NewOrderFinderById(mysqlDb, read.NewOrderItemFinderById(mysqlDb))) + retrieveOrderAction := actions.NewRetrieveOrder(read.NewOrderFinderById(mysqlDb, read.NewOrderItemFinderById(mysqlDb)).Find) // Controllers. - createOrderCntrlr := rest.NewCreateOrder(actions.NewCreateOrder(repo), respndr) - retrieveOrderCntrlr := rest.NewRetrieveOrder(retrieveOrderAction, respndr) + createOrderCntrlr := rest.NewCreateOrder(actions.NewCreateOrder(repo.Save), respndr) + retrieveOrderCntrlr := rest.NewRetrieveOrder(retrieveOrderAction.Retrieve, respndr) modifyOrderCntrlr := rest.NewDeleteProduct(actions.NewProductDeleter(orderModifier, orderItemFinder), respndr) addProductCntrl := rest.NewAddProduct(actions.NewProductAdder(orderModifier, orderActiveFinder), respndr) checkoutCntrlr := rest.NewCheckoutTransfer(actions.NewCheckoutTransfer("http://checkout.url"), retrieveOrderAction, respndr) diff --git a/helpers/sqlNullConvert_test.go b/helpers/sqlNullConvert_test.go new file mode 100644 index 0000000..d0fc2f5 --- /dev/null +++ b/helpers/sqlNullConvert_test.go @@ -0,0 +1,16 @@ +package helpers_test + +import ( + "database/sql" + "orders/helpers" + "testing" +) + +func Test_SqlNullConvert(t *testing.T) { + want := "a string" + nlstr := sql.NullString{String: want, Valid: true} + got := helpers.NullStringToString(nlstr) + if want != got { + t.Fatalf("%s != %s", want, got) + } +} diff --git a/model/read/order.go b/model/read/order.go index 3e11338..3e6211c 100644 --- a/model/read/order.go +++ b/model/read/order.go @@ -16,6 +16,9 @@ type Order struct { CreatedAt sql.NullTime } +// FindOrder repository function type. +type FindOrder func(uuid string) (*Order, error) +// Same as above, but in interface type OrderFinderById interface { Find(uuid string) (*Order, error) } @@ -113,3 +116,4 @@ AND uuid = ?; return &order, nil } + diff --git a/rest/retrieveOrder.go b/rest/retrieveOrder.go index 44e7271..5c33d5e 100644 --- a/rest/retrieveOrder.go +++ b/rest/retrieveOrder.go @@ -1,22 +1,23 @@ package rest import ( - "github.com/gorilla/mux" "net/http" - "orders/actions" + "orders/model/read" + + "github.com/gorilla/mux" ) // Create order controller. type RetrieveOrder struct { - action *actions.RetrieveOrder - rspndr *Responder + findOrder read.FindOrder + rspndr *Responder } // Constructor. -func NewRetrieveOrder(action *actions.RetrieveOrder, rspndr *Responder) *RetrieveOrder { +func NewRetrieveOrder(findOrder read.FindOrder, rspndr *Responder) *RetrieveOrder { return &RetrieveOrder{ - action: action, - rspndr: rspndr, + findOrder: findOrder, + rspndr: rspndr, } } @@ -24,10 +25,16 @@ func (c *RetrieveOrder) Retrieve(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) uuid := vars["uuid"] - order, err := c.action.Retrieve(uuid) + order, err := c.findOrder(uuid) if err != nil { c.rspndr.Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if order == nil { + c.rspndr.Error(w, http.StatusInternalServerError, "order is nil") + return } c.rspndr.Success(w, order) diff --git a/rest/retrieveOrder_test.go b/rest/retrieveOrder_test.go new file mode 100644 index 0000000..6a68ef4 --- /dev/null +++ b/rest/retrieveOrder_test.go @@ -0,0 +1,56 @@ +package rest_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "orders/model/read" + "orders/rest" + "testing" +) + +func Test_RetrieveOrder(t *testing.T) { + + tests := []struct { + name string + findOrder read.FindOrder + wantHttpStatus int + }{ + {"success", + func(uuid string) (*read.Order, error) { + return &read.Order{}, nil + }, + http.StatusOK, + }, + {"failure - order finder error", + func(uuid string) (*read.Order, error) { + return nil, errors.New("order not found") + }, + http.StatusInternalServerError, + }, + {"failure - order is nil", + func(uuid string) (*read.Order, error) { + return nil, nil + }, + http.StatusInternalServerError, + }, + } + + // Loop through each test case + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Init http testing server. + r := httptest.NewRequest(http.MethodGet, "http://localhost", nil) + w := httptest.NewRecorder() + // Init our controller. + rspndr := rest.NewResponder("2006-01-02 15:04:05") + cntrlr := rest.NewRetrieveOrder(tt.findOrder, rspndr) + cntrlr.Retrieve(w, r) + + gotHttpStatus := w.Result().StatusCode + if gotHttpStatus != tt.wantHttpStatus { + t.Errorf(tt.name, gotHttpStatus, tt.wantHttpStatus) + } + }) + } +}