├── .gitignore ├── datastore ├── datastore.go ├── beers.go └── database │ ├── database.go │ └── beers.go ├── migrations └── 00001-create-beers.sql ├── config ├── config_test.go └── config.go ├── model └── beer.go ├── Gopkg.toml ├── Makefile ├── main.go ├── README.md ├── controller ├── beers.go └── beers_test.go └── Gopkg.lock /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | coverage.txt 3 | -------------------------------------------------------------------------------- /datastore/datastore.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | type Datastore interface { 4 | BeersDatastore 5 | } 6 | -------------------------------------------------------------------------------- /migrations/00001-create-beers.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE seq_beers; 2 | CREATE TABLE beers( 3 | id bigint primary key default nextval('seq_beers'), 4 | name varchar(60) not null, 5 | price double precision, 6 | created_at timestamp default current_timestamp, 7 | updated_at timestamp default current_timestamp 8 | ); 9 | -------------------------------------------------------------------------------- /datastore/beers.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import "github.com/caarlos0/go-web-api-example/model" 4 | 5 | type BeersDatastore interface { 6 | AllBeers() (beers []model.Beer, err error) 7 | GetBeer(id int64) (beer model.Beer, err error) 8 | CreateBeer(beer model.Beer) (err error) 9 | DeleteBeer(id int64) (err error) 10 | } 11 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLoadDefaultConfig(t *testing.T) { 10 | var assert = assert.New(t) 11 | var cfg = Get() 12 | assert.Equal("3000", cfg.Port) 13 | assert.Equal("postgres://localhost:5432/beers?sslmode=disable", cfg.DatabaseURL) 14 | } 15 | -------------------------------------------------------------------------------- /model/beer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // Beer is a beer 6 | type Beer struct { 7 | ID int64 `json:"id" db:"id"` 8 | Name string `json:"name" db:"name"` 9 | Price float64 `json:"price" db:"price"` 10 | CreatedAt time.Time `json:"createdAt" db:"created_at"` 11 | UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` 12 | } 13 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/apex/log" 5 | "github.com/caarlos0/env" 6 | ) 7 | 8 | // Config of the app 9 | type Config struct { 10 | Port string `env:"PORT" envDefault:"3000"` 11 | DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/beers?sslmode=disable"` 12 | } 13 | 14 | // Get the config 15 | func Get() (cfg Config) { 16 | if err := env.Parse(&cfg); err != nil { 17 | log.WithError(err).Fatal("failed to load config") 18 | } 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /datastore/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/apex/log" 7 | "github.com/caarlos0/go-web-api-example/datastore" 8 | "github.com/jmoiron/sqlx" 9 | ) 10 | 11 | func Connect(url string) *sql.DB { 12 | var log = log.WithField("url", url) 13 | db, err := sql.Open("postgres", url) 14 | if err != nil { 15 | log.WithError(err).Fatal("failed to open connection to database") 16 | } 17 | if err := db.Ping(); err != nil { 18 | log.WithError(err).Fatal("failed to ping database") 19 | } 20 | return db 21 | } 22 | 23 | func New(db *sql.DB) datastore.Datastore { 24 | var dbx = sqlx.NewDb(db, "postgres") 25 | return struct { 26 | *beerstore 27 | }{ 28 | &beerstore{dbx}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /datastore/database/beers.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/caarlos0/go-web-api-example/model" 5 | "github.com/jmoiron/sqlx" 6 | ) 7 | 8 | type beerstore struct { 9 | *sqlx.DB 10 | } 11 | 12 | func (db *beerstore) AllBeers() (beers []model.Beer, err error) { 13 | return beers, db.Select(&beers, "SELECT * FROM beers") 14 | } 15 | 16 | func (db *beerstore) GetBeer(id int64) (beer model.Beer, err error) { 17 | return beer, db.Get(&beer, "SELECT * FROM beers WHERE id = $1", id) 18 | } 19 | 20 | func (db *beerstore) CreateBeer(beer model.Beer) (err error) { 21 | _, err = db.Exec( 22 | "INSERT INTO beers(name, price) VALUES($1, $2)", 23 | beer.Name, 24 | beer.Price, 25 | ) 26 | return 27 | } 28 | 29 | func (db *beerstore) DeleteBeer(id int64) (err error) { 30 | _, err = db.Exec("DELETE FROM beers WHERE id = $1", id) 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/apex/httplog" 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/apex/log" 31 | 32 | [[constraint]] 33 | name = "github.com/caarlos0/env" 34 | version = "3.0.0" 35 | 36 | [[constraint]] 37 | name = "github.com/gorilla/mux" 38 | version = "1.4.0" 39 | 40 | [[constraint]] 41 | branch = "master" 42 | name = "github.com/jmoiron/sqlx" 43 | 44 | [[constraint]] 45 | branch = "master" 46 | name = "github.com/lib/pq" 47 | 48 | [[constraint]] 49 | name = "github.com/pkg/errors" 50 | version = "0.8.0" 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) 2 | TEST_PATTERN?=. 3 | TEST_OPTIONS?= 4 | 5 | setup: ## Install all the build and lint dependencies 6 | go get -u github.com/alecthomas/gometalinter 7 | go get -u github.com/golang/dep/cmd/dep 8 | go get -u github.com/pierrre/gotestcover 9 | go get -u golang.org/x/tools/cmd/cover 10 | dep ensure 11 | gometalinter --install 12 | 13 | test: ## Run all the tests 14 | gotestcover $(TEST_OPTIONS) -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m 15 | 16 | cover: test ## Run all the tests and opens the coverage report 17 | go tool cover -html=coverage.txt 18 | 19 | fmt: ## gofmt and goimports all go files 20 | find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done 21 | 22 | lint: ## Run all the linters 23 | gometalinter --vendor --disable-all \ 24 | --enable=deadcode \ 25 | --enable=ineffassign \ 26 | --enable=gosimple \ 27 | --enable=staticcheck \ 28 | --enable=gofmt \ 29 | --enable=goimports \ 30 | --enable=dupl \ 31 | --enable=misspell \ 32 | --enable=errcheck \ 33 | --enable=vet \ 34 | --enable=vetshadow \ 35 | --deadline=10m \ 36 | ./... 37 | 38 | ci: lint test ## Run all the tests and code checks 39 | 40 | # Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 41 | help: 42 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 43 | 44 | .DEFAULT_GOAL := build 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/apex/httplog" 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/logfmt" 10 | "github.com/caarlos0/go-web-api-example/config" 11 | "github.com/caarlos0/go-web-api-example/controller" 12 | "github.com/caarlos0/go-web-api-example/datastore/database" 13 | "github.com/gorilla/mux" 14 | _ "github.com/lib/pq" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | func main() { 19 | log.SetHandler(logfmt.Default) 20 | log.SetLevel(log.InfoLevel) 21 | log.Info("starting up...") 22 | var cfg = config.Get() 23 | 24 | var db = database.Connect(cfg.DatabaseURL) 25 | defer func() { 26 | if err := db.Close(); err != nil { 27 | log.WithError(err).Error("failed to close database connections") 28 | } 29 | }() 30 | var ds = database.New(db) 31 | 32 | var mux = mux.NewRouter() 33 | mux.Path("/metrics").Handler(promhttp.Handler()) 34 | 35 | var beersMux = mux.PathPrefix("/beers").Subrouter() 36 | beersMux.Methods(http.MethodGet).HandlerFunc(controller.BeersIndex(ds)) 37 | beersMux.Methods(http.MethodPost).HandlerFunc(controller.CreateBeer(ds)) 38 | beersMux.Path("{id}").Methods(http.MethodGet).HandlerFunc(controller.GetBeer(ds)) 39 | beersMux.Path("{id}").Methods(http.MethodDelete).HandlerFunc(controller.DeleteBeer(ds)) 40 | 41 | var server = &http.Server{ 42 | Handler: httplog.New(mux), 43 | Addr: ":" + cfg.Port, 44 | WriteTimeout: 15 * time.Second, 45 | ReadTimeout: 15 * time.Second, 46 | } 47 | log.WithField("addr", server.Addr).Info("started") 48 | if err := server.ListenAndServe(); err != nil { 49 | log.WithError(err).Fatal("failed to start up server") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-web-api-example 2 | 3 | ## Why? 4 | 5 | The idea is to provide a sample architecture for web API's written in Go, 6 | using PostgreSQL as database and Prometheus for metrics. 7 | 8 | ## What it depends on? 9 | 10 | - gorilla/mux for better routing 11 | - apex/log and apex/httplog for logging 12 | - caarlos0/env for environment config parsing 13 | - prometheus/client_golang for prometheus metrics 14 | - jmoiron/sqlx for improved sql 15 | - lib/pq as postgresql driver 16 | 17 | ## How is it organized? 18 | 19 | ```console 20 | $ tree -d . 21 | . 22 | ├── config 23 | ├── controller 24 | ├── datastore 25 | │   └── database 26 | ├── migrations 27 | └── model 28 | ``` 29 | 30 | - `config`: contains the config parsing logic; 31 | - `controller`: contains the controllers, which usually take an instance 32 | of the datasource as parameter; 33 | - `datasource`: contains the interfaces that define the access to the database; 34 | - `datasource/database`: contains the real implementation of the `datasource` 35 | interfaces; 36 | - `migrations`: simple SQL files to setup the database; 37 | - `model`: contains the models, which are usually shared accross several 38 | packages. 39 | 40 | ## Clonning, installing the dependencies, etc 41 | 42 | Golang needs packages to be placed in the right folders inside `$GOPATH` 43 | to work properly: 44 | 45 | ```console 46 | $ cd $GOPATH 47 | $ mkdir -p src/github.com/caarlos0 48 | $ cd $_ 49 | $ git clone git@github.com:caarlos0/go-web-api-example.git 50 | $ cd go-web-api-example 51 | ``` 52 | 53 | To install all dependencies, just run: 54 | 55 | ```console 56 | $ make setup 57 | ``` 58 | 59 | There are several tasks available, try `make help` 60 | 61 | ## Running locally 62 | 63 | #### 1. Create and migrate the database 64 | 65 | ```console 66 | $ createdb beers 67 | $ for sql in ./migrations/*; do psql ab -f $sql; done 68 | ``` 69 | 70 | #### 2. Start it up 71 | 72 | ```console 73 | $ go run main.go 74 | ``` 75 | 76 | It should be alive at port 3000! 77 | -------------------------------------------------------------------------------- /controller/beers.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/apex/log" 9 | "github.com/caarlos0/go-web-api-example/datastore" 10 | "github.com/caarlos0/go-web-api-example/model" 11 | "github.com/gorilla/mux" 12 | "github.com/pkg/errors" 13 | "github.com/prometheus/client_golang/prometheus" 14 | ) 15 | 16 | var beersCounter = prometheus.NewCounter(prometheus.CounterOpts{ 17 | Name: "beers_created", 18 | Help: "Number of beers created", 19 | }) 20 | 21 | func init() { 22 | prometheus.MustRegister(beersCounter) 23 | } 24 | 25 | func BeersIndex(ds datastore.BeersDatastore) http.HandlerFunc { 26 | return func(w http.ResponseWriter, r *http.Request) { 27 | beers, err := ds.AllBeers() 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | return 31 | } 32 | if err := json.NewEncoder(w).Encode(beers); err != nil { 33 | http.Error(w, err.Error(), http.StatusInternalServerError) 34 | } 35 | } 36 | } 37 | 38 | func CreateBeer(ds datastore.BeersDatastore) http.HandlerFunc { 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | var beer model.Beer 41 | defer func() { 42 | if err := r.Body.Close(); err != nil { 43 | log.WithError(err).Error("failed to close body") 44 | } 45 | }() 46 | if err := json.NewDecoder(r.Body).Decode(&beer); err != nil { 47 | http.Error(w, err.Error(), http.StatusBadRequest) 48 | return 49 | } 50 | if err := ds.CreateBeer(beer); err != nil { 51 | http.Error(w, err.Error(), http.StatusBadRequest) 52 | return 53 | } 54 | w.WriteHeader(http.StatusCreated) 55 | beersCounter.Inc() 56 | } 57 | } 58 | 59 | func DeleteBeer(ds datastore.BeersDatastore) http.HandlerFunc { 60 | return func(w http.ResponseWriter, r *http.Request) { 61 | id, err := getIdFromPath(r) 62 | if err != nil { 63 | http.Error(w, err.Error(), http.StatusBadRequest) 64 | return 65 | } 66 | if err := ds.DeleteBeer(id); err != nil { 67 | http.Error(w, err.Error(), http.StatusBadRequest) 68 | } 69 | } 70 | } 71 | 72 | func GetBeer(ds datastore.BeersDatastore) http.HandlerFunc { 73 | return func(w http.ResponseWriter, r *http.Request) { 74 | id, err := getIdFromPath(r) 75 | if err != nil { 76 | http.Error(w, err.Error(), http.StatusBadRequest) 77 | return 78 | } 79 | beer, err := ds.GetBeer(id) 80 | if err != nil { 81 | http.Error(w, err.Error(), http.StatusBadRequest) 82 | return 83 | } 84 | if err := json.NewEncoder(w).Encode(beer); err != nil { 85 | http.Error(w, err.Error(), http.StatusInternalServerError) 86 | } 87 | } 88 | } 89 | 90 | func getIdFromPath(r *http.Request) (id int64, err error) { 91 | id, err = strconv.ParseInt(mux.Vars(r)["id"], 10, 64) 92 | if err != nil { 93 | return id, errors.Wrap(err, "failed to parse id") 94 | } 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/apex/httplog" 7 | packages = ["."] 8 | revision = "d677fdf2ae1fa75d8111faf17f2d6fcf46dd9af7" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/apex/log" 13 | packages = [".","handlers/logfmt"] 14 | revision = "8f3a15d95392c8fc202d1e1059f46df21dff2992" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/beorn7/perks" 19 | packages = ["quantile"] 20 | revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" 21 | 22 | [[projects]] 23 | name = "github.com/caarlos0/env" 24 | packages = ["."] 25 | revision = "0cf029d5748c52beb2c9d20c81880cb4bdf8f788" 26 | version = "v3.0" 27 | 28 | [[projects]] 29 | name = "github.com/davecgh/go-spew" 30 | packages = ["spew"] 31 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 32 | version = "v1.1.0" 33 | 34 | [[projects]] 35 | name = "github.com/go-logfmt/logfmt" 36 | packages = ["."] 37 | revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" 38 | version = "v0.3.0" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "github.com/golang/protobuf" 43 | packages = ["proto"] 44 | revision = "6a1fa9404c0aebf36c879bc50152edcc953910d2" 45 | 46 | [[projects]] 47 | name = "github.com/gorilla/context" 48 | packages = ["."] 49 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 50 | version = "v1.1" 51 | 52 | [[projects]] 53 | name = "github.com/gorilla/mux" 54 | packages = ["."] 55 | revision = "bcd8bc72b08df0f70df986b97f95590779502d31" 56 | version = "v1.4.0" 57 | 58 | [[projects]] 59 | branch = "master" 60 | name = "github.com/jmoiron/sqlx" 61 | packages = [".","reflectx"] 62 | revision = "d9bd385d68c068f1fabb5057e3dedcbcbb039d0f" 63 | 64 | [[projects]] 65 | branch = "master" 66 | name = "github.com/kr/logfmt" 67 | packages = ["."] 68 | revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" 69 | 70 | [[projects]] 71 | branch = "master" 72 | name = "github.com/lib/pq" 73 | packages = [".","oid"] 74 | revision = "8837942c3e09574accbc5f150e2c5e057189cace" 75 | 76 | [[projects]] 77 | name = "github.com/matttproud/golang_protobuf_extensions" 78 | packages = ["pbutil"] 79 | revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" 80 | version = "v1.0.0" 81 | 82 | [[projects]] 83 | name = "github.com/pkg/errors" 84 | packages = ["."] 85 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 86 | version = "v0.8.0" 87 | 88 | [[projects]] 89 | name = "github.com/pmezard/go-difflib" 90 | packages = ["difflib"] 91 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 92 | version = "v1.0.0" 93 | 94 | [[projects]] 95 | name = "github.com/prometheus/client_golang" 96 | packages = ["prometheus","prometheus/promhttp"] 97 | revision = "c5b7fccd204277076155f10851dad72b76a49317" 98 | version = "v0.8.0" 99 | 100 | [[projects]] 101 | branch = "master" 102 | name = "github.com/prometheus/client_model" 103 | packages = ["go"] 104 | revision = "6f3806018612930941127f2a7c6c453ba2c527d2" 105 | 106 | [[projects]] 107 | branch = "master" 108 | name = "github.com/prometheus/common" 109 | packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"] 110 | revision = "0866df4b85a18d652b6965be022d007cdf076822" 111 | 112 | [[projects]] 113 | branch = "master" 114 | name = "github.com/prometheus/procfs" 115 | packages = [".","xfs"] 116 | revision = "822d4a1f8edcbcbc71e8d1fd6527b12331a6d0ad" 117 | 118 | [[projects]] 119 | branch = "master" 120 | name = "github.com/stretchr/objx" 121 | packages = ["."] 122 | revision = "1a9d0bb9f541897e62256577b352fdbc1fb4fd94" 123 | 124 | [[projects]] 125 | name = "github.com/stretchr/testify" 126 | packages = ["assert","mock"] 127 | revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" 128 | version = "v1.1.4" 129 | 130 | [solve-meta] 131 | analyzer-name = "dep" 132 | analyzer-version = 1 133 | inputs-digest = "5962ab78201c7bdeb70c011445eff78454093e0e0c31551b90f56a5efb54788e" 134 | solver-name = "gps-cdcl" 135 | solver-version = 1 136 | -------------------------------------------------------------------------------- /controller/beers_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | 9 | "encoding/json" 10 | 11 | "bytes" 12 | 13 | "github.com/caarlos0/go-web-api-example/model" 14 | "github.com/gorilla/mux" 15 | "github.com/pkg/errors" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/mock" 18 | ) 19 | 20 | func TestBeersIndexSuccess(t *testing.T) { 21 | var assert = assert.New(t) 22 | var mockedBeers = []model.Beer{ 23 | { 24 | ID: 1, 25 | Name: "Opa IPA", 26 | Price: 15.1, 27 | CreatedAt: time.Now(), 28 | UpdatedAt: time.Now(), 29 | }, 30 | } 31 | var ds = new(mockBeersDatastore) 32 | ds.On("AllBeers").Return(mockedBeers, nil) 33 | var ts = httptest.NewServer(BeersIndex(ds)) 34 | defer ts.Close() 35 | res, err := http.Get(ts.URL) 36 | assert.NoError(err) 37 | assert.Equal(http.StatusOK, res.StatusCode) 38 | var beers []model.Beer 39 | _ = json.NewDecoder(res.Body).Decode(&beers) 40 | defer func() { 41 | _ = res.Body.Close() 42 | }() 43 | assert.Equal(mockedBeers, beers) 44 | } 45 | 46 | func TestBeersIndexDatabaseError(t *testing.T) { 47 | var assert = assert.New(t) 48 | var ds = new(mockBeersDatastore) 49 | ds.On("AllBeers").Return([]model.Beer{}, errors.New("random failure")) 50 | var ts = httptest.NewServer(BeersIndex(ds)) 51 | defer ts.Close() 52 | res, err := http.Get(ts.URL) 53 | assert.NoError(err) 54 | assert.Equal(http.StatusInternalServerError, res.StatusCode) 55 | } 56 | 57 | func TestCreateBeer(t *testing.T) { 58 | var assert = assert.New(t) 59 | var beer = model.Beer{ 60 | Name: "brahma", 61 | Price: 2.5, 62 | } 63 | var ds = new(mockBeersDatastore) 64 | ds.On("CreateBeer", beer).Return(nil) 65 | var ts = httptest.NewServer(CreateBeer(ds)) 66 | defer ts.Close() 67 | body, _ := json.Marshal(&beer) 68 | res, err := http.Post(ts.URL, "application/json", bytes.NewReader(body)) 69 | assert.NoError(err) 70 | assert.Equal(http.StatusCreated, res.StatusCode) 71 | } 72 | 73 | func TestCreateBeerDSError(t *testing.T) { 74 | var assert = assert.New(t) 75 | var beer = model.Beer{ 76 | Name: "skoll", 77 | Price: 2.1, 78 | } 79 | var ds = new(mockBeersDatastore) 80 | ds.On("CreateBeer", beer).Return(errors.New("error")) 81 | var ts = httptest.NewServer(CreateBeer(ds)) 82 | defer ts.Close() 83 | body, _ := json.Marshal(&beer) 84 | res, err := http.Post(ts.URL, "application/json", bytes.NewReader(body)) 85 | assert.NoError(err) 86 | assert.Equal(http.StatusBadRequest, res.StatusCode) 87 | } 88 | 89 | func TestGetBeer(t *testing.T) { 90 | var assert = assert.New(t) 91 | var beer = model.Beer{ 92 | ID: 1, 93 | Name: "Coruja", 94 | Price: 52.3, 95 | UpdatedAt: time.Now(), 96 | CreatedAt: time.Now(), 97 | } 98 | var ds = new(mockBeersDatastore) 99 | ds.On("GetBeer", int64(1)).Return(beer, nil) 100 | 101 | var mux = mux.NewRouter() 102 | mux.HandleFunc("/{id}", GetBeer(ds)) 103 | var ts = httptest.NewServer(mux) 104 | defer ts.Close() 105 | 106 | var respBeer model.Beer 107 | res, err := http.Get(ts.URL + "/1") 108 | assert.NoError(err) 109 | assert.NoError(json.NewDecoder(res.Body).Decode(&respBeer)) 110 | assert.Equal(beer, respBeer) 111 | assert.Equal(http.StatusOK, res.StatusCode) 112 | } 113 | 114 | func TestGetBeerDSError(t *testing.T) { 115 | var assert = assert.New(t) 116 | var ds = new(mockBeersDatastore) 117 | ds.On("GetBeer", int64(1)).Return(model.Beer{}, errors.New("fake err")) 118 | 119 | var mux = mux.NewRouter() 120 | mux.HandleFunc("/{id}", GetBeer(ds)) 121 | var ts = httptest.NewServer(mux) 122 | defer ts.Close() 123 | 124 | res, err := http.Get(ts.URL + "/1") 125 | assert.NoError(err) 126 | assert.Equal(http.StatusBadRequest, res.StatusCode) 127 | } 128 | 129 | func TestGetBeerInvalidURL(t *testing.T) { 130 | var assert = assert.New(t) 131 | var ds = new(mockBeersDatastore) 132 | 133 | var mux = mux.NewRouter() 134 | mux.HandleFunc("/{id}", GetBeer(ds)) 135 | var ts = httptest.NewServer(mux) 136 | defer ts.Close() 137 | 138 | res, err := http.Get(ts.URL + "/asdasd") 139 | assert.NoError(err) 140 | assert.Equal(http.StatusBadRequest, res.StatusCode) 141 | } 142 | 143 | func TestDeleteBeer(t *testing.T) { 144 | var assert = assert.New(t) 145 | var ds = new(mockBeersDatastore) 146 | ds.On("DeleteBeer", int64(1)).Return(nil) 147 | 148 | var mux = mux.NewRouter() 149 | mux.HandleFunc("/{id}", DeleteBeer(ds)) 150 | var ts = httptest.NewServer(mux) 151 | defer ts.Close() 152 | 153 | res, err := http.Get(ts.URL + "/1") 154 | assert.NoError(err) 155 | assert.Equal(http.StatusOK, res.StatusCode) 156 | } 157 | 158 | func TestDeleteBeerDSError(t *testing.T) { 159 | var assert = assert.New(t) 160 | var ds = new(mockBeersDatastore) 161 | ds.On("DeleteBeer", int64(1)).Return(errors.New("fake")) 162 | 163 | var mux = mux.NewRouter() 164 | mux.HandleFunc("/{id}", DeleteBeer(ds)) 165 | var ts = httptest.NewServer(mux) 166 | defer ts.Close() 167 | 168 | res, err := http.Get(ts.URL + "/1") 169 | assert.NoError(err) 170 | assert.Equal(http.StatusBadRequest, res.StatusCode) 171 | } 172 | 173 | type mockBeersDatastore struct { 174 | mock.Mock 175 | } 176 | 177 | func (m mockBeersDatastore) AllBeers() (beers []model.Beer, err error) { 178 | var args = m.Called() 179 | return args.Get(0).([]model.Beer), args.Error(1) 180 | } 181 | 182 | func (m mockBeersDatastore) GetBeer(id int64) (beer model.Beer, err error) { 183 | var args = m.Called(id) 184 | return args.Get(0).(model.Beer), args.Error(1) 185 | } 186 | 187 | func (m mockBeersDatastore) CreateBeer(beer model.Beer) (err error) { 188 | var args = m.Called(beer) 189 | return args.Error(0) 190 | } 191 | 192 | func (m mockBeersDatastore) DeleteBeer(id int64) (err error) { 193 | var args = m.Called(id) 194 | return args.Error(0) 195 | } 196 | --------------------------------------------------------------------------------