From 9155ee88f102718b6c6488340274b32120d9bc69 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 09:23:02 -0800 Subject: [PATCH 1/6] Genera: router & models --- models/client.go | 4 +- models/genera.go | 135 ++++++++++++++++++++++++++++++++++++++++++ models/genera_test.go | 107 +++++++++++++++++++++++++++++++++ router/api.go | 7 +++ router/routes.go | 9 ++- 5 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 models/genera.go create mode 100644 models/genera_test.go diff --git a/models/client.go b/models/client.go index e65f082..bb321e7 100644 --- a/models/client.go +++ b/models/client.go @@ -16,7 +16,8 @@ import ( // A Client communicates with bactdb's HTTP API. type Client struct { - Users UsersService + Users UsersService + Genera GeneraService // BaseURL for HTTP requests to bactdb's API. BaseURL *url.URL @@ -45,6 +46,7 @@ func NewClient(httpClient *http.Client) *Client { httpClient: httpClient, } c.Users = &usersService{c} + c.Genera = &generaService{c} return c } diff --git a/models/genera.go b/models/genera.go new file mode 100644 index 0000000..2cad177 --- /dev/null +++ b/models/genera.go @@ -0,0 +1,135 @@ +package models + +import ( + "errors" + "net/http" + "strconv" + "time" + + "github.com/thermokarst/bactdb/router" +) + +// A Genus is a high-level classifier in bactdb. +type Genus struct { + Id int64 `json:"id,omitempty"` + GenusName string `json:"genus_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` +} + +// GeneraService interacts with the genus-related endpoints in bactdb's API. +type GeneraService interface { + // Get a genus. + Get(id int64) (*Genus, error) + + // List all genera. + List(opt *GenusListOptions) ([]*Genus, error) + + // Create a new genus. The newly created genus's ID is written to genus.Id + Create(genus *Genus) (created bool, err error) +} + +var ( + ErrGenusNotFound = errors.New("genus not found") +) + +type generaService struct { + client *Client +} + +func (s *generaService) Get(id int64) (*Genus, error) { + // Pass in key value pairs as strings, so that the gorilla mux URL + // generation is happy. + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.Genus, map[string]string{"Id": strId}, nil) + if err != nil { + return nil, err + } + + req, err := s.client.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + + var genus *Genus + _, err = s.client.Do(req, &genus) + if err != nil { + return nil, err + } + + return genus, nil +} + +func (s *generaService) Create(genus *Genus) (bool, error) { + url, err := s.client.url(router.CreateGenus, nil, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("POST", url.String(), genus) + if err != nil { + return false, err + } + + resp, err := s.client.Do(req, &genus) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusCreated, nil +} + +type GenusListOptions struct { + ListOptions +} + +func (s *generaService) List(opt *GenusListOptions) ([]*Genus, error) { + url, err := s.client.url(router.Genera, nil, opt) + if err != nil { + return nil, err + } + + req, err := s.client.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + + var genera []*Genus + _, err = s.client.Do(req, &genera) + if err != nil { + return nil, err + } + + return genera, nil +} + +type MockGeneraService struct { + Get_ func(id int64) (*Genus, error) + List_ func(opt *GenusListOptions) ([]*Genus, error) + Create_ func(post *Genus) (bool, error) +} + +var _ GeneraService = &MockGeneraService{} + +func (s *MockGeneraService) Get(id int64) (*Genus, error) { + if s.Get_ == nil { + return nil, nil + } + return s.Get_(id) +} + +func (s *MockGeneraService) Create(genus *Genus) (bool, error) { + if s.Create_ == nil { + return false, nil + } + return s.Create_(genus) +} + +func (s *MockGeneraService) List(opt *GenusListOptions) ([]*Genus, error) { + if s.List_ == nil { + return nil, nil + } + return s.List_(opt) +} diff --git a/models/genera_test.go b/models/genera_test.go new file mode 100644 index 0000000..08269c5 --- /dev/null +++ b/models/genera_test.go @@ -0,0 +1,107 @@ +package models + +import ( + "net/http" + "reflect" + "testing" + + "github.com/thermokarst/bactdb/router" +) + +func TestGeneraService_Get(t *testing.T) { + setup() + defer teardown() + + want := &Genus{Id: 1, GenusName: "Test Genus"} + + var called bool + mux.HandleFunc(urlPath(t, router.Genus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "GET") + + writeJSON(w, want) + }) + + genus, err := client.Genera.Get(1) + if err != nil { + t.Errorf("Genera.Get returned error: %v", err) + } + + if !called { + t.Fatal("!called") + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + + if !reflect.DeepEqual(genus, want) { + t.Errorf("Genera.Get returned %+v, want %+v", genus, want) + } +} + +func TestGeneraService_Create(t *testing.T) { + setup() + defer teardown() + + want := &Genus{Id: 1, GenusName: "Test Genus"} + + var called bool + mux.HandleFunc(urlPath(t, router.CreateGenus, nil), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "POST") + testBody(t, r, `{"id":1,"genus_name":"Test Genus","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n") + + w.WriteHeader(http.StatusCreated) + writeJSON(w, want) + }) + + genus := &Genus{Id: 1, GenusName: "Test Genus"} + created, err := client.Genera.Create(genus) + if err != nil { + t.Errorf("Genera.Create returned error: %v", err) + } + + if !created { + t.Error("!created") + } + + if !called { + t.Fatal("!called") + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + if !reflect.DeepEqual(genus, want) { + t.Errorf("Genera.Create returned %+v, want %+v", genus, want) + } +} + +func TestGeneraService_List(t *testing.T) { + setup() + defer teardown() + + want := []*Genus{{Id: 1, GenusName: "Test Genus"}} + + var called bool + mux.HandleFunc(urlPath(t, router.Genera, nil), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "GET") + testFormValues(t, r, values{}) + + writeJSON(w, want) + }) + + genera, err := client.Genera.List(nil) + if err != nil { + t.Errorf("Genera.List returned error: %v", err) + } + + if !called { + t.Fatal("!called") + } + + for _, u := range want { + normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) + } + if !reflect.DeepEqual(genera, want) { + t.Errorf("Genera.List return %+v, want %+v", genera, want) + } +} diff --git a/router/api.go b/router/api.go index df2effb..f545a6e 100644 --- a/router/api.go +++ b/router/api.go @@ -4,8 +4,15 @@ import "github.com/gorilla/mux" func API() *mux.Router { m := mux.NewRouter() + + // Users m.Path("/users").Methods("GET").Name(Users) m.Path("/users").Methods("POST").Name(CreateUser) m.Path("/users/{Id:.+}").Methods("GET").Name(User) + + // Genera + m.Path("/genera").Methods("GET").Name(Genera) + m.Path("/genera").Methods("POST").Name(CreateGenus) + m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) return m } diff --git a/router/routes.go b/router/routes.go index 4e7dd46..4abfa86 100644 --- a/router/routes.go +++ b/router/routes.go @@ -1,7 +1,10 @@ package router const ( - User = "user" - CreateUser = "user:create" - Users = "users" + User = "user" + CreateUser = "user:create" + Users = "users" + Genus = "genus" + CreateGenus = "genus:create" + Genera = "genera" ) From 533adbdd09cd15090dfcb3bf6636d9fb23db8a6c Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 09:55:20 -0800 Subject: [PATCH 2/6] Genera: datastore. --- datastore/datastore.go | 9 ++- datastore/genera.go | 47 +++++++++++ datastore/genera_test.go | 81 +++++++++++++++++++ datastore/migrations/00002_AddGenera_down.sql | 5 ++ datastore/migrations/00002_AddGenera_up.sql | 19 +++++ 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 datastore/genera.go create mode 100644 datastore/genera_test.go create mode 100644 datastore/migrations/00002_AddGenera_down.sql create mode 100644 datastore/migrations/00002_AddGenera_up.sql diff --git a/datastore/datastore.go b/datastore/datastore.go index b18c012..94ff786 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -7,8 +7,9 @@ import ( // A datastore access point (in PostgreSQL) type Datastore struct { - Users models.UsersService - dbh modl.SqlExecutor + Users models.UsersService + Genera models.GeneraService + dbh modl.SqlExecutor } // NewDatastore creates a new client for accessing the datastore (in PostgreSQL). @@ -20,11 +21,13 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore { d := &Datastore{dbh: dbh} d.Users = &usersStore{d} + d.Genera = &generaStore{d} return d } func NewMockDatastore() *Datastore { return &Datastore{ - Users: &models.MockUsersService{}, + Users: &models.MockUsersService{}, + Genera: &models.MockGeneraService{}, } } diff --git a/datastore/genera.go b/datastore/genera.go new file mode 100644 index 0000000..260ff07 --- /dev/null +++ b/datastore/genera.go @@ -0,0 +1,47 @@ +package datastore + +import ( + "strings" + + "github.com/thermokarst/bactdb/models" +) + +func init() { + DB.AddTableWithName(models.Genus{}, "genera").SetKeys(true, "Id") +} + +type generaStore struct { + *Datastore +} + +func (s *generaStore) Get(id int64) (*models.Genus, error) { + var genus []*models.Genus + if err := s.dbh.Select(&genus, `SELECT * FROM genera WHERE id=$1;`, id); err != nil { + return nil, err + } + if len(genus) == 0 { + return nil, models.ErrGenusNotFound + } + return genus[0], nil +} + +func (s *generaStore) Create(genus *models.Genus) (bool, error) { + if err := s.dbh.Insert(genus); err != nil { + if strings.Contains(err.Error(), `violates unique constraint "genus_idx"`) { + return false, err + } + } + return true, nil +} + +func (s *generaStore) List(opt *models.GenusListOptions) ([]*models.Genus, error) { + if opt == nil { + opt = &models.GenusListOptions{} + } + var genera []*models.Genus + err := s.dbh.Select(&genera, `SELECT * FROM genera LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset()) + if err != nil { + return nil, err + } + return genera, nil +} diff --git a/datastore/genera_test.go b/datastore/genera_test.go new file mode 100644 index 0000000..5eebae2 --- /dev/null +++ b/datastore/genera_test.go @@ -0,0 +1,81 @@ +package datastore + +import ( + "reflect" + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestGeneraStore_Get_db(t *testing.T) { + want := &models.Genus{Id: 1, GenusName: "Test Genus"} + + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM genera;`) + if err := tx.Insert(want); err != nil { + t.Fatal(err) + } + + d := NewDatastore(tx) + genus, err := d.Genera.Get(1) + if err != nil { + t.Fatal(err) + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + if !reflect.DeepEqual(genus, want) { + t.Errorf("got genus %+v, want %+v", genus, want) + } +} + +func TestGeneraStore_Create_db(t *testing.T) { + genus := &models.Genus{Id: 1, GenusName: "Test Genus"} + + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM genera;`) + + d := NewDatastore(tx) + created, err := d.Genera.Create(genus) + if err != nil { + t.Fatal(err) + } + + if !created { + t.Error("!created") + } + if genus.Id == 0 { + t.Error("want nonzero genus.Id after submitting") + } +} + +func TestGeneraStore_List_db(t *testing.T) { + want := []*models.Genus{{Id: 1, GenusName: "Test Genus"}} + + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM genera;`) + if err := tx.Insert(want[0]); err != nil { + t.Fatal(err) + } + + d := NewDatastore(tx) + genera, err := d.Genera.List(&models.GenusListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}) + if err != nil { + t.Fatal(err) + } + + for _, g := range want { + normalizeTime(&g.CreatedAt, &g.UpdatedAt, &g.DeletedAt) + } + if !reflect.DeepEqual(genera, want) { + t.Errorf("got genera %+v, want %+v", genera, want) + } +} diff --git a/datastore/migrations/00002_AddGenera_down.sql b/datastore/migrations/00002_AddGenera_down.sql new file mode 100644 index 0000000..672e1fc --- /dev/null +++ b/datastore/migrations/00002_AddGenera_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE genera; + diff --git a/datastore/migrations/00002_AddGenera_up.sql b/datastore/migrations/00002_AddGenera_up.sql new file mode 100644 index 0000000..a9e0f5c --- /dev/null +++ b/datastore/migrations/00002_AddGenera_up.sql @@ -0,0 +1,19 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE genera ( + id BIGSERIAL NOT NULL, + genusname CHARACTER VARYING(100), + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT genus_pkey PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX genusname_idx + ON genera + USING btree + (genusname COLLATE pg_catalog."default"); + From 59a0cfd4bf6afcd946fc2c4398a6836497c73235 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 10:15:33 -0800 Subject: [PATCH 3/6] Genera: api. --- api/genera.go | 61 ++++++++++++++ api/genera_test.go | 90 +++++++++++++++++++++ api/handler.go | 6 ++ datastore/migrations/00002_AddGenera_up.sql | 6 +- 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 api/genera.go create mode 100644 api/genera_test.go diff --git a/api/genera.go b/api/genera.go new file mode 100644 index 0000000..3abc161 --- /dev/null +++ b/api/genera.go @@ -0,0 +1,61 @@ +package api + +import ( + "encoding/json" + "strconv" + + "github.com/gorilla/mux" + + "net/http" + + "github.com/thermokarst/bactdb/models" +) + +func serveGenus(w http.ResponseWriter, r *http.Request) error { + id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + if err != nil { + return err + } + + genus, err := store.Genera.Get(id) + if err != nil { + return err + } + + return writeJSON(w, genus) +} + +func serveCreateGenus(w http.ResponseWriter, r *http.Request) error { + var genus models.Genus + err := json.NewDecoder(r.Body).Decode(&genus) + if err != nil { + return err + } + + created, err := store.Genera.Create(&genus) + if err != nil { + return err + } + if created { + w.WriteHeader(http.StatusCreated) + } + + return writeJSON(w, genus) +} + +func serveGenera(w http.ResponseWriter, r *http.Request) error { + var opt models.GenusListOptions + if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { + return err + } + + genera, err := store.Genera.List(&opt) + if err != nil { + return err + } + if genera == nil { + genera = []*models.Genus{} + } + + return writeJSON(w, genera) +} diff --git a/api/genera_test.go b/api/genera_test.go new file mode 100644 index 0000000..d2ab758 --- /dev/null +++ b/api/genera_test.go @@ -0,0 +1,90 @@ +package api + +import ( + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestGenus_Get(t *testing.T) { + setup() + + want := &models.Genus{Id: 1, GenusName: "Test Genus"} + + calledGet := false + store.Genera.(*models.MockGeneraService).Get_ = func(id int64) (*models.Genus, error) { + if id != want.Id { + t.Errorf("wanted request for genus %d but got %d", want.Id, id) + } + calledGet = true + return want, nil + } + + got, err := apiClient.Genera.Get(want.Id) + if err != nil { + t.Fatal(err) + } + + if !calledGet { + t.Error("!calledGet") + } + if !normalizeDeepEqual(want, got) { + t.Errorf("got genus %+v but wanted genus %+v", got, want) + } +} + +func TestGenus_Create(t *testing.T) { + setup() + + want := &models.Genus{Id: 1, GenusName: "Test Genus"} + + calledPost := false + store.Genera.(*models.MockGeneraService).Create_ = func(genus *models.Genus) (bool, error) { + if !normalizeDeepEqual(want, genus) { + t.Errorf("wanted request for genus %d but got %d", want, genus) + } + calledPost = true + return true, nil + } + + success, err := apiClient.Genera.Create(want) + if err != nil { + t.Fatal(err) + } + + if !calledPost { + t.Error("!calledPost") + } + if !success { + t.Error("!success") + } +} + +func TestGenus_List(t *testing.T) { + setup() + + want := []*models.Genus{{Id: 1, GenusName: "Test Genus"}} + wantOpt := &models.GenusListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}} + + calledList := false + store.Genera.(*models.MockGeneraService).List_ = func(opt *models.GenusListOptions) ([]*models.Genus, error) { + if !normalizeDeepEqual(wantOpt, opt) { + t.Errorf("wanted options %d but got %d", wantOpt, opt) + } + calledList = true + return want, nil + } + + genera, err := apiClient.Genera.List(wantOpt) + if err != nil { + t.Fatal(err) + } + + if !calledList { + t.Error("!calledList") + } + + if !normalizeDeepEqual(&want, &genera) { + t.Errorf("got genera %+v but wanted genera %+v", genera, want) + } +} diff --git a/api/handler.go b/api/handler.go index 93dd042..59e1718 100644 --- a/api/handler.go +++ b/api/handler.go @@ -18,9 +18,15 @@ var ( func Handler() *mux.Router { m := router.API() + m.Get(router.User).Handler(handler(serveUser)) m.Get(router.CreateUser).Handler(handler(serveCreateUser)) m.Get(router.Users).Handler(handler(serveUsers)) + + m.Get(router.Genus).Handler(handler(serveGenus)) + m.Get(router.CreateGenus).Handler(handler(serveCreateGenus)) + m.Get(router.Genera).Handler(handler(serveGenera)) + return m } diff --git a/datastore/migrations/00002_AddGenera_up.sql b/datastore/migrations/00002_AddGenera_up.sql index a9e0f5c..3fa4d79 100644 --- a/datastore/migrations/00002_AddGenera_up.sql +++ b/datastore/migrations/00002_AddGenera_up.sql @@ -5,9 +5,9 @@ CREATE TABLE genera ( id BIGSERIAL NOT NULL, genusname CHARACTER VARYING(100), - created_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE, - deleted_at TIMESTAMP WITH TIME ZONE, + createdat TIMESTAMP WITH TIME ZONE, + updatedat TIMESTAMP WITH TIME ZONE, + deletedat TIMESTAMP WITH TIME ZONE, CONSTRAINT genus_pkey PRIMARY KEY (id) ); From c271f4b6b3589d52b168ddd82c11b977229d5776 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 10:17:20 -0800 Subject: [PATCH 4/6] sslmode declared in datastore. --- test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 9670bba..a20af0e 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -PGTZ=UTC PGSSLMODE=disable go test -v ./... +PGTZ=UTC go test -v ./... From 33194ffd2fcc268dd5b4f5d3a033a77320900845 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 12:22:50 -0800 Subject: [PATCH 5/6] Genera: update record. --- api/genera.go | 19 +++++++++++++++++++ api/genera_test.go | 30 ++++++++++++++++++++++++++++++ api/handler.go | 1 + datastore/genera.go | 11 +++++++++++ models/genera.go | 34 +++++++++++++++++++++++++++++++++- models/genera_test.go | 31 +++++++++++++++++++++++++++++++ router/api.go | 1 + router/routes.go | 8 +++++--- 8 files changed, 131 insertions(+), 4 deletions(-) diff --git a/api/genera.go b/api/genera.go index 3abc161..6d71264 100644 --- a/api/genera.go +++ b/api/genera.go @@ -59,3 +59,22 @@ func serveGenera(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, genera) } + +func serveUpdateGenus(w http.ResponseWriter, r *http.Request) error { + id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + var genus models.Genus + err := json.NewDecoder(r.Body).Decode(&genus) + if err != nil { + return err + } + + updated, err := store.Genera.Update(id, &genus) + if err != nil { + return err + } + if updated { + w.WriteHeader(http.StatusOK) + } + + return writeJSON(w, genus) +} diff --git a/api/genera_test.go b/api/genera_test.go index d2ab758..d99da53 100644 --- a/api/genera_test.go +++ b/api/genera_test.go @@ -88,3 +88,33 @@ func TestGenus_List(t *testing.T) { t.Errorf("got genera %+v but wanted genera %+v", genera, want) } } + +func TestGenus_Update(t *testing.T) { + setup() + + want := &models.Genus{Id: 1, GenusName: "Test Genus"} + + calledPut := false + store.Genera.(*models.MockGeneraService).Update_ = func(id int64, genus *models.Genus) (bool, error) { + if id != want.Id { + t.Errorf("wanted request for genus %d but got %d", want.Id, id) + } + if !normalizeDeepEqual(want, genus) { + t.Errorf("wanted request for genus %d but got %d", want, genus) + } + calledPut = true + return true, nil + } + + success, err := apiClient.Genera.Update(1, want) + if err != nil { + t.Fatal(err) + } + + if !calledPut { + t.Error("!calledPut") + } + if !success { + t.Error("!success") + } +} diff --git a/api/handler.go b/api/handler.go index 59e1718..e66546f 100644 --- a/api/handler.go +++ b/api/handler.go @@ -26,6 +26,7 @@ func Handler() *mux.Router { m.Get(router.Genus).Handler(handler(serveGenus)) m.Get(router.CreateGenus).Handler(handler(serveCreateGenus)) m.Get(router.Genera).Handler(handler(serveGenera)) + m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) return m } diff --git a/datastore/genera.go b/datastore/genera.go index 260ff07..f492f50 100644 --- a/datastore/genera.go +++ b/datastore/genera.go @@ -45,3 +45,14 @@ func (s *generaStore) List(opt *models.GenusListOptions) ([]*models.Genus, error } return genera, nil } + +func (s *generaStore) Update(id int64, genus *models.Genus) (bool, error) { + ret, err := s.dbh.Exec(`UPDATE genera SET genusname=$1 WHERE id=$2;`, genus.GenusName, id) + if err != nil { + return false, err + } + if rows, err := ret.RowsAffected(); rows == 0 { + return false, err + } + return true, nil +} diff --git a/models/genera.go b/models/genera.go index 2cad177..d6be8ee 100644 --- a/models/genera.go +++ b/models/genera.go @@ -28,6 +28,9 @@ type GeneraService interface { // Create a new genus. The newly created genus's ID is written to genus.Id Create(genus *Genus) (created bool, err error) + + // Update an existing genus. + Update(id int64, genus *Genus) (updated bool, err error) } var ( @@ -105,10 +108,32 @@ func (s *generaService) List(opt *GenusListOptions) ([]*Genus, error) { return genera, nil } +func (s *generaService) Update(id int64, genus *Genus) (bool, error) { + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.UpdateGenus, map[string]string{"Id": strId}, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("PUT", url.String(), genus) + if err != nil { + return false, err + } + + resp, err := s.client.Do(req, &genus) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + type MockGeneraService struct { Get_ func(id int64) (*Genus, error) List_ func(opt *GenusListOptions) ([]*Genus, error) - Create_ func(post *Genus) (bool, error) + Create_ func(genus *Genus) (bool, error) + Update_ func(id int64, genus *Genus) (bool, error) } var _ GeneraService = &MockGeneraService{} @@ -133,3 +158,10 @@ func (s *MockGeneraService) List(opt *GenusListOptions) ([]*Genus, error) { } return s.List_(opt) } + +func (s *MockGeneraService) Update(id int64, genus *Genus) (bool, error) { + if s.Update_ == nil { + return false, nil + } + return s.Update_(id, genus) +} diff --git a/models/genera_test.go b/models/genera_test.go index 08269c5..2374a10 100644 --- a/models/genera_test.go +++ b/models/genera_test.go @@ -105,3 +105,34 @@ func TestGeneraService_List(t *testing.T) { t.Errorf("Genera.List return %+v, want %+v", genera, want) } } + +func TestGeneraService_Update(t *testing.T) { + setup() + defer teardown() + + want := &Genus{Id: 1, GenusName: "Test Genus"} + + var called bool + mux.HandleFunc(urlPath(t, router.UpdateGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "PUT") + testBody(t, r, `{"id":1,"genus_name":"Test Genus Updated","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n") + + w.WriteHeader(http.StatusOK) + writeJSON(w, want) + }) + + genus := &Genus{Id: 1, GenusName: "Test Genus Updated"} + updated, err := client.Genera.Update(1, genus) + if err != nil { + t.Errorf("Genera.Update returned error: %v", err) + } + + if !updated { + t.Error("!updated") + } + + if !called { + t.Fatal("!called") + } +} diff --git a/router/api.go b/router/api.go index f545a6e..bf5a396 100644 --- a/router/api.go +++ b/router/api.go @@ -14,5 +14,6 @@ func API() *mux.Router { m.Path("/genera").Methods("GET").Name(Genera) m.Path("/genera").Methods("POST").Name(CreateGenus) m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) + m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) return m } diff --git a/router/routes.go b/router/routes.go index 4abfa86..ef4d7d8 100644 --- a/router/routes.go +++ b/router/routes.go @@ -1,10 +1,12 @@ package router const ( - User = "user" - CreateUser = "user:create" - Users = "users" + User = "user" + CreateUser = "user:create" + Users = "users" + Genus = "genus" CreateGenus = "genus:create" Genera = "genera" + UpdateGenus = "genus:update" ) From 279651930e208309b65cfd62fe4b3aad624006e8 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Mon, 13 Oct 2014 15:26:35 -0800 Subject: [PATCH 6/6] A few big changes - Adding timezone to source config - genera: update record (missed a few pieces) - genera: delete record --- api/genera.go | 14 ++++++++++ api/genera_test.go | 27 ++++++++++++++++++ api/handler.go | 1 + datastore/datastore.go | 7 +++++ datastore/db.go | 2 +- datastore/genera.go | 31 +++++++++++++++++++-- datastore/genera_test.go | 59 ++++++++++++++++++++++++++++++++++++++++ models/genera.go | 33 ++++++++++++++++++++++ models/genera_test.go | 29 ++++++++++++++++++++ router/api.go | 1 + router/routes.go | 1 + test.sh | 2 +- 12 files changed, 203 insertions(+), 4 deletions(-) diff --git a/api/genera.go b/api/genera.go index 6d71264..e34196a 100644 --- a/api/genera.go +++ b/api/genera.go @@ -78,3 +78,17 @@ func serveUpdateGenus(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, genus) } + +func serveDeleteGenus(w http.ResponseWriter, r *http.Request) error { + id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + + deleted, err := store.Genera.Delete(id) + if err != nil { + return err + } + if deleted { + w.WriteHeader(http.StatusOK) + } + + return writeJSON(w, &models.Genus{}) +} diff --git a/api/genera_test.go b/api/genera_test.go index d99da53..c5b1949 100644 --- a/api/genera_test.go +++ b/api/genera_test.go @@ -118,3 +118,30 @@ func TestGenus_Update(t *testing.T) { t.Error("!success") } } + +func TestGenus_Delete(t *testing.T) { + setup() + + want := &models.Genus{Id: 1, GenusName: "Test Genus"} + + calledDelete := false + store.Genera.(*models.MockGeneraService).Delete_ = func(id int64) (bool, error) { + if id != want.Id { + t.Errorf("wanted request for genus %d but got %d", want.Id, id) + } + calledDelete = true + return true, nil + } + + success, err := apiClient.Genera.Delete(1) + if err != nil { + t.Fatal(err) + } + + if !calledDelete { + t.Error("!calledDelete") + } + if !success { + t.Error("!success") + } +} diff --git a/api/handler.go b/api/handler.go index e66546f..a01506b 100644 --- a/api/handler.go +++ b/api/handler.go @@ -27,6 +27,7 @@ func Handler() *mux.Router { m.Get(router.CreateGenus).Handler(handler(serveCreateGenus)) m.Get(router.Genera).Handler(handler(serveGenera)) m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) + m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) return m } diff --git a/datastore/datastore.go b/datastore/datastore.go index 94ff786..43faafd 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -1,6 +1,8 @@ package datastore import ( + "errors" + "github.com/jmoiron/modl" "github.com/thermokarst/bactdb/models" ) @@ -12,6 +14,11 @@ type Datastore struct { dbh modl.SqlExecutor } +var ( + ErrNoRowsUpdated = errors.New(`no rows updated`) + ErrNoRowsDeleted = errors.New(`no rows deleted`) +) + // NewDatastore creates a new client for accessing the datastore (in PostgreSQL). // If dbh is nil, it uses the global DB handle. func NewDatastore(dbh modl.SqlExecutor) *Datastore { diff --git a/datastore/db.go b/datastore/db.go index e8a6b93..71d613e 100644 --- a/datastore/db.go +++ b/datastore/db.go @@ -26,7 +26,7 @@ var connectOnce sync.Once func Connect() { connectOnce.Do(func() { var err error - DB.Dbx, err = sqlx.Open("postgres", "sslmode=disable") + DB.Dbx, err = sqlx.Open("postgres", "timezone=UTC sslmode=disable") if err != nil { log.Fatal("Error connecting to PostgreSQL database (using PG* environment variables): ", err) } diff --git a/datastore/genera.go b/datastore/genera.go index f492f50..637c58b 100644 --- a/datastore/genera.go +++ b/datastore/genera.go @@ -47,12 +47,39 @@ func (s *generaStore) List(opt *models.GenusListOptions) ([]*models.Genus, error } func (s *generaStore) Update(id int64, genus *models.Genus) (bool, error) { - ret, err := s.dbh.Exec(`UPDATE genera SET genusname=$1 WHERE id=$2;`, genus.GenusName, id) + _, err := s.Get(id) if err != nil { return false, err } - if rows, err := ret.RowsAffected(); rows == 0 { + + if id != genus.Id { + return false, models.ErrGenusNotFound + } + + changed, err := s.dbh.Update(genus) + if err != nil { return false, err } + + if changed == 0 { + return false, ErrNoRowsUpdated + } + + return true, nil +} + +func (s *generaStore) Delete(id int64) (bool, error) { + genus, err := s.Get(id) + if err != nil { + return false, err + } + + deleted, err := s.dbh.Delete(genus) + if err != nil { + return false, err + } + if deleted == 0 { + return false, ErrNoRowsDeleted + } return true, nil } diff --git a/datastore/genera_test.go b/datastore/genera_test.go index 5eebae2..32b9d77 100644 --- a/datastore/genera_test.go +++ b/datastore/genera_test.go @@ -79,3 +79,62 @@ func TestGeneraStore_List_db(t *testing.T) { t.Errorf("got genera %+v, want %+v", genera, want) } } + +func TestGeneraStore_Update_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM genera;`) + + d := NewDatastore(nil) + // Add a new record + genus := &models.Genus{GenusName: "Test Genus"} + created, err := d.Genera.Create(genus) + if err != nil { + t.Fatal(err) + } + if !created { + t.Error("!created") + } + + // Tweak it + genus.GenusName = "Updated Genus" + updated, err := d.Genera.Update(genus.Id, genus) + if err != nil { + t.Fatal(err) + } + + if !updated { + t.Error("!updated") + } +} + +func TestGeneraStore_Delete_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM genera;`) + + d := NewDatastore(tx) + // Add a new record + genus := &models.Genus{GenusName: "Test Genus"} + created, err := d.Genera.Create(genus) + if err != nil { + t.Fatal(err) + } + if !created { + t.Error("!created") + } + + // Delete it + deleted, err := d.Genera.Delete(genus.Id) + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Error("!delete") + } +} diff --git a/models/genera.go b/models/genera.go index d6be8ee..fb87694 100644 --- a/models/genera.go +++ b/models/genera.go @@ -31,6 +31,9 @@ type GeneraService interface { // Update an existing genus. Update(id int64, genus *Genus) (updated bool, err error) + + // Delete an existing genus. + Delete(id int64) (deleted bool, err error) } var ( @@ -129,11 +132,34 @@ func (s *generaService) Update(id int64, genus *Genus) (bool, error) { return resp.StatusCode == http.StatusOK, nil } +func (s *generaService) Delete(id int64) (bool, error) { + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.DeleteGenus, map[string]string{"Id": strId}, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("DELETE", url.String(), nil) + if err != nil { + return false, err + } + + var genus *Genus + resp, err := s.client.Do(req, &genus) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + type MockGeneraService struct { Get_ func(id int64) (*Genus, error) List_ func(opt *GenusListOptions) ([]*Genus, error) Create_ func(genus *Genus) (bool, error) Update_ func(id int64, genus *Genus) (bool, error) + Delete_ func(id int64) (bool, error) } var _ GeneraService = &MockGeneraService{} @@ -165,3 +191,10 @@ func (s *MockGeneraService) Update(id int64, genus *Genus) (bool, error) { } return s.Update_(id, genus) } + +func (s *MockGeneraService) Delete(id int64) (bool, error) { + if s.Delete_ == nil { + return false, nil + } + return s.Delete_(id) +} diff --git a/models/genera_test.go b/models/genera_test.go index 2374a10..453f0bf 100644 --- a/models/genera_test.go +++ b/models/genera_test.go @@ -136,3 +136,32 @@ func TestGeneraService_Update(t *testing.T) { t.Fatal("!called") } } + +func TestGeneraService_Delete(t *testing.T) { + setup() + defer teardown() + + want := &Genus{Id: 1, GenusName: "Test Genus"} + + var called bool + mux.HandleFunc(urlPath(t, router.DeleteGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "DELETE") + + w.WriteHeader(http.StatusOK) + writeJSON(w, want) + }) + + deleted, err := client.Genera.Delete(1) + if err != nil { + t.Errorf("Genera.Delete returned error: %v", err) + } + + if !deleted { + t.Error("!deleted") + } + + if !called { + t.Fatal("!called") + } +} diff --git a/router/api.go b/router/api.go index bf5a396..1df34ee 100644 --- a/router/api.go +++ b/router/api.go @@ -15,5 +15,6 @@ func API() *mux.Router { m.Path("/genera").Methods("POST").Name(CreateGenus) m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) + m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) return m } diff --git a/router/routes.go b/router/routes.go index ef4d7d8..e3bb7bb 100644 --- a/router/routes.go +++ b/router/routes.go @@ -9,4 +9,5 @@ const ( CreateGenus = "genus:create" Genera = "genera" UpdateGenus = "genus:update" + DeleteGenus = "genus:delete" ) diff --git a/test.sh b/test.sh index a20af0e..89a7703 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -PGTZ=UTC go test -v ./... +go test -v ./...