diff --git a/api/location.go b/api/location.go index 3923068..f16fef3 100644 --- a/api/location.go +++ b/api/location.go @@ -12,6 +12,7 @@ import ( "git.nochill.in/nochill/hiling_go/util" "github.com/gin-gonic/gin" "github.com/lib/pq" + "github.com/yiplee/sqlc" ) type createLocationReq struct { @@ -60,10 +61,20 @@ func (server *Server) createLocation(ctx *gin.Context) { } err := server.Store.CreateLocation(ctx, arg) + if err != nil { if pqErr, ok := err.(*pq.Error); ok { - ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Something went wrong, code: %s", pqErr.Code.Name()))) - return + switch pqErr.Code.Name() { + case "foreign_key_violation", "unique_violation": + if pqErr.Constraint == "locations_regency_id_fkey" { + ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Failed to submit location, there's no regency with id: %d", arg.RegencyID))) + return + } + if pqErr.Constraint == "submitted_by_fkey" { + ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Failed to submit location, there's no user with id: %d", arg.SubmittedBy))) + return + } + } } ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return @@ -85,12 +96,12 @@ func (server *Server) getListLocations(ctx *gin.Context) { return } - arg := db.GetListLocationsParams{ - Limit: req.PageSize, - Offset: (req.Page - 1) * req.PageSize, - } + locations, err := server.Store.GetListLocations(sqlc.Build(ctx, func(builder *sqlc.Builder) { + builder.Limit(int(req.PageSize)) + builder.Offset(int(req.Page-1) * int(req.PageSize)) + builder.Order("created_at ASC") + })) - locations, err := server.Store.GetListLocations(ctx, arg) if err != nil { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return @@ -112,6 +123,10 @@ func (server *Server) getLocation(ctx *gin.Context) { location, err := server.Store.GetLocation(ctx, req.ID) if err != nil { + if err == sql.ErrNoRows { + ctx.JSON(http.StatusNotFound, ErrorResponse(err, "")) + return + } ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) return } diff --git a/api/test/location_test.go b/api/test/location_test.go index 74015aa..7136e46 100644 --- a/api/test/location_test.go +++ b/api/test/location_test.go @@ -1,31 +1,166 @@ package api_test import ( + "bytes" "database/sql" + "fmt" + "mime/multipart" + "net/http" + "net/http/httptest" "testing" + "time" + mockdb "git.nochill.in/nochill/hiling_go/db/mock" db "git.nochill.in/nochill/hiling_go/db/sqlc" "git.nochill.in/nochill/hiling_go/util" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) -func TestGetListLocationsAPI(t *testing.T) { +func TestGetLocationsAPI(t *testing.T) { + location := randomLocation() + + testCases := []struct { + name string + id int32 + buildStubs func(store *mockdb.MockStore) + checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) + }{ + { + name: "OK", + id: location.ID, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + GetLocation(gomock.Any(), gomock.Eq(location.ID)). + Times(1). + Return(location, nil) + }, + checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusOK, 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 := newTestServer(t, store) + recorder := httptest.NewRecorder() + + url := fmt.Sprintf("/location/%d", tc.id) + request, err := http.NewRequest(http.MethodGet, url, nil) + require.NoError(t, err) + + server.Router.ServeHTTP(recorder, request) + tc.checkResponse(t, recorder) + }) + } } func TestCreateLocationAPI(t *testing.T) { - _ = db.CreateLocationParams{ + location := db.CreateLocationParams{ Address: util.RandomString(10), Name: util.RandomString(10), - SubmittedBy: int32(util.RandomInt(0, 10)), + SubmittedBy: 1, RegencyID: 1305, GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)}, } - t.Run("OK", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() + testCases := []struct { + name string + body db.CreateLocationParams + buildStubs func(store *mockdb.MockStore) + checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder) + }{ + { + name: "OK", + body: location, + buildStubs: func(store *mockdb.MockStore) { + arg := db.CreateLocationParams{ + Address: location.Address, + Name: location.Name, + SubmittedBy: location.SubmittedBy, + RegencyID: location.RegencyID, + GoogleMapsLink: location.GoogleMapsLink, + } + store.EXPECT(). + CreateLocation(gomock.Any(), gomock.Eq(arg)). + Times(1). + Return(nil) + }, + checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusOK, recorder.Code) + }, + }, + { + name: "Bad Request", + body: db.CreateLocationParams{}, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateLocation(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + } - // store := mockdb.MockStore - }) + for i := range testCases { + tc := testCases[i] + + t.Run(tc.name, func(t *testing.T) { + bodyBuffer := new(bytes.Buffer) + mw := multipart.NewWriter(bodyBuffer) + mw.WriteField("address", tc.body.Address) + mw.WriteField("name", tc.body.Name) + mw.WriteField("submitted_by", fmt.Sprintf("%d", tc.body.SubmittedBy)) + mw.WriteField("regency_id", fmt.Sprintf("%d", tc.body.RegencyID)) + mw.WriteField("google_maps_link", tc.body.GoogleMapsLink.String) + mw.Close() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mockdb.NewMockStore(ctrl) + tc.buildStubs(store) + + server := newTestServer(t, store) + recorder := httptest.NewRecorder() + + url := "/locations" + request, err := http.NewRequest(http.MethodPost, url, bodyBuffer) + request.Header.Set("Content-Type", mw.FormDataContentType()) + require.NoError(t, err) + + server.Router.ServeHTTP(recorder, request) + tc.checkResponse(t, recorder) + + }) + } +} + +func randomLocation() db.Location { + return db.Location{ + ID: int32(util.RandomInt(1, 20)), + Address: util.RandomString(10), + Name: util.RandomString(10), + GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(20)}, + SubmittedBy: 1, + TotalVisited: sql.NullInt32{Valid: true, Int32: int32(util.RandomInt(0, 32))}, + Thumbnail: sql.NullString{Valid: false, String: ""}, + RegencyID: 1305, + IsDeleted: sql.NullBool{Valid: true, Bool: false}, + CreatedAt: sql.NullTime{Valid: true, Time: time.Now()}, + UpdatedAt: sql.NullTime{Valid: true, Time: time.Now()}, + ApprovedBy: sql.NullInt32{Valid: true, Int32: 1}, + ApprovedAt: sql.NullTime{Valid: true, Time: time.Now()}, + } } diff --git a/db/mock/store.go b/db/mock/store.go index 2c059a9..f99e3a7 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -65,18 +65,18 @@ func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call } // GetListLocations mocks base method. -func (m *MockStore) GetListLocations(arg0 context.Context, arg1 db.GetListLocationsParams) ([]db.Location, error) { +func (m *MockStore) GetListLocations(arg0 context.Context) ([]db.Location, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetListLocations", arg0, arg1) + ret := m.ctrl.Call(m, "GetListLocations", arg0) ret0, _ := ret[0].([]db.Location) ret1, _ := ret[1].(error) return ret0, ret1 } // GetListLocations indicates an expected call of GetListLocations. -func (mr *MockStoreMockRecorder) GetListLocations(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetListLocations(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0) } // GetLocation mocks base method. diff --git a/db/queries/locations.sql b/db/queries/locations.sql index e7923cb..a79e1d7 100644 --- a/db/queries/locations.sql +++ b/db/queries/locations.sql @@ -1,7 +1,5 @@ -- name: GetListLocations :many -SELECT * FROM locations -LIMIT $1 -OFFSET $2; +SELECT * FROM locations; -- name: GetLocation :one SELECT * FROM locations diff --git a/db/sqlc/locations.sql.go b/db/sqlc/locations.sql.go index e9a028a..7d2fb5c 100644 --- a/db/sqlc/locations.sql.go +++ b/db/sqlc/locations.sql.go @@ -43,17 +43,10 @@ func (q *Queries) CreateLocation(ctx context.Context, arg CreateLocationParams) const getListLocations = `-- name: GetListLocations :many SELECT id, address, name, google_maps_link, submitted_by, total_visited, thumbnail, regency_id, is_deleted, created_at, updated_at, approved_by, approved_at FROM locations -LIMIT $1 -OFFSET $2 ` -type GetListLocationsParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` -} - -func (q *Queries) GetListLocations(ctx context.Context, arg GetListLocationsParams) ([]Location, error) { - rows, err := q.db.QueryContext(ctx, getListLocations, arg.Limit, arg.Offset) +func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) { + rows, err := q.db.QueryContext(ctx, getListLocations) if err != nil { return nil, err } diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 1c8f530..993b026 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -11,7 +11,7 @@ import ( type Querier interface { CreateLocation(ctx context.Context, arg CreateLocationParams) error CreateUser(ctx context.Context, arg CreateUserParams) (User, error) - GetListLocations(ctx context.Context, arg GetListLocationsParams) ([]Location, error) + GetListLocations(ctx context.Context) ([]Location, error) GetLocation(ctx context.Context, id int32) (Location, error) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) diff --git a/db/sqlc/store.go b/db/sqlc/store.go index 9ad1580..76b8881 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -2,6 +2,8 @@ package db import ( "database/sql" + + "github.com/yiplee/sqlc" ) type Store interface { @@ -16,7 +18,7 @@ type SQLStore struct { func NewStore(db *sql.DB) Store { return &SQLStore{ db: db, - Queries: New(db), + Queries: New(sqlc.Wrap(db)), } } diff --git a/db/sqlc/test/locations_test.go b/db/sqlc/test/locations_test.go index a133d17..7886855 100644 --- a/db/sqlc/test/locations_test.go +++ b/db/sqlc/test/locations_test.go @@ -11,12 +11,7 @@ import ( ) func TestGetLocationsList(t *testing.T) { - arg := db.GetListLocationsParams{ - Limit: 10, - Offset: 0, - } - - locations, err := testQueries.GetListLocations(context.Background(), arg) + locations, err := testQueries.GetListLocations(context.Background()) require.NoError(t, err) require.NotEmpty(t, locations) } diff --git a/go.mod b/go.mod index d190f2e..0ccb485 100644 --- a/go.mod +++ b/go.mod @@ -45,10 +45,13 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/yiplee/nap v1.0.1 // indirect + github.com/yiplee/sqlc v1.0.2 // indirect go.uber.org/mock v0.2.0 // indirect golang.org/x/arch v0.5.0 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/net v0.15.0 // indirect + golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect diff --git a/go.sum b/go.sum index 8d63038..2e8bd82 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,7 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -230,6 +231,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yiplee/nap v1.0.1 h1:5p8KAIkYy+PIMGSk+ScF13Hh/OFkIEBHPuD14OFvStg= +github.com/yiplee/nap v1.0.1/go.mod h1:7Zvro/en8ARhkqgv3vpj037yJSBRvGeNyj5Np5XUFgc= +github.com/yiplee/sqlc v1.0.2 h1:GcWRpoKb0jRPuaYhiXex/LhJWkiCWQNnUjgOMQHgvJQ= +github.com/yiplee/sqlc v1.0.2/go.mod h1:jZaVoaxbMKXkjmZAl1uOyBKf71/rhIStVcoZrg68X3c= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -349,6 +354,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=