├── .gitignore ├── apispec.json ├── app ├── database.go └── router.go ├── controller ├── category_controller.go └── category_controller_impl.go ├── exception ├── error_handler.go └── not_found_error.go ├── go.mod ├── go.sum ├── helper ├── error.go ├── json.go ├── model.go └── tx.go ├── main.go ├── middleware └── auth_middleware.go ├── model ├── domain │ └── category.go └── web │ ├── category_create_request.go │ ├── category_response.go │ ├── category_update_request.go │ └── web_response.go ├── repository ├── category_repository.go └── category_repository_impl.go ├── service ├── category_service.go └── category_service_impl.go ├── test.http └── test └── category_controller_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /apispec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "Category RESTful API", 5 | "description": "API Spec for Category RESTful API", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "http://localhost:3000/api" 11 | } 12 | ], 13 | "paths": { 14 | "/categories": { 15 | "get": { 16 | "security": [{ 17 | "CategoryAuth" : [] 18 | }], 19 | "tags": [ 20 | "Category API" 21 | ], 22 | "description": "List all Categories", 23 | "summary": "List all Categories", 24 | "responses": { 25 | "200": { 26 | "description": "Success get all categories", 27 | "content": { 28 | "application/json": { 29 | "schema": { 30 | "type": "object", 31 | "properties": { 32 | "code": { 33 | "type": "number" 34 | }, 35 | "status": { 36 | "type": "string" 37 | }, 38 | "data": { 39 | "type": "array", 40 | "items": { 41 | "$ref": "#/components/schemas/Category" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "post": { 52 | "security": [{ 53 | "CategoryAuth" : [] 54 | }], 55 | "tags": ["Category API"], 56 | "description": "Create new Category", 57 | "summary": "Create new Category", 58 | "requestBody": { 59 | "content": { 60 | "application/json": { 61 | "schema": { 62 | "$ref": "#/components/schemas/CreateOrUpdateCategory" 63 | } 64 | } 65 | } 66 | }, 67 | "responses": { 68 | "200": { 69 | "description": "Success Create Category", 70 | "content": { 71 | "application/json": { 72 | "schema": { 73 | "type": "object", 74 | "properties": { 75 | "code" : { 76 | "type": "number" 77 | }, 78 | "status" : { 79 | "type": "string" 80 | }, 81 | "data" : { 82 | "$ref": "#/components/schemas/Category" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | "/categories/{categoryId}" : { 93 | "get" : { 94 | "security": [{ 95 | "CategoryAuth" : [] 96 | }], 97 | "tags": ["Category API"], 98 | "summary": "Get category by Id", 99 | "description": "Get category by id", 100 | "parameters": [ 101 | { 102 | "name": "categoryId", 103 | "in": "path", 104 | "description": "Category Id" 105 | } 106 | ], 107 | "responses": { 108 | "200" : { 109 | "description": "Success get category", 110 | "content": { 111 | "application/json": { 112 | "schema": { 113 | "type": "object", 114 | "properties": { 115 | "code" : { 116 | "type": "number" 117 | }, 118 | "status" : { 119 | "type": "string" 120 | }, 121 | "data" : { 122 | "$ref": "#/components/schemas/Category" 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "put" : { 132 | "security": [{ 133 | "CategoryAuth" : [] 134 | }], 135 | "tags": ["Category API"], 136 | "summary": "Update category by Id", 137 | "description": "Update category by Id", 138 | "parameters": [ 139 | { 140 | "name": "categoryId", 141 | "in": "path", 142 | "description": "Category Id" 143 | } 144 | ], 145 | "requestBody": { 146 | "content": { 147 | "application/json": { 148 | "schema": { 149 | "$ref": "#/components/schemas/CreateOrUpdateCategory" 150 | } 151 | } 152 | } 153 | }, 154 | "responses": { 155 | "200" : { 156 | "description": "Success get category", 157 | "content": { 158 | "application/json": { 159 | "schema": { 160 | "type": "object", 161 | "properties": { 162 | "code" : { 163 | "type": "number" 164 | }, 165 | "status" : { 166 | "type": "string" 167 | }, 168 | "data" : { 169 | "$ref": "#/components/schemas/Category" 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | }, 178 | "delete": { 179 | "security": [{ 180 | "CategoryAuth" : [] 181 | }], 182 | "tags": ["Category API"], 183 | "summary": "Delete category by Id", 184 | "description": "Delete category by id", 185 | "parameters": [ 186 | { 187 | "name": "categoryId", 188 | "in": "path", 189 | "description": "Category Id" 190 | } 191 | ], 192 | "responses": { 193 | "200" : { 194 | "description": "Success delete category", 195 | "content": { 196 | "application/json": { 197 | "schema": { 198 | "type": "object", 199 | "properties": { 200 | "code" : { 201 | "type": "number" 202 | }, 203 | "status" : { 204 | "type": "string" 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | } 214 | }, 215 | "components": { 216 | "securitySchemes": { 217 | "CategoryAuth" : { 218 | "type": "apiKey", 219 | "in" : "header", 220 | "name": "X-API-Key", 221 | "description": "Authentication for Category API" 222 | } 223 | }, 224 | "schemas": { 225 | "CreateOrUpdateCategory" : { 226 | "type": "object", 227 | "properties": { 228 | "name": { 229 | "type": "string" 230 | } 231 | } 232 | }, 233 | "Category" : { 234 | "type": "object", 235 | "properties": { 236 | "id": { 237 | "type": "number" 238 | }, 239 | "name": { 240 | "type": "string" 241 | } 242 | } 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /app/database.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "programmerzamannow/belajar-golang-restful-api/helper" 6 | "time" 7 | ) 8 | 9 | func NewDB() *sql.DB { 10 | db, err := sql.Open("mysql", "root@tcp(localhost:3306)/belajar_golang_restful_api") 11 | helper.PanicIfError(err) 12 | 13 | db.SetMaxIdleConns(5) 14 | db.SetMaxOpenConns(20) 15 | db.SetConnMaxLifetime(60 * time.Minute) 16 | db.SetConnMaxIdleTime(10 * time.Minute) 17 | 18 | return db 19 | } 20 | -------------------------------------------------------------------------------- /app/router.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "programmerzamannow/belajar-golang-restful-api/controller" 6 | "programmerzamannow/belajar-golang-restful-api/exception" 7 | ) 8 | 9 | func NewRouter(categoryController controller.CategoryController) *httprouter.Router { 10 | router := httprouter.New() 11 | 12 | router.GET("/api/categories", categoryController.FindAll) 13 | router.GET("/api/categories/:categoryId", categoryController.FindById) 14 | router.POST("/api/categories", categoryController.Create) 15 | router.PUT("/api/categories/:categoryId", categoryController.Update) 16 | router.DELETE("/api/categories/:categoryId", categoryController.Delete) 17 | 18 | router.PanicHandler = exception.ErrorHandler 19 | 20 | return router 21 | } 22 | -------------------------------------------------------------------------------- /controller/category_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | ) 7 | 8 | type CategoryController interface { 9 | Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 10 | Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 11 | Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 12 | FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 13 | FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) 14 | } 15 | -------------------------------------------------------------------------------- /controller/category_controller_impl.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | "net/http" 6 | "programmerzamannow/belajar-golang-restful-api/helper" 7 | "programmerzamannow/belajar-golang-restful-api/model/web" 8 | "programmerzamannow/belajar-golang-restful-api/service" 9 | "strconv" 10 | ) 11 | 12 | type CategoryControllerImpl struct { 13 | CategoryService service.CategoryService 14 | } 15 | 16 | func NewCategoryController(categoryService service.CategoryService) CategoryController { 17 | return &CategoryControllerImpl{ 18 | CategoryService: categoryService, 19 | } 20 | } 21 | 22 | func (controller *CategoryControllerImpl) Create(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 23 | categoryCreateRequest := web.CategoryCreateRequest{} 24 | helper.ReadFromRequestBody(request, &categoryCreateRequest) 25 | 26 | categoryResponse := controller.CategoryService.Create(request.Context(), categoryCreateRequest) 27 | webResponse := web.WebResponse{ 28 | Code: 200, 29 | Status: "OK", 30 | Data: categoryResponse, 31 | } 32 | 33 | helper.WriteToResponseBody(writer, webResponse) 34 | } 35 | 36 | func (controller *CategoryControllerImpl) Update(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 37 | categoryUpdateRequest := web.CategoryUpdateRequest{} 38 | helper.ReadFromRequestBody(request, &categoryUpdateRequest) 39 | 40 | categoryId := params.ByName("categoryId") 41 | id, err := strconv.Atoi(categoryId) 42 | helper.PanicIfError(err) 43 | 44 | categoryUpdateRequest.Id = id 45 | 46 | categoryResponse := controller.CategoryService.Update(request.Context(), categoryUpdateRequest) 47 | webResponse := web.WebResponse{ 48 | Code: 200, 49 | Status: "OK", 50 | Data: categoryResponse, 51 | } 52 | 53 | helper.WriteToResponseBody(writer, webResponse) 54 | } 55 | 56 | func (controller *CategoryControllerImpl) Delete(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 57 | categoryId := params.ByName("categoryId") 58 | id, err := strconv.Atoi(categoryId) 59 | helper.PanicIfError(err) 60 | 61 | controller.CategoryService.Delete(request.Context(), id) 62 | webResponse := web.WebResponse{ 63 | Code: 200, 64 | Status: "OK", 65 | } 66 | 67 | helper.WriteToResponseBody(writer, webResponse) 68 | } 69 | 70 | func (controller *CategoryControllerImpl) FindById(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 71 | categoryId := params.ByName("categoryId") 72 | id, err := strconv.Atoi(categoryId) 73 | helper.PanicIfError(err) 74 | 75 | categoryResponse := controller.CategoryService.FindById(request.Context(), id) 76 | webResponse := web.WebResponse{ 77 | Code: 200, 78 | Status: "OK", 79 | Data: categoryResponse, 80 | } 81 | 82 | helper.WriteToResponseBody(writer, webResponse) 83 | } 84 | 85 | func (controller *CategoryControllerImpl) FindAll(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { 86 | categoryResponses := controller.CategoryService.FindAll(request.Context()) 87 | webResponse := web.WebResponse{ 88 | Code: 200, 89 | Status: "OK", 90 | Data: categoryResponses, 91 | } 92 | 93 | helper.WriteToResponseBody(writer, webResponse) 94 | } 95 | -------------------------------------------------------------------------------- /exception/error_handler.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "net/http" 6 | "programmerzamannow/belajar-golang-restful-api/helper" 7 | "programmerzamannow/belajar-golang-restful-api/model/web" 8 | ) 9 | 10 | func ErrorHandler(writer http.ResponseWriter, request *http.Request, err interface{}) { 11 | 12 | if notFoundError(writer, request, err) { 13 | return 14 | } 15 | 16 | if validationErrors(writer, request, err) { 17 | return 18 | } 19 | 20 | internalServerError(writer, request, err) 21 | } 22 | 23 | func validationErrors(writer http.ResponseWriter, request *http.Request, err interface{}) bool { 24 | exception, ok := err.(validator.ValidationErrors) 25 | if ok { 26 | writer.Header().Set("Content-Type", "application/json") 27 | writer.WriteHeader(http.StatusBadRequest) 28 | 29 | webResponse := web.WebResponse{ 30 | Code: http.StatusBadRequest, 31 | Status: "BAD REQUEST", 32 | Data: exception.Error(), 33 | } 34 | 35 | helper.WriteToResponseBody(writer, webResponse) 36 | return true 37 | }else{ 38 | return false 39 | } 40 | } 41 | 42 | func notFoundError(writer http.ResponseWriter, request *http.Request, err interface{}) bool { 43 | exception, ok := err.(NotFoundError) 44 | if ok { 45 | writer.Header().Set("Content-Type", "application/json") 46 | writer.WriteHeader(http.StatusNotFound) 47 | 48 | webResponse := web.WebResponse{ 49 | Code: http.StatusNotFound, 50 | Status: "NOT FOUND", 51 | Data: exception.Error, 52 | } 53 | 54 | helper.WriteToResponseBody(writer, webResponse) 55 | return true 56 | } else { 57 | return false 58 | } 59 | } 60 | 61 | func internalServerError(writer http.ResponseWriter, request *http.Request, err interface{}) { 62 | writer.Header().Set("Content-Type", "application/json") 63 | writer.WriteHeader(http.StatusInternalServerError) 64 | 65 | webResponse := web.WebResponse{ 66 | Code: http.StatusInternalServerError, 67 | Status: "INTERNAL SERVER ERROR", 68 | Data: err, 69 | } 70 | 71 | helper.WriteToResponseBody(writer, webResponse) 72 | } 73 | -------------------------------------------------------------------------------- /exception/not_found_error.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | type NotFoundError struct { 4 | Error string 5 | } 6 | 7 | func NewNotFoundError(error string) NotFoundError { 8 | return NotFoundError{Error: error} 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module programmerzamannow/belajar-golang-restful-api 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.9.0 // indirect 7 | github.com/go-sql-driver/mysql v1.6.0 // indirect 8 | github.com/julienschmidt/httprouter v1.3.0 // indirect 9 | github.com/stretchr/testify v1.7.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 6 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 7 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 8 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 9 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 10 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 11 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 12 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 13 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 14 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 15 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 18 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 19 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 20 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 23 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 24 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 28 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 29 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 33 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 34 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 35 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 36 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 40 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 43 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 44 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 49 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /helper/error.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | func PanicIfError(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /helper/json.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func ReadFromRequestBody(request *http.Request, result interface{}) { 9 | decoder := json.NewDecoder(request.Body) 10 | err := decoder.Decode(result) 11 | PanicIfError(err) 12 | } 13 | 14 | func WriteToResponseBody(writer http.ResponseWriter, response interface{}) { 15 | writer.Header().Add("Content-Type", "application/json") 16 | encoder := json.NewEncoder(writer) 17 | err := encoder.Encode(response) 18 | PanicIfError(err) 19 | } 20 | -------------------------------------------------------------------------------- /helper/model.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "programmerzamannow/belajar-golang-restful-api/model/domain" 5 | "programmerzamannow/belajar-golang-restful-api/model/web" 6 | ) 7 | 8 | func ToCategoryResponse(category domain.Category) web.CategoryResponse { 9 | return web.CategoryResponse{ 10 | Id: category.Id, 11 | Name: category.Name, 12 | } 13 | } 14 | 15 | func ToCategoryResponses(categories []domain.Category) []web.CategoryResponse { 16 | var categoryResponses []web.CategoryResponse 17 | for _, category := range categories { 18 | categoryResponses = append(categoryResponses, ToCategoryResponse(category)) 19 | } 20 | return categoryResponses 21 | } 22 | -------------------------------------------------------------------------------- /helper/tx.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "database/sql" 4 | 5 | func CommitOrRollback(tx *sql.Tx) { 6 | err := recover() 7 | if err != nil { 8 | errorRollback := tx.Rollback() 9 | PanicIfError(errorRollback) 10 | panic(err) 11 | } else { 12 | errorCommit := tx.Commit() 13 | PanicIfError(errorCommit) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | _ "github.com/go-sql-driver/mysql" 6 | "net/http" 7 | "programmerzamannow/belajar-golang-restful-api/app" 8 | "programmerzamannow/belajar-golang-restful-api/controller" 9 | "programmerzamannow/belajar-golang-restful-api/helper" 10 | "programmerzamannow/belajar-golang-restful-api/middleware" 11 | "programmerzamannow/belajar-golang-restful-api/repository" 12 | "programmerzamannow/belajar-golang-restful-api/service" 13 | ) 14 | 15 | func main() { 16 | 17 | db := app.NewDB() 18 | validate := validator.New() 19 | categoryRepository := repository.NewCategoryRepository() 20 | categoryService := service.NewCategoryService(categoryRepository, db, validate) 21 | categoryController := controller.NewCategoryController(categoryService) 22 | router := app.NewRouter(categoryController) 23 | 24 | server := http.Server{ 25 | Addr: "localhost:3000", 26 | Handler: middleware.NewAuthMiddleware(router), 27 | } 28 | 29 | err := server.ListenAndServe() 30 | helper.PanicIfError(err) 31 | } 32 | -------------------------------------------------------------------------------- /middleware/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "programmerzamannow/belajar-golang-restful-api/helper" 6 | "programmerzamannow/belajar-golang-restful-api/model/web" 7 | ) 8 | 9 | type AuthMiddleware struct { 10 | Handler http.Handler 11 | } 12 | 13 | func NewAuthMiddleware(handler http.Handler) *AuthMiddleware { 14 | return &AuthMiddleware{Handler: handler} 15 | } 16 | 17 | func (middleware *AuthMiddleware) ServeHTTP(writer http.ResponseWriter, request *http.Request) { 18 | if "RAHASIA" == request.Header.Get("X-API-Key") { 19 | // ok 20 | middleware.Handler.ServeHTTP(writer, request) 21 | } else { 22 | // error 23 | writer.Header().Set("Content-Type", "application/json") 24 | writer.WriteHeader(http.StatusUnauthorized) 25 | 26 | webResponse := web.WebResponse{ 27 | Code: http.StatusUnauthorized, 28 | Status: "UNAUTHORIZED", 29 | } 30 | 31 | helper.WriteToResponseBody(writer, webResponse) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /model/domain/category.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Category struct { 4 | Id int 5 | Name string 6 | } 7 | -------------------------------------------------------------------------------- /model/web/category_create_request.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type CategoryCreateRequest struct { 4 | Name string `validate:"required,min=1,max=100" json:"name"` 5 | } 6 | -------------------------------------------------------------------------------- /model/web/category_response.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type CategoryResponse struct { 4 | Id int `json:"id"` 5 | Name string `json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /model/web/category_update_request.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type CategoryUpdateRequest struct { 4 | Id int `validate:"required"` 5 | Name string `validate:"required,max=200,min=1" json:"name"` 6 | } 7 | -------------------------------------------------------------------------------- /model/web/web_response.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type WebResponse struct { 4 | Code int `json:"code"` 5 | Status string `json:"status"` 6 | Data interface{} `json:"data"` 7 | } 8 | -------------------------------------------------------------------------------- /repository/category_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "programmerzamannow/belajar-golang-restful-api/model/domain" 7 | ) 8 | 9 | type CategoryRepository interface { 10 | Save(ctx context.Context, tx *sql.Tx, category domain.Category) domain.Category 11 | Update(ctx context.Context, tx *sql.Tx, category domain.Category) domain.Category 12 | Delete(ctx context.Context, tx *sql.Tx, category domain.Category) 13 | FindById(ctx context.Context, tx *sql.Tx, categoryId int) (domain.Category, error) 14 | FindAll(ctx context.Context, tx *sql.Tx) []domain.Category 15 | } 16 | -------------------------------------------------------------------------------- /repository/category_repository_impl.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "programmerzamannow/belajar-golang-restful-api/helper" 8 | "programmerzamannow/belajar-golang-restful-api/model/domain" 9 | ) 10 | 11 | type CategoryRepositoryImpl struct { 12 | } 13 | 14 | func NewCategoryRepository() CategoryRepository { 15 | return &CategoryRepositoryImpl{} 16 | } 17 | 18 | func (repository *CategoryRepositoryImpl) Save(ctx context.Context, tx *sql.Tx, category domain.Category) domain.Category { 19 | SQL := "insert into category(name) values (?)" 20 | result, err := tx.ExecContext(ctx, SQL, category.Name) 21 | helper.PanicIfError(err) 22 | 23 | id, err := result.LastInsertId() 24 | helper.PanicIfError(err) 25 | 26 | category.Id = int(id) 27 | return category 28 | } 29 | 30 | func (repository *CategoryRepositoryImpl) Update(ctx context.Context, tx *sql.Tx, category domain.Category) domain.Category { 31 | SQL := "update category set name = ? where id = ?" 32 | _, err := tx.ExecContext(ctx, SQL, category.Name, category.Id) 33 | helper.PanicIfError(err) 34 | 35 | return category 36 | } 37 | 38 | func (repository *CategoryRepositoryImpl) Delete(ctx context.Context, tx *sql.Tx, category domain.Category) { 39 | SQL := "delete from category where id = ?" 40 | _, err := tx.ExecContext(ctx, SQL, category.Id) 41 | helper.PanicIfError(err) 42 | } 43 | 44 | func (repository *CategoryRepositoryImpl) FindById(ctx context.Context, tx *sql.Tx, categoryId int) (domain.Category, error) { 45 | SQL := "select id, name from category where id = ?" 46 | rows, err := tx.QueryContext(ctx, SQL, categoryId) 47 | helper.PanicIfError(err) 48 | defer rows.Close() 49 | 50 | category := domain.Category{} 51 | if rows.Next() { 52 | err := rows.Scan(&category.Id, &category.Name) 53 | helper.PanicIfError(err) 54 | return category, nil 55 | } else { 56 | return category, errors.New("category is not found") 57 | } 58 | } 59 | 60 | func (repository *CategoryRepositoryImpl) FindAll(ctx context.Context, tx *sql.Tx) []domain.Category { 61 | SQL := "select id, name from category" 62 | rows, err := tx.QueryContext(ctx, SQL) 63 | helper.PanicIfError(err) 64 | defer rows.Close() 65 | 66 | var categories []domain.Category 67 | for rows.Next() { 68 | category := domain.Category{} 69 | err := rows.Scan(&category.Id, &category.Name) 70 | helper.PanicIfError(err) 71 | categories = append(categories, category) 72 | } 73 | return categories 74 | } 75 | -------------------------------------------------------------------------------- /service/category_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "programmerzamannow/belajar-golang-restful-api/model/web" 6 | ) 7 | 8 | type CategoryService interface { 9 | Create(ctx context.Context, request web.CategoryCreateRequest) web.CategoryResponse 10 | Update(ctx context.Context, request web.CategoryUpdateRequest) web.CategoryResponse 11 | Delete(ctx context.Context, categoryId int) 12 | FindById(ctx context.Context, categoryId int) web.CategoryResponse 13 | FindAll(ctx context.Context) []web.CategoryResponse 14 | } 15 | -------------------------------------------------------------------------------- /service/category_service_impl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "github.com/go-playground/validator/v10" 7 | "programmerzamannow/belajar-golang-restful-api/exception" 8 | "programmerzamannow/belajar-golang-restful-api/helper" 9 | "programmerzamannow/belajar-golang-restful-api/model/domain" 10 | "programmerzamannow/belajar-golang-restful-api/model/web" 11 | "programmerzamannow/belajar-golang-restful-api/repository" 12 | ) 13 | 14 | type CategoryServiceImpl struct { 15 | CategoryRepository repository.CategoryRepository 16 | DB *sql.DB 17 | Validate *validator.Validate 18 | } 19 | 20 | func NewCategoryService(categoryRepository repository.CategoryRepository, DB *sql.DB, validate *validator.Validate) CategoryService { 21 | return &CategoryServiceImpl{ 22 | CategoryRepository: categoryRepository, 23 | DB: DB, 24 | Validate: validate, 25 | } 26 | } 27 | 28 | func (service *CategoryServiceImpl) Create(ctx context.Context, request web.CategoryCreateRequest) web.CategoryResponse { 29 | err := service.Validate.Struct(request) 30 | helper.PanicIfError(err) 31 | 32 | tx, err := service.DB.Begin() 33 | helper.PanicIfError(err) 34 | defer helper.CommitOrRollback(tx) 35 | 36 | category := domain.Category{ 37 | Name: request.Name, 38 | } 39 | 40 | category = service.CategoryRepository.Save(ctx, tx, category) 41 | 42 | return helper.ToCategoryResponse(category) 43 | } 44 | 45 | func (service *CategoryServiceImpl) Update(ctx context.Context, request web.CategoryUpdateRequest) web.CategoryResponse { 46 | err := service.Validate.Struct(request) 47 | helper.PanicIfError(err) 48 | 49 | tx, err := service.DB.Begin() 50 | helper.PanicIfError(err) 51 | defer helper.CommitOrRollback(tx) 52 | 53 | category, err := service.CategoryRepository.FindById(ctx, tx, request.Id) 54 | if err != nil { 55 | panic(exception.NewNotFoundError(err.Error())) 56 | } 57 | 58 | category.Name = request.Name 59 | 60 | category = service.CategoryRepository.Update(ctx, tx, category) 61 | 62 | return helper.ToCategoryResponse(category) 63 | } 64 | 65 | func (service *CategoryServiceImpl) Delete(ctx context.Context, categoryId int) { 66 | tx, err := service.DB.Begin() 67 | helper.PanicIfError(err) 68 | defer helper.CommitOrRollback(tx) 69 | 70 | category, err := service.CategoryRepository.FindById(ctx, tx, categoryId) 71 | if err != nil { 72 | panic(exception.NewNotFoundError(err.Error())) 73 | } 74 | 75 | service.CategoryRepository.Delete(ctx, tx, category) 76 | } 77 | 78 | func (service *CategoryServiceImpl) FindById(ctx context.Context, categoryId int) web.CategoryResponse { 79 | tx, err := service.DB.Begin() 80 | helper.PanicIfError(err) 81 | defer helper.CommitOrRollback(tx) 82 | 83 | category, err := service.CategoryRepository.FindById(ctx, tx, categoryId) 84 | if err != nil { 85 | panic(exception.NewNotFoundError(err.Error())) 86 | } 87 | 88 | return helper.ToCategoryResponse(category) 89 | } 90 | 91 | func (service *CategoryServiceImpl) FindAll(ctx context.Context) []web.CategoryResponse { 92 | tx, err := service.DB.Begin() 93 | helper.PanicIfError(err) 94 | defer helper.CommitOrRollback(tx) 95 | 96 | categories := service.CategoryRepository.FindAll(ctx, tx) 97 | 98 | return helper.ToCategoryResponses(categories) 99 | } 100 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | ### Get all categories 2 | GET http://localhost:3000/api/categories 3 | X-API-Key: RAHASIA 4 | Accept: application/json 5 | 6 | ### Create new category 7 | POST http://localhost:3000/api/categories 8 | X-API-Key: RAHASIA 9 | Accept: application/json 10 | Content-Type: application/json 11 | 12 | { 13 | "name" : "Food" 14 | } 15 | 16 | ### Get category by Id 17 | GET http://localhost:3000/api/categories/2 18 | X-API-Key: RAHASIA 19 | Accept: application/json 20 | 21 | ### Update category by id 22 | PUT http://localhost:3000/api/categories/2 23 | X-API-Key: RAHASIA 24 | Accept: application/json 25 | Content-Type: application/json 26 | 27 | { 28 | "name" : "Fashion" 29 | } 30 | 31 | ### Delete category by id 32 | DELETE http://localhost:3000/api/categories/2 33 | X-API-Key: RAHASIA 34 | Accept: application/json 35 | -------------------------------------------------------------------------------- /test/category_controller_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/go-playground/validator/v10" 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/stretchr/testify/assert" 11 | "io" 12 | "net/http" 13 | "net/http/httptest" 14 | "programmerzamannow/belajar-golang-restful-api/app" 15 | "programmerzamannow/belajar-golang-restful-api/controller" 16 | "programmerzamannow/belajar-golang-restful-api/helper" 17 | "programmerzamannow/belajar-golang-restful-api/middleware" 18 | "programmerzamannow/belajar-golang-restful-api/model/domain" 19 | "programmerzamannow/belajar-golang-restful-api/repository" 20 | "programmerzamannow/belajar-golang-restful-api/service" 21 | "strconv" 22 | "strings" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func setupTestDB() *sql.DB { 28 | db, err := sql.Open("mysql", "root@tcp(localhost:3306)/belajar_golang_restful_api_test") 29 | helper.PanicIfError(err) 30 | 31 | db.SetMaxIdleConns(5) 32 | db.SetMaxOpenConns(20) 33 | db.SetConnMaxLifetime(60 * time.Minute) 34 | db.SetConnMaxIdleTime(10 * time.Minute) 35 | 36 | return db 37 | } 38 | 39 | func setupRouter(db *sql.DB) http.Handler { 40 | validate := validator.New() 41 | categoryRepository := repository.NewCategoryRepository() 42 | categoryService := service.NewCategoryService(categoryRepository, db, validate) 43 | categoryController := controller.NewCategoryController(categoryService) 44 | router := app.NewRouter(categoryController) 45 | 46 | return middleware.NewAuthMiddleware(router) 47 | } 48 | 49 | func truncateCategory(db *sql.DB) { 50 | db.Exec("TRUNCATE category") 51 | } 52 | 53 | func TestCreateCategorySuccess(t *testing.T) { 54 | db := setupTestDB() 55 | truncateCategory(db) 56 | router := setupRouter(db) 57 | 58 | requestBody := strings.NewReader(`{"name" : "Gadget"}`) 59 | request := httptest.NewRequest(http.MethodPost, "http://localhost:3000/api/categories", requestBody) 60 | request.Header.Add("Content-Type", "application/json") 61 | request.Header.Add("X-API-Key", "RAHASIA") 62 | 63 | recorder := httptest.NewRecorder() 64 | 65 | router.ServeHTTP(recorder, request) 66 | 67 | response := recorder.Result() 68 | assert.Equal(t, 200, response.StatusCode) 69 | 70 | body, _ := io.ReadAll(response.Body) 71 | var responseBody map[string]interface{} 72 | json.Unmarshal(body, &responseBody) 73 | 74 | assert.Equal(t, 200, int(responseBody["code"].(float64))) 75 | assert.Equal(t, "OK", responseBody["status"]) 76 | assert.Equal(t, "Gadget", responseBody["data"].(map[string]interface{})["name"]) 77 | } 78 | 79 | func TestCreateCategoryFailed(t *testing.T) { 80 | db := setupTestDB() 81 | truncateCategory(db) 82 | router := setupRouter(db) 83 | 84 | requestBody := strings.NewReader(`{"name" : ""}`) 85 | request := httptest.NewRequest(http.MethodPost, "http://localhost:3000/api/categories", requestBody) 86 | request.Header.Add("Content-Type", "application/json") 87 | request.Header.Add("X-API-Key", "RAHASIA") 88 | 89 | recorder := httptest.NewRecorder() 90 | 91 | router.ServeHTTP(recorder, request) 92 | 93 | response := recorder.Result() 94 | assert.Equal(t, 400, response.StatusCode) 95 | 96 | body, _ := io.ReadAll(response.Body) 97 | var responseBody map[string]interface{} 98 | json.Unmarshal(body, &responseBody) 99 | 100 | assert.Equal(t, 400, int(responseBody["code"].(float64))) 101 | assert.Equal(t, "BAD REQUEST", responseBody["status"]) 102 | } 103 | 104 | func TestUpdateCategorySuccess(t *testing.T) { 105 | db := setupTestDB() 106 | truncateCategory(db) 107 | 108 | tx, _ := db.Begin() 109 | categoryRepository := repository.NewCategoryRepository() 110 | category := categoryRepository.Save(context.Background(), tx, domain.Category{ 111 | Name: "Gadget", 112 | }) 113 | tx.Commit() 114 | 115 | router := setupRouter(db) 116 | 117 | requestBody := strings.NewReader(`{"name" : "Gadget"}`) 118 | request := httptest.NewRequest(http.MethodPut, "http://localhost:3000/api/categories/"+strconv.Itoa(category.Id), requestBody) 119 | request.Header.Add("Content-Type", "application/json") 120 | request.Header.Add("X-API-Key", "RAHASIA") 121 | 122 | recorder := httptest.NewRecorder() 123 | 124 | router.ServeHTTP(recorder, request) 125 | 126 | response := recorder.Result() 127 | assert.Equal(t, 200, response.StatusCode) 128 | 129 | body, _ := io.ReadAll(response.Body) 130 | var responseBody map[string]interface{} 131 | json.Unmarshal(body, &responseBody) 132 | 133 | assert.Equal(t, 200, int(responseBody["code"].(float64))) 134 | assert.Equal(t, "OK", responseBody["status"]) 135 | assert.Equal(t, category.Id, int(responseBody["data"].(map[string]interface{})["id"].(float64))) 136 | assert.Equal(t, "Gadget", responseBody["data"].(map[string]interface{})["name"]) 137 | } 138 | 139 | func TestUpdateCategoryFailed(t *testing.T) { 140 | db := setupTestDB() 141 | truncateCategory(db) 142 | 143 | tx, _ := db.Begin() 144 | categoryRepository := repository.NewCategoryRepository() 145 | category := categoryRepository.Save(context.Background(), tx, domain.Category{ 146 | Name: "Gadget", 147 | }) 148 | tx.Commit() 149 | 150 | router := setupRouter(db) 151 | 152 | requestBody := strings.NewReader(`{"name" : ""}`) 153 | request := httptest.NewRequest(http.MethodPut, "http://localhost:3000/api/categories/"+strconv.Itoa(category.Id), requestBody) 154 | request.Header.Add("Content-Type", "application/json") 155 | request.Header.Add("X-API-Key", "RAHASIA") 156 | 157 | recorder := httptest.NewRecorder() 158 | 159 | router.ServeHTTP(recorder, request) 160 | 161 | response := recorder.Result() 162 | assert.Equal(t, 400, response.StatusCode) 163 | 164 | body, _ := io.ReadAll(response.Body) 165 | var responseBody map[string]interface{} 166 | json.Unmarshal(body, &responseBody) 167 | 168 | assert.Equal(t, 400, int(responseBody["code"].(float64))) 169 | assert.Equal(t, "BAD REQUEST", responseBody["status"]) 170 | } 171 | 172 | func TestGetCategorySuccess(t *testing.T) { 173 | db := setupTestDB() 174 | truncateCategory(db) 175 | 176 | tx, _ := db.Begin() 177 | categoryRepository := repository.NewCategoryRepository() 178 | category := categoryRepository.Save(context.Background(), tx, domain.Category{ 179 | Name: "Gadget", 180 | }) 181 | tx.Commit() 182 | 183 | router := setupRouter(db) 184 | 185 | request := httptest.NewRequest(http.MethodGet, "http://localhost:3000/api/categories/"+strconv.Itoa(category.Id), nil) 186 | request.Header.Add("X-API-Key", "RAHASIA") 187 | 188 | recorder := httptest.NewRecorder() 189 | 190 | router.ServeHTTP(recorder, request) 191 | 192 | response := recorder.Result() 193 | assert.Equal(t, 200, response.StatusCode) 194 | 195 | body, _ := io.ReadAll(response.Body) 196 | var responseBody map[string]interface{} 197 | json.Unmarshal(body, &responseBody) 198 | 199 | assert.Equal(t, 200, int(responseBody["code"].(float64))) 200 | assert.Equal(t, "OK", responseBody["status"]) 201 | assert.Equal(t, category.Id, int(responseBody["data"].(map[string]interface{})["id"].(float64))) 202 | assert.Equal(t, category.Name, responseBody["data"].(map[string]interface{})["name"]) 203 | } 204 | 205 | func TestGetCategoryFailed(t *testing.T) { 206 | db := setupTestDB() 207 | truncateCategory(db) 208 | router := setupRouter(db) 209 | 210 | request := httptest.NewRequest(http.MethodGet, "http://localhost:3000/api/categories/404", nil) 211 | request.Header.Add("X-API-Key", "RAHASIA") 212 | 213 | recorder := httptest.NewRecorder() 214 | 215 | router.ServeHTTP(recorder, request) 216 | 217 | response := recorder.Result() 218 | assert.Equal(t, 404, response.StatusCode) 219 | 220 | body, _ := io.ReadAll(response.Body) 221 | var responseBody map[string]interface{} 222 | json.Unmarshal(body, &responseBody) 223 | 224 | assert.Equal(t, 404, int(responseBody["code"].(float64))) 225 | assert.Equal(t, "NOT FOUND", responseBody["status"]) 226 | } 227 | 228 | func TestDeleteCategorySuccess(t *testing.T) { 229 | db := setupTestDB() 230 | truncateCategory(db) 231 | 232 | tx, _ := db.Begin() 233 | categoryRepository := repository.NewCategoryRepository() 234 | category := categoryRepository.Save(context.Background(), tx, domain.Category{ 235 | Name: "Gadget", 236 | }) 237 | tx.Commit() 238 | 239 | router := setupRouter(db) 240 | 241 | request := httptest.NewRequest(http.MethodDelete, "http://localhost:3000/api/categories/"+strconv.Itoa(category.Id), nil) 242 | request.Header.Add("Content-Type", "application/json") 243 | request.Header.Add("X-API-Key", "RAHASIA") 244 | 245 | recorder := httptest.NewRecorder() 246 | 247 | router.ServeHTTP(recorder, request) 248 | 249 | response := recorder.Result() 250 | assert.Equal(t, 200, response.StatusCode) 251 | 252 | body, _ := io.ReadAll(response.Body) 253 | var responseBody map[string]interface{} 254 | json.Unmarshal(body, &responseBody) 255 | 256 | assert.Equal(t, 200, int(responseBody["code"].(float64))) 257 | assert.Equal(t, "OK", responseBody["status"]) 258 | } 259 | 260 | func TestDeleteCategoryFailed(t *testing.T) { 261 | db := setupTestDB() 262 | truncateCategory(db) 263 | router := setupRouter(db) 264 | 265 | request := httptest.NewRequest(http.MethodDelete, "http://localhost:3000/api/categories/404", nil) 266 | request.Header.Add("Content-Type", "application/json") 267 | request.Header.Add("X-API-Key", "RAHASIA") 268 | 269 | recorder := httptest.NewRecorder() 270 | 271 | router.ServeHTTP(recorder, request) 272 | 273 | response := recorder.Result() 274 | assert.Equal(t, 404, response.StatusCode) 275 | 276 | body, _ := io.ReadAll(response.Body) 277 | var responseBody map[string]interface{} 278 | json.Unmarshal(body, &responseBody) 279 | 280 | assert.Equal(t, 404, int(responseBody["code"].(float64))) 281 | assert.Equal(t, "NOT FOUND", responseBody["status"]) 282 | } 283 | 284 | func TestListCategoriesSuccess(t *testing.T) { 285 | db := setupTestDB() 286 | truncateCategory(db) 287 | 288 | tx, _ := db.Begin() 289 | categoryRepository := repository.NewCategoryRepository() 290 | category1 := categoryRepository.Save(context.Background(), tx, domain.Category{ 291 | Name: "Gadget", 292 | }) 293 | category2 := categoryRepository.Save(context.Background(), tx, domain.Category{ 294 | Name: "Computer", 295 | }) 296 | tx.Commit() 297 | 298 | router := setupRouter(db) 299 | 300 | request := httptest.NewRequest(http.MethodGet, "http://localhost:3000/api/categories", nil) 301 | request.Header.Add("X-API-Key", "RAHASIA") 302 | 303 | recorder := httptest.NewRecorder() 304 | 305 | router.ServeHTTP(recorder, request) 306 | 307 | response := recorder.Result() 308 | assert.Equal(t, 200, response.StatusCode) 309 | 310 | body, _ := io.ReadAll(response.Body) 311 | var responseBody map[string]interface{} 312 | json.Unmarshal(body, &responseBody) 313 | 314 | assert.Equal(t, 200, int(responseBody["code"].(float64))) 315 | assert.Equal(t, "OK", responseBody["status"]) 316 | 317 | fmt.Println(responseBody) 318 | 319 | var categories = responseBody["data"].([]interface{}) 320 | 321 | categoryResponse1 := categories[0].(map[string]interface{}) 322 | categoryResponse2 := categories[1].(map[string]interface{}) 323 | 324 | assert.Equal(t, category1.Id, int(categoryResponse1["id"].(float64))) 325 | assert.Equal(t, category1.Name, categoryResponse1["name"]) 326 | 327 | assert.Equal(t, category2.Id, int(categoryResponse2["id"].(float64))) 328 | assert.Equal(t, category2.Name, categoryResponse2["name"]) 329 | } 330 | 331 | func TestUnauthorized(t *testing.T) { 332 | db := setupTestDB() 333 | truncateCategory(db) 334 | router := setupRouter(db) 335 | 336 | request := httptest.NewRequest(http.MethodGet, "http://localhost:3000/api/categories", nil) 337 | request.Header.Add("X-API-Key", "SALAH") 338 | 339 | recorder := httptest.NewRecorder() 340 | 341 | router.ServeHTTP(recorder, request) 342 | 343 | response := recorder.Result() 344 | assert.Equal(t, 401, response.StatusCode) 345 | 346 | body, _ := io.ReadAll(response.Body) 347 | var responseBody map[string]interface{} 348 | json.Unmarshal(body, &responseBody) 349 | 350 | assert.Equal(t, 401, int(responseBody["code"].(float64))) 351 | assert.Equal(t, "UNAUTHORIZED", responseBody["status"]) 352 | } 353 | --------------------------------------------------------------------------------