-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from viralparmarme/feature/http-server
Add REST APIs, mocking, config via env
- Loading branch information
Showing
22 changed files
with
1,305 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package api | ||
|
||
import ( | ||
"database/sql" | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
db "github.com/viralparmarme/simple-bank/db/sqlc" | ||
) | ||
|
||
func errorResponse(err error) gin.H { | ||
return gin.H{"error": err.Error()} | ||
} | ||
|
||
type createAccountRequest struct { | ||
Owner string `json:"owner" binding:"required"` | ||
Currency string `json:"currency" binding:"required,currency"` | ||
} | ||
|
||
func (server *Server) CreateAccount(ctx *gin.Context) { | ||
var req createAccountRequest | ||
err := ctx.ShouldBindJSON(&req) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, errorResponse(err)) | ||
return | ||
} | ||
|
||
arg := db.CreateAccountParams{ | ||
Owner: req.Owner, | ||
Currency: req.Currency, | ||
Balance: 0, | ||
} | ||
|
||
account, err := server.store.CreateAccount(ctx, arg) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) | ||
return | ||
} | ||
|
||
ctx.JSON(http.StatusOK, account) | ||
} | ||
|
||
type getAccountRequest struct { | ||
ID int64 `uri:"id" binding:"required,min=1"` | ||
} | ||
|
||
func (server *Server) GetAccount(ctx *gin.Context) { | ||
var req getAccountRequest | ||
err := ctx.ShouldBindUri(&req) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, errorResponse(err)) | ||
return | ||
} | ||
|
||
account, err := server.store.GetAccount(ctx, req.ID) | ||
if err != nil { | ||
if err == sql.ErrNoRows { | ||
ctx.JSON(http.StatusNotFound, errorResponse(err)) | ||
return | ||
} | ||
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) | ||
return | ||
} | ||
|
||
ctx.JSON(http.StatusOK, account) | ||
} | ||
|
||
type listAccountRequest struct { | ||
PageID int32 `form:"page_id" binding:"required,min=1"` | ||
PageSize int32 `form:"page_size" binding:"required,min=5,max=10"` | ||
} | ||
|
||
func (server *Server) ListAccount(ctx *gin.Context) { | ||
var req listAccountRequest | ||
err := ctx.ShouldBindQuery(&req) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, errorResponse(err)) | ||
return | ||
} | ||
|
||
arg := db.ListAccountsParams{ | ||
Limit: req.PageSize, | ||
Offset: (req.PageID - 1) * req.PageSize, | ||
} | ||
|
||
accounts, err := server.store.ListAccounts(ctx, arg) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) | ||
return | ||
} | ||
|
||
ctx.JSON(http.StatusOK, accounts) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package api | ||
|
||
import ( | ||
"bytes" | ||
"database/sql" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/golang/mock/gomock" | ||
"github.com/stretchr/testify/require" | ||
mockdb "github.com/viralparmarme/simple-bank/db/mock" | ||
db "github.com/viralparmarme/simple-bank/db/sqlc" | ||
"github.com/viralparmarme/simple-bank/util" | ||
) | ||
|
||
func TestGetAccountAPI(t *testing.T) { | ||
account := randomAccount() | ||
|
||
testCases := []struct { | ||
name string | ||
accountID int64 | ||
buildStubs func(store *mockdb.MockStore) | ||
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) | ||
}{ | ||
{ | ||
name: "OK", | ||
accountID: account.ID, | ||
buildStubs: func(store *mockdb.MockStore) { | ||
store.EXPECT(). | ||
GetAccount(gomock.Any(), gomock.Eq(account.ID)). | ||
Times(1). | ||
Return(account, nil) | ||
}, | ||
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { | ||
require.Equal(t, http.StatusOK, recorder.Code) | ||
requireBodyMatchAccount(t, recorder.Body, account) | ||
}, | ||
}, | ||
{ | ||
name: "NotFound", | ||
accountID: account.ID, | ||
buildStubs: func(store *mockdb.MockStore) { | ||
store.EXPECT(). | ||
GetAccount(gomock.Any(), gomock.Eq(account.ID)). | ||
Times(1). | ||
Return(db.Account{}, sql.ErrNoRows) | ||
}, | ||
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { | ||
require.Equal(t, http.StatusNotFound, recorder.Code) | ||
}, | ||
}, | ||
{ | ||
name: "InternalError", | ||
accountID: account.ID, | ||
buildStubs: func(store *mockdb.MockStore) { | ||
store.EXPECT(). | ||
GetAccount(gomock.Any(), gomock.Eq(account.ID)). | ||
Times(1). | ||
Return(db.Account{}, sql.ErrConnDone) | ||
}, | ||
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { | ||
require.Equal(t, http.StatusInternalServerError, recorder.Code) | ||
}, | ||
}, | ||
{ | ||
name: "InvalidID", | ||
accountID: 0, | ||
buildStubs: func(store *mockdb.MockStore) { | ||
store.EXPECT(). | ||
GetAccount(gomock.Any(), gomock.Any()). | ||
Times(0) | ||
}, | ||
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { | ||
require.Equal(t, http.StatusBadRequest, recorder.Code) | ||
}, | ||
}, | ||
} | ||
|
||
for i := range testCases { | ||
tc := testCases[i] | ||
|
||
t.Run(tc.name, func(t *testing.T) { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
store := mockdb.NewMockStore(ctrl) | ||
tc.buildStubs(store) | ||
|
||
server := NewServer(store) | ||
recorder := httptest.NewRecorder() | ||
|
||
url := fmt.Sprintf("/accounts/%d", tc.accountID) | ||
request, err := http.NewRequest(http.MethodGet, url, nil) | ||
require.NoError(t, err) | ||
|
||
server.router.ServeHTTP(recorder, request) | ||
tc.checkResponse(t, recorder) | ||
}) | ||
} | ||
} | ||
|
||
func randomAccount() db.Account { | ||
return db.Account{ | ||
ID: util.RandomInt(1, 1000), | ||
Owner: util.RandomOwner(), | ||
Balance: util.RandomMoney(), | ||
Currency: util.RandomCurrency(), | ||
} | ||
} | ||
|
||
func requireBodyMatchAccount(t *testing.T, body *bytes.Buffer, account db.Account) { | ||
data, err := ioutil.ReadAll(body) | ||
require.NoError(t, err) | ||
|
||
var gotAccount db.Account | ||
err = json.Unmarshal(data, &gotAccount) | ||
require.NoError(t, err) | ||
require.Equal(t, account, gotAccount) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package api | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
gin.SetMode(gin.TestMode) | ||
|
||
os.Exit(m.Run()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package api | ||
|
||
import ( | ||
"github.com/gin-gonic/gin" | ||
"github.com/gin-gonic/gin/binding" | ||
"github.com/go-playground/validator/v10" | ||
db "github.com/viralparmarme/simple-bank/db/sqlc" | ||
) | ||
|
||
type Server struct { | ||
store db.Store | ||
router *gin.Engine | ||
} | ||
|
||
func NewServer(store db.Store) *Server { | ||
server := &Server{store: store} | ||
router := gin.Default() | ||
|
||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok { | ||
v.RegisterValidation("currency", validCurrency) | ||
} | ||
|
||
router.POST("/accounts", server.CreateAccount) | ||
router.GET("/accounts/:id", server.GetAccount) | ||
router.GET("/accounts", server.ListAccount) | ||
|
||
router.POST("/transfers", server.CreateTransfer) | ||
|
||
server.router = router | ||
return server | ||
} | ||
|
||
func (server *Server) Start(address string) error { | ||
return server.router.Run(address) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package api | ||
|
||
import ( | ||
"database/sql" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
db "github.com/viralparmarme/simple-bank/db/sqlc" | ||
) | ||
|
||
type transferRequest struct { | ||
FromAccountId int64 `json:"from_account_id" binding:"required,min=1"` | ||
ToAccountId int64 `json:"to_account_id" binding:"required,min=1"` | ||
Amount int64 `json:"amount" binding:"required,gt=0"` | ||
Currency string `json:"currency" binding:"required,currency"` | ||
} | ||
|
||
func (server *Server) CreateTransfer(ctx *gin.Context) { | ||
var req transferRequest | ||
err := ctx.ShouldBindJSON(&req) | ||
if err != nil { | ||
ctx.JSON(http.StatusBadRequest, errorResponse(err)) | ||
return | ||
} | ||
|
||
if !server.validAccount(ctx, req.FromAccountId, req.Currency) { | ||
return | ||
} | ||
if !server.validAccount(ctx, req.ToAccountId, req.Currency) { | ||
return | ||
} | ||
|
||
arg := db.TransferTxParams{ | ||
FromAccountID: req.FromAccountId, | ||
ToAccountID: req.ToAccountId, | ||
Amount: req.Amount, | ||
} | ||
|
||
result, err := server.store.TransferTx(ctx, arg) | ||
if err != nil { | ||
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) | ||
return | ||
} | ||
|
||
ctx.JSON(http.StatusOK, result) | ||
} | ||
|
||
func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) bool { | ||
account, err := server.store.GetAccount(ctx, accountID) | ||
if err != nil { | ||
if err == sql.ErrNoRows { | ||
ctx.JSON(http.StatusNotFound, errorResponse(err)) | ||
return false | ||
} | ||
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) | ||
return false | ||
} | ||
|
||
if account.Currency != currency { | ||
err := fmt.Errorf("account %d mismatch: %s vs %s", accountID, account.Currency, currency) | ||
ctx.JSON(http.StatusBadRequest, errorResponse(err)) | ||
return false | ||
} | ||
|
||
return true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package api | ||
|
||
import ( | ||
"github.com/go-playground/validator/v10" | ||
"github.com/viralparmarme/simple-bank/util" | ||
) | ||
|
||
var validCurrency validator.Func = func(fieldlevel validator.FieldLevel) bool { | ||
if currency, ok := fieldlevel.Field().Interface().(string); ok { | ||
return util.IsSupportedCurrency(currency) | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
DB_DRIVER="postgres" | ||
DB_SOURCE="postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" | ||
SERVER_ADDRESS="0.0.0.0:8080" |
Oops, something went wrong.