Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions actions/createOrder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions actions/createOrder_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
12 changes: 8 additions & 4 deletions actions/retrieveOrder.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions actions/retrieveOrder_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
21 changes: 21 additions & 0 deletions actions/transferToCheckout_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion cmd/fixtures.go → cmd/fixtures/fixtures.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package fixtures

import (
"github.com/joho/godotenv"
Expand Down
2 changes: 1 addition & 1 deletion cmd/migrations.go → cmd/migrations/migrations.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package migrations

import (
"github.com/joho/godotenv"
Expand Down
8 changes: 4 additions & 4 deletions cmd/webserver.go → cmd/webserver/webserver.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package webserver

import (
"database/sql"
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions helpers/sqlNullConvert_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 4 additions & 0 deletions model/read/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -113,3 +116,4 @@ AND uuid = ?;

return &order, nil
}

23 changes: 15 additions & 8 deletions rest/retrieveOrder.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
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,
}
}

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)
Expand Down
56 changes: 56 additions & 0 deletions rest/retrieveOrder_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}