Skip to content

fix(handlers_test.go): add unit tests for handlers #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Mar 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
738f96a
fix(handlers_test.go): begin adding unit tests for handlers
Mar 18, 2016
702880c
feat(tests): add fakes for DB and Count
Mar 18, 2016
313c8c5
fix(glide): add asserts library and sqlite3 lib
Mar 18, 2016
d63af6b
feat(handlers_test.go): add preliminary test for ClustersHandler
Mar 18, 2016
7009144
fix(handlers_test.go): add comma to fix compile err
Mar 18, 2016
c0cfbcf
fix(mem_db.go): open sqlite3 DB the canonical way
Mar 18, 2016
5aa7633
fix(handlers/handlers_test.go): add necessary imports
Mar 21, 2016
450f417
fix(handlers/handlers_test.go): fix compile err
Mar 21, 2016
ef2e316
doc(data/mem_db.go): add godoc for in-memory DB
Mar 21, 2016
634b86d
fix(tests): make server tests compile
Mar 21, 2016
1e9cc5f
fix(handlers/handlers_test.go): remove unused var
Mar 21, 2016
d251afd
ref(server.go): parameterize the getRoutes func
Mar 21, 2016
fb09699
ref(server_test.go): take advantage of new getRoutes func
Mar 21, 2016
2c2a89b
fix(server_test.go): get more tests passing
Mar 21, 2016
8c77890
fix(data/data.go): print error on creating checking record
Mar 21, 2016
9272ccc
fix(handler_test.go): remove handlers test
Mar 21, 2016
37f2093
fix(data,server.go,server_test.go): make VerifyPersistentStorage take…
Mar 21, 2016
423a36e
fix(server_test.go): print more error details
Mar 21, 2016
7684881
fix(handlers/handlers.go): add better internal logging
Mar 21, 2016
adc114b
fix(data): add timestamp type
Mar 21, 2016
b314f87
fix(server_test.go): add one more check to the post test
Mar 21, 2016
2a52886
fix(data/timestamp.go): remove errant logging
Mar 21, 2016
e7d50e0
fix(data/timestamp.go): remove unused import
Mar 21, 2016
e378605
fix(data/timestamp_test.go): add tests for Timestamp
Mar 21, 2016
391a86d
fix(data/timestamp.go): return known error on invalid type
Mar 21, 2016
c0b76d2
doc(data/timestamp_test.go): clarify comment
Mar 21, 2016
c9f8f75
fix(glide.yaml): placate glide
Mar 21, 2016
f8bef7b
fix(glide): make glide happy, again
Mar 21, 2016
8222183
fix(data/data.go): split long line into multiple
Mar 21, 2016
c9b0b3b
fix(db): move the sqlite code into a test file
Mar 21, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 42 additions & 28 deletions data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"os"
"strconv"
"sync"
"time"

"database/sql"

Expand Down Expand Up @@ -52,23 +51,23 @@ var (
// ClustersTable type that expresses the `clusters` postgres table schema
type ClustersTable struct {
clusterID string // PRIMARY KEY
firstSeen time.Time
lastSeen time.Time
firstSeen *Timestamp
lastSeen *Timestamp
data sqlxTypes.JSONText
}

// ClustersCheckinsTable type that expresses the `clusters_checkins` postgres table schema
type ClustersCheckinsTable struct {
checkinID string // PRIMARY KEY, type uuid
clusterID string // indexed
createdAt time.Time // indexed
checkinID string // PRIMARY KEY, type uuid
clusterID string // indexed
createdAt *Timestamp // indexed
data sqlxTypes.JSONText
}

// VersionsTable type that expresses the `deis_component_versions` postgres table schema
type VersionsTable struct {
componentName string // PRIMARY KEY
lastUpdated time.Time
lastUpdated *Timestamp
data sqlxTypes.JSONText
}

Expand Down Expand Up @@ -99,8 +98,8 @@ func (c ClusterFromDB) Get(db *sql.DB, id string) (types.Cluster, error) {
log.Println("error parsing cluster")
return types.Cluster{}, err
}
cluster.FirstSeen = rowResult.firstSeen
cluster.LastSeen = rowResult.lastSeen
cluster.FirstSeen = *rowResult.firstSeen.Time
cluster.LastSeen = *rowResult.lastSeen.Time
return cluster, nil
}

Expand Down Expand Up @@ -153,7 +152,7 @@ func (c ClusterFromDB) Checkin(db *sql.DB, id string, cluster types.Cluster) (sq
}
result, err := newClusterCheckinsDBRecord(db, id, js)
if err != nil {
log.Println("cluster checkin db record not created")
log.Println("cluster checkin db record not created", err)
return nil, err
}
return result, nil
Expand Down Expand Up @@ -253,14 +252,12 @@ func (r RDSDB) Get() (*sql.DB, error) {
}

// VerifyPersistentStorage is a high level interace for verifying storage abstractions
func VerifyPersistentStorage() error {
db, err := getRDSDB()
func VerifyPersistentStorage(dbGetter DB) error {
db, err := dbGetter.Get()
if err != nil {
log.Println("couldn't get a db connection")
return err
}
err = verifyVersionsTable(db)
if err != nil {
if err := verifyVersionsTable(db); err != nil {
log.Println("unable to verify " + versionsTableName + " table")
return err
}
Expand Down Expand Up @@ -398,15 +395,37 @@ func getRDSDB() (*sql.DB, error) {
}

func createClustersTable(db *sql.DB) (sql.Result, error) {
return db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( %s uuid PRIMARY KEY, %s timestamp, %s timestamp DEFAULT current_timestamp, %s json )", clustersTableName, clustersTableIDKey, clustersTableFirstSeenKey, clustersTableLastSeenKey, clustersTableDataKey))
return db.Exec(fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s ( %s uuid PRIMARY KEY, %s timestamp, %s timestamp DEFAULT current_timestamp, %s json )",
clustersTableName,
clustersTableIDKey,
clustersTableFirstSeenKey,
clustersTableLastSeenKey,
clustersTableDataKey,
))
}

func createClustersCheckinsTable(db *sql.DB) (sql.Result, error) {
return db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( %s bigserial PRIMARY KEY, %s uuid, %s timestamp, %s json, unique (%s, %s) )", clustersCheckinsTableName, clustersCheckinsTableIDKey, clustersTableIDKey, clustersCheckinsTableClusterCreatedAtKey, clustersCheckinsTableDataKey, clustersCheckinsTableClusterIDKey, clustersCheckinsTableClusterCreatedAtKey))
return db.Exec(fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s ( %s bigserial PRIMARY KEY, %s uuid, %s timestamp, %s json, unique (%s, %s) )",
clustersCheckinsTableName,
clustersCheckinsTableIDKey,
clustersTableIDKey,
clustersCheckinsTableClusterCreatedAtKey,
clustersCheckinsTableDataKey,
clustersCheckinsTableClusterIDKey,
clustersCheckinsTableClusterCreatedAtKey,
))
}

func createVersionsTable(db *sql.DB) (sql.Result, error) {
return db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( %s varchar(64) PRIMARY KEY, %s timestamp, %s json )", versionsTableName, versionsTableComponentNameKey, versionsTableLastUpdatedKey, versionsTableDataKey))
return db.Exec(fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s ( %s varchar(64) PRIMARY KEY, %s timestamp, %s json )",
versionsTableName,
versionsTableComponentNameKey,
versionsTableLastUpdatedKey,
versionsTableDataKey,
))
}

func verifyClustersTable(db *sql.DB) error {
Expand Down Expand Up @@ -457,31 +476,26 @@ func getTableCount(db *sql.DB, table string) (int, error) {
}

func newClusterDBRecord(db *sql.DB, id string, data []byte) (sql.Result, error) {
now := time.Now().Format(time.RFC3339)
insert := fmt.Sprintf("INSERT INTO %s (cluster_id, first_seen, last_seen, data) VALUES('%s', '%s', '%s', '%s')", clustersTableName, id, now, now, string(data))
insert := fmt.Sprintf("INSERT INTO %s (cluster_id, first_seen, last_seen, data) VALUES('%s', '%s', '%s', '%s')", clustersTableName, id, now(), now(), string(data))
return db.Exec(insert)
}

func newVersionDBRecord(db *sql.DB, component string, data []byte) (sql.Result, error) {
now := time.Now().Format(time.RFC3339)
insert := fmt.Sprintf("INSERT INTO %s (component_name, last_updated, data) VALUES('%s', '%s', '%s')", versionsTableName, component, now, string(data))
insert := fmt.Sprintf("INSERT INTO %s (component_name, last_updated, data) VALUES('%s', '%s', '%s')", versionsTableName, component, now(), string(data))
return db.Exec(insert)
}

func updateClusterDBRecord(db *sql.DB, id string, data []byte) (sql.Result, error) {
now := time.Now().Format(time.RFC3339)
update := fmt.Sprintf("UPDATE %s SET data='%s', last_seen='%s' WHERE cluster_id='%s'", clustersTableName, string(data), now, id)
update := fmt.Sprintf("UPDATE %s SET data='%s', last_seen='%s' WHERE cluster_id='%s'", clustersTableName, string(data), now(), id)
return db.Exec(update)
}

func newClusterCheckinsDBRecord(db *sql.DB, id string, data []byte) (sql.Result, error) {
now := time.Now().Format(time.RFC3339)
update := fmt.Sprintf("INSERT INTO %s (data, created_at, cluster_id) VALUES('%s', '%s', '%s')", clustersCheckinsTableName, string(data), now, id)
update := fmt.Sprintf("INSERT INTO %s (data, created_at, cluster_id) VALUES('%s', '%s', '%s')", clustersCheckinsTableName, string(data), now(), id)
return db.Exec(update)
}

func updateVersionDBRecord(db *sql.DB, component string, data []byte) (sql.Result, error) {
now := time.Now().Format(time.RFC3339)
update := fmt.Sprintf("UPDATE %s SET data='%s', last_updated='%s' WHERE component_name='%s'", versionsTableName, string(data), now, component)
update := fmt.Sprintf("UPDATE %s SET data='%s', last_updated='%s' WHERE component_name='%s'", versionsTableName, string(data), now(), component)
return db.Exec(update)
}
59 changes: 59 additions & 0 deletions data/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package data

import (
"database/sql/driver"
"errors"
"time"
)

const (
stdTimestampFmt = time.RFC3339
)

var (
errInvalidType = errors.New("invalid type for current timestamp")
)

// Timestamp is a fmt.Stringer, sql.Scanner and driver.Valuer implementation which is able to encode and decode
// time.Time values into and out of a database. This implementation was inspired heavily from
// https://groups.google.com/forum/#!topic/golang-nuts/P6Wrm_uVvJ0
type Timestamp struct {
Time *time.Time
}

// Scan is the Scanner interface implementation
func (ts *Timestamp) Scan(value interface{}) error {
switch v := value.(type) {
case string:
t, err := time.Parse(stdTimestampFmt, v)
if err != nil {
return err
}
ts.Time = &t
return nil
case []byte:
t, err := time.Parse(stdTimestampFmt, string(v))
if err != nil {
return err
}
ts.Time = &t
return nil
default:
return errInvalidType
}
}

// Value is the Valuer interface implementation
func (ts *Timestamp) Value() (driver.Value, error) {
str := ts.Time.Format(stdTimestampFmt)
return str, nil
}

// String is the fmt.Stringer interface implementation
func (ts *Timestamp) String() string {
return ts.Time.Format(stdTimestampFmt)
}

func now() string {
return time.Now().Format(stdTimestampFmt)
}
98 changes: 98 additions & 0 deletions data/timestamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package data

import (
"testing"
"time"
)

// since various times have been decoded from different encodings, they have different bits of
// information (and missing info is assumed to be the zero value by time.Parse).
// For example, a time decoded from time.Kitchen (3:04PM) has much less information
// than time.RFC3339Nano (2006-01-02T15:04:05.999999999Z07:00), so t1.Equal(t2) will be false.
// this func tries to determine whether they are "close enough"
func fuzzyTimeEqual(t1 time.Time, t2 time.Time) bool {
return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
}

type timestampScanTestCase struct {
val interface{}
expectedTime time.Time
expectedErr bool
}

func TestTimestampScan(t *testing.T) {
now := time.Now()
testCases := []timestampScanTestCase{
timestampScanTestCase{val: now.Format(stdTimestampFmt), expectedTime: now, expectedErr: false},
timestampScanTestCase{val: []byte(now.Format(stdTimestampFmt)), expectedTime: now, expectedErr: false},
timestampScanTestCase{val: now.Format(time.ANSIC), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.UnixDate), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RubyDate), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC822), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC822Z), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC850), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC1123), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC1123Z), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: now.Format(time.RFC3339), expectedTime: now, expectedErr: false},
// RFC3339Nano is a superset of RFC3339, so the time package can parse it
timestampScanTestCase{val: now.Format(time.RFC3339Nano), expectedTime: now, expectedErr: false},
timestampScanTestCase{val: now.Format(time.Kitchen), expectedTime: now, expectedErr: true},
timestampScanTestCase{val: true, expectedTime: now, expectedErr: true},
}
for i, testCase := range testCases {
ts := new(Timestamp)
err := ts.Scan(testCase.val)
if testCase.expectedErr && err == nil {
t.Errorf("test case %d expected err", i+1)
continue
}
if !testCase.expectedErr && err != nil {
t.Errorf("test case %d didn't expect err", i+1)
continue
}
if testCase.expectedErr && err != nil {
continue
}
if ts.Time == nil {
t.Errorf("test case %d expected non-nil time", i+1)
continue
}
if !fuzzyTimeEqual(testCase.expectedTime, *ts.Time) {
t.Errorf("test case %d expected time %s doesn't match actual %s", i+1, testCase.expectedTime, *ts.Time)
continue
}
}
}

func TestTimestampNow(t *testing.T) {
n := now()
if _, err := time.Parse(stdTimestampFmt, n); err != nil {
t.Fatal(err)
}
}

func TestTimestampString(t *testing.T) {
now := time.Now()
ts := new(Timestamp)
ts.Time = &now
if _, err := time.Parse(stdTimestampFmt, ts.String()); err != nil {
t.Fatal(err)
}
}

func TestTimestampValue(t *testing.T) {
now := time.Now()
ts := new(Timestamp)
ts.Time = &now
val, err := ts.Value()
if err != nil {
t.Fatal(err)
}
str, ok := val.(string)
if !ok {
t.Fatalf("returned value was not a string")
}
if _, err := time.Parse(stdTimestampFmt, str); err != nil {
t.Fatalf("returned value was not in expected format %s (%s)", stdTimestampFmt, err)
}
}
12 changes: 9 additions & 3 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ import:
subpackages:
- types
- package: github.com/lib/pq
- package: github.com/arschles/assert
- package: github.com/mxk/go-sqlite/sqlite3
4 changes: 4 additions & 0 deletions handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handlers
// handler echoes the HTTP request.
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
Expand Down Expand Up @@ -59,11 +60,14 @@ func ClustersPostHandler(d data.DB, c data.Cluster) func(http.ResponseWriter, *h
var result types.Cluster
result, err = data.SetCluster(id, cluster, d, c)
if err != nil {
log.Printf("SetCluster error (%s)", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
http.Error(w, fmt.Sprintf("JSON marshaling failed", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(data))
Expand Down
Loading