├── .gitignore ├── .semaphore └── semaphore.yml ├── LICENSE ├── README.md ├── app.go ├── app.go.tutorial ├── env-sample ├── env-test ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── model.go └── schema.sql /.gitignore: -------------------------------------------------------------------------------- 1 | go-mux-api.bin 2 | mod/ 3 | vendor/ 4 | .env 5 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: Go 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu1804 7 | blocks: 8 | - name: Test 9 | task: 10 | jobs: 11 | - name: go test 12 | commands: 13 | - sem-service start postgres 14 | - sem-version go 1.16 15 | - export GO111MODULE=on 16 | - export GOPATH=~/go 17 | - 'export PATH=/home/semaphore/go/bin:$PATH' 18 | - checkout 19 | - go get ./... 20 | - source env-test 21 | - go test ./... 22 | - go build -v . 23 | dependencies: [] 24 | - name: Download 25 | dependencies: [] 26 | task: 27 | env_vars: 28 | - name: GO111MODULE 29 | value: 'on' 30 | - name: GOFLAGS 31 | value: '-mod=vendor' 32 | jobs: 33 | - name: go get 34 | commands: 35 | - sem-version go 1.16 36 | - checkout 37 | - 'cache restore ' 38 | - go mod vendor 39 | - cache store 40 | - name: Tests 41 | dependencies: 42 | - Download 43 | task: 44 | env_vars: 45 | - name: GO111MODULE 46 | value: 'on' 47 | - name: GOFLAGS 48 | value: '-mod=vendor' 49 | prologue: 50 | commands: 51 | - sem-version go 1.16 52 | - sem-service start postgres 53 | - checkout 54 | - cache restore 55 | - go mod vendor 56 | - source env-sample 57 | jobs: 58 | - name: go test 59 | commands: 60 | - go test ./... 61 | - name: Build 62 | dependencies: 63 | - Tests 64 | task: 65 | env_vars: 66 | - name: GO111MODULE 67 | value: 'on' 68 | - name: GOFLAG 69 | value: '-mod=vendor' 70 | prologue: 71 | commands: 72 | - sem-version go 1.16 73 | - sem-service start postgres 74 | - checkout 75 | - cache restore 76 | - go mod vendor 77 | jobs: 78 | - name: go build 79 | commands: 80 | - go build -v -o go-mux.bin 81 | - artifact push project --force go-mux.bin 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rendered Text 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Mux Demo 2 | 3 | [![Build Status](https://tomfern.semaphoreci.com/badges/go-mux-api/branches/master.svg)](https://tomfern.semaphoreci.com/projects/go-mux-api) 4 | 5 | Read the complete post with the explanation here: 6 | 7 | https://semaphoreci.com/community/tutorials/building-and-testing-a-rest-api-in-go-with-gorilla-mux-and-postgresql 8 | 9 | ## Run locally 10 | 11 | - Start postgres 12 | - Prepare environment, fill DB parameters: 13 | 14 | ``` bash 15 | $ source env-sample 16 | ``` 17 | 18 | - Build and run: 19 | 20 | ```bash 21 | $ export GO111MODULE=on 22 | $ export GOFLAGS=-mod=vendor 23 | $ go mod download 24 | $ go build -o go-mux-api.bin 25 | $ ./go-mux-api.bin 26 | ``` 27 | 28 | Server is listening on localhost:8010 29 | 30 | ## Test 31 | 32 | ```bash 33 | $ go test -v 34 | === RUN TestEmptyTable 35 | --- PASS: TestEmptyTable (0.00s) 36 | === RUN TestGetNonExistentProduct 37 | --- PASS: TestGetNonExistentProduct (0.00s) 38 | === RUN TestCreateProduct 39 | --- PASS: TestCreateProduct (0.00s) 40 | === RUN TestGetProduct 41 | --- PASS: TestGetProduct (0.00s) 42 | === RUN TestUpdateProduct 43 | --- PASS: TestUpdateProduct (0.01s) 44 | === RUN TestDeleteProduct 45 | --- PASS: TestDeleteProduct (0.01s) 46 | PASS 47 | ok _/home/tom/r/go-mux-api 0.034s 48 | ``` 49 | 50 | ## License 51 | 52 | Copyright (c) 2021 Rendered Text 53 | 54 | Distributed under the MIT License. See the file LICENSE. 55 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | // app.go 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | 8 | // tom: for Initialize 9 | "fmt" 10 | "log" 11 | 12 | // tom: for route handlers 13 | "net/http" 14 | "encoding/json" 15 | "strconv" 16 | 17 | // tom: go get required 18 | "github.com/gorilla/mux" 19 | _ "github.com/lib/pq" 20 | 21 | ) 22 | 23 | type App struct { 24 | Router *mux.Router 25 | DB *sql.DB 26 | } 27 | 28 | // tom: initial function is empty, it's filled afterwards 29 | // func (a *App) Initialize(user, password, dbname string) { } 30 | 31 | // tom: added "sslmode=disable" to connection string 32 | func (a *App) Initialize(user, password, dbname string) { 33 | connectionString := 34 | fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, password, dbname) 35 | 36 | var err error 37 | a.DB, err = sql.Open("postgres", connectionString) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | a.Router = mux.NewRouter() 43 | 44 | // tom: this line is added after initializeRoutes is created later on 45 | a.initializeRoutes() 46 | } 47 | 48 | // tom: initial version 49 | // func (a *App) Run(addr string) { } 50 | // improved version 51 | func (a *App) Run(addr string) { 52 | log.Fatal(http.ListenAndServe(":8010", a.Router)) 53 | } 54 | 55 | // tom: these are added later 56 | func (a *App) getProduct(w http.ResponseWriter, r *http.Request) { 57 | vars := mux.Vars(r) 58 | id, err := strconv.Atoi(vars["id"]) 59 | if err != nil { 60 | respondWithError(w, http.StatusBadRequest, "Invalid product ID") 61 | return 62 | } 63 | 64 | p := product{ID: id} 65 | if err := p.getProduct(a.DB); err != nil { 66 | switch err { 67 | case sql.ErrNoRows: 68 | respondWithError(w, http.StatusNotFound, "Product not found") 69 | default: 70 | respondWithError(w, http.StatusInternalServerError, err.Error()) 71 | } 72 | return 73 | } 74 | 75 | respondWithJSON(w, http.StatusOK, p) 76 | } 77 | 78 | func respondWithError(w http.ResponseWriter, code int, message string) { 79 | respondWithJSON(w, code, map[string]string{"error": message}) 80 | } 81 | 82 | func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { 83 | response, _ := json.Marshal(payload) 84 | 85 | w.Header().Set("Content-Type", "application/json") 86 | w.WriteHeader(code) 87 | w.Write(response) 88 | } 89 | 90 | 91 | func (a *App) getProducts(w http.ResponseWriter, r *http.Request) { 92 | count, _ := strconv.Atoi(r.FormValue("count")) 93 | start, _ := strconv.Atoi(r.FormValue("start")) 94 | 95 | if count > 10 || count < 1 { 96 | count = 10 97 | } 98 | if start < 0 { 99 | start = 0 100 | } 101 | 102 | products, err := getProducts(a.DB, start, count) 103 | if err != nil { 104 | respondWithError(w, http.StatusInternalServerError, err.Error()) 105 | return 106 | } 107 | 108 | respondWithJSON(w, http.StatusOK, products) 109 | } 110 | 111 | func (a *App) createProduct(w http.ResponseWriter, r *http.Request) { 112 | var p product 113 | decoder := json.NewDecoder(r.Body) 114 | if err := decoder.Decode(&p); err != nil { 115 | respondWithError(w, http.StatusBadRequest, "Invalid request payload") 116 | return 117 | } 118 | defer r.Body.Close() 119 | 120 | if err := p.createProduct(a.DB); err != nil { 121 | respondWithError(w, http.StatusInternalServerError, err.Error()) 122 | return 123 | } 124 | 125 | respondWithJSON(w, http.StatusCreated, p) 126 | } 127 | 128 | func (a *App) updateProduct(w http.ResponseWriter, r *http.Request) { 129 | vars := mux.Vars(r) 130 | id, err := strconv.Atoi(vars["id"]) 131 | if err != nil { 132 | respondWithError(w, http.StatusBadRequest, "Invalid product ID") 133 | return 134 | } 135 | 136 | var p product 137 | decoder := json.NewDecoder(r.Body) 138 | if err := decoder.Decode(&p); err != nil { 139 | respondWithError(w, http.StatusBadRequest, "Invalid resquest payload") 140 | return 141 | } 142 | defer r.Body.Close() 143 | p.ID = id 144 | 145 | if err := p.updateProduct(a.DB); err != nil { 146 | respondWithError(w, http.StatusInternalServerError, err.Error()) 147 | return 148 | } 149 | 150 | respondWithJSON(w, http.StatusOK, p) 151 | } 152 | 153 | func (a *App) deleteProduct(w http.ResponseWriter, r *http.Request) { 154 | vars := mux.Vars(r) 155 | id, err := strconv.Atoi(vars["id"]) 156 | if err != nil { 157 | respondWithError(w, http.StatusBadRequest, "Invalid Product ID") 158 | return 159 | } 160 | 161 | p := product{ID: id} 162 | if err := p.deleteProduct(a.DB); err != nil { 163 | respondWithError(w, http.StatusInternalServerError, err.Error()) 164 | return 165 | } 166 | 167 | respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"}) 168 | } 169 | 170 | 171 | func (a *App) initializeRoutes() { 172 | a.Router.HandleFunc("/products", a.getProducts).Methods("GET") 173 | a.Router.HandleFunc("/product", a.createProduct).Methods("POST") 174 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.getProduct).Methods("GET") 175 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.updateProduct).Methods("PUT") 176 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.deleteProduct).Methods("DELETE") 177 | } 178 | -------------------------------------------------------------------------------- /app.go.tutorial: -------------------------------------------------------------------------------- 1 | // app.go 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/gorilla/mux" 14 | _ "github.com/lib/pq" 15 | ) 16 | 17 | type App struct { 18 | Router *mux.Router 19 | DB *sql.DB 20 | } 21 | 22 | func (a *App) Initialize(user, password, dbname string) { 23 | connectionString := 24 | fmt.Sprintf("user=%s password=%s dbname=%s", user, password, dbname) 25 | 26 | var err error 27 | a.DB, err = sql.Open("postgres", connectionString) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | a.Router = mux.NewRouter() 33 | a.initializeRoutes() 34 | } 35 | 36 | func (a *App) Run(addr string) { 37 | log.Fatal(http.ListenAndServe(":8000", a.Router)) 38 | } 39 | 40 | func (a *App) initializeRoutes() { 41 | a.Router.HandleFunc("/products", a.getProducts).Methods("GET") 42 | a.Router.HandleFunc("/product", a.createProduct).Methods("POST") 43 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.getProduct).Methods("GET") 44 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.updateProduct).Methods("PUT") 45 | a.Router.HandleFunc("/product/{id:[0-9]+}", a.deleteProduct).Methods("DELETE") 46 | } 47 | 48 | func (a *App) getProducts(w http.ResponseWriter, r *http.Request) { 49 | count, _ := strconv.Atoi(r.FormValue("count")) 50 | start, _ := strconv.Atoi(r.FormValue("start")) 51 | 52 | if count > 10 || count < 1 { 53 | count = 10 54 | } 55 | if start < 0 { 56 | start = 0 57 | } 58 | 59 | products, err := getProducts(a.DB, start, count) 60 | if err != nil { 61 | respondWithError(w, http.StatusInternalServerError, err.Error()) 62 | return 63 | } 64 | 65 | respondWithJSON(w, http.StatusOK, products) 66 | } 67 | 68 | func (a *App) createProduct(w http.ResponseWriter, r *http.Request) { 69 | var p product 70 | decoder := json.NewDecoder(r.Body) 71 | if err := decoder.Decode(&p); err != nil { 72 | respondWithError(w, http.StatusBadRequest, "Invalid request payload") 73 | return 74 | } 75 | defer r.Body.Close() 76 | 77 | if err := p.createProduct(a.DB); err != nil { 78 | respondWithError(w, http.StatusInternalServerError, err.Error()) 79 | return 80 | } 81 | 82 | respondWithJSON(w, http.StatusCreated, p) 83 | } 84 | 85 | func (a *App) getProduct(w http.ResponseWriter, r *http.Request) { 86 | vars := mux.Vars(r) 87 | id, err := strconv.Atoi(vars["id"]) 88 | if err != nil { 89 | respondWithError(w, http.StatusBadRequest, "Invalid product ID") 90 | return 91 | } 92 | 93 | p := product{ID: id} 94 | if err := p.getProduct(a.DB); err != nil { 95 | switch err { 96 | case sql.ErrNoRows: 97 | respondWithError(w, http.StatusNotFound, "Product not found") 98 | default: 99 | respondWithError(w, http.StatusInternalServerError, err.Error()) 100 | } 101 | return 102 | } 103 | 104 | respondWithJSON(w, http.StatusOK, p) 105 | } 106 | 107 | func (a *App) updateProduct(w http.ResponseWriter, r *http.Request) { 108 | vars := mux.Vars(r) 109 | id, err := strconv.Atoi(vars["id"]) 110 | if err != nil { 111 | respondWithError(w, http.StatusBadRequest, "Invalid product ID") 112 | return 113 | } 114 | 115 | var p product 116 | decoder := json.NewDecoder(r.Body) 117 | if err := decoder.Decode(&p); err != nil { 118 | respondWithError(w, http.StatusBadRequest, "Invalid resquest payload") 119 | return 120 | } 121 | defer r.Body.Close() 122 | p.ID = id 123 | 124 | if err := p.updateProduct(a.DB); err != nil { 125 | respondWithError(w, http.StatusInternalServerError, err.Error()) 126 | return 127 | } 128 | 129 | respondWithJSON(w, http.StatusOK, p) 130 | } 131 | 132 | func (a *App) deleteProduct(w http.ResponseWriter, r *http.Request) { 133 | vars := mux.Vars(r) 134 | id, err := strconv.Atoi(vars["id"]) 135 | if err != nil { 136 | respondWithError(w, http.StatusBadRequest, "Invalid Product ID") 137 | return 138 | } 139 | 140 | p := product{ID: id} 141 | if err := p.deleteProduct(a.DB); err != nil { 142 | respondWithError(w, http.StatusInternalServerError, err.Error()) 143 | return 144 | } 145 | 146 | respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"}) 147 | } 148 | 149 | func respondWithError(w http.ResponseWriter, code int, message string) { 150 | respondWithJSON(w, code, map[string]string{"error": message}) 151 | } 152 | 153 | func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { 154 | response, _ := json.Marshal(payload) 155 | 156 | w.Header().Set("Content-Type", "application/json") 157 | w.WriteHeader(code) 158 | w.Write(response) 159 | } 160 | -------------------------------------------------------------------------------- /env-sample: -------------------------------------------------------------------------------- 1 | export APP_DB_USERNAME=postgres 2 | export APP_DB_PASSWORD= 3 | export APP_DB_NAME=postgres 4 | 5 | export TEST_DB_USERNAME=$APP_DB_USERNAME 6 | export TEST_DB_PASSWORD=$APP_DB_PASSWORD 7 | export TEST_DB_NAME=$APP_DB_NAME 8 | -------------------------------------------------------------------------------- /env-test: -------------------------------------------------------------------------------- 1 | export APP_DB_USERNAME=postgres 2 | export APP_DB_PASSWORD= 3 | export APP_DB_NAME=postgres 4 | 5 | export TEST_DB_USERNAME=$APP_DB_USERNAME 6 | export TEST_DB_PASSWORD=$APP_DB_PASSWORD 7 | export TEST_DB_NAME=$APP_DB_NAME 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TomFern/go-mux-api 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/lib/pq v1.10.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= 4 | github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // main.go 2 | 3 | package main 4 | 5 | import "os" 6 | 7 | func main() { 8 | a := App{} 9 | a.Initialize( 10 | os.Getenv("APP_DB_USERNAME"), 11 | os.Getenv("APP_DB_PASSWORD"), 12 | os.Getenv("APP_DB_NAME")) 13 | 14 | a.Run(":8010") 15 | } 16 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // main_test.go 2 | 3 | package main_test 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | "log" 9 | 10 | "net/http" 11 | "net/http/httptest" 12 | "strconv" 13 | "encoding/json" 14 | "bytes" 15 | 16 | "github.com/TomFern/go-mux-api" 17 | 18 | ) 19 | 20 | var a main.App 21 | 22 | func TestMain(m *testing.M) { 23 | a = main.App{} 24 | a.Initialize( 25 | os.Getenv("TEST_DB_USERNAME"), 26 | os.Getenv("TEST_DB_PASSWORD"), 27 | os.Getenv("TEST_DB_NAME")) 28 | 29 | ensureTableExists() 30 | 31 | code := m.Run() 32 | 33 | clearTable() 34 | 35 | os.Exit(code) 36 | } 37 | 38 | func ensureTableExists() { 39 | if _, err := a.DB.Exec(tableCreationQuery); err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | func clearTable() { 45 | a.DB.Exec("DELETE FROM products") 46 | a.DB.Exec("ALTER SEQUENCE products_id_seq RESTART WITH 1") 47 | } 48 | 49 | const tableCreationQuery = `CREATE TABLE IF NOT EXISTS products 50 | ( 51 | id SERIAL, 52 | name TEXT NOT NULL, 53 | price NUMERIC(10,2) NOT NULL DEFAULT 0.00, 54 | CONSTRAINT products_pkey PRIMARY KEY (id) 55 | )` 56 | 57 | 58 | // tom: next functions added later, these require more modules: net/http net/http/httptest 59 | func TestEmptyTable(t *testing.T) { 60 | clearTable() 61 | 62 | req, _ := http.NewRequest("GET", "/products", nil) 63 | response := executeRequest(req) 64 | 65 | checkResponseCode(t, http.StatusOK, response.Code) 66 | 67 | if body := response.Body.String(); body != "[]" { 68 | t.Errorf("Expected an empty array. Got %s", body) 69 | } 70 | } 71 | 72 | func executeRequest(req *http.Request) *httptest.ResponseRecorder { 73 | rr := httptest.NewRecorder() 74 | a.Router.ServeHTTP(rr, req) 75 | 76 | return rr 77 | } 78 | 79 | func checkResponseCode(t *testing.T, expected, actual int) { 80 | if expected != actual { 81 | t.Errorf("Expected response code %d. Got %d\n", expected, actual) 82 | } 83 | } 84 | 85 | func TestGetNonExistentProduct(t *testing.T) { 86 | clearTable() 87 | 88 | req, _ := http.NewRequest("GET", "/product/11", nil) 89 | response := executeRequest(req) 90 | 91 | checkResponseCode(t, http.StatusNotFound, response.Code) 92 | 93 | var m map[string]string 94 | json.Unmarshal(response.Body.Bytes(), &m) 95 | if m["error"] != "Product not found" { 96 | t.Errorf("Expected the 'error' key of the response to be set to 'Product not found'. Got '%s'", m["error"]) 97 | } 98 | } 99 | 100 | // tom: rewritten function 101 | func TestCreateProduct(t *testing.T) { 102 | 103 | clearTable() 104 | 105 | var jsonStr = []byte(`{"name":"test product", "price": 11.22}`) 106 | req, _ := http.NewRequest("POST", "/product", bytes.NewBuffer(jsonStr)) 107 | req.Header.Set("Content-Type", "application/json") 108 | 109 | response := executeRequest(req) 110 | checkResponseCode(t, http.StatusCreated, response.Code) 111 | 112 | var m map[string]interface{} 113 | json.Unmarshal(response.Body.Bytes(), &m) 114 | 115 | if m["name"] != "test product" { 116 | t.Errorf("Expected product name to be 'test product'. Got '%v'", m["name"]) 117 | } 118 | 119 | if m["price"] != 11.22 { 120 | t.Errorf("Expected product price to be '11.22'. Got '%v'", m["price"]) 121 | } 122 | 123 | // the id is compared to 1.0 because JSON unmarshaling converts numbers to 124 | // floats, when the target is a map[string]interface{} 125 | if m["id"] != 1.0 { 126 | t.Errorf("Expected product ID to be '1'. Got '%v'", m["id"]) 127 | } 128 | } 129 | 130 | 131 | func TestGetProduct(t *testing.T) { 132 | clearTable() 133 | addProducts(1) 134 | 135 | req, _ := http.NewRequest("GET", "/product/1", nil) 136 | response := executeRequest(req) 137 | 138 | checkResponseCode(t, http.StatusOK, response.Code) 139 | } 140 | 141 | func addProducts(count int) { 142 | if count < 1 { 143 | count = 1 144 | } 145 | 146 | for i := 0; i < count; i++ { 147 | a.DB.Exec("INSERT INTO products(name, price) VALUES($1, $2)", "Product "+strconv.Itoa(i), (i+1.0)*10) 148 | } 149 | } 150 | 151 | func TestUpdateProduct(t *testing.T) { 152 | 153 | clearTable() 154 | addProducts(1) 155 | 156 | req, _ := http.NewRequest("GET", "/product/1", nil) 157 | response := executeRequest(req) 158 | var originalProduct map[string]interface{} 159 | json.Unmarshal(response.Body.Bytes(), &originalProduct) 160 | 161 | var jsonStr = []byte(`{"name":"test product - updated name", "price": 11.22}`) 162 | req, _ = http.NewRequest("PUT", "/product/1", bytes.NewBuffer(jsonStr)) 163 | req.Header.Set("Content-Type", "application/json") 164 | 165 | // req, _ = http.NewRequest("PUT", "/product/1", bytes.NewBuffer(payload)) 166 | response = executeRequest(req) 167 | 168 | checkResponseCode(t, http.StatusOK, response.Code) 169 | 170 | var m map[string]interface{} 171 | json.Unmarshal(response.Body.Bytes(), &m) 172 | 173 | if m["id"] != originalProduct["id"] { 174 | t.Errorf("Expected the id to remain the same (%v). Got %v", originalProduct["id"], m["id"]) 175 | } 176 | 177 | if m["name"] == originalProduct["name"] { 178 | t.Errorf("Expected the name to change from '%v' to '%v'. Got '%v'", originalProduct["name"], m["name"], m["name"]) 179 | } 180 | 181 | if m["price"] == originalProduct["price"] { 182 | t.Errorf("Expected the price to change from '%v' to '%v'. Got '%v'", originalProduct["price"], m["price"], m["price"]) 183 | } 184 | } 185 | 186 | func TestDeleteProduct(t *testing.T) { 187 | clearTable() 188 | addProducts(1) 189 | 190 | req, _ := http.NewRequest("GET", "/product/1", nil) 191 | response := executeRequest(req) 192 | checkResponseCode(t, http.StatusOK, response.Code) 193 | 194 | req, _ = http.NewRequest("DELETE", "/product/1", nil) 195 | response = executeRequest(req) 196 | 197 | checkResponseCode(t, http.StatusOK, response.Code) 198 | 199 | req, _ = http.NewRequest("GET", "/product/1", nil) 200 | response = executeRequest(req) 201 | checkResponseCode(t, http.StatusNotFound, response.Code) 202 | } 203 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | // model.go 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | // tom: errors is removed once functions are implemented 8 | // "errors" 9 | ) 10 | 11 | 12 | // tom: add backticks to json 13 | type product struct { 14 | ID int `json:"id"` 15 | Name string `json:"name"` 16 | Price float64 `json:"price"` 17 | } 18 | 19 | // tom: these are initial empty definitions 20 | // func (p *product) getProduct(db *sql.DB) error { 21 | // return errors.New("Not implemented") 22 | // } 23 | 24 | // func (p *product) updateProduct(db *sql.DB) error { 25 | // return errors.New("Not implemented") 26 | // } 27 | 28 | // func (p *product) deleteProduct(db *sql.DB) error { 29 | // return errors.New("Not implemented") 30 | // } 31 | 32 | // func (p *product) createProduct(db *sql.DB) error { 33 | // return errors.New("Not implemented") 34 | // } 35 | 36 | // func getProducts(db *sql.DB, start, count int) ([]product, error) { 37 | // return nil, errors.New("Not implemented") 38 | // } 39 | 40 | // tom: these are added after tdd tests 41 | func (p *product) getProduct(db *sql.DB) error { 42 | return db.QueryRow("SELECT name, price FROM products WHERE id=$1", 43 | p.ID).Scan(&p.Name, &p.Price) 44 | } 45 | 46 | func (p *product) updateProduct(db *sql.DB) error { 47 | _, err := 48 | db.Exec("UPDATE products SET name=$1, price=$2 WHERE id=$3", 49 | p.Name, p.Price, p.ID) 50 | 51 | return err 52 | } 53 | 54 | func (p *product) deleteProduct(db *sql.DB) error { 55 | _, err := db.Exec("DELETE FROM products WHERE id=$1", p.ID) 56 | 57 | return err 58 | } 59 | 60 | func (p *product) createProduct(db *sql.DB) error { 61 | err := db.QueryRow( 62 | "INSERT INTO products(name, price) VALUES($1, $2) RETURNING id", 63 | p.Name, p.Price).Scan(&p.ID) 64 | 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func getProducts(db *sql.DB, start, count int) ([]product, error) { 73 | rows, err := db.Query( 74 | "SELECT id, name, price FROM products LIMIT $1 OFFSET $2", 75 | count, start) 76 | 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | defer rows.Close() 82 | 83 | products := []product{} 84 | 85 | for rows.Next() { 86 | var p product 87 | if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil { 88 | return nil, err 89 | } 90 | products = append(products, p) 91 | } 92 | 93 | return products, nil 94 | } 95 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE products 2 | ( 3 | id SERIAL, 4 | name TEXT NOT NULL, 5 | price NUMERIC(10,2) NOT NULL DEFAULT 0.00, 6 | CONSTRAINT products_pkey PRIMARY KEY (id) 7 | ) 8 | --------------------------------------------------------------------------------