├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── README.md ├── api └── api.go ├── config ├── config.go └── config.yaml ├── main.go ├── model ├── category.go └── product.go ├── service ├── category_service.go └── product_service.go ├── spec └── api.json ├── store ├── 1_0_init_store.sql ├── store.go └── store.yaml └── test ├── api_test.go ├── category_service_test.go ├── config_test.go ├── mock ├── category_service_mock.go ├── product_service_mock.go └── store_mock.go ├── product_service_test.go └── store_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/BurntSushi/toml" 6 | packages = ["."] 7 | revision = "b26d9c308763d68093482582cea63d69be07a0f0" 8 | version = "v0.3.0" 9 | 10 | [[projects]] 11 | name = "github.com/davecgh/go-spew" 12 | packages = ["spew"] 13 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 14 | version = "v1.1.0" 15 | 16 | [[projects]] 17 | name = "github.com/dgrijalva/jwt-go" 18 | packages = ["."] 19 | revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29" 20 | version = "v3.1.0" 21 | 22 | [[projects]] 23 | name = "github.com/go-playground/locales" 24 | packages = [ 25 | ".", 26 | "currency" 27 | ] 28 | revision = "e4cbcb5d0652150d40ad0646651076b6bd2be4f6" 29 | version = "v0.11.2" 30 | 31 | [[projects]] 32 | name = "github.com/go-playground/universal-translator" 33 | packages = ["."] 34 | revision = "b32fa301c9fe55953584134cb6853a13c87ec0a1" 35 | version = "v0.16.0" 36 | 37 | [[projects]] 38 | name = "github.com/golang/mock" 39 | packages = ["gomock"] 40 | revision = "13f360950a79f5864a972c786a10a50e44b69541" 41 | version = "v1.0.0" 42 | 43 | [[projects]] 44 | branch = "master" 45 | name = "github.com/jinzhu/configor" 46 | packages = ["."] 47 | revision = "6ecfe629230f24c2e92c7894c6ab21b83b757a39" 48 | 49 | [[projects]] 50 | name = "github.com/labstack/echo" 51 | packages = [ 52 | ".", 53 | "middleware" 54 | ] 55 | revision = "b338075a0fc6e1a0683dbf03d09b4957a289e26f" 56 | version = "3.2.6" 57 | 58 | [[projects]] 59 | name = "github.com/labstack/gommon" 60 | packages = [ 61 | "bytes", 62 | "color", 63 | "log", 64 | "random" 65 | ] 66 | revision = "57409ada9da0f2afad6664c49502f8c50fbd8476" 67 | version = "0.2.3" 68 | 69 | [[projects]] 70 | branch = "master" 71 | name = "github.com/lib/pq" 72 | packages = [ 73 | ".", 74 | "oid" 75 | ] 76 | revision = "88edab0803230a3898347e77b474f8c1820a1f20" 77 | 78 | [[projects]] 79 | name = "github.com/mattn/go-colorable" 80 | packages = ["."] 81 | revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" 82 | version = "v0.0.9" 83 | 84 | [[projects]] 85 | name = "github.com/mattn/go-isatty" 86 | packages = ["."] 87 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 88 | version = "v0.0.3" 89 | 90 | [[projects]] 91 | name = "github.com/pmezard/go-difflib" 92 | packages = ["difflib"] 93 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 94 | version = "v1.0.0" 95 | 96 | [[projects]] 97 | name = "github.com/sirupsen/logrus" 98 | packages = ["."] 99 | revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" 100 | version = "v1.0.4" 101 | 102 | [[projects]] 103 | name = "github.com/stretchr/testify" 104 | packages = ["assert"] 105 | revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" 106 | version = "v1.2.1" 107 | 108 | [[projects]] 109 | branch = "master" 110 | name = "github.com/valyala/bytebufferpool" 111 | packages = ["."] 112 | revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" 113 | 114 | [[projects]] 115 | branch = "master" 116 | name = "github.com/valyala/fasttemplate" 117 | packages = ["."] 118 | revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0" 119 | 120 | [[projects]] 121 | branch = "master" 122 | name = "golang.org/x/crypto" 123 | packages = [ 124 | "acme", 125 | "acme/autocert", 126 | "ssh/terminal" 127 | ] 128 | revision = "91a49db82a88618983a78a06c1cbd4e00ab749ab" 129 | 130 | [[projects]] 131 | branch = "master" 132 | name = "golang.org/x/sys" 133 | packages = [ 134 | "unix", 135 | "windows" 136 | ] 137 | revision = "dd2ff4accc098aceecb86b36eaa7829b2a17b1c9" 138 | 139 | [[projects]] 140 | name = "gopkg.in/go-playground/validator.v9" 141 | packages = ["."] 142 | revision = "150fe5b6a4ccc9cd6ffdbf5d184b67fbea75efcc" 143 | version = "v9.11.0" 144 | 145 | [[projects]] 146 | name = "gopkg.in/yaml.v2" 147 | packages = ["."] 148 | revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" 149 | version = "v2.1.1" 150 | 151 | [solve-meta] 152 | analyzer-name = "dep" 153 | analyzer-version = 1 154 | inputs-digest = "d5f937a0c29718ba2108a745bde944f7a7a9bf0a663f41cda518952144a8a6e9" 155 | solver-name = "gps-cdcl" 156 | solver-version = 1 157 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | name = "github.com/golang/mock" 30 | version = "1.0.0" 31 | 32 | [[constraint]] 33 | branch = "master" 34 | name = "github.com/jinzhu/configor" 35 | 36 | [[constraint]] 37 | name = "github.com/labstack/echo" 38 | version = "3.2.6" 39 | 40 | [[constraint]] 41 | name = "github.com/labstack/gommon" 42 | version = "0.2.3" 43 | 44 | [[constraint]] 45 | branch = "master" 46 | name = "github.com/lib/pq" 47 | 48 | [[constraint]] 49 | name = "github.com/sirupsen/logrus" 50 | version = "1.0.4" 51 | 52 | [[constraint]] 53 | name = "github.com/stretchr/testify" 54 | version = "1.2.1" 55 | 56 | [[constraint]] 57 | name = "gopkg.in/go-playground/validator.v9" 58 | version = "9.11.0" 59 | 60 | [prune] 61 | go-tests = true 62 | unused-packages = true 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | if ! hash dep 2>/dev/null; then go get -u github.com/golang/dep/cmd/dep; fi 3 | if ! hash sql-migrate 2>/dev/null; then go get -v github.com/rubenv/sql-migrate/...; fi 4 | if ! hash mockgen 2>/dev/null; then go get github.com/golang/mock/mockgen; fi 5 | if ! hash swagger 2>/dev/null; then go get -u github.com/go-swagger/go-swagger/cmd/swagger; ficlear 6 | dep ensure 7 | 8 | run: db-run db-migrate 9 | dep ensure 10 | go run main.go -c=config/config.yaml 11 | 12 | test: db-run db-migrate 13 | dep ensure 14 | go test -v ./test/ 15 | $(MAKE) db-stop 16 | 17 | bench: db-run db-migrate 18 | go test -bench=. -benchmem ./test/ 19 | $(MAKE) db-stop 20 | 21 | spec: 22 | swagger generate spec -m -o ./spec/api.json 23 | 24 | spec-ui: spec 25 | swagger serve -F=swagger ./spec/api.json 26 | 27 | db-run: db-stop 28 | docker run --rm -d -p 5433:5432 --name echo-rest-api-db postgres 29 | sleep 2 30 | 31 | db-stop: 32 | docker container stop echo-rest-api-db >/dev/null 2>&1 || exit 0 33 | 34 | db-migrate: 35 | sql-migrate up -config=store/store.yaml 36 | 37 | .PHONY: run test db-run db-stop db-migrate spec spec-ui -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # echo-rest-api 2 | В данном проекте показан пример создания REST API на Go с использованием Echo framework. 3 | 4 | ### В проекте использованы 5 | #### HTTP 6 | - `github.com/labstack/echo` - для создания rest сервиса; использованы: _router_, _data binding_ и _data rendering_, _logger middleware_ 7 | 8 | #### Валидация 9 | - `gopkg.in/go-playground/validator.v9` - для _data validation_ 10 | 11 | #### База данных 12 | - `database/sql` - для работы с запросами и транзакционностью 13 | - `github.com/lib/pq` - в качестве СУБД использовалась postgresql 14 | - `github.com/rubenv/sql-migrate` - миграционная тулза для sql 15 | - `docker postgres image` - для запуска postgresql 16 | #### Конфигурация 17 | - `flag` - для передачи параметров при запуске 18 | - `github.com/jinzhu/configor` - для загрузки конфигурации из yaml 19 | 20 | #### Логгирование 21 | - `github.com/sirupsen/logrus` - для логов приложения 22 | 23 | #### Тестирование 24 | - `testing` - для написания тестов 25 | - `github.com/golang/mock/gomock` - для генерации моков 26 | - `github.com/stretchr/testify/assert` - исключительно для _assert_ в тестах 27 | 28 | #### Зависимости 29 | `github.com/golang/dep` - менеджер зависимостей 30 | 31 | ### Для запуска 32 | - `make init` - установит необходимые тулзы (dep, sql-migrate, mockgen), затем через `dep` установит зависимости проекта 33 | - `make run` - запустит приложение, при этом запустив docker контейнер с БД 34 | - `make test` - запустит приложение, при этом запустив docker контейнер с БД 35 | - `make spec` - сгенерирует open-api спецификацию в _spec/api.json_. 36 | После запуска сервис отдает спеку как статику по пути _/spec/api.json_, можно также выполнить `swagger serve -F=swagger http://localhost:8081/spec/api.json`, что бы отобразить в html виде c запущенного сервиса. 37 | - `spec-ui` - откроет спеку в html 38 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // Echo-REST-API. 2 | // Пример создания REST API на Echo framework 3 | // Version: 0.0.1 4 | // Schemes: http 5 | // Host: localhost 6 | // BasePath: /api 7 | // Consumes: 8 | // - application/json 9 | // Produces: 10 | // - application/json 11 | // Contact: uchonyy@gmail.com 12 | // swagger:meta 13 | package api 14 | 15 | import ( 16 | "database/sql" 17 | "echo-rest-api/config" 18 | "echo-rest-api/model" 19 | "echo-rest-api/service" 20 | "fmt" 21 | "github.com/labstack/echo" 22 | "github.com/labstack/echo/middleware" 23 | "github.com/labstack/gommon/log" 24 | "gopkg.in/go-playground/validator.v9" 25 | "net/http" 26 | "strconv" 27 | ) 28 | 29 | type Api struct { 30 | Http *echo.Echo 31 | conf *config.Config 32 | cs service.CategoryService 33 | ps service.ProductService 34 | apiInfo ApiInfo 35 | validate *validator.Validate 36 | } 37 | 38 | type ApiInfo struct { 39 | Address string 40 | MW []string 41 | Routs []string 42 | } 43 | 44 | func NewApi(conf *config.Config, cs service.CategoryService, ps service.ProductService) *Api { 45 | api := &Api{} 46 | api.validate = validator.New() 47 | api.conf = conf 48 | api.cs = cs 49 | api.ps = ps 50 | api.Http = echo.New() 51 | api.Http.Logger.SetLevel(log.Lvl(conf.LogLevel)) 52 | api.apiInfo.Address = ":" + strconv.Itoa(api.conf.Api.HttpPort) 53 | api.Http.HideBanner = true 54 | api.Http.Pre(middleware.RemoveTrailingSlash()) 55 | if conf.Api.Logging { 56 | api.Http.Use(middleware.Logger()) 57 | api.apiInfo.MW = append(api.apiInfo.MW, "Logger") 58 | } 59 | api.Http.GET("/", api.index) 60 | api.Http.Static("/spec", "spec") 61 | api.Http.GET("/api/categories", api.getCategories) 62 | api.Http.GET("/api/categories/:id", api.getCategory) 63 | api.Http.POST("/api/categories", api.createCategory) 64 | api.Http.PUT("/api/categories/:id", api.updateCategory) 65 | api.Http.DELETE("/api/categories/:id", api.deleteCategory) 66 | 67 | api.Http.GET("/api/products", api.getProducts) 68 | api.Http.GET("/api/products/:id", api.getProduct) 69 | api.Http.POST("/api/products", api.createProduct) 70 | api.Http.PUT("/api/products/:id", api.updateProduct) 71 | api.Http.DELETE("/api/products/:id", api.deleteProduct) 72 | for _, r := range api.Http.Routes() { 73 | api.apiInfo.Routs = append(api.apiInfo.Routs, fmt.Sprintf("%s %s", r.Path, r.Method)) 74 | } 75 | return api 76 | } 77 | 78 | // Запустить api 79 | func (api *Api) Start() error { 80 | return api.Http.Start(":" + strconv.Itoa(api.conf.Api.HttpPort)) 81 | } 82 | 83 | // Инфо об api 84 | func (api *Api) GetApiInfo() ApiInfo { 85 | return api.apiInfo 86 | } 87 | 88 | func (api *Api) index(c echo.Context) error { 89 | return c.Redirect(http.StatusMovedPermanently, "/spec/api.json") 90 | } 91 | 92 | func (api *Api) spec(c echo.Context) error { 93 | return c.Inline("spec/api.json", "api.json") 94 | } 95 | 96 | // swagger:operation GET /categories/{id} getCategory 97 | // --- 98 | // description: Получить категорию 99 | // parameters: 100 | // - name: id 101 | // in: path 102 | // description: id необходимой категории 103 | // required: true 104 | // type: int 105 | // responses: 106 | // '200': 107 | // schema: 108 | // $ref: '#/definitions/Category' 109 | // '400': 110 | // description: Bad request param `id` 111 | // '404': 112 | // description: Category `id`= not found 113 | // 114 | func (api *Api) getCategory(c echo.Context) error { 115 | id, err := strconv.Atoi(c.Param("id")) 116 | if err != nil { 117 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param `id`") 118 | } 119 | cat, err := api.cs.GetCategory(id) 120 | if err != nil { 121 | return err 122 | } 123 | if cat == nil { 124 | return echo.NewHTTPError(http.StatusNotFound, "Category `id` = ", id, " not found") 125 | } 126 | return c.JSON(http.StatusOK, cat) 127 | } 128 | 129 | // swagger:operation GET /categories getCategories 130 | // --- 131 | // description: Получить список категорий 132 | // responses: 133 | // '200': 134 | // schema: 135 | // type: array 136 | // items: 137 | // $ref: '#/definitions/Category' 138 | // 139 | func (api *Api) getCategories(c echo.Context) error { 140 | cats, err := api.cs.GetCategories() 141 | if err != nil { 142 | return err 143 | } 144 | if cats == nil { 145 | cats = []*model.Category{} 146 | } 147 | return c.JSON(http.StatusOK, cats) 148 | } 149 | 150 | // swagger:operation POST /categories createCategory 151 | // --- 152 | // description: Создать категорию 153 | // parameters: 154 | // - name: category 155 | // in: body 156 | // description: новая категория 157 | // required: true 158 | // schema: 159 | // $ref: '#/definitions/Category' 160 | // responses: 161 | // '201': 162 | // schema: 163 | // $ref: '#/definitions/Category' 164 | // '400': 165 | // description: Bad request param 166 | // 167 | func (api *Api) createCategory(c echo.Context) error { 168 | req := &model.Category{} 169 | if err := c.Bind(req); err != nil { 170 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 171 | return err 172 | } 173 | if err := api.validate.Struct(req); err != nil { 174 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 175 | } 176 | res, err := api.cs.CreateCategory(req) 177 | if err != nil { 178 | return err 179 | } 180 | return c.JSON(http.StatusCreated, map[string]*int{"id": res}) 181 | } 182 | 183 | // swagger:operation PUT /categories/{id} updateCategory 184 | // --- 185 | // description: Обновить категорию 186 | // parameters: 187 | // - name: id 188 | // in: path 189 | // description: id необходимой категории 190 | // required: true 191 | // type: int 192 | // - name: category 193 | // in: body 194 | // description: измененная категория 195 | // required: true 196 | // schema: 197 | // $ref: '#/definitions/Category' 198 | // responses: 199 | // '204': 200 | // description: Категория обновлена 201 | // '400': 202 | // description: Bad request param 203 | // 204 | func (api *Api) updateCategory(c echo.Context) error { 205 | id, err := strconv.Atoi(c.Param("id")) 206 | if err != nil { 207 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param `id`") 208 | } 209 | req := &model.Category{} 210 | if err := c.Bind(req); err != nil { 211 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 212 | } 213 | if err := api.validate.Struct(req); err != nil { 214 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 215 | } 216 | req.Id = id 217 | if err = api.cs.UpdateCategory(req); err != nil { 218 | if err != sql.ErrNoRows { 219 | return err 220 | } else { 221 | return echo.NewHTTPError(http.StatusNotFound, "Category `id` = ", id, " not found") 222 | } 223 | 224 | } 225 | return c.NoContent(http.StatusNoContent) 226 | } 227 | 228 | // swagger:operation DELETE /categories/{id} deleteCategory 229 | // --- 230 | // description: Удалить категорию 231 | // parameters: 232 | // - name: id 233 | // in: path 234 | // description: id необходимой категории 235 | // required: true 236 | // type: int 237 | // responses: 238 | // '204': 239 | // description: Категория удалена 240 | // '400': 241 | // description: Bad request param `id` 242 | // '404': 243 | // description: Category `id`= not found 244 | // 245 | func (api *Api) deleteCategory(c echo.Context) error { 246 | id, err := strconv.Atoi(c.Param("id")) 247 | if err != nil { 248 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param `id`") 249 | } 250 | if err = api.cs.DeleteCategory(id); err != nil { 251 | if err != sql.ErrNoRows { 252 | return err 253 | } else { 254 | return echo.NewHTTPError(http.StatusNotFound, "Category `id` = ", id, " not found") 255 | } 256 | } 257 | 258 | return c.NoContent(http.StatusNoContent) 259 | } 260 | 261 | // swagger:operation GET /products/{id} getProduct 262 | // --- 263 | // description: Получить продукт 264 | // parameters: 265 | // - name: id 266 | // in: path 267 | // description: id необходимого продукта 268 | // required: true 269 | // type: int 270 | // responses: 271 | // '200': 272 | // schema: 273 | // $ref: '#/definitions/Product' 274 | // '400': 275 | // description: Bad request param `id` 276 | // '404': 277 | // description: Product `id`= not found 278 | // 279 | func (api *Api) getProduct(c echo.Context) error { 280 | id, err := strconv.Atoi(c.Param("id")) 281 | if err != nil { 282 | return echo.NewHTTPError(http.StatusNotFound, "Product `id` = ", id, " not found") 283 | } 284 | prod, err := api.ps.GetProduct(id) 285 | if err != nil { 286 | return err 287 | } 288 | if prod == nil { 289 | return c.String(http.StatusNotFound, "") 290 | } 291 | return c.JSON(http.StatusOK, prod) 292 | } 293 | 294 | // swagger:operation GET /products getProducts 295 | // --- 296 | // description: Получить список продуктов 297 | // parameters: 298 | // - name: category 299 | // in: path 300 | // description: id категории по которой выбрать продукты 301 | // required: false 302 | // type: int 303 | // responses: 304 | // '200': 305 | // schema: 306 | // type: array 307 | // items: 308 | // $ref: '#/definitions/Product' 309 | // 310 | func (api *Api) getProducts(c echo.Context) error { 311 | var products []*model.Product 312 | var err error 313 | category, err := strconv.Atoi(c.QueryParam("category")) 314 | if err != nil { 315 | products, err = api.ps.GetProducts(nil) 316 | } else { 317 | products, err = api.ps.GetProducts(&category) 318 | } 319 | if err != nil { 320 | return err 321 | } 322 | if products == nil { 323 | products = []*model.Product{} 324 | } 325 | return c.JSON(http.StatusOK, products) 326 | } 327 | 328 | // swagger:operation POST /products createProduct 329 | // --- 330 | // description: Создать продукт 331 | // parameters: 332 | // - name: product 333 | // in: body 334 | // description: новый продукт 335 | // required: true 336 | // schema: 337 | // $ref: '#/definitions/Product' 338 | // responses: 339 | // '201': 340 | // schema: 341 | // $ref: '#/definitions/Product' 342 | // '400': 343 | // description: Bad request param 344 | // 345 | func (api *Api) createProduct(c echo.Context) error { 346 | req := &model.Product{} 347 | if err := c.Bind(req); err != nil { 348 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 349 | } 350 | if err := api.validate.Struct(req); err != nil { 351 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 352 | } 353 | res, err := api.ps.CreateProduct(req) 354 | if err != nil { 355 | return err 356 | } 357 | return c.JSON(http.StatusCreated, map[string]*int{"id": res}) 358 | } 359 | 360 | // swagger:operation PUT /products/{id} updateProduct 361 | // --- 362 | // description: Обновить продукт 363 | // parameters: 364 | // - name: id 365 | // in: path 366 | // description: id необходимого продукта 367 | // required: true 368 | // type: int 369 | // - name: product 370 | // in: body 371 | // description: измененный продукт 372 | // required: true 373 | // schema: 374 | // $ref: '#/definitions/Product' 375 | // responses: 376 | // '204': 377 | // description: Продукт обновлена 378 | // '400': 379 | // description: Bad request param 380 | // 381 | func (api *Api) updateProduct(c echo.Context) error { 382 | id, err := strconv.Atoi(c.Param("id")) 383 | if err != nil { 384 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param `id`") 385 | } 386 | req := &model.Product{} 387 | if err := c.Bind(req); err != nil { 388 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 389 | } 390 | if err := api.validate.Struct(req); err != nil { 391 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param: "+err.Error()) 392 | } 393 | req.Id = id 394 | if err = api.ps.UpdateProduct(req); err != nil { 395 | if err != sql.ErrNoRows { 396 | return err 397 | } else { 398 | return echo.NewHTTPError(http.StatusNotFound, "Category `id` = ", id, " not found") 399 | } 400 | } 401 | return c.NoContent(http.StatusNoContent) 402 | } 403 | 404 | // swagger:operation DELETE /products/{id} deleteProduct 405 | // --- 406 | // description: Удалить продукт 407 | // parameters: 408 | // - name: id 409 | // in: path 410 | // description: id необходимого продукта 411 | // required: true 412 | // type: int 413 | // responses: 414 | // '204': 415 | // description: Категория удалена 416 | // '400': 417 | // description: Bad request param `id` 418 | // '404': 419 | // description: Product `id`= not found 420 | // 421 | func (api *Api) deleteProduct(c echo.Context) error { 422 | id, err := strconv.Atoi(c.Param("id")) 423 | if err != nil { 424 | return echo.NewHTTPError(http.StatusBadRequest, "Bad request param `id`") 425 | } 426 | if err = api.ps.DeleteProduct(id); err != nil { 427 | if err != sql.ErrNoRows { 428 | return err 429 | } else { 430 | return echo.NewHTTPError(http.StatusNotFound, "Product `id` = ", id, " not found") 431 | } 432 | } 433 | 434 | return c.NoContent(http.StatusNoContent) 435 | } 436 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/jinzhu/configor" 5 | ) 6 | 7 | // Структура конфигурации приложения 8 | type Config struct { 9 | ConfigFile string 10 | LogLevel uint32 `default:"4"` 11 | Api struct { 12 | HttpPort int `default:"8080"` 13 | Logging bool `default:"false"` 14 | } 15 | Store struct { 16 | Host string `required:"true"` 17 | Port int `required:"true"` 18 | User string `required:"true"` 19 | Password string `required:"true"` 20 | Dbname string `required:"true"` 21 | } 22 | } 23 | 24 | // Создать конфигурацию из файла configFile 25 | func NewConfig(configFile string) (*Config, error) { 26 | config := &Config{ConfigFile: configFile} 27 | if err := configor.Load(config, configFile); err != nil { 28 | return nil, err 29 | } 30 | return config, nil 31 | } 32 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | loglevel: 0 2 | api: 3 | httpport: 8081 4 | logging: true 5 | store: 6 | host: "localhost" 7 | port: 5433 8 | user: "postgres" 9 | password: "postgres" 10 | dbname: "postgres" -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "echo-rest-api/api" 5 | "echo-rest-api/config" 6 | "echo-rest-api/service" 7 | "echo-rest-api/store" 8 | "flag" 9 | _ "github.com/lib/pq" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func main() { 14 | var err error 15 | log.SetFormatter(&log.JSONFormatter{}) 16 | // Получаем флаги запуска приложения 17 | configFile := flag.String("c", "config.yaml", "Path to config file") 18 | flag.Parse() 19 | // Загружаем конфиг 20 | var conf *config.Config 21 | if conf, err = config.NewConfig(*configFile); err != nil { 22 | log.Fatal(err) 23 | } 24 | // Конфигурируем логгер 25 | log.SetLevel(log.Level(conf.LogLevel)) 26 | log.Info("Starting service with configuration: ", conf.ConfigFile) 27 | // Создаем сторедж 28 | store, err := store.NewStore(conf) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer store.Close() 33 | log.Info("Store created successfully") 34 | // Создаем сервисы 35 | cs := service.NewCategoryService(store) 36 | ps := service.NewProductService(store) 37 | log.Info("Services created successfully") 38 | // Создаем Api 39 | api := api.NewApi(conf, cs, ps) 40 | log.WithField("address", api.GetApiInfo().Address). 41 | WithField("mw", api.GetApiInfo().MW). 42 | WithField("routs", api.GetApiInfo().Routs). 43 | Info("Starting api") 44 | log.Fatal(api.Start()) 45 | } 46 | -------------------------------------------------------------------------------- /model/category.go: -------------------------------------------------------------------------------- 1 | package model 2 | // Категория. 3 | // Сущность категория продукта 4 | // swagger:model 5 | type Category struct { 6 | // id категории 7 | Id int `json:"id"` 8 | // название категории 9 | Name string `json:"name" validate:"required,min=3"` 10 | } 11 | -------------------------------------------------------------------------------- /model/product.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Продукт. 4 | // Сущность продукт 5 | // swagger:model 6 | type Product struct { 7 | // id продукта 8 | Id int `json:"id"` 9 | // название 10 | Name string `json:"name" validate:"required,min=3"` 11 | // описание 12 | Description string `json:"desc"` 13 | // id категория 14 | Category int `json:"category" validate:"required"` 15 | // цена 16 | Price float64 `json:"price" validate:"required,gt=0"` 17 | } 18 | -------------------------------------------------------------------------------- /service/category_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "echo-rest-api/model" 5 | "echo-rest-api/store" 6 | ) 7 | 8 | type CategoryService interface { 9 | // Получить категорию по ид 10 | GetCategory(id int) (*model.Category, error) 11 | // Получить все категории 12 | GetCategories() ([]*model.Category, error) 13 | // Создать категорию 14 | CreateCategory(category *model.Category) (*int, error) 15 | // Обновить категорию 16 | UpdateCategory(category *model.Category) error 17 | // Удалить категорию 18 | DeleteCategory(id int) error 19 | } 20 | 21 | func NewCategoryService(store store.Store) CategoryService { 22 | return &CategoryServiceContext{store: store} 23 | } 24 | 25 | type CategoryServiceContext struct { 26 | store store.Store 27 | } 28 | 29 | func (csc *CategoryServiceContext) GetCategory(id int) (*model.Category, error) { 30 | return csc.store.GetCategory(nil, id) 31 | } 32 | 33 | func (csc *CategoryServiceContext) GetCategories() ([]*model.Category, error) { 34 | return csc.store.GetCategories(nil) 35 | } 36 | 37 | func (csc *CategoryServiceContext) CreateCategory(category *model.Category) (*int, error) { 38 | tx, err := csc.store.Begin() 39 | if err != nil { 40 | return nil, err 41 | } 42 | cat, err := csc.store.CreateCategory(tx, category) 43 | if err != nil { 44 | csc.store.Rollback(tx) 45 | return nil, err 46 | } 47 | if err = csc.store.Commit(tx); err != nil { 48 | return nil, err 49 | } 50 | return cat, nil 51 | } 52 | 53 | func (csc *CategoryServiceContext) UpdateCategory(category *model.Category) error { 54 | tx, err := csc.store.Begin() 55 | if err != nil { 56 | return err 57 | } 58 | err = csc.store.UpdateCategory(tx, category) 59 | if err != nil { 60 | csc.store.Rollback(tx) 61 | return err 62 | } 63 | if err = csc.store.Commit(tx); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | func (csc *CategoryServiceContext) DeleteCategory(id int) error { 70 | tx, err := csc.store.Begin() 71 | if err != nil { 72 | return err 73 | } 74 | err = csc.store.DeleteCategory(tx, id) 75 | if err != nil { 76 | csc.store.Rollback(tx) 77 | return err 78 | } 79 | if err = csc.store.Commit(tx); err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /service/product_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "echo-rest-api/model" 5 | "echo-rest-api/store" 6 | ) 7 | 8 | type ProductService interface { 9 | // Получить продукт по id 10 | GetProduct(id int) (*model.Product, error) 11 | // Получить все продукты 12 | GetProducts(category *int) ([]*model.Product, error) 13 | // Создать продукт 14 | CreateProduct(product *model.Product) (*int, error) 15 | // Обновить продукт 16 | UpdateProduct(product *model.Product) error 17 | // Удалить продукт 18 | DeleteProduct(id int) error 19 | } 20 | 21 | func NewProductService(store store.Store) ProductService { 22 | return &ProductServiceContext{store: store} 23 | } 24 | 25 | type ProductServiceContext struct { 26 | store store.Store 27 | } 28 | 29 | func (psc *ProductServiceContext) GetProduct(id int) (*model.Product, error) { 30 | return psc.store.GetProduct(nil, id) 31 | } 32 | 33 | func (psc *ProductServiceContext) GetProducts(category *int) ([]*model.Product, error) { 34 | return psc.store.GetProducts(nil, category) 35 | } 36 | 37 | func (psc *ProductServiceContext) CreateProduct(product *model.Product) (*int, error) { 38 | tx, err := psc.store.Begin() 39 | if err != nil { 40 | return nil, err 41 | } 42 | cat, err := psc.store.CreateProduct(tx, product) 43 | if err != nil { 44 | psc.store.Rollback(tx) 45 | return nil, err 46 | } 47 | if err = psc.store.Commit(tx); err != nil { 48 | return nil, err 49 | } 50 | return cat, nil 51 | } 52 | 53 | func (psc *ProductServiceContext) UpdateProduct(product *model.Product) error { 54 | tx, err := psc.store.Begin() 55 | if err != nil { 56 | return err 57 | } 58 | err = psc.store.UpdateProduct(tx, product) 59 | if err != nil { 60 | psc.store.Rollback(tx) 61 | return err 62 | } 63 | if err = psc.store.Commit(tx); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | func (psc *ProductServiceContext) DeleteProduct(id int) error { 70 | tx, err := psc.store.Begin() 71 | if err != nil { 72 | return err 73 | } 74 | err = psc.store.DeleteProduct(tx, id) 75 | if err != nil { 76 | psc.store.Rollback(tx) 77 | return err 78 | } 79 | if err = psc.store.Commit(tx); err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /spec/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "Пример создания REST API на Echo framework", 14 | "title": "Echo-REST-API.", 15 | "contact": { 16 | "email": "uchonyy@gmail.com" 17 | }, 18 | "version": "0.0.1" 19 | }, 20 | "host": "localhost", 21 | "basePath": "/api", 22 | "paths": { 23 | "/categories": { 24 | "get": { 25 | "description": "Получить список категорий", 26 | "operationId": "getCategories", 27 | "responses": { 28 | "200": { 29 | "schema": { 30 | "type": "array", 31 | "items": { 32 | "$ref": "#/definitions/Category" 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "post": { 39 | "description": "Создать категорию", 40 | "operationId": "createCategory", 41 | "parameters": [ 42 | { 43 | "description": "новая категория", 44 | "name": "category", 45 | "in": "body", 46 | "required": true, 47 | "schema": { 48 | "$ref": "#/definitions/Category" 49 | } 50 | } 51 | ], 52 | "responses": { 53 | "201": { 54 | "schema": { 55 | "$ref": "#/definitions/Category" 56 | } 57 | }, 58 | "400": { 59 | "description": "Bad request param" 60 | } 61 | } 62 | } 63 | }, 64 | "/categories/{id}": { 65 | "get": { 66 | "description": "Получить категорию", 67 | "operationId": "getCategory", 68 | "parameters": [ 69 | { 70 | "type": "int", 71 | "description": "id необходимой категории", 72 | "name": "id", 73 | "in": "path", 74 | "required": true 75 | } 76 | ], 77 | "responses": { 78 | "200": { 79 | "schema": { 80 | "$ref": "#/definitions/Category" 81 | } 82 | }, 83 | "400": { 84 | "description": "Bad request param `id`" 85 | }, 86 | "404": { 87 | "description": "Category `id`= not found" 88 | } 89 | } 90 | }, 91 | "put": { 92 | "description": "Обновить категорию", 93 | "operationId": "updateCategory", 94 | "parameters": [ 95 | { 96 | "type": "int", 97 | "description": "id необходимой категории", 98 | "name": "id", 99 | "in": "path", 100 | "required": true 101 | }, 102 | { 103 | "description": "измененная категория", 104 | "name": "category", 105 | "in": "body", 106 | "required": true, 107 | "schema": { 108 | "$ref": "#/definitions/Category" 109 | } 110 | } 111 | ], 112 | "responses": { 113 | "204": { 114 | "description": "Категория обновлена" 115 | }, 116 | "400": { 117 | "description": "Bad request param" 118 | } 119 | } 120 | }, 121 | "delete": { 122 | "description": "Удалить категорию", 123 | "operationId": "deleteCategory", 124 | "parameters": [ 125 | { 126 | "type": "int", 127 | "description": "id необходимой категории", 128 | "name": "id", 129 | "in": "path", 130 | "required": true 131 | } 132 | ], 133 | "responses": { 134 | "204": { 135 | "description": "Категория удалена" 136 | }, 137 | "400": { 138 | "description": "Bad request param `id`" 139 | }, 140 | "404": { 141 | "description": "Category `id`= not found" 142 | } 143 | } 144 | } 145 | }, 146 | "/products": { 147 | "get": { 148 | "description": "Получить список продуктов", 149 | "operationId": "getProducts", 150 | "parameters": [ 151 | { 152 | "type": "int", 153 | "description": "id категории по которой выбрать продукты", 154 | "name": "category", 155 | "in": "path" 156 | } 157 | ], 158 | "responses": { 159 | "200": { 160 | "schema": { 161 | "type": "array", 162 | "items": { 163 | "$ref": "#/definitions/Product" 164 | } 165 | } 166 | } 167 | } 168 | }, 169 | "post": { 170 | "description": "Создать продукт", 171 | "operationId": "createProduct", 172 | "parameters": [ 173 | { 174 | "description": "новый продукт", 175 | "name": "product", 176 | "in": "body", 177 | "required": true, 178 | "schema": { 179 | "$ref": "#/definitions/Product" 180 | } 181 | } 182 | ], 183 | "responses": { 184 | "201": { 185 | "schema": { 186 | "$ref": "#/definitions/Product" 187 | } 188 | }, 189 | "400": { 190 | "description": "Bad request param" 191 | } 192 | } 193 | } 194 | }, 195 | "/products/{id}": { 196 | "get": { 197 | "description": "Получить продукт", 198 | "operationId": "getProduct", 199 | "parameters": [ 200 | { 201 | "type": "int", 202 | "description": "id необходимого продукта", 203 | "name": "id", 204 | "in": "path", 205 | "required": true 206 | } 207 | ], 208 | "responses": { 209 | "200": { 210 | "schema": { 211 | "$ref": "#/definitions/Product" 212 | } 213 | }, 214 | "400": { 215 | "description": "Bad request param `id`" 216 | }, 217 | "404": { 218 | "description": "Product `id`= not found" 219 | } 220 | } 221 | }, 222 | "put": { 223 | "description": "Обновить продукт", 224 | "operationId": "updateProduct", 225 | "parameters": [ 226 | { 227 | "type": "int", 228 | "description": "id необходимого продукта", 229 | "name": "id", 230 | "in": "path", 231 | "required": true 232 | }, 233 | { 234 | "description": "измененный продукт", 235 | "name": "product", 236 | "in": "body", 237 | "required": true, 238 | "schema": { 239 | "$ref": "#/definitions/Product" 240 | } 241 | } 242 | ], 243 | "responses": { 244 | "204": { 245 | "description": "Продукт обновлена" 246 | }, 247 | "400": { 248 | "description": "Bad request param" 249 | } 250 | } 251 | }, 252 | "delete": { 253 | "description": "Удалить продукт", 254 | "operationId": "deleteProduct", 255 | "parameters": [ 256 | { 257 | "type": "int", 258 | "description": "id необходимого продукта", 259 | "name": "id", 260 | "in": "path", 261 | "required": true 262 | } 263 | ], 264 | "responses": { 265 | "204": { 266 | "description": "Категория удалена" 267 | }, 268 | "400": { 269 | "description": "Bad request param `id`" 270 | }, 271 | "404": { 272 | "description": "Product `id`= not found" 273 | } 274 | } 275 | } 276 | } 277 | }, 278 | "definitions": { 279 | "Category": { 280 | "description": "Сущность категория продукта", 281 | "type": "object", 282 | "title": "Категория.", 283 | "properties": { 284 | "id": { 285 | "description": "id категории", 286 | "type": "integer", 287 | "format": "int64", 288 | "x-go-name": "Id" 289 | }, 290 | "name": { 291 | "description": "название категории", 292 | "type": "string", 293 | "x-go-name": "Name" 294 | } 295 | }, 296 | "x-go-package": "echo-rest-api/model" 297 | }, 298 | "Product": { 299 | "description": "Сущность продукт", 300 | "type": "object", 301 | "title": "Продукт.", 302 | "properties": { 303 | "category": { 304 | "description": "id категория", 305 | "type": "integer", 306 | "format": "int64", 307 | "x-go-name": "Category" 308 | }, 309 | "desc": { 310 | "description": "описание", 311 | "type": "string", 312 | "x-go-name": "Description" 313 | }, 314 | "id": { 315 | "description": "id продукта", 316 | "type": "integer", 317 | "format": "int64", 318 | "x-go-name": "Id" 319 | }, 320 | "name": { 321 | "description": "название", 322 | "type": "string", 323 | "x-go-name": "Name" 324 | }, 325 | "price": { 326 | "description": "цена", 327 | "type": "number", 328 | "format": "double", 329 | "x-go-name": "Price" 330 | } 331 | }, 332 | "x-go-package": "echo-rest-api/model" 333 | } 334 | } 335 | } -------------------------------------------------------------------------------- /store/1_0_init_store.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | CREATE TABLE category( 3 | id SERIAL, 4 | name VARCHAR(100) NOT NULL, 5 | constraint category_pk primary key(id) 6 | ); 7 | 8 | CREATE TABLE product( 9 | id SERIAL, 10 | category INTEGER NOT NULL, 11 | name VARCHAR(200) NOT NULL, 12 | description TEXT NOT NULL, 13 | price numeric(10,2) NOT NULL, 14 | constraint product_pk primary key(id), 15 | constraint product_to_category foreign key (category) references category(id) ON DELETE CASCADE 16 | ); 17 | 18 | -- +migrate Down 19 | DROP TABLE product; 20 | DROP TABLE category; -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "database/sql" 5 | "echo-rest-api/config" 6 | "echo-rest-api/model" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | type Store interface { 12 | // Закрыть сторедж 13 | Close() error 14 | // Начать транзакцию 15 | Begin() (*sql.Tx, error) 16 | // Закомитить транзакцию 17 | Commit(tx *sql.Tx) error 18 | // Откатить транзакцию 19 | Rollback(tx *sql.Tx) error 20 | // Получить категорию по id 21 | GetCategory(tx *sql.Tx, id int) (*model.Category, error) 22 | // Получить все категории 23 | GetCategories(tx *sql.Tx) ([]*model.Category, error) 24 | // Создать категорию 25 | CreateCategory(tx *sql.Tx, category *model.Category) (*int, error) 26 | // Обновить категорию 27 | UpdateCategory(tx *sql.Tx, category *model.Category) error 28 | // Удалить категорию 29 | DeleteCategory(tx *sql.Tx, id int) error 30 | // Получить продукт по id 31 | GetProduct(tx *sql.Tx, id int) (*model.Product, error) 32 | // Получить все продукты 33 | GetProducts(tx *sql.Tx, category *int) ([]*model.Product, error) 34 | // Создать продукт 35 | CreateProduct(tx *sql.Tx, product *model.Product) (*int, error) 36 | // Обновить продукт 37 | UpdateProduct(tx *sql.Tx, product *model.Product) error 38 | // Удалить продукт 39 | DeleteProduct(tx *sql.Tx, id int) error 40 | } 41 | 42 | // Контекст стореджа 43 | type StoreContext struct { 44 | db *sql.DB 45 | } 46 | 47 | // Создать сторедж 48 | func NewStore(conf *config.Config) (Store, error) { 49 | storeConfig := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", 50 | conf.Store.Host, conf.Store.Port, conf.Store.User, conf.Store.Password, conf.Store.Dbname, 51 | ) 52 | db, err := sql.Open("postgres", storeConfig) 53 | if err != nil { 54 | return nil, err 55 | } 56 | err = db.Ping() 57 | if err != nil { 58 | return nil, err 59 | } 60 | return &StoreContext{db}, nil 61 | } 62 | 63 | // Закрыть сторедж 64 | func (sc *StoreContext) Close() error { 65 | return sc.db.Close() 66 | } 67 | 68 | // Начать транзакцию 69 | func (sc *StoreContext) Begin() (*sql.Tx, error) { 70 | return sc.db.Begin() 71 | } 72 | 73 | // Закомитить транзакцию 74 | func (sc *StoreContext) Commit(tx *sql.Tx) error { 75 | if tx == nil { 76 | return errors.New("tx is nil") 77 | } 78 | return tx.Commit() 79 | } 80 | 81 | // Откатить транзакцию 82 | func (sc *StoreContext) Rollback(tx *sql.Tx) error { 83 | if tx == nil { 84 | return errors.New("tx is nil") 85 | } 86 | return tx.Rollback() 87 | } 88 | 89 | // Получить категорию по id 90 | func (sc *StoreContext) GetCategory(tx *sql.Tx, id int) (*model.Category, error) { 91 | var query = "SELECT id, name FROM category WHERE id= $1;" 92 | var row *sql.Row 93 | if tx != nil { 94 | row = tx.QueryRow(query, id) 95 | } else { 96 | row = sc.db.QueryRow(query, id) 97 | } 98 | category := &model.Category{} 99 | if err := row.Scan(&category.Id, &category.Name); err != nil { 100 | if err != sql.ErrNoRows { 101 | return nil, err 102 | } else { 103 | return nil, nil 104 | } 105 | } 106 | return category, nil 107 | } 108 | 109 | // Получить все категории 110 | func (sc *StoreContext) GetCategories(tx *sql.Tx) ([]*model.Category, error) { 111 | query := "SELECT id, name FROM category;" 112 | var rows *sql.Rows 113 | var err error 114 | if tx != nil { 115 | rows, err = tx.Query(query) 116 | } else { 117 | rows, err = sc.db.Query(query) 118 | } 119 | if err != nil { 120 | return nil, err 121 | } 122 | defer rows.Close() 123 | var categories []*model.Category 124 | for rows.Next() { 125 | category := &model.Category{} 126 | if err := rows.Scan(&category.Id, &category.Name); err != nil { 127 | return nil, err 128 | } 129 | categories = append(categories, category) 130 | } 131 | return categories, nil 132 | } 133 | 134 | // Создать категорию 135 | func (sc *StoreContext) CreateCategory(tx *sql.Tx, category *model.Category) (*int, error) { 136 | var query = "INSERT INTO category(name) VALUES($1) RETURNING id;" 137 | var id int 138 | var err error 139 | if tx != nil { 140 | err = tx.QueryRow(query, category.Name).Scan(&id) 141 | } else { 142 | err = sc.db.QueryRow(query, category.Name).Scan(&id) 143 | } 144 | if err != nil { 145 | return nil, err 146 | } 147 | return &id, nil 148 | } 149 | 150 | // Обновить категорию 151 | func (sc *StoreContext) UpdateCategory(tx *sql.Tx, category *model.Category) error { 152 | query := "UPDATE category SET name =$1 WHERE id = $2;" 153 | var res sql.Result 154 | var err error 155 | if tx != nil { 156 | res, err = tx.Exec(query, category.Name, category.Id) 157 | } else { 158 | res, err = sc.db.Exec(query, category.Name, category.Id) 159 | } 160 | if err != nil { 161 | return err 162 | } 163 | if a, err := res.RowsAffected(); err != nil { 164 | return err 165 | } else if a == 0 { 166 | return sql.ErrNoRows 167 | } 168 | return nil 169 | } 170 | 171 | // Удалить категорию 172 | func (sc *StoreContext) DeleteCategory(tx *sql.Tx, id int) error { 173 | query := "DELETE FROM category WHERE id = $1;" 174 | var res sql.Result 175 | var err error 176 | if tx != nil { 177 | res, err = tx.Exec(query, id) 178 | } else { 179 | res, err = sc.db.Exec(query, id) 180 | } 181 | if err != nil { 182 | return err 183 | } 184 | if a, err := res.RowsAffected(); err != nil { 185 | return err 186 | } else if a == 0 { 187 | return sql.ErrNoRows 188 | } 189 | return nil 190 | } 191 | 192 | // Получить продукт по id 193 | func (sc *StoreContext) GetProduct(tx *sql.Tx, id int) (*model.Product, error) { 194 | var query = "SELECT id, name, description, category, price FROM product WHERE id= $1;" 195 | var row *sql.Row 196 | if tx != nil { 197 | row = tx.QueryRow(query, id) 198 | } else { 199 | row = sc.db.QueryRow(query, id) 200 | } 201 | product := &model.Product{} 202 | if err := row.Scan(&product.Id, &product.Name, &product.Description, &product.Category, &product.Price); err != nil { 203 | if err != sql.ErrNoRows { 204 | return nil, err 205 | } else { 206 | return nil, nil 207 | } 208 | } 209 | return product, nil 210 | } 211 | 212 | // Получить все продукты либо все продукты из категории если указана category 213 | func (sc *StoreContext) GetProducts(tx *sql.Tx, category *int) ([]*model.Product, error) { 214 | var query string 215 | var rows *sql.Rows 216 | var err error 217 | if category == nil { 218 | query = "SELECT id, name, description, category, price FROM product;" 219 | if tx != nil { 220 | rows, err = tx.Query(query) 221 | } else { 222 | rows, err = sc.db.Query(query) 223 | } 224 | } else { 225 | query = "SELECT id, name, description, category, price FROM product WHERE category= $1;" 226 | if tx != nil { 227 | rows, err = tx.Query(query, category) 228 | } else { 229 | rows, err = sc.db.Query(query, category) 230 | } 231 | } 232 | if err != nil { 233 | return nil, err 234 | } 235 | defer rows.Close() 236 | var products []*model.Product 237 | product := &model.Product{} 238 | for rows.Next() { 239 | if err := rows.Scan(&product.Id, &product.Name, &product.Description, &product.Category, &product.Price); err != nil { 240 | return nil, err 241 | } 242 | products = append(products, product) 243 | } 244 | return products, nil 245 | } 246 | 247 | // Создать продукт 248 | func (sc *StoreContext) CreateProduct(tx *sql.Tx, product *model.Product) (*int, error) { 249 | var query = "INSERT INTO product( name, description, category, price) VALUES($1, $2, $3, $4) RETURNING id;" 250 | var id int 251 | var err error 252 | if tx != nil { 253 | err = tx.QueryRow(query, product.Name, product.Description, product.Category, product.Price).Scan(&id) 254 | } else { 255 | err = sc.db.QueryRow(query, product.Name, product.Description, product.Category, product.Price).Scan(&id) 256 | } 257 | if err != nil { 258 | return nil, err 259 | } 260 | return &id, nil 261 | } 262 | 263 | // Обновить продукт 264 | func (sc *StoreContext) UpdateProduct(tx *sql.Tx, product *model.Product) error { 265 | query := "UPDATE product SET name=$1, description=$2, category=$3, price=$4 WHERE id = $5;" 266 | var res sql.Result 267 | var err error 268 | if tx != nil { 269 | res, err = tx.Exec(query, product.Name, product.Description, product.Category, product.Price, product.Id) 270 | } else { 271 | res, err = sc.db.Exec(query, product.Name, product.Description, product.Category, product.Price, product.Id) 272 | } 273 | if err != nil { 274 | return err 275 | } 276 | if a, err := res.RowsAffected(); err != nil { 277 | return err 278 | } else if a == 0 { 279 | return sql.ErrNoRows 280 | } 281 | return nil 282 | } 283 | 284 | // Удалить продукт 285 | func (sc *StoreContext) DeleteProduct(tx *sql.Tx, id int) error { 286 | query := "DELETE FROM product WHERE id = $1;" 287 | var res sql.Result 288 | var err error 289 | if tx != nil { 290 | res, err = tx.Exec(query, id) 291 | } else { 292 | res, err = sc.db.Exec(query, id) 293 | } 294 | if err != nil { 295 | return err 296 | } 297 | if a, err := res.RowsAffected(); err != nil { 298 | return err 299 | } else if a == 0 { 300 | return sql.ErrNoRows 301 | } 302 | return nil 303 | } 304 | -------------------------------------------------------------------------------- /store/store.yaml: -------------------------------------------------------------------------------- 1 | development: 2 | dialect: postgres 3 | datasource: host=localhost port=5433 user=postgres password=postgres dbname=postgres sslmode=disable 4 | dir: store -------------------------------------------------------------------------------- /test/api_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "database/sql" 5 | "echo-rest-api/api" 6 | "echo-rest-api/config" 7 | "echo-rest-api/model" 8 | "echo-rest-api/test/mock" 9 | "encoding/json" 10 | "github.com/golang/mock/gomock" 11 | "github.com/labstack/echo" 12 | "github.com/stretchr/testify/assert" 13 | "net/http" 14 | "net/http/httptest" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | func TestApi_GetCategories(t *testing.T) { 20 | mockCtrl := gomock.NewController(t) 21 | defer mockCtrl.Finish() 22 | conf := &config.Config{LogLevel: 0} 23 | cs := mock.NewMockCategoryService(mockCtrl) 24 | api := api.NewApi(conf, cs, nil) 25 | req := httptest.NewRequest(echo.GET, "/api/categories", nil) 26 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 27 | // 200 [] - ничего не найдено 28 | rec := httptest.NewRecorder() 29 | var cats []*model.Category 30 | cs.EXPECT().GetCategories().Return(cats, nil).Times(1) 31 | api.Http.ServeHTTP(rec, req) 32 | assert.Equal(t, http.StatusOK, rec.Code) 33 | assert.Equal(t, rec.Body.String(), "[]") 34 | // 200 - ок 35 | cats = append(cats, &model.Category{Id: 1, Name: "Name1"}) 36 | cats = append(cats, &model.Category{Id: 2, Name: "Name2"}) 37 | cs.EXPECT().GetCategories().Return(cats, nil).Times(1) 38 | rec = httptest.NewRecorder() 39 | api.Http.ServeHTTP(rec, req) 40 | assert.Equal(t, http.StatusOK, rec.Code) 41 | res, _ := json.Marshal(cats) 42 | assert.Equal(t, rec.Body.String(), string(res)) 43 | } 44 | 45 | func TestApi_GetCategory(t *testing.T) { 46 | mockCtrl := gomock.NewController(t) 47 | defer mockCtrl.Finish() 48 | conf := &config.Config{LogLevel: 5} 49 | cs := mock.NewMockCategoryService(mockCtrl) 50 | api := api.NewApi(conf, cs, nil) 51 | req := httptest.NewRequest(echo.GET, "/api/categories/2", nil) 52 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 53 | // 404 54 | rec := httptest.NewRecorder() 55 | cs.EXPECT().GetCategory(2).Return(nil, nil).Times(1) 56 | api.Http.ServeHTTP(rec, req) 57 | assert.Equal(t, http.StatusNotFound, rec.Code) 58 | // 200 59 | cat := &model.Category{Id: 2, Name: "Name2"} 60 | cs.EXPECT().GetCategory(2).Return(cat, nil).Times(1) 61 | rec = httptest.NewRecorder() 62 | api.Http.ServeHTTP(rec, req) 63 | assert.Equal(t, http.StatusOK, rec.Code) 64 | res, _ := json.Marshal(cat) 65 | assert.Equal(t, rec.Body.String(), string(res)) 66 | } 67 | 68 | func TestApi_CreateCategory(t *testing.T) { 69 | mockCtrl := gomock.NewController(t) 70 | defer mockCtrl.Finish() 71 | conf := &config.Config{LogLevel: 5} 72 | cs := mock.NewMockCategoryService(mockCtrl) 73 | api := api.NewApi(conf, cs, nil) 74 | // 400 75 | catJSON := `{"name": "te"}` 76 | req := httptest.NewRequest(echo.POST, "/api/categories/", strings.NewReader(catJSON)) 77 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 78 | rec := httptest.NewRecorder() 79 | api.Http.ServeHTTP(rec, req) 80 | assert.Equal(t, http.StatusBadRequest, rec.Code) 81 | // 200 82 | catJSON = `{"name": "test"}` 83 | id := 2 84 | req = httptest.NewRequest(echo.POST, "/api/categories/", strings.NewReader(catJSON)) 85 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 86 | cs.EXPECT().CreateCategory(gomock.Any()).Return(&id, nil).Times(1) 87 | rec = httptest.NewRecorder() 88 | api.Http.ServeHTTP(rec, req) 89 | assert.Equal(t, http.StatusCreated, rec.Code) 90 | var res map[string]int 91 | json.Unmarshal(rec.Body.Bytes(), &res) 92 | assert.Equal(t, res["id"], 2) 93 | } 94 | 95 | func TestApi_UpdateCategory(t *testing.T) { 96 | mockCtrl := gomock.NewController(t) 97 | defer mockCtrl.Finish() 98 | conf := &config.Config{LogLevel: 5} 99 | cs := mock.NewMockCategoryService(mockCtrl) 100 | api := api.NewApi(conf, cs, nil) 101 | // 400 102 | catJSON := `{"name": "te"}` 103 | req := httptest.NewRequest(echo.PUT, "/api/categories/2", strings.NewReader(catJSON)) 104 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 105 | rec := httptest.NewRecorder() 106 | api.Http.ServeHTTP(rec, req) 107 | assert.Equal(t, http.StatusBadRequest, rec.Code) 108 | // 404 109 | catJSON = `{"name": "test"}` 110 | req = httptest.NewRequest(echo.PUT, "/api/categories/1", strings.NewReader(catJSON)) 111 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 112 | cs.EXPECT().UpdateCategory(gomock.Any()).Return(sql.ErrNoRows).Times(1) 113 | rec = httptest.NewRecorder() 114 | api.Http.ServeHTTP(rec, req) 115 | assert.Equal(t, http.StatusNotFound, rec.Code) 116 | // 204 117 | catJSON = `{"name": "test"}` 118 | req = httptest.NewRequest(echo.PUT, "/api/categories/2", strings.NewReader(catJSON)) 119 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 120 | cs.EXPECT().UpdateCategory(gomock.Any()).Return(nil).Times(1) 121 | rec = httptest.NewRecorder() 122 | api.Http.ServeHTTP(rec, req) 123 | assert.Equal(t, http.StatusNoContent, rec.Code) 124 | } 125 | 126 | func TestApi_DeleteCategory(t *testing.T) { 127 | mockCtrl := gomock.NewController(t) 128 | defer mockCtrl.Finish() 129 | conf := &config.Config{LogLevel: 5} 130 | cs := mock.NewMockCategoryService(mockCtrl) 131 | api := api.NewApi(conf, cs, nil) 132 | // 404 133 | req := httptest.NewRequest(echo.DELETE, "/api/categories/1", nil) 134 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 135 | cs.EXPECT().DeleteCategory(gomock.Any()).Return(sql.ErrNoRows).Times(1) 136 | rec := httptest.NewRecorder() 137 | api.Http.ServeHTTP(rec, req) 138 | assert.Equal(t, http.StatusNotFound, rec.Code) 139 | // 201 140 | req = httptest.NewRequest(echo.DELETE, "/api/categories/2", nil) 141 | cs.EXPECT().DeleteCategory(gomock.Any()).Return(nil).Times(1) 142 | rec = httptest.NewRecorder() 143 | api.Http.ServeHTTP(rec, req) 144 | assert.Equal(t, http.StatusNoContent, rec.Code) 145 | } 146 | 147 | func TestApi_GetProducts(t *testing.T) { 148 | mockCtrl := gomock.NewController(t) 149 | defer mockCtrl.Finish() 150 | conf := &config.Config{LogLevel: 5} 151 | ps := mock.NewMockProductService(mockCtrl) 152 | api := api.NewApi(conf, nil, ps) 153 | req := httptest.NewRequest(echo.GET, "/api/products", nil) 154 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 155 | // 200 [] - ничего не найдено 156 | rec := httptest.NewRecorder() 157 | var cats []*model.Product 158 | ps.EXPECT().GetProducts(gomock.Any()).Return(cats, nil).Times(1) 159 | api.Http.ServeHTTP(rec, req) 160 | assert.Equal(t, http.StatusOK, rec.Code) 161 | assert.Equal(t, rec.Body.String(), "[]") 162 | // 200 - ок 163 | cats = append(cats, &model.Product{Id: 1, Name: "Name1"}) 164 | cats = append(cats, &model.Product{Id: 2, Name: "Name2"}) 165 | ps.EXPECT().GetProducts(gomock.Any()).Return(cats, nil).Times(1) 166 | rec = httptest.NewRecorder() 167 | api.Http.ServeHTTP(rec, req) 168 | assert.Equal(t, http.StatusOK, rec.Code) 169 | res, _ := json.Marshal(cats) 170 | assert.Equal(t, rec.Body.String(), string(res)) 171 | // 200 172 | rec = httptest.NewRecorder() 173 | req = httptest.NewRequest(echo.GET, "/api/products?category=2", nil) 174 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 175 | id := 2 176 | ps.EXPECT().GetProducts(&id).Return(cats, nil).Times(1) 177 | api.Http.ServeHTTP(rec, req) 178 | assert.Equal(t, http.StatusOK, rec.Code) 179 | res, _ = json.Marshal(cats) 180 | assert.Equal(t, rec.Body.String(), string(res)) 181 | } 182 | 183 | func TestApi_GetProduct(t *testing.T) { 184 | mockCtrl := gomock.NewController(t) 185 | defer mockCtrl.Finish() 186 | conf := &config.Config{LogLevel: 5} 187 | ps := mock.NewMockProductService(mockCtrl) 188 | api := api.NewApi(conf, nil, ps) 189 | req := httptest.NewRequest(echo.GET, "/api/products/2", nil) 190 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 191 | // 404 192 | rec := httptest.NewRecorder() 193 | ps.EXPECT().GetProduct(2).Return(nil, nil).Times(1) 194 | api.Http.ServeHTTP(rec, req) 195 | assert.Equal(t, http.StatusNotFound, rec.Code) 196 | // 200 197 | cat := &model.Product{Id: 2, Name: "Name2"} 198 | ps.EXPECT().GetProduct(2).Return(cat, nil).Times(1) 199 | rec = httptest.NewRecorder() 200 | api.Http.ServeHTTP(rec, req) 201 | assert.Equal(t, http.StatusOK, rec.Code) 202 | res, _ := json.Marshal(cat) 203 | assert.Equal(t, rec.Body.String(), string(res)) 204 | } 205 | 206 | func TestApi_CreateProduct(t *testing.T) { 207 | mockCtrl := gomock.NewController(t) 208 | defer mockCtrl.Finish() 209 | conf := &config.Config{LogLevel: 5} 210 | ps := mock.NewMockProductService(mockCtrl) 211 | api := api.NewApi(conf, nil, ps) 212 | // 400 213 | catJSON := `{"name": "test","description":"test","category":1}` 214 | req := httptest.NewRequest(echo.POST, "/api/products/", strings.NewReader(catJSON)) 215 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 216 | rec := httptest.NewRecorder() 217 | api.Http.ServeHTTP(rec, req) 218 | assert.Equal(t, http.StatusBadRequest, rec.Code) 219 | // 200 220 | catJSON = `{"name": "test","description":"test","category":1,"price":10.1}` 221 | id := 2 222 | req = httptest.NewRequest(echo.POST, "/api/products/", strings.NewReader(catJSON)) 223 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 224 | ps.EXPECT().CreateProduct(gomock.Any()).Return(&id, nil).Times(1) 225 | rec = httptest.NewRecorder() 226 | api.Http.ServeHTTP(rec, req) 227 | assert.Equal(t, http.StatusCreated, rec.Code) 228 | var res map[string]int 229 | json.Unmarshal(rec.Body.Bytes(), &res) 230 | assert.Equal(t, res["id"], 2) 231 | } 232 | 233 | func TestApi_UpdateProduct(t *testing.T) { 234 | mockCtrl := gomock.NewController(t) 235 | defer mockCtrl.Finish() 236 | conf := &config.Config{LogLevel: 5} 237 | ps := mock.NewMockProductService(mockCtrl) 238 | api := api.NewApi(conf, nil, ps) 239 | // 400 240 | catJSON := `{"name": "test","description":"test","category":1}` 241 | req := httptest.NewRequest(echo.PUT, "/api/products/2", strings.NewReader(catJSON)) 242 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 243 | rec := httptest.NewRecorder() 244 | api.Http.ServeHTTP(rec, req) 245 | assert.Equal(t, http.StatusBadRequest, rec.Code) 246 | // 404 247 | catJSON = `{"name": "test","description":"test","category":1,"price":10.1}` 248 | req = httptest.NewRequest(echo.PUT, "/api/products/1", strings.NewReader(catJSON)) 249 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 250 | ps.EXPECT().UpdateProduct(gomock.Any()).Return(sql.ErrNoRows).Times(1) 251 | rec = httptest.NewRecorder() 252 | api.Http.ServeHTTP(rec, req) 253 | assert.Equal(t, http.StatusNotFound, rec.Code) 254 | // 204 255 | catJSON = `{"name": "test","description":"test","category":1,"price":10.1}` 256 | req = httptest.NewRequest(echo.PUT, "/api/products/2", strings.NewReader(catJSON)) 257 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 258 | ps.EXPECT().UpdateProduct(gomock.Any()).Return(nil).Times(1) 259 | rec = httptest.NewRecorder() 260 | api.Http.ServeHTTP(rec, req) 261 | assert.Equal(t, http.StatusNoContent, rec.Code) 262 | } 263 | 264 | func TestApi_DeleteProduct(t *testing.T) { 265 | mockCtrl := gomock.NewController(t) 266 | defer mockCtrl.Finish() 267 | conf := &config.Config{LogLevel: 5} 268 | ps := mock.NewMockProductService(mockCtrl) 269 | api := api.NewApi(conf, nil, ps) 270 | // 404 271 | req := httptest.NewRequest(echo.DELETE, "/api/products/1", nil) 272 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 273 | ps.EXPECT().DeleteProduct(gomock.Any()).Return(sql.ErrNoRows).Times(1) 274 | rec := httptest.NewRecorder() 275 | api.Http.ServeHTTP(rec, req) 276 | assert.Equal(t, http.StatusNotFound, rec.Code) 277 | // 201 278 | req = httptest.NewRequest(echo.DELETE, "/api/products/2", nil) 279 | ps.EXPECT().DeleteProduct(gomock.Any()).Return(nil).Times(1) 280 | rec = httptest.NewRecorder() 281 | api.Http.ServeHTTP(rec, req) 282 | assert.Equal(t, http.StatusNoContent, rec.Code) 283 | } 284 | -------------------------------------------------------------------------------- /test/category_service_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "database/sql" 5 | "echo-rest-api/model" 6 | "echo-rest-api/service" 7 | "echo-rest-api/test/mock" 8 | "errors" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | ) 13 | 14 | func TestCategoryService_GetCategory(t *testing.T) { 15 | mockCtrl := gomock.NewController(t) 16 | defer mockCtrl.Finish() 17 | mockStore := mock.NewMockStore(mockCtrl) 18 | mockStore.EXPECT().GetCategory(nil, 1).Return(nil, errors.New("test")).Times(1) 19 | mockStore.EXPECT().GetCategory(nil, 2).Return(&model.Category{2, "test"}, nil).Times(1) 20 | cs := service.NewCategoryService(mockStore) 21 | r, e := cs.GetCategory(1) 22 | assert.NotNil(t, e) 23 | assert.Nil(t, r) 24 | r, e = cs.GetCategory(2) 25 | assert.Nil(t, e) 26 | assert.NotNil(t, r) 27 | } 28 | 29 | func TestCategoryService_GetCategories(t *testing.T) { 30 | mockCtrl := gomock.NewController(t) 31 | defer mockCtrl.Finish() 32 | mockStore := mock.NewMockStore(mockCtrl) 33 | mockStore.EXPECT().GetCategories(nil).Return(nil, errors.New("test")).Times(1) 34 | cs := service.NewCategoryService(mockStore) 35 | r, e := cs.GetCategories() 36 | assert.NotNil(t, e) 37 | assert.Nil(t, r) 38 | mockStore.EXPECT().GetCategories(nil).Return([]*model.Category{}, nil).Times(1) 39 | r, e = cs.GetCategories() 40 | assert.Nil(t, e) 41 | assert.NotNil(t, r) 42 | } 43 | 44 | func TestCategoryService_CreateCategory(t *testing.T) { 45 | mockCtrl := gomock.NewController(t) 46 | defer mockCtrl.Finish() 47 | 48 | mockStore := mock.NewMockStore(mockCtrl) 49 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 50 | cs := service.NewCategoryService(mockStore) 51 | r, e := cs.CreateCategory(&model.Category{Name: "Test"}) 52 | assert.NotNil(t, e) 53 | assert.Nil(t, r) 54 | 55 | mockStore = mock.NewMockStore(mockCtrl) 56 | tx := new(sql.Tx) 57 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 58 | mockStore.EXPECT().CreateCategory(tx, &model.Category{Name: "Test"}).Return(nil, errors.New("test")).Times(1) 59 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 60 | cs = service.NewCategoryService(mockStore) 61 | r, e = cs.CreateCategory(&model.Category{Name: "Test"}) 62 | assert.NotNil(t, e) 63 | assert.Nil(t, r) 64 | 65 | mockStore = mock.NewMockStore(mockCtrl) 66 | tx = new(sql.Tx) 67 | var id = 1 68 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 69 | mockStore.EXPECT().CreateCategory(tx, &model.Category{Name: "Test"}).Return(&id, nil).Times(1) 70 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 71 | cs = service.NewCategoryService(mockStore) 72 | r, e = cs.CreateCategory(&model.Category{Name: "Test"}) 73 | assert.Nil(t, e) 74 | assert.NotNil(t, r) 75 | } 76 | 77 | func TestCategoryService_UpdateCategory(t *testing.T) { 78 | mockCtrl := gomock.NewController(t) 79 | defer mockCtrl.Finish() 80 | 81 | mockStore := mock.NewMockStore(mockCtrl) 82 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 83 | cs := service.NewCategoryService(mockStore) 84 | e := cs.UpdateCategory(nil) 85 | assert.NotNil(t, e) 86 | 87 | mockStore = mock.NewMockStore(mockCtrl) 88 | tx := new(sql.Tx) 89 | cat := &model.Category{1, "test"} 90 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 91 | mockStore.EXPECT().UpdateCategory(tx, cat).Return(errors.New("test")).Times(1) 92 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 93 | cs = service.NewCategoryService(mockStore) 94 | e = cs.UpdateCategory(cat) 95 | assert.NotNil(t, e) 96 | 97 | mockStore = mock.NewMockStore(mockCtrl) 98 | tx = new(sql.Tx) 99 | cat = &model.Category{1, "test"} 100 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 101 | mockStore.EXPECT().UpdateCategory(tx, cat).Return(nil).Times(1) 102 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 103 | cs = service.NewCategoryService(mockStore) 104 | e = cs.UpdateCategory(cat) 105 | assert.Nil(t, e) 106 | } 107 | 108 | func TestCategoryService_DeleteCategory(t *testing.T) { 109 | mockCtrl := gomock.NewController(t) 110 | defer mockCtrl.Finish() 111 | 112 | mockStore := mock.NewMockStore(mockCtrl) 113 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 114 | cs := service.NewCategoryService(mockStore) 115 | e := cs.DeleteCategory(1) 116 | assert.NotNil(t, e) 117 | 118 | mockStore = mock.NewMockStore(mockCtrl) 119 | tx := new(sql.Tx) 120 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 121 | mockStore.EXPECT().DeleteCategory(tx, 1).Return(errors.New("test")).Times(1) 122 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 123 | cs = service.NewCategoryService(mockStore) 124 | e = cs.DeleteCategory(1) 125 | assert.NotNil(t, e) 126 | 127 | mockStore = mock.NewMockStore(mockCtrl) 128 | tx = new(sql.Tx) 129 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 130 | mockStore.EXPECT().DeleteCategory(tx, 1).Return(nil).Times(1) 131 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 132 | cs = service.NewCategoryService(mockStore) 133 | e = cs.DeleteCategory(1) 134 | assert.Nil(t, e) 135 | } 136 | -------------------------------------------------------------------------------- /test/config_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "echo-rest-api/config" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestConfig_NewConfig(t *testing.T) { 10 | c, err := config.NewConfig("../config/config.yaml") 11 | assert.Nil(t, err) 12 | assert.NotNil(t, c) 13 | } 14 | -------------------------------------------------------------------------------- /test/mock/category_service_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: category_service.go 3 | 4 | // Package test is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | model "echo-rest-api/model" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockCategoryService is a mock of CategoryService interface 14 | type MockCategoryService struct { 15 | ctrl *gomock.Controller 16 | recorder *MockCategoryServiceMockRecorder 17 | } 18 | 19 | // MockCategoryServiceMockRecorder is the mock recorder for MockCategoryService 20 | type MockCategoryServiceMockRecorder struct { 21 | mock *MockCategoryService 22 | } 23 | 24 | // NewMockCategoryService creates a new mock instance 25 | func NewMockCategoryService(ctrl *gomock.Controller) *MockCategoryService { 26 | mock := &MockCategoryService{ctrl: ctrl} 27 | mock.recorder = &MockCategoryServiceMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockCategoryService) EXPECT() *MockCategoryServiceMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetCategory mocks base method 37 | func (m *MockCategoryService) GetCategory(id int) (*model.Category, error) { 38 | ret := m.ctrl.Call(m, "GetCategory", id) 39 | ret0, _ := ret[0].(*model.Category) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetCategory indicates an expected call of GetCategory 45 | func (mr *MockCategoryServiceMockRecorder) GetCategory(id interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockCategoryService)(nil).GetCategory), id) 47 | } 48 | 49 | // GetCategories mocks base method 50 | func (m *MockCategoryService) GetCategories() ([]*model.Category, error) { 51 | ret := m.ctrl.Call(m, "GetCategories") 52 | ret0, _ := ret[0].([]*model.Category) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // GetCategories indicates an expected call of GetCategories 58 | func (mr *MockCategoryServiceMockRecorder) GetCategories() *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategories", reflect.TypeOf((*MockCategoryService)(nil).GetCategories)) 60 | } 61 | 62 | // CreateCategory mocks base method 63 | func (m *MockCategoryService) CreateCategory(category *model.Category) (*int, error) { 64 | ret := m.ctrl.Call(m, "CreateCategory", category) 65 | ret0, _ := ret[0].(*int) 66 | ret1, _ := ret[1].(error) 67 | return ret0, ret1 68 | } 69 | 70 | // CreateCategory indicates an expected call of CreateCategory 71 | func (mr *MockCategoryServiceMockRecorder) CreateCategory(category interface{}) *gomock.Call { 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCategory", reflect.TypeOf((*MockCategoryService)(nil).CreateCategory), category) 73 | } 74 | 75 | // UpdateCategory mocks base method 76 | func (m *MockCategoryService) UpdateCategory(category *model.Category) error { 77 | ret := m.ctrl.Call(m, "UpdateCategory", category) 78 | ret0, _ := ret[0].(error) 79 | return ret0 80 | } 81 | 82 | // UpdateCategory indicates an expected call of UpdateCategory 83 | func (mr *MockCategoryServiceMockRecorder) UpdateCategory(category interface{}) *gomock.Call { 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCategory", reflect.TypeOf((*MockCategoryService)(nil).UpdateCategory), category) 85 | } 86 | 87 | // DeleteCategory mocks base method 88 | func (m *MockCategoryService) DeleteCategory(id int) error { 89 | ret := m.ctrl.Call(m, "DeleteCategory", id) 90 | ret0, _ := ret[0].(error) 91 | return ret0 92 | } 93 | 94 | // DeleteCategory indicates an expected call of DeleteCategory 95 | func (mr *MockCategoryServiceMockRecorder) DeleteCategory(id interface{}) *gomock.Call { 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCategory", reflect.TypeOf((*MockCategoryService)(nil).DeleteCategory), id) 97 | } 98 | -------------------------------------------------------------------------------- /test/mock/product_service_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: product_service.go 3 | 4 | // Package test is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | model "echo-rest-api/model" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockProductService is a mock of ProductService interface 14 | type MockProductService struct { 15 | ctrl *gomock.Controller 16 | recorder *MockProductServiceMockRecorder 17 | } 18 | 19 | // MockProductServiceMockRecorder is the mock recorder for MockProductService 20 | type MockProductServiceMockRecorder struct { 21 | mock *MockProductService 22 | } 23 | 24 | // NewMockProductService creates a new mock instance 25 | func NewMockProductService(ctrl *gomock.Controller) *MockProductService { 26 | mock := &MockProductService{ctrl: ctrl} 27 | mock.recorder = &MockProductServiceMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockProductService) EXPECT() *MockProductServiceMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetProduct mocks base method 37 | func (m *MockProductService) GetProduct(id int) (*model.Product, error) { 38 | ret := m.ctrl.Call(m, "GetProduct", id) 39 | ret0, _ := ret[0].(*model.Product) 40 | ret1, _ := ret[1].(error) 41 | return ret0, ret1 42 | } 43 | 44 | // GetProduct indicates an expected call of GetProduct 45 | func (mr *MockProductServiceMockRecorder) GetProduct(id interface{}) *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProduct", reflect.TypeOf((*MockProductService)(nil).GetProduct), id) 47 | } 48 | 49 | // GetProducts mocks base method 50 | func (m *MockProductService) GetProducts(category *int) ([]*model.Product, error) { 51 | ret := m.ctrl.Call(m, "GetProducts", category) 52 | ret0, _ := ret[0].([]*model.Product) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // GetProducts indicates an expected call of GetProducts 58 | func (mr *MockProductServiceMockRecorder) GetProducts(category interface{}) *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProducts", reflect.TypeOf((*MockProductService)(nil).GetProducts), category) 60 | } 61 | 62 | // CreateProduct mocks base method 63 | func (m *MockProductService) CreateProduct(product *model.Product) (*int, error) { 64 | ret := m.ctrl.Call(m, "CreateProduct", product) 65 | ret0, _ := ret[0].(*int) 66 | ret1, _ := ret[1].(error) 67 | return ret0, ret1 68 | } 69 | 70 | // CreateProduct indicates an expected call of CreateProduct 71 | func (mr *MockProductServiceMockRecorder) CreateProduct(product interface{}) *gomock.Call { 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProduct", reflect.TypeOf((*MockProductService)(nil).CreateProduct), product) 73 | } 74 | 75 | // UpdateProduct mocks base method 76 | func (m *MockProductService) UpdateProduct(product *model.Product) error { 77 | ret := m.ctrl.Call(m, "UpdateProduct", product) 78 | ret0, _ := ret[0].(error) 79 | return ret0 80 | } 81 | 82 | // UpdateProduct indicates an expected call of UpdateProduct 83 | func (mr *MockProductServiceMockRecorder) UpdateProduct(product interface{}) *gomock.Call { 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProduct", reflect.TypeOf((*MockProductService)(nil).UpdateProduct), product) 85 | } 86 | 87 | // DeleteProduct mocks base method 88 | func (m *MockProductService) DeleteProduct(id int) error { 89 | ret := m.ctrl.Call(m, "DeleteProduct", id) 90 | ret0, _ := ret[0].(error) 91 | return ret0 92 | } 93 | 94 | // DeleteProduct indicates an expected call of DeleteProduct 95 | func (mr *MockProductServiceMockRecorder) DeleteProduct(id interface{}) *gomock.Call { 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProduct", reflect.TypeOf((*MockProductService)(nil).DeleteProduct), id) 97 | } 98 | -------------------------------------------------------------------------------- /test/mock/store_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: store.go 3 | 4 | // Package test is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | sql "database/sql" 9 | model "echo-rest-api/model" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockStore is a mock of Store interface 15 | type MockStore struct { 16 | ctrl *gomock.Controller 17 | recorder *MockStoreMockRecorder 18 | } 19 | 20 | // MockStoreMockRecorder is the mock recorder for MockStore 21 | type MockStoreMockRecorder struct { 22 | mock *MockStore 23 | } 24 | 25 | // NewMockStore creates a new mock instance 26 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 27 | mock := &MockStore{ctrl: ctrl} 28 | mock.recorder = &MockStoreMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Close mocks base method 38 | func (m *MockStore) Close() error { 39 | ret := m.ctrl.Call(m, "Close") 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // Close indicates an expected call of Close 45 | func (mr *MockStoreMockRecorder) Close() *gomock.Call { 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStore)(nil).Close)) 47 | } 48 | 49 | // Begin mocks base method 50 | func (m *MockStore) Begin() (*sql.Tx, error) { 51 | ret := m.ctrl.Call(m, "Begin") 52 | ret0, _ := ret[0].(*sql.Tx) 53 | ret1, _ := ret[1].(error) 54 | return ret0, ret1 55 | } 56 | 57 | // Begin indicates an expected call of Begin 58 | func (mr *MockStoreMockRecorder) Begin() *gomock.Call { 59 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockStore)(nil).Begin)) 60 | } 61 | 62 | // Commit mocks base method 63 | func (m *MockStore) Commit(tx *sql.Tx) error { 64 | ret := m.ctrl.Call(m, "Commit", tx) 65 | ret0, _ := ret[0].(error) 66 | return ret0 67 | } 68 | 69 | // Commit indicates an expected call of Commit 70 | func (mr *MockStoreMockRecorder) Commit(tx interface{}) *gomock.Call { 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockStore)(nil).Commit), tx) 72 | } 73 | 74 | // Rollback mocks base method 75 | func (m *MockStore) Rollback(tx *sql.Tx) error { 76 | ret := m.ctrl.Call(m, "Rollback", tx) 77 | ret0, _ := ret[0].(error) 78 | return ret0 79 | } 80 | 81 | // Rollback indicates an expected call of Rollback 82 | func (mr *MockStoreMockRecorder) Rollback(tx interface{}) *gomock.Call { 83 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockStore)(nil).Rollback), tx) 84 | } 85 | 86 | // GetCategory mocks base method 87 | func (m *MockStore) GetCategory(tx *sql.Tx, id int) (*model.Category, error) { 88 | ret := m.ctrl.Call(m, "GetCategory", tx, id) 89 | ret0, _ := ret[0].(*model.Category) 90 | ret1, _ := ret[1].(error) 91 | return ret0, ret1 92 | } 93 | 94 | // GetCategory indicates an expected call of GetCategory 95 | func (mr *MockStoreMockRecorder) GetCategory(tx, id interface{}) *gomock.Call { 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategory", reflect.TypeOf((*MockStore)(nil).GetCategory), tx, id) 97 | } 98 | 99 | // GetCategories mocks base method 100 | func (m *MockStore) GetCategories(tx *sql.Tx) ([]*model.Category, error) { 101 | ret := m.ctrl.Call(m, "GetCategories", tx) 102 | ret0, _ := ret[0].([]*model.Category) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetCategories indicates an expected call of GetCategories 108 | func (mr *MockStoreMockRecorder) GetCategories(tx interface{}) *gomock.Call { 109 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCategories", reflect.TypeOf((*MockStore)(nil).GetCategories), tx) 110 | } 111 | 112 | // CreateCategory mocks base method 113 | func (m *MockStore) CreateCategory(tx *sql.Tx, category *model.Category) (*int, error) { 114 | ret := m.ctrl.Call(m, "CreateCategory", tx, category) 115 | ret0, _ := ret[0].(*int) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // CreateCategory indicates an expected call of CreateCategory 121 | func (mr *MockStoreMockRecorder) CreateCategory(tx, category interface{}) *gomock.Call { 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCategory", reflect.TypeOf((*MockStore)(nil).CreateCategory), tx, category) 123 | } 124 | 125 | // UpdateCategory mocks base method 126 | func (m *MockStore) UpdateCategory(tx *sql.Tx, category *model.Category) error { 127 | ret := m.ctrl.Call(m, "UpdateCategory", tx, category) 128 | ret0, _ := ret[0].(error) 129 | return ret0 130 | } 131 | 132 | // UpdateCategory indicates an expected call of UpdateCategory 133 | func (mr *MockStoreMockRecorder) UpdateCategory(tx, category interface{}) *gomock.Call { 134 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCategory", reflect.TypeOf((*MockStore)(nil).UpdateCategory), tx, category) 135 | } 136 | 137 | // DeleteCategory mocks base method 138 | func (m *MockStore) DeleteCategory(tx *sql.Tx, id int) error { 139 | ret := m.ctrl.Call(m, "DeleteCategory", tx, id) 140 | ret0, _ := ret[0].(error) 141 | return ret0 142 | } 143 | 144 | // DeleteCategory indicates an expected call of DeleteCategory 145 | func (mr *MockStoreMockRecorder) DeleteCategory(tx, id interface{}) *gomock.Call { 146 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCategory", reflect.TypeOf((*MockStore)(nil).DeleteCategory), tx, id) 147 | } 148 | 149 | // GetProduct mocks base method 150 | func (m *MockStore) GetProduct(tx *sql.Tx, id int) (*model.Product, error) { 151 | ret := m.ctrl.Call(m, "GetProduct", tx, id) 152 | ret0, _ := ret[0].(*model.Product) 153 | ret1, _ := ret[1].(error) 154 | return ret0, ret1 155 | } 156 | 157 | // GetProduct indicates an expected call of GetProduct 158 | func (mr *MockStoreMockRecorder) GetProduct(tx, id interface{}) *gomock.Call { 159 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProduct", reflect.TypeOf((*MockStore)(nil).GetProduct), tx, id) 160 | } 161 | 162 | // GetProducts mocks base method 163 | func (m *MockStore) GetProducts(tx *sql.Tx, category *int) ([]*model.Product, error) { 164 | ret := m.ctrl.Call(m, "GetProducts", tx, category) 165 | ret0, _ := ret[0].([]*model.Product) 166 | ret1, _ := ret[1].(error) 167 | return ret0, ret1 168 | } 169 | 170 | // GetProducts indicates an expected call of GetProducts 171 | func (mr *MockStoreMockRecorder) GetProducts(tx, category interface{}) *gomock.Call { 172 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProducts", reflect.TypeOf((*MockStore)(nil).GetProducts), tx, category) 173 | } 174 | 175 | // CreateProduct mocks base method 176 | func (m *MockStore) CreateProduct(tx *sql.Tx, product *model.Product) (*int, error) { 177 | ret := m.ctrl.Call(m, "CreateProduct", tx, product) 178 | ret0, _ := ret[0].(*int) 179 | ret1, _ := ret[1].(error) 180 | return ret0, ret1 181 | } 182 | 183 | // CreateProduct indicates an expected call of CreateProduct 184 | func (mr *MockStoreMockRecorder) CreateProduct(tx, product interface{}) *gomock.Call { 185 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProduct", reflect.TypeOf((*MockStore)(nil).CreateProduct), tx, product) 186 | } 187 | 188 | // UpdateProduct mocks base method 189 | func (m *MockStore) UpdateProduct(tx *sql.Tx, product *model.Product) error { 190 | ret := m.ctrl.Call(m, "UpdateProduct", tx, product) 191 | ret0, _ := ret[0].(error) 192 | return ret0 193 | } 194 | 195 | // UpdateProduct indicates an expected call of UpdateProduct 196 | func (mr *MockStoreMockRecorder) UpdateProduct(tx, product interface{}) *gomock.Call { 197 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProduct", reflect.TypeOf((*MockStore)(nil).UpdateProduct), tx, product) 198 | } 199 | 200 | // DeleteProduct mocks base method 201 | func (m *MockStore) DeleteProduct(tx *sql.Tx, id int) error { 202 | ret := m.ctrl.Call(m, "DeleteProduct", tx, id) 203 | ret0, _ := ret[0].(error) 204 | return ret0 205 | } 206 | 207 | // DeleteProduct indicates an expected call of DeleteProduct 208 | func (mr *MockStoreMockRecorder) DeleteProduct(tx, id interface{}) *gomock.Call { 209 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProduct", reflect.TypeOf((*MockStore)(nil).DeleteProduct), tx, id) 210 | } 211 | -------------------------------------------------------------------------------- /test/product_service_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "database/sql" 5 | "echo-rest-api/model" 6 | "echo-rest-api/service" 7 | "echo-rest-api/test/mock" 8 | "errors" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | ) 13 | 14 | func TestProductService_GetProduct(t *testing.T) { 15 | mockCtrl := gomock.NewController(t) 16 | defer mockCtrl.Finish() 17 | mockStore := mock.NewMockStore(mockCtrl) 18 | mockStore.EXPECT().GetProduct(nil, 1).Return(nil, errors.New("test")).Times(1) 19 | mockStore.EXPECT().GetProduct(nil, 2).Return(&model.Product{Id: 2, Name: "test"}, nil).Times(1) 20 | ps := service.NewProductService(mockStore) 21 | r, e := ps.GetProduct(1) 22 | assert.NotNil(t, e) 23 | assert.Nil(t, r) 24 | r, e = ps.GetProduct(2) 25 | assert.Nil(t, e) 26 | assert.NotNil(t, r) 27 | } 28 | 29 | func TestProductService_GetProducts(t *testing.T) { 30 | mockCtrl := gomock.NewController(t) 31 | defer mockCtrl.Finish() 32 | mockStore := mock.NewMockStore(mockCtrl) 33 | mockStore.EXPECT().GetProducts(nil, nil).Return(nil, errors.New("test")).Times(1) 34 | ps := service.NewProductService(mockStore) 35 | r, e := ps.GetProducts(nil) 36 | assert.NotNil(t, e) 37 | assert.Nil(t, r) 38 | mockStore.EXPECT().GetProducts(nil, nil).Return([]*model.Product{}, nil).Times(1) 39 | r, e = ps.GetProducts(nil) 40 | assert.Nil(t, e) 41 | assert.NotNil(t, r) 42 | } 43 | 44 | func TestProductService_CreateProduct(t *testing.T) { 45 | mockCtrl := gomock.NewController(t) 46 | defer mockCtrl.Finish() 47 | 48 | mockStore := mock.NewMockStore(mockCtrl) 49 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 50 | ps := service.NewProductService(mockStore) 51 | r, e := ps.CreateProduct(&model.Product{Name: "test"}) 52 | assert.NotNil(t, e) 53 | assert.Nil(t, r) 54 | 55 | mockStore = mock.NewMockStore(mockCtrl) 56 | tx := new(sql.Tx) 57 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 58 | mockStore.EXPECT().CreateProduct(tx, &model.Product{Name: "test"}).Return(nil, errors.New("test")).Times(1) 59 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 60 | ps = service.NewProductService(mockStore) 61 | r, e = ps.CreateProduct(&model.Product{Name: "test"}) 62 | assert.NotNil(t, e) 63 | assert.Nil(t, r) 64 | 65 | mockStore = mock.NewMockStore(mockCtrl) 66 | tx = new(sql.Tx) 67 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 68 | var id = 1 69 | mockStore.EXPECT().CreateProduct(tx, &model.Product{Name: "test"}).Return(&id, nil).Times(1) 70 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 71 | ps = service.NewProductService(mockStore) 72 | r, e = ps.CreateProduct(&model.Product{Name: "test"}) 73 | assert.Nil(t, e) 74 | assert.NotNil(t, r) 75 | } 76 | 77 | func TestProductService_UpdateProduct(t *testing.T) { 78 | mockCtrl := gomock.NewController(t) 79 | defer mockCtrl.Finish() 80 | 81 | mockStore := mock.NewMockStore(mockCtrl) 82 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 83 | ps := service.NewProductService(mockStore) 84 | e := ps.UpdateProduct(nil) 85 | assert.NotNil(t, e) 86 | 87 | mockStore = mock.NewMockStore(mockCtrl) 88 | tx := new(sql.Tx) 89 | prod := &model.Product{Id: 1, Name: "test"} 90 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 91 | mockStore.EXPECT().UpdateProduct(tx, prod).Return(errors.New("test")).Times(1) 92 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 93 | ps = service.NewProductService(mockStore) 94 | e = ps.UpdateProduct(prod) 95 | assert.NotNil(t, e) 96 | 97 | mockStore = mock.NewMockStore(mockCtrl) 98 | tx = new(sql.Tx) 99 | prod = &model.Product{Id: 1, Name: "test"} 100 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 101 | mockStore.EXPECT().UpdateProduct(tx, prod).Return(nil).Times(1) 102 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 103 | ps = service.NewProductService(mockStore) 104 | e = ps.UpdateProduct(prod) 105 | assert.Nil(t, e) 106 | } 107 | 108 | func TestProductService_DeleteProduct(t *testing.T) { 109 | mockCtrl := gomock.NewController(t) 110 | defer mockCtrl.Finish() 111 | 112 | mockStore := mock.NewMockStore(mockCtrl) 113 | mockStore.EXPECT().Begin().Return(nil, errors.New("test")).Times(1) 114 | ps := service.NewProductService(mockStore) 115 | e := ps.DeleteProduct(1) 116 | assert.NotNil(t, e) 117 | 118 | mockStore = mock.NewMockStore(mockCtrl) 119 | tx := new(sql.Tx) 120 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 121 | mockStore.EXPECT().DeleteProduct(tx, 1).Return(errors.New("test")).Times(1) 122 | mockStore.EXPECT().Rollback(tx).Return(nil).Times(1) 123 | ps = service.NewProductService(mockStore) 124 | e = ps.DeleteProduct(1) 125 | assert.NotNil(t, e) 126 | 127 | mockStore = mock.NewMockStore(mockCtrl) 128 | tx = new(sql.Tx) 129 | mockStore.EXPECT().Begin().Return(tx, nil).Times(1) 130 | mockStore.EXPECT().DeleteProduct(tx, 1).Return(nil).Times(1) 131 | mockStore.EXPECT().Commit(tx).Return(nil).Times(1) 132 | ps = service.NewProductService(mockStore) 133 | e = ps.DeleteProduct(1) 134 | assert.Nil(t, e) 135 | } 136 | -------------------------------------------------------------------------------- /test/store_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "echo-rest-api/config" 5 | "echo-rest-api/model" 6 | "echo-rest-api/store" 7 | _ "github.com/lib/pq" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | var st store.Store 13 | 14 | func init() { 15 | conf, _ := config.NewConfig("../config/config.yaml") 16 | st, _ = store.NewStore(conf) 17 | } 18 | 19 | func TestStore_CreateCategory(t *testing.T) { 20 | tx, _ := st.Begin() 21 | defer st.Rollback(tx) 22 | id, err := st.CreateCategory(tx, &model.Category{Name: "text"}) 23 | assert.NoError(t, err) 24 | assert.NotNil(t, id) 25 | } 26 | 27 | func TestStore_GetCategory(t *testing.T) { 28 | c, err := st.GetCategory(nil, -1) 29 | assert.Nil(t, err) 30 | assert.Nil(t, c) 31 | tx, _ := st.Begin() 32 | defer st.Rollback(tx) 33 | id, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 34 | cat, _ := st.GetCategory(tx, *id) 35 | assert.Equal(t, *id, cat.Id) 36 | } 37 | 38 | func TestStore_GetCategories(t *testing.T) { 39 | _, err := st.GetCategories(nil) 40 | assert.NoError(t, err) 41 | tx, _ := st.Begin() 42 | defer st.Rollback(tx) 43 | st.CreateCategory(tx, &model.Category{Name: "text"}) 44 | res, _ := st.GetCategories(tx) 45 | assert.NotEmpty(t, res) 46 | } 47 | 48 | func TestStore_UpdateCategory(t *testing.T) { 49 | tx, _ := st.Begin() 50 | defer st.Rollback(tx) 51 | id, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 52 | cat, _ := st.GetCategory(tx, *id) 53 | cat.Name = "text2" 54 | st.UpdateCategory(tx, cat) 55 | cat2, _ := st.GetCategory(tx, cat.Id) 56 | assert.Equal(t, cat2.Name, "text2") 57 | } 58 | 59 | func TestStore_DeleteCategory(t *testing.T) { 60 | tx, _ := st.Begin() 61 | defer st.Rollback(tx) 62 | id, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 63 | st.DeleteCategory(tx, *id) 64 | cat2, _ := st.GetCategory(tx, *id) 65 | assert.Nil(t, cat2) 66 | } 67 | 68 | func TestStore_CreateProduct(t *testing.T) { 69 | tx, _ := st.Begin() 70 | defer st.Rollback(tx) 71 | category, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 72 | p, err := st.CreateProduct(tx, &model.Product{Name: "test_name", Description: "test_description", Category: *category, Price: 65.5}) 73 | assert.NoError(t, err) 74 | product, err := st.GetProduct(tx, *p) 75 | assert.NotNil(t, product) 76 | assert.Equal(t, product.Name, "test_name") 77 | assert.Equal(t, product.Description, "test_description") 78 | assert.Equal(t, product.Category, *category) 79 | assert.Equal(t, product.Price, 65.5) 80 | } 81 | 82 | func TestStore_GetProduct(t *testing.T) { 83 | tx, _ := st.Begin() 84 | defer st.Rollback(tx) 85 | category, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 86 | p, _ := st.CreateProduct(tx, &model.Product{Name: "test_name", Description: "test_description", Category: *category, Price: 65.5}) 87 | ps, err := st.GetProduct(tx, *p) 88 | assert.Nil(t, err) 89 | assert.NotNil(t, ps) 90 | ps, err = st.GetProduct(tx, -1) 91 | assert.Nil(t, err) 92 | assert.Nil(t, ps) 93 | } 94 | 95 | func TestStore_GetProducts(t *testing.T) { 96 | tx, _ := st.Begin() 97 | defer st.Rollback(tx) 98 | category, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 99 | st.CreateProduct(tx, &model.Product{Name: "test_name", Description: "test_description", Category: *category, Price: 65.5}) 100 | st.CreateProduct(tx, &model.Product{Name: "test_name2", Description: "test_description2", Category: *category, Price: 65.52}) 101 | ps, err := st.GetProducts(tx, nil) 102 | assert.Nil(t, err) 103 | assert.NotEmpty(t, ps) 104 | ps, _ = st.GetProducts(tx, category) 105 | assert.Len(t, ps, 2) 106 | } 107 | 108 | func TestStore_UpdateProduct(t *testing.T) { 109 | tx, _ := st.Begin() 110 | defer st.Rollback(tx) 111 | category, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 112 | category2, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 113 | id, _ := st.CreateProduct(tx, &model.Product{Name: "test_name", Description: "test_description", Category: *category, Price: 65.5}) 114 | p, _ := st.GetProduct(tx, *id) 115 | p.Name = "test_name2" 116 | p.Description = "test_description2" 117 | p.Category = *category2 118 | p.Price = 65.7 119 | err := st.UpdateProduct(tx, p) 120 | assert.Nil(t, err) 121 | p2, _ := st.GetProduct(tx, p.Id) 122 | assert.Equal(t, p2.Name, "test_name2") 123 | assert.Equal(t, p2.Description, "test_description2") 124 | assert.Equal(t, p2.Category, *category2) 125 | assert.Equal(t, p2.Price, 65.7) 126 | } 127 | 128 | func TestStore_DeleteProduct(t *testing.T) { 129 | tx, _ := st.Begin() 130 | defer st.Rollback(tx) 131 | id, _ := st.CreateCategory(tx, &model.Category{Name: "text"}) 132 | product, _ := st.CreateProduct(tx, &model.Product{Name: "test_name", Description: "test_description", Category: *id, Price: 65.5}) 133 | err := st.DeleteProduct(tx, *product) 134 | assert.Nil(t, err) 135 | p, _ := st.GetProduct(tx, *product) 136 | assert.Nil(t, p) 137 | } 138 | 139 | // &model.Category{Name:"text"} 140 | // 141 | --------------------------------------------------------------------------------