Compare commits

...

87 Commits

Author SHA1 Message Date
c3be0b8e54 Adds put plan route
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-02-07 15:27:12 -06:00
8ca7858069 Adds update plan method
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-02-07 15:11:44 -06:00
5e030a5bc3 Uses plan description instead of date 2021-02-02 15:58:18 -06:00
b47abbfc0b Adds put route tests for current plans
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-31 14:29:53 -06:00
4905d18222 Adds tests for post current plan 2021-01-31 14:24:05 -06:00
eb8838ab75 Add store/postgres tests for current plan 2021-01-31 13:57:54 -06:00
6ad0112683 Adds tests for inmemory store 2021-01-31 12:26:39 -06:00
b9cea2347c Adds error store tests 2021-01-31 12:18:58 -06:00
e420bf303a Adds tests for model code 2021-01-31 12:15:30 -06:00
92ddd9e0fe Current plan beats primary plan 2021-01-31 11:54:41 -06:00
8c17aa9d6d Adds current plan implementation 2021-01-31 11:42:47 -06:00
d37c53c60b Add put test unauthorized
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-30 12:44:47 -06:00
2465b0a73a Adds user id to actions 2021-01-30 12:40:58 -06:00
12515c2a1e Merge pull request 'auth' (#3) from auth into master
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
Reviewed-on: #3
2021-01-25 22:31:49 +00:00
6dd91f5a1b Adds tests for auth route
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
gitea-deepak/gogmagog/pipeline/pr-master This commit looks good
2021-01-25 16:23:42 -06:00
249fabbd7a Adds tests for register route 2021-01-25 15:18:56 -06:00
c9675b1573 Adds last currentUser route tests
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-25 15:00:07 -06:00
63a9e2ff58 Adds tests for context errors 2021-01-25 14:58:06 -06:00
28325c8d7b Adds current user tests. 2021-01-25 14:51:28 -06:00
93a5e9c1ba Adds middleware set userid on context method. 2021-01-25 14:50:54 -06:00
4d093ed99a Adds plan route unauthorised tests
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-25 14:34:17 -06:00
77e6e6bc04 Adds test for context methods 2021-01-25 14:25:12 -06:00
c28939d2b8 Adds middleware testing methods
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-25 13:33:52 -06:00
cfe5d89b22 Adds test for token encode/decode 2021-01-25 12:51:19 -06:00
f84c9b6ea2 Adds error store test for missing store 2021-01-25 12:30:38 -06:00
42d808165b Adds current user route
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-24 19:45:56 -06:00
96b22a2254 Adds user by username model method 2021-01-24 19:26:47 -06:00
c1ae0706f9 Route now gets user ID from context with custom middleware
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-17 17:33:40 -06:00
8eff1115c5 Fixes failing tests, but incompletely 2021-01-17 15:10:24 -06:00
4708a2b8ff adds user_id as a selectable field
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-17 11:37:44 -06:00
4989848335 Fixes table create order 2021-01-17 11:25:38 -06:00
d02d48e7c8 Adds user id field to plan 2021-01-17 11:22:14 -06:00
2a3d789292 Makes error store allow update if error only on get 2021-01-17 11:13:29 -06:00
169afed5c9 Adds some stuff for only failing on the subsequent retrieve
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-12 19:12:39 -06:00
3648d5a1cc Removing unnecessary type 2021-01-12 18:53:54 -06:00
1c3555d8b7 Removed multistore
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-12 18:46:24 -06:00
d8604dc3cc Adds inmemorystore to help refactor tests
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-12 18:16:47 -06:00
417a7cf982 Decouples routers and adds auth for plans and actions call
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-12 16:15:28 -06:00
ab1dab6619 jwt auth token return 2021-01-12 15:52:46 -06:00
262321a1e2 Adds auth route that checks username and password
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-12 12:43:45 -06:00
c8b8f87f6c Change to select by username because that makes more sense 2021-01-12 11:36:44 -06:00
f59593e9e8 adds create user func 2021-01-12 11:17:03 -06:00
c8e33884c0 fixes lint issues
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-11 20:39:29 -06:00
55c8c739b0 fmt
A
2021-01-11 20:38:11 -06:00
1d45635530 adds no password return by default, model cares about password but nothing downstream does
Some checks failed
gitea-deepak/gogmagog/pipeline/head There was a failure building this commit
2021-01-11 20:37:35 -06:00
3ea8603368 Adds user table and adds support for config flag to drop DB on restart
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-11 20:15:36 -06:00
c0175fc9bc fixes content-type for post and put on actions
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-10 11:58:16 -06:00
7e7558dd5e Merge pull request 'put_actions' (#2) from put_actions into master
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
Reviewed-on: #2
2021-01-10 17:35:20 +00:00
86a4a28aee adds put action route
All checks were successful
gitea-deepak/gogmagog/pipeline/pr-master This commit looks good
2021-01-10 11:27:32 -06:00
a5528be456 Adding new func
All checks were successful
gitea-deepak/gogmagog/pipeline/pr-master This commit looks good
2021-01-10 10:38:03 -06:00
0faef20441 fmt changes
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
gitea-deepak/gogmagog/pipeline/pr-master This commit looks good
2021-01-09 23:54:52 -06:00
395231e1bf adds test for errors in update
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-09 23:53:16 -06:00
bc68115ce1 adds update action method 2021-01-09 23:39:03 -06:00
dfb5ead740 Merge pull request 'actions_post' (#1) from actions_post into master
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
Reviewed-on: #1
2021-01-10 04:53:13 +00:00
2c630aff95 Adds additional postgres insert fields for action
Some checks are pending
gitea-deepak/gogmagog/pipeline/head This commit looks good
gitea-deepak/gogmagog/pipeline/pr-master Build queued...
2021-01-09 22:25:14 -06:00
dc18440821 Fixes test to improve coverage
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-09 22:10:44 -06:00
4b4070df98 Adds test for action post 2021-01-09 22:05:54 -06:00
325aa46cf2 Adds actions insert to store 2021-01-09 21:50:54 -06:00
ebee8a304b codestyle fix
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-09 16:18:21 -06:00
3f13118312 Adds more error handling and sets plan date
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-09 16:11:43 -06:00
27cdde132d Adds post endpoint for creating plans
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-03 20:28:46 -06:00
f8b1949c28 Adds route to create plans 2021-01-03 20:15:39 -06:00
9b276bd644 Adds route for filtering actions by planID
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-03 11:12:31 -06:00
70ddd91d6b Adds actions, changes time to pointers to make nillable.
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-01 11:49:22 -06:00
95d945dda7 Added plan by id route and error handling and organisation 2021-01-01 08:41:17 -06:00
bbb0cf3f42 Adds way to check for not found error on id search
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2021-01-01 08:07:55 -06:00
cd7f919eb2 Removes calls to log.Fatal, adds check for null store
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-31 18:56:21 -06:00
e77d3c4d5e Adds health check and test 2020-12-31 18:47:27 -06:00
0e16bb5361 Adds healthy check to model 2020-12-31 18:05:06 -06:00
2bda056ca7 Adds route to retrieve all plans and tests thereof, and json tags for model
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-31 11:20:00 -06:00
6d104dc72a adds missing test from postgres select actions by plan id
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-30 11:11:52 -06:00
72e3fbe05b adds test for commit failure check
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 19:16:13 -06:00
acf793ae41 Adds select actions by plan function 2020-12-29 19:10:54 -06:00
e6158e680f Adds extra test for postgres case
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 19:02:15 -06:00
9f5f5413d1 Adds insert plan function
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 18:44:37 -06:00
9c56ea56e2 Adds insertplan to model 2020-12-29 18:20:28 -06:00
b6a6c9375f Adds timezone to config 2020-12-29 18:12:04 -06:00
0c6f686ce5 added select plan by id
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 16:26:30 -06:00
ba18d011bd Fixes Postgres to be postgres style bindvars
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 16:16:09 -06:00
ad47895597 Add select by action id 2020-12-29 16:08:31 -06:00
50bbcdc71d adds full action fields to struct
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 15:05:00 -06:00
7ae7f294da adds go fmt to do.sh commands and makes indents friendlier spaced 2020-12-29 15:04:41 -06:00
c0d5a543f0 No need for empty main_test file
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 13:59:08 -06:00
8f78b80043 Adds some db tests using sql mock and slightly reorganises to facilitate testing
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 13:43:32 -06:00
506e7f64b6 Moved os dependence to main.go and had db code return error 2020-12-29 10:39:42 -06:00
52646fbfb7 Less verbose 2020-12-29 10:12:03 -06:00
240f3f5aae Add run function and make test robust and return correctly 2020-12-29 10:10:02 -06:00
67 changed files with 7117 additions and 177 deletions

View File

@@ -1,7 +1,7 @@
app:
environment: "devel"
port: 5151
timezone: Africa/Abidjan
db:
type: "aoeu"
host: "aeihn"
@@ -9,3 +9,4 @@ db:
user: USER
password: PASSWORD
database: g2
droponstart: true # don't use this in production!

View File

@@ -10,16 +10,18 @@ import (
type AppConfig struct {
Environment string
Port string
Timezone string
}
// DBConfig is the config for the DB connection.
type DBConfig struct {
Type string
Host string
Port string
User string
Password string
Database string
Type string
Host string
Port string
User string
Password string
Database string
DropOnStart bool
}
// Conf represents the overall configuration of the application.
@@ -33,14 +35,16 @@ func createDefaultConf() *Conf {
App: AppConfig{
Environment: "local",
Port: "8080",
Timezone: "America/New_York",
},
Db: DBConfig{
Type: "postgres",
Host: "localhost",
Port: "5432",
User: "<user>",
Password: "<password>",
Database: "gogmagog",
Type: "postgres",
Host: "localhost",
Port: "5432",
User: "<user>",
Password: "<password>",
Database: "gogmagog",
DropOnStart: false,
},
}

View File

@@ -16,6 +16,7 @@ func TestSample(t *testing.T) {
appConf := conf.App
assert.Equal("devel", appConf.Environment)
assert.Equal("5151", appConf.Port)
assert.Equal("Africa/Abidjan", appConf.Timezone)
dbConf := conf.Db
assert.Equal("aoeu", dbConf.Type)
@@ -24,6 +25,7 @@ func TestSample(t *testing.T) {
assert.Equal("USER", dbConf.User)
assert.Equal("PASSWORD", dbConf.Password)
assert.Equal("g2", dbConf.Database)
assert.True(dbConf.DropOnStart)
}
func TestDefault(t *testing.T) {
@@ -35,6 +37,7 @@ func TestDefault(t *testing.T) {
appConf := conf.App
assert.Equal("missingfield", appConf.Environment)
assert.Equal("8080", appConf.Port)
assert.Equal("America/New_York", appConf.Timezone)
dbConf := conf.Db
assert.Equal("typical", dbConf.Type)
@@ -43,6 +46,7 @@ func TestDefault(t *testing.T) {
assert.Equal("<user>", dbConf.User)
assert.Equal("<password>", dbConf.Password)
assert.Equal("gogmagog", dbConf.Database)
assert.False(dbConf.DropOnStart)
}
func TestMissingFile(t *testing.T) {

View File

@@ -1,83 +0,0 @@
package db
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"log"
"os"
"gitea.deepak.science/deepak/gogmagog/config"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/util"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
// Blank imports to provide drivers.
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jackc/pgx/v4/stdlib"
)
type postgresStore struct {
db *sqlx.DB
}
// GetStore provides a store to back the model based on a postgres connection.
func GetStore(dbConf *config.DBConfig) models.Store {
if dbConf.Type != "postgres" {
log.Fatalf("Unsupported database type: " + dbConf.Type)
os.Exit(1)
}
connStr := fmt.Sprintf("user=%s password=%s host=%s port=%s database=%s sslmode=disable",
dbConf.User,
dbConf.Password,
dbConf.Host,
dbConf.Port,
dbConf.Database)
tmp, err := sql.Open("pgx", connStr)
if err != nil {
log.Fatal("Could not connect to database: \n", err)
os.Exit(1)
}
db := sqlx.NewDb(tmp, "pgx")
if err := db.Ping(); err != nil {
log.Fatal("database ping failed\n", err)
os.Exit(1)
}
driver, err := postgres.WithInstance(tmp, &postgres.Config{})
if err != nil {
log.Fatal("Could not create driver for db migration", err)
os.Exit(1)
}
m, err := migrate.NewWithDatabaseInstance("file://db/migrations", "postgres", driver)
if err != nil {
log.Fatal("Could not perform migration", err)
os.Exit(1)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatalf("An error occurred while syncing the database.. %v", err)
}
db.MapperFunc(util.ToSnake)
return &postgresStore{db: db}
}
func (store *postgresStore) SelectActions() ([]*models.Action, error) {
actions := make([]*models.Action, 0)
err := store.db.Select(&actions, "SELECT action_id, action_description, created_at, updated_at FROM actions")
if err != nil {
return nil, err
}
return actions, nil
}
func (store *postgresStore) SelectPlans() ([]*models.Plan, error) {
plans := make([]*models.Plan, 0)
err := store.db.Select(&plans, "SELECT plan_id, plan_date FROM plans")
if err != nil {
return nil, err
}
return plans, nil
}

View File

@@ -1 +0,0 @@
package db_test

37
do.sh
View File

@@ -5,35 +5,44 @@
set -Eeuo pipefail # -e "Automatic exit from bash shell script on error" -u "Treat unset variables and parameters as errors"
build() {
echo "I am ${FUNCNAME[0]}ing"
go version
go build
echo "I am ${FUNCNAME[0]}ing"
go version
go build
}
test() {
echo "I am ${FUNCNAME[0]}ing"
_test
_lint
_vet
echo "I am ${FUNCNAME[0]}ing"
fmt && _lint && _vet && _test
}
run() {
echo "I am ${FUNCNAME[0]}ing"
fmt && test && go run main.go
}
fmt() {
echo "I am ${FUNCNAME[0]}ing"
go fmt ./...
}
_test() {
echo "I am ${FUNCNAME[0]}ing"
go test -v -coverprofile=coverage.out -covermode count ./... | tee tests.out
go test -v -coverprofile=coverage.out -covermode count ./... | tee tests.out
}
_testhtml() {
_test && go tool cover -html=coverage.out
}
_lint() {
echo "I am ${FUNCNAME[0]}ing"
golint -set_exit_status ./...
golint -set_exit_status ./...
}
_vet() {
echo "I am ${FUNCNAME[0]}ing"
go vet ./...
go vet ./...
}
all() {
test && build
fmt && test && build
}
"$@" # <- execute the task

5
go.mod
View File

@@ -3,9 +3,14 @@ module gitea.deepak.science/deepak/gogmagog
go 1.15
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/jwtauth v1.1.1
github.com/golang-migrate/migrate/v4 v4.14.1
github.com/jackc/pgx/v4 v4.10.1
github.com/jmoiron/sqlx v1.2.0
github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.5.1
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
)

46
go.sum
View File

@@ -34,11 +34,15 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -64,9 +68,11 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -82,10 +88,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dhui/dktest v0.3.3 h1:DBuH/9GFaWbDRa42qsut/hbQu+srAQ0rPWnUoiGX7CA=
github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -97,6 +108,11 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/jwtauth v1.1.1 h1:CtUHwzvXUfZeZSbASLgzaTZQ8mL7p+vitX59NBTL1vY=
github.com/go-chi/jwtauth v1.1.1/go.mod h1:znOWz9e5/GfBOKiZlOUoEfjSjUF+cLZO3GcpkoGXvFI=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -104,16 +120,17 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-migrate/migrate v1.3.2 h1:QAlFV1QF9zdkzy/jujlBVkVu+L/+k18cg8tuY1/4JDY=
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE=
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -143,6 +160,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -155,6 +173,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -178,6 +197,7 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -226,6 +246,7 @@ github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -249,7 +270,6 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
@@ -290,6 +310,11 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY=
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029 h1:+HTAqhgKkKqizghOYb4uEpZ7wK8tl3Y48ZbUTHF521c=
github.com/lestrrat-go/jwx v1.0.6-0.20201127121120-26218808f029/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0=
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -310,6 +335,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -324,6 +350,7 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -334,7 +361,9 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
@@ -343,6 +372,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -367,11 +397,13 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -511,6 +543,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -571,6 +604,7 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -582,6 +616,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -624,6 +659,7 @@ golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -697,6 +733,7 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 h1:sg8vLDNIxFPHTchfhH1E3AI32BL3f23oie38xUWnJM8=
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -711,6 +748,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -721,6 +759,7 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -742,6 +781,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

36
main.go
View File

@@ -2,9 +2,12 @@ package main
import (
"gitea.deepak.science/deepak/gogmagog/config"
"gitea.deepak.science/deepak/gogmagog/db"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/store"
"gitea.deepak.science/deepak/gogmagog/tokens"
"log"
"net/http"
"os"
)
@@ -12,7 +15,7 @@ func main() {
// Config
conf, err := config.GetConf("config")
if err != nil {
log.Fatal("Could not get config", err)
log.Println("Could not get config", err)
os.Exit(1)
}
@@ -20,28 +23,27 @@ func main() {
port := appConf.Port
env := appConf.Environment
log.Print("Running server on " + port)
log.Print("App environment is " + env)
// DB
store := db.GetStore(&conf.Db)
store, err := store.GetStore(&conf.Db)
if err != nil {
log.Println("Could not get store", err)
os.Exit(1)
}
if store != nil {
log.Print("Got DB connection")
log.Println("Got DB connection")
}
m := models.New(store)
if m != nil {
log.Print("created model")
log.Println("created model")
}
acts, acterr := m.Actions()
if acterr != nil {
log.Fatal("whoopsies", acterr)
} else {
log.Printf("Got %d actions", len(acts))
for i, act := range acts {
log.Printf("%d: %v", i, act)
}
}
router := routes.NewRouter(m, tokens.New("my secret"))
log.Println("Running server on " + port)
http.ListenAndServe(":"+port, router)
log.Println("App environment is " + env)
}

View File

@@ -1 +0,0 @@
package main

View File

@@ -6,13 +6,34 @@ import (
// Action represents a single action item.
type Action struct {
ActionID int64
ActionDescription string
CreatedAt time.Time
UpdatedAt time.Time
ActionID int64 `json:"action_id"`
ActionDescription string `json:"action_description"`
UserID int64 `json:"user_id"`
EstimatedChunks int `json:"estimated_chunks"`
CompletedChunks int `json:"completed_chunks"`
CompletedOn *time.Time `json:"completed_on,omitempty"`
PlanID int `json:"plan_id"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// Actions returns all actions from the model.
func (m *Model) Actions() ([]*Action, error) {
return m.SelectActions()
func (m *Model) Actions(userID int) ([]*Action, error) {
return m.SelectActions(userID)
}
// Action returns a single action from its ID
func (m *Model) Action(id int, userID int) (*Action, error) {
act, err := m.SelectActionByID(id, userID)
return act, wrapNotFound(err)
}
// AddAction inserts a given action into the store, returning the generated ActionID. The provided ActionID is ignored.
func (m *Model) AddAction(action *Action, userID int) (int, error) {
return m.InsertAction(action, userID)
}
// SaveAction saves and updates an action.
func (m *Model) SaveAction(action *Action, userID int) error {
return m.UpdateAction(action, userID)
}

23
models/current_plan.go Normal file
View File

@@ -0,0 +1,23 @@
package models
// CurrentPlan represents the primary plan ID for a particular user ID.
type CurrentPlan struct {
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
}
// CurrentPlan returns the primary plan for a provided user ID in the given model.
func (m *Model) CurrentPlan(userID int) (*CurrentPlan, error) {
pp, err := m.SelectCurrentPlan(userID)
return pp, wrapNotFound(err)
}
// AddCurrentPlan inserts a given primary plan into the store, returning nothing.
func (m *Model) AddCurrentPlan(pp *CurrentPlan, userID int) error {
return m.InsertCurrentPlan(pp, userID)
}
// SaveCurrentPlan saves and updates a primary plan.
func (m *Model) SaveCurrentPlan(pp *CurrentPlan, userID int) error {
return m.UpdateCurrentPlan(pp, userID)
}

View File

@@ -0,0 +1,45 @@
package models_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
func TestModelCurrentPlan(t *testing.T) {
assert := assert.New(t)
a1 := &models.Action{ActionID: 3}
userID := 3
p := &models.Plan{PlanID: 6}
str, _ := store.GetInMemoryStore()
str.InsertAction(a1, userID)
str.InsertPlan(p, userID)
str.InsertPlan(p, userID)
m := models.New(str)
_, err := m.CurrentPlan(userID)
assert.NotNil(err)
assert.True(models.IsNotFoundError(err))
err = m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, userID)
assert.Nil(err)
pp, err := m.CurrentPlan(userID)
assert.Nil(err)
assert.EqualValues(1, pp.PlanID)
assert.EqualValues(userID, pp.UserID)
err = m.AddCurrentPlan(&models.CurrentPlan{PlanID: 2}, userID)
assert.NotNil(err)
err = m.SaveCurrentPlan(&models.CurrentPlan{PlanID: 2}, userID)
assert.Nil(err)
pp, err = m.CurrentPlan(userID)
assert.Nil(err)
assert.EqualValues(2, pp.PlanID)
assert.EqualValues(userID, pp.UserID)
}

11
models/err_model_test.go Normal file
View File

@@ -0,0 +1,11 @@
package models_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
)
func getErrorModel(err error) *models.Model {
str := store.GetErrorStoreForError(err, true)
return models.New(str)
}

54
models/errors.go Normal file
View File

@@ -0,0 +1,54 @@
package models
import (
"database/sql"
"golang.org/x/crypto/bcrypt"
)
type notFoundError struct {
error
}
func (e *notFoundError) NotFound() bool {
return true
}
// IsNotFoundError returns true if the model deems it a not found error.
func IsNotFoundError(err error) bool {
type notFound interface {
NotFound() bool
}
te, ok := err.(notFound)
return ok && te.NotFound()
}
func wrapNotFound(err error) error {
if err == sql.ErrNoRows {
return &notFoundError{error: err}
}
return err
}
type invalidLoginError struct {
error
}
func (e *invalidLoginError) InvalidLogin() bool {
return true
}
// IsInvalidLoginError returns true if the model deems it an invalid login error.
func IsInvalidLoginError(err error) bool {
type invalidLogin interface {
InvalidLogin() bool
}
te, ok := err.(invalidLogin)
return ok && te.InvalidLogin()
}
func wrapInvalidLogin(err error) error {
if err == sql.ErrNoRows || err == bcrypt.ErrMismatchedHashAndPassword {
return &invalidLoginError{error: err}
}
return err
}

49
models/errors_test.go Normal file
View File

@@ -0,0 +1,49 @@
package models_test
import (
"database/sql"
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/stretchr/testify/assert"
"testing"
)
func TestRandomErrNotNotFound(t *testing.T) {
assert := assert.New(t)
err := fmt.Errorf("example")
assert.False(models.IsNotFoundError(err))
}
type MyError struct {
error
}
func (e *MyError) NotFound() bool {
return true
}
func TestCustomInterface(t *testing.T) {
assert := assert.New(t)
err := &MyError{fmt.Errorf("example")}
assert.True(models.IsNotFoundError(err))
}
func TestErrorModelWrapping(t *testing.T) {
assert := assert.New(t)
m := getErrorModel(sql.ErrNoRows)
_, err := m.Plan(0, 0)
assert.True(models.IsNotFoundError(err))
_, err = m.Action(0, 0)
assert.True(models.IsNotFoundError(err))
}
func TestErrorModelInvalidLogin(t *testing.T) {
assert := assert.New(t)
m := getErrorModel(sql.ErrNoRows)
_, err := m.VerifyUserByUsernamePassword("duck", "duck")
assert.True(models.IsInvalidLoginError(err))
}

View File

@@ -1,9 +1,26 @@
package models
import (
"fmt"
)
// Store represents the backing store.
type Store interface {
SelectActions() ([]*Action, error)
SelectPlans() ([]*Plan, error)
ConnectionLive() error
SelectActions(userID int) ([]*Action, error)
SelectActionByID(id int, userID int) (*Action, error)
InsertAction(action *Action, userID int) (int, error)
UpdateAction(action *Action, userID int) error
SelectPlans(userID int) ([]*Plan, error)
SelectPlanByID(id int, userID int) (*Plan, error)
InsertPlan(plan *Plan, userID int) (int, error)
UpdatePlan(plan *Plan, userID int) error
SelectActionsByPlanID(plan *Plan, userID int) ([]*Action, error)
SelectUserByUsername(username string) (*User, error)
InsertUser(user *User) (int, error)
SelectCurrentPlan(userID int) (*CurrentPlan, error)
InsertCurrentPlan(currentPlan *CurrentPlan, userID int) error
UpdateCurrentPlan(currentPlan *CurrentPlan, userID int) error
}
// Model represents a current model item.
@@ -15,3 +32,12 @@ type Model struct {
func New(store Store) *Model {
return &Model{Store: store}
}
// Healthy returns an error if the connection is healthy.
// Wrapper over db.Ping()
func (m *Model) Healthy() error {
if m.Store == nil {
return fmt.Errorf("No store available")
}
return m.ConnectionLive()
}

View File

@@ -2,42 +2,90 @@ package models_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
type store interface {
SelectActions() ([]*models.Action, error)
SelectPlans() ([]*models.Plan, error)
}
type multiStore struct {
actions []*models.Action
plans []*models.Plan
}
func (ms *multiStore) SelectActions() ([]*models.Action, error) {
return ms.actions, nil
}
func (ms *multiStore) SelectPlans() ([]*models.Plan, error) {
return ms.plans, nil
}
func TestModelActions(t *testing.T) {
assert := assert.New(t)
a1 := &models.Action{ActionID: 3}
a2 := &models.Action{ActionID: 4}
userID := 3
p := &models.Plan{PlanID: 6}
ss := &multiStore{
[]*models.Action{a1, a2},
[]*models.Plan{p}}
m := models.New(ss)
actions, err := m.Actions()
str, _ := store.GetInMemoryStore()
str.InsertAction(a1, userID)
str.InsertPlan(p, userID)
m := models.New(str)
actions, err := m.Actions(userID)
assert.Nil(err)
assert.Equal(2, len(actions))
assert.Equal(1, len(actions))
plans, err := m.Plans()
firstAction, err := m.Action(1, userID)
assert.Nil(err)
assert.EqualValues(1, firstAction.ActionID)
actionID, err := m.AddAction(a2, userID)
assert.Nil(err)
assert.EqualValues(2, actionID)
err = m.SaveAction(a1, userID)
assert.Nil(err)
}
func TestModelPlanMethods(t *testing.T) {
assert := assert.New(t)
userID := 3
a1 := &models.Action{ActionID: 3, PlanID: 1}
a2 := &models.Action{ActionID: 4}
p := &models.Plan{}
str, _ := store.GetInMemoryStore()
str.InsertPlan(p, userID)
str.InsertAction(a1, userID)
str.InsertAction(a2, userID)
m := models.New(str)
plans, err := m.Plans(userID)
assert.Nil(err)
assert.Equal(1, len(plans))
firstPlan, err := m.Plan(1, userID)
assert.Nil(err)
assert.EqualValues(1, firstPlan.PlanID)
p2 := &models.Plan{PlanDescription: "testing", PlanID: 1}
m.SavePlan(p2, userID)
p2, err = m.Plan(1, userID)
assert.Nil(err)
assert.Equal("testing", p2.PlanDescription)
actions, err := m.GetActions(firstPlan, userID)
assert.Nil(err)
assert.Equal(1, len(actions))
planId, err := m.AddPlan(&models.Plan{}, userID)
assert.Nil(err)
assert.EqualValues(2, planId)
}
func TestModelHealthy(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
m := models.New(str)
err := m.Healthy()
assert.Nil(err)
}
func TestNilModelUnhealthy(t *testing.T) {
assert := assert.New(t)
m := models.New(nil)
err := m.Healthy()
assert.NotNil(err)
}

View File

@@ -1,16 +1,34 @@
package models
import (
"time"
)
// Plan represents a single day's agenda of actions.
type Plan struct {
PlanID int64
PlanDate time.Time
PlanID int64 `json:"plan_id"`
PlanDescription string `json:"plan_description"`
UserID int64 `json:"user_id"`
}
// Plans returns all plans in the model.
func (m *Model) Plans() ([]*Plan, error) {
return m.SelectPlans()
func (m *Model) Plans(userID int) ([]*Plan, error) {
return m.SelectPlans(userID)
}
// Plan returns a single plan from the store by plan_id.
func (m *Model) Plan(id int, userID int) (*Plan, error) {
plan, err := m.SelectPlanByID(id, userID)
return plan, wrapNotFound(err)
}
// AddPlan inserts a given plan into the store, returning the generated PlanID. The provided PlanID is ignored.
func (m *Model) AddPlan(plan *Plan, userID int) (int, error) {
return m.InsertPlan(plan, userID)
}
// SavePlan saves and updates a plan.
func (m *Model) SavePlan(plan *Plan, userID int) error {
return m.UpdatePlan(plan, userID)
}
// GetActions returns the actions associated with a particular plan.
func (m *Model) GetActions(plan *Plan, userID int) ([]*Action, error) {
return m.SelectActionsByPlanID(plan, userID)
}

96
models/user.go Normal file
View File

@@ -0,0 +1,96 @@
package models
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
// User represents the full DB user field, for inserts and compares.
// No reason to return the hashed pw on the route though.
type User struct {
UserID int64
Username string
DisplayName string
Password []byte
}
// UserNoPassword contains the non password user fields.
// This is preferred outside of the model / store.
type UserNoPassword struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
}
// VerifyUserByUsernamePassword returns a single user by the unique username, if the provided password is correct.
func (m *Model) VerifyUserByUsernamePassword(username string, password string) (*UserNoPassword, error) {
user, err := m.SelectUserByUsername(username)
if err != nil {
// throwaway to pad time
hashPassword(username)
return nil, wrapInvalidLogin(err)
}
err = bcrypt.CompareHashAndPassword(user.Password, []byte(password))
if err != nil {
return nil, wrapInvalidLogin(err)
}
return user.NoPassword(), nil
}
// NoPassword strips the user of password.
func (u *User) NoPassword() *UserNoPassword {
return &UserNoPassword{
UserID: u.UserID,
Username: u.Username,
DisplayName: u.DisplayName,
}
}
// CreateUserRequest represents a desired user creation.
type CreateUserRequest struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
Password string `json:"password"`
}
// CreateUser takes in a create user request and returns the ID of the newly created user.
func (m *Model) CreateUser(req *CreateUserRequest) (int, error) {
if req.Username == "" {
return -1, fmt.Errorf("No username provided")
}
if req.Password == "" {
return -1, fmt.Errorf("No password provided")
}
hash, err := hashPassword(req.Password)
if err != nil {
return -1, err
}
desiredUser := &User{
Username: req.Username,
DisplayName: req.DisplayName,
Password: hash,
}
return m.InsertUser(desiredUser)
}
// UserByUsername retrieves a single username from the store, verifying the passed in userID.
func (m *Model) UserByUsername(username string, userID int) (*UserNoPassword, error) {
user, err := m.SelectUserByUsername(username)
if user == nil {
return nil, wrapNotFound(err)
}
if int(user.UserID) != userID {
return nil, &notFoundError{error: fmt.Errorf("provided userID does not match the retrieved user")}
}
return user.NoPassword(), wrapNotFound(err)
}
// hashPassword hashes a password
func hashPassword(password string) ([]byte, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
return bytes, err
}

104
models/user_test.go Normal file
View File

@@ -0,0 +1,104 @@
package models_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
func TestModelUsers(t *testing.T) {
assert := assert.New(t)
a1 := &models.Action{ActionID: 3}
a2 := &models.Action{ActionID: 4}
p := &models.Plan{PlanID: 6}
username := "test1"
// password := password
user1 := &models.User{Username: username, DisplayName: "Ted Est", Password: []byte("$2y$05$6SVV35GX4cB4PDPhRaDD/exsL.HV8QtMMr60YL6dLyqtX4l58q.cy")}
str, _ := store.GetInMemoryStore()
str.InsertPlan(p, 3)
str.InsertAction(a1, 3)
str.InsertAction(a2, 3)
str.InsertUser(user1)
m := models.New(str)
userNoPass, err := m.UserByUsername("test1", 1)
assert.Nil(err)
assert.NotNil(userNoPass)
userNoPass, err = m.UserByUsername("test1", 2)
assert.NotNil(err)
assert.True(models.IsNotFoundError(err))
assert.Nil(userNoPass)
userNoPass, err = m.UserByUsername("test2", 2)
assert.NotNil(err)
assert.True(models.IsNotFoundError(err))
assert.Nil(userNoPass)
user, err := m.VerifyUserByUsernamePassword("test1", "password")
assert.Nil(err)
assert.NotNil(user)
user, err = m.VerifyUserByUsernamePassword("test1", "wrong_password")
assert.NotNil(err)
assert.Nil(user)
user, err = m.VerifyUserByUsernamePassword("test2", "password")
assert.NotNil(err)
assert.Nil(user)
}
func TestErrorUsers(t *testing.T) {
assert := assert.New(t)
m := getErrorModel(fmt.Errorf("err"))
user, err := m.VerifyUserByUsernamePassword("snth", "aoeu")
assert.Nil(user)
assert.NotNil(err)
}
func TestCreateUser(t *testing.T) {
assert := assert.New(t)
username := "test"
displayName := "Ted Est"
pass := "abc"
u := &models.CreateUserRequest{Username: username, DisplayName: displayName, Password: pass}
str, _ := store.GetInMemoryStore()
m := models.New(str)
id, err := m.CreateUser(u)
assert.Nil(err)
assert.EqualValues(1, id)
}
func TestCreateUserFailValidation(t *testing.T) {
assert := assert.New(t)
username := ""
displayName := "Ted Est"
pass := "abc"
u := &models.CreateUserRequest{Username: username, DisplayName: displayName, Password: pass}
str, _ := store.GetInMemoryStore()
m := models.New(str)
_, err := m.CreateUser(u)
assert.NotNil(err)
}
func TestCreateUserFailValidationPassword(t *testing.T) {
assert := assert.New(t)
username := "aoeu"
displayName := "Ted Est"
pass := ""
u := &models.CreateUserRequest{Username: username, DisplayName: displayName, Password: pass}
str, _ := store.GetInMemoryStore()
m := models.New(str)
_, err := m.CreateUser(u)
assert.NotNil(err)
}

215
routes/actions.go Normal file
View File

@@ -0,0 +1,215 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"io"
"net/http"
"strconv"
)
// NewActionRouter returns a new action router
func NewActionRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getActionsFunc(m))
router.Post("/", postActionFunc(m))
router.Get("/{actionid}", getActionByIDFunc(m))
router.Put("/{actionid}", putActionFunc(m))
return router
}
func getActionsFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, userErr := tokens.GetUserID(r.Context())
if userErr != nil {
unauthorizedHandler(w, r)
return
}
var (
actions []*models.Action
err error
)
planIDString := r.URL.Query().Get("plan_id")
if planIDString == "" {
actions, err = m.Actions(userID)
} else {
planID, convErr := strconv.ParseInt(planIDString, 10, 64)
if convErr != nil {
actions = []*models.Action{}
err = nil
} else {
plan := &models.Plan{PlanID: planID}
actions, err = m.GetActions(plan, userID)
}
}
if err != nil {
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(actions); err != nil {
serverError(w, err)
}
}
}
func getActionByIDFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, userErr := tokens.GetUserID(r.Context())
if userErr != nil {
unauthorizedHandler(w, r)
return
}
id, err := strconv.Atoi(chi.URLParam(r, "actionid"))
if err != nil {
notFoundHandler(w, r)
return
}
action, err := m.Action(id, userID)
if err != nil {
if models.IsNotFoundError(err) {
notFoundHandler(w, r)
return
}
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(action); err != nil {
serverError(w, err)
}
}
}
type createActionResponse struct {
CreatedAction *models.Action `json:"created_action"`
ID int64 `json:"id"`
}
func postActionFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var a models.Action
err = dec.Decode(&a)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
action := &models.Action{
ActionDescription: a.ActionDescription,
EstimatedChunks: a.EstimatedChunks,
CompletedChunks: a.CompletedChunks,
CompletedOn: a.CompletedOn,
PlanID: a.PlanID,
}
id, err := m.AddAction(action, userID)
if err != nil {
serverError(w, err)
return
}
action, err = m.Action(id, userID)
if err != nil {
serverError(w, err)
return
}
response := &createActionResponse{
CreatedAction: action,
ID: int64(id),
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}
type updateActionResponse struct {
UpdatedAction *models.Action `json:"updated_action"`
ID int64 `json:"id"`
}
func putActionFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
id, err := strconv.Atoi(chi.URLParam(r, "actionid"))
if err != nil {
notFoundHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var a models.Action
err = dec.Decode(&a)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
action := &models.Action{
ActionDescription: a.ActionDescription,
EstimatedChunks: a.EstimatedChunks,
CompletedChunks: a.CompletedChunks,
CompletedOn: a.CompletedOn,
PlanID: a.PlanID,
ActionID: int64(id),
}
err = m.SaveAction(action, userID)
if err != nil {
serverError(w, err)
return
}
action, err = m.Action(id, userID)
if err != nil {
serverError(w, err)
return
}
response := &updateActionResponse{
UpdatedAction: action,
ID: int64(id),
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}

297
routes/actions_test.go Normal file
View File

@@ -0,0 +1,297 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestEmptyActions(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `[]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestOneAction(t *testing.T) {
// set up
assert := assert.New(t)
createdDate, _ := time.Parse("2006-01-02", "2021-01-01")
updatedDate, _ := time.Parse("2006-01-02", "2021-01-02")
completedDate, _ := time.Parse("2006-01-02", "2021-01-03")
a1 := &models.Action{ActionID: 3, ActionDescription: "testing", CompletedChunks: 1, CompletedOn: &completedDate, CreatedAt: &createdDate, UpdatedAt: &updatedDate, EstimatedChunks: 3, PlanID: 0}
m := getEmptyModel()
m.AddAction(a1, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `[
{
"action_id": 1,
"action_description": "testing",
"user_id": 3,
"estimated_chunks": 3,
"completed_chunks": 1,
"completed_on": "2021-01-03T00:00:00Z",
"updated_at": "2021-01-02T00:00:00Z",
"created_at": "2021-01-01T00:00:00Z",
"plan_id": 0
}
]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestErrorAction(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyActionErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestOneActionByID(t *testing.T) {
// set up
assert := assert.New(t)
createdDate, _ := time.Parse("2006-01-02", "2021-01-01")
updatedDate, _ := time.Parse("2006-01-02", "2021-01-02")
a := &models.Action{ActionID: 6, ActionDescription: "howdy", CompletedOn: nil, CreatedAt: &createdDate, UpdatedAt: &updatedDate, CompletedChunks: 0, EstimatedChunks: 54, PlanID: 3}
m := getEmptyModel()
m.InsertAction(a, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"action_id": 1,
"action_description": "howdy",
"user_id": 3,
"estimated_chunks": 54,
"completed_chunks": 0,
"updated_at": "2021-01-02T00:00:00Z",
"created_at": "2021-01-01T00:00:00Z",
"plan_id": 3
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestErrorActionByID(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/5", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyActionErrorWriterByID(t *testing.T) {
// set up
assert := assert.New(t)
a := &models.Action{ActionID: 6}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestNotFoundActionByIDText(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/wo", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestNotFoundActionByIDEmpty(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestActionsByPlanID(t *testing.T) {
// set up
assert := assert.New(t)
createdDate, _ := time.Parse("2006-01-02", "2021-01-01")
updatedDate, _ := time.Parse("2006-01-02", "2021-01-02")
a := &models.Action{ActionID: 1, ActionDescription: "howdy", CompletedOn: nil, CreatedAt: &createdDate, UpdatedAt: &updatedDate, CompletedChunks: 0, EstimatedChunks: 54, PlanID: 6}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/?plan_id=6", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `[
{
"action_id": 1,
"action_description": "howdy",
"user_id": 3,
"estimated_chunks": 54,
"completed_chunks": 0,
"updated_at": "2021-01-02T00:00:00Z",
"created_at": "2021-01-01T00:00:00Z",
"plan_id": 6
}
]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestActionsByPlanIDInvalidID(t *testing.T) {
// set up
assert := assert.New(t)
createdDate, _ := time.Parse("2006-01-02", "2021-01-01")
updatedDate, _ := time.Parse("2006-01-02", "2021-01-02")
a := &models.Action{ActionID: 6, ActionDescription: "howdy", CompletedOn: nil, CreatedAt: &createdDate, UpdatedAt: &updatedDate, CompletedChunks: 0, EstimatedChunks: 54, PlanID: 3}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/?plan_id=aoeu", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `[]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}

View File

@@ -0,0 +1,119 @@
package routes_test
import (
"bytes"
"context"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestEmptyActionEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestOneActionEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2006-01-02"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestOneActionByIDEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewActionRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestPureJSONActionEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
data := []byte(`{
"plan_description": "2021-01-01T00:00:00Z",
"plan_id": 1,
"user_id": 3
}`)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestPutActionEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
a := &models.Action{PlanID: 6}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(context.Background(), "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}

103
routes/auth.go Normal file
View File

@@ -0,0 +1,103 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"io"
"net/http"
)
// NewAuthRouter returns a new auth router.
func NewAuthRouter(m *models.Model, tok tokens.Toker) http.Handler {
router := chi.NewRouter()
router.Post("/register", postUserFunc(m))
router.Post("/tokens", createTokenFunc(m, tok))
return router
}
type createUserResponse struct {
Username string `json:"username"`
}
func postUserFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var req models.CreateUserRequest
err := dec.Decode(&req)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
_, err = m.CreateUser(&req)
if err != nil {
serverError(w, err)
return
}
response := &createUserResponse{
Username: req.Username,
}
w.WriteHeader(http.StatusCreated)
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}
type loginCreds struct {
Username string `json:"username"`
Password string `json:"password"`
}
type createdToken struct {
Token string `json:"token"`
}
func createTokenFunc(m *models.Model, tok tokens.Toker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var creds loginCreds
err := dec.Decode(&creds)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
user, err := m.VerifyUserByUsernamePassword(creds.Username, creds.Password)
if err != nil {
if models.IsInvalidLoginError(err) {
unauthorizedHandler(w, r)
return
}
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
response := &createdToken{Token: tok.EncodeUser(user)}
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
return
}
}
}

202
routes/auth_login_test.go Normal file
View File

@@ -0,0 +1,202 @@
package routes_test
import (
"bytes"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestLoginAuth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_username",
"password": "pass"
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{
"token": "{\"ID\":1,\"Username\":\"testing_username\"}"
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestLoginBadCreds(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_use
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
}
func TestLoginBadRequestTwoBodies(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_username",
"password": "pass"
}{
"username": "testing_username",
"password": "pass"
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
}
func TestLoginAuthWrongPass(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_username",
"password": "badpass"
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestLoginErrorModel(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("error")
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_username",
"password": "badpass"
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestLoginBadWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
toker := tokens.GetDeterministicToker()
data := []byte(`{
"username": "testing_username",
"password": "pass"
}`)
req, _ := http.NewRequest("POST", "/tokens", bytes.NewBuffer(data))
rr := NewBadWriter()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
//
// func TestRegisterBadWriter(t *testing.T) {
// // set up
// assert := assert.New(t)
// m := getEmptyModel()
// toker := tokens.New("secret")
// data := []byte(`{
// "username": "test",
// "password": "pass",
// "display_name": "My Display Name"
// }`)
// req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
//
// rr := NewBadWriter()
//
// // function under test
// router := routes.NewAuthRouter(m, toker)
// router.ServeHTTP(rr, req)
//
// // check results
// status := rr.Code
// assert.Equal(http.StatusInternalServerError, status)
//
// }

View File

@@ -0,0 +1,139 @@
package routes_test
import (
"bytes"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestRegisterAuth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.New("secret")
data := []byte(`{
"username": "test",
"password": "pass",
"display_name": "My Display Name"
}`)
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusCreated, status)
expected := `{
"username": "test"
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestRegisterBadRequestAuth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.New("secret")
data := []byte(`{
"username": y Display Name"
}`)
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
}
func TestRegisterBadRequestTwoBodies(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.New("secret")
data := []byte(`{
"username": "test",
"password": "pass",
"display_name": "My Display Name"
}, {
"username": "test",
"password": "pass",
"display_name": "My Display Name"
}`)
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
}
func TestRegisterErrorModel(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("here's an error")
toker := tokens.New("secret")
data := []byte(`{
"username": "test",
"password": "pass",
"display_name": "My Display Name"
}`)
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
rr := httptest.NewRecorder()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestRegisterBadWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
toker := tokens.New("secret")
data := []byte(`{
"username": "test",
"password": "pass",
"display_name": "My Display Name"
}`)
req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(data))
rr := NewBadWriter()
// function under test
router := routes.NewAuthRouter(m, toker)
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

48
routes/currentUser.go Normal file
View File

@@ -0,0 +1,48 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"log"
"net/http"
)
// NewCurrentUserRouter returns a new router for getting the current user.
func NewCurrentUserRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getMeFunc(m))
return router
}
func getMeFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
log.Print(err)
unauthorizedHandler(w, r)
return
}
username, err := tokens.GetUsername(r.Context())
if err != nil {
log.Print(err)
unauthorizedHandler(w, r)
return
}
user, err := m.UserByUsername(username, userID)
if err != nil {
if models.IsNotFoundError(err) {
notFoundHandler(w, r)
return
}
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(user); err != nil {
serverError(w, err)
}
}
}

158
routes/currentUser_test.go Normal file
View File

@@ -0,0 +1,158 @@
package routes_test
import (
"context"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestEmptyCurrentUser(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(tokens.GetContextForUserValues(3, "testing"), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestSingleUser(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
idToUse := 1
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(tokens.GetContextForUserValues(idToUse, username), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{
"user_id": 1,
"username": "testing_username",
"display_name": "testing_name"
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestSingleUserEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestSingleUserContextNoUserID(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
idToUse := 1
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(tokens.SetUserID(context.Background(), idToUse), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestErrorUserContextNoUserID(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Here's an error.")
idToUse := 1
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(tokens.GetContextForUserValues(idToUse, "username"), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestSingleUserErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
idToUse := 1
username := "testing_username"
displayName := "testing_name"
password := "pass"
m.CreateUser(&models.CreateUserRequest{Username: username, DisplayName: displayName, Password: password})
router := routes.NewCurrentUserRouter(m)
req, _ := http.NewRequestWithContext(tokens.GetContextForUserValues(idToUse, username), "GET", "/", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

160
routes/current_plan.go Normal file
View File

@@ -0,0 +1,160 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"io"
"net/http"
)
// NewCurrentPlanRouter returns a new primary plan router
func NewCurrentPlanRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getCurrentPlanFunc(m))
router.Post("/", postCurrentPlanFunc(m))
router.Put("/", putCurrentPlanFunc(m))
return router
}
func getCurrentPlanFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, userErr := tokens.GetUserID(r.Context())
if userErr != nil {
unauthorizedHandler(w, r)
return
}
pp, err := m.CurrentPlan(userID)
if err != nil {
if models.IsNotFoundError(err) {
notFoundHandler(w, r)
return
}
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(pp); err != nil {
serverError(w, err)
}
}
}
type createCurrentPlanResponse struct {
CreatedCurrentPlan *models.CurrentPlan `json:"created_current_plan"`
}
func postCurrentPlanFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var pp models.CurrentPlan
err = dec.Decode(&pp)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
newPP := &models.CurrentPlan{
PlanID: pp.PlanID,
UserID: int64(userID),
}
err = m.AddCurrentPlan(newPP, userID)
if err != nil {
serverError(w, err)
return
}
finishedPP, err := m.CurrentPlan(userID)
if err != nil {
serverError(w, err)
return
}
response := &createCurrentPlanResponse{
CreatedCurrentPlan: finishedPP,
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}
type updateCurrentPlanResponse struct {
UpdatedCurrentPlan *models.CurrentPlan `json:"updated_current_plan"`
}
func putCurrentPlanFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
_, err = m.CurrentPlan(userID)
if models.IsNotFoundError(err) {
notFoundHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var pp models.CurrentPlan
err = dec.Decode(&pp)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
newPP := &models.CurrentPlan{
PlanID: pp.PlanID,
UserID: int64(userID),
}
err = m.SaveCurrentPlan(newPP, userID)
if err != nil {
serverError(w, err)
return
}
newPP, err = m.CurrentPlan(userID)
if err != nil {
serverError(w, err)
return
}
response := &updateCurrentPlanResponse{
UpdatedCurrentPlan: newPP,
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}

View File

@@ -0,0 +1,228 @@
package routes_test
import (
"bytes"
"context"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPostSinglePlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentPlanRouter(m)
plan := &models.Plan{}
m.AddPlan(plan, 3)
pp := &models.CurrentPlan{PlanID: 1}
data, _ := json.Marshal(pp)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusCreated, status)
}
func TestPostCurrentPlanUnauth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
pp := &models.CurrentPlan{PlanID: 1}
data, _ := json.Marshal(pp)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestExtraFieldJSONCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(`{
"plan_id": 5,
"sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBodyJSONCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBodyCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 7
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorCreateCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("error model")
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrieveCreateCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("error model")
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterCreateCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

View File

@@ -0,0 +1,269 @@
package routes_test
import (
"bytes"
"context"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPutSinglePlanNotFound(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentPlanRouter(m)
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
pp := &models.CurrentPlan{PlanID: 1}
data, _ := json.Marshal(pp)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestPutSinglePlanNonDup(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentPlanRouter(m)
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
pp := &models.CurrentPlan{PlanID: 2}
data, _ := json.Marshal(pp)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
}
func TestPutCurrentPlanUnauth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
pp := &models.CurrentPlan{PlanID: 1}
data, _ := json.Marshal(pp)
req, _ := http.NewRequestWithContext(context.Background(), "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestExtraFieldJSONPutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(`{
"plan_id": 5,
"sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBodyJSONPutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBodyPutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 7
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorPutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("error model")
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrievePutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("error model")
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterPutCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
plan := &models.Plan{}
m.AddPlan(plan, 3)
m.AddPlan(plan, 3)
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 1}, 3)
data := []byte(`{
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
router := routes.NewCurrentPlanRouter(m)
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

117
routes/current_plan_test.go Normal file
View File

@@ -0,0 +1,117 @@
package routes_test
import (
"context"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
// "gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestEmptyCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestEmptyCurrentPlanUnauth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewCurrentPlanRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestErrorCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewCurrentPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyCurrentPlanErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 3}, 3)
router := routes.NewCurrentPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestSingleCurrentPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
m.AddCurrentPlan(&models.CurrentPlan{PlanID: 3}, 3)
router := routes.NewCurrentPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"plan_id": 3,
"user_id": 3
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}

32
routes/errors.go Normal file
View File

@@ -0,0 +1,32 @@
package routes
import (
"log"
"net/http"
)
func serverError(w http.ResponseWriter, err error) {
code := http.StatusInternalServerError
log.Printf("received error: {%v}", err)
http.Error(w, http.StatusText(code), code)
}
func badRequestError(w http.ResponseWriter, err error) {
code := http.StatusBadRequest
log.Printf("received error: {%v}", err)
http.Error(w, http.StatusText(code), code)
}
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusMethodNotAllowed
http.Error(w, http.StatusText(code), code)
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusNotFound
http.Error(w, http.StatusText(code), code)
}
func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
}

58
routes/health.go Normal file
View File

@@ -0,0 +1,58 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/go-chi/chi"
"net/http"
)
func newHealthRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getHealthFunc(m))
return router
}
type healthCheck struct {
Name string `json:"name"`
Healthy bool `json:"healthy"`
Message string `json:"message"`
}
func getHealthFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var healths []*healthCheck
healths = append(healths, dbHealth(m))
code := http.StatusOK
for _, h := range healths {
if !h.Healthy {
code = http.StatusInternalServerError
break
}
}
w.WriteHeader(code)
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(healths); err != nil {
serverError(w, err)
}
}
}
func dbHealth(m *models.Model) *healthCheck {
errMessage := ""
health := true
name := "Store"
err := m.Healthy()
if err != nil {
errMessage = err.Error()
health = false
}
return &healthCheck{
Name: name,
Healthy: health,
Message: errMessage,
}
}

88
routes/health_test.go Normal file
View File

@@ -0,0 +1,88 @@
package routes_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestEmptyHeatlhErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("GET", "/health", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestEmptyHealth(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("GET", "/health", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `[
{
"name": "Store",
"healthy": true,
"message": ""
}
]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestUnhealthyDB(t *testing.T) {
// set up
assert := assert.New(t)
errorMsg := "error"
m := getErrorModel(errorMsg)
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("GET", "/health", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := fmt.Sprintf(`[
{
"name": "Store",
"healthy": false,
"message": "%s"
}
]`, errorMsg)
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}

29
routes/http_util_test.go Normal file
View File

@@ -0,0 +1,29 @@
package routes_test
import (
"fmt"
"net/http"
)
type BadResponseWriter struct {
Code int
header http.Header
}
func NewBadWriter() *BadResponseWriter {
return &BadResponseWriter{
header: http.Header{},
}
}
func (w *BadResponseWriter) Header() http.Header {
return w.header
}
func (w *BadResponseWriter) Write(b []byte) (int, error) {
return 0, fmt.Errorf("always an error")
}
func (w *BadResponseWriter) WriteHeader(statusCode int) {
w.Code = statusCode
}

218
routes/plan_put_test.go Normal file
View File

@@ -0,0 +1,218 @@
package routes_test
import (
"bytes"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPureJSONPutPlan(t *testing.T) {
// set up
assert := assert.New(t)
p := &models.Plan{
PlanID: 1,
PlanDescription: "hn",
}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_id": 1,
"plan_description": "testing"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{
"updated_plan": {
"plan_description": "testing",
"plan_id": 1,
"user_id": 3
},
"id": 1
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestExtraFieldPlanPutJSON(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
p := &models.Plan{
PlanID: 1,
PlanDescription: "hn",
}
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_id": 1,
"plan_description": "testing",
"sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBodyPlanPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBodyPlanPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestBadPlanIDPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/text", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
expected := `Not Found`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorUpdatePlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewPlanRouter(m)
p := &models.Plan{PlanID: 6}
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrieveUpdatePlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("Model always errors")
router := routes.NewPlanRouter(m)
p := &models.Plan{PlanID: 6}
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterUpdatePlan(t *testing.T) {
// set up
assert := assert.New(t)
p := &models.Plan{PlanID: 6}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

188
routes/plans.go Normal file
View File

@@ -0,0 +1,188 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"io"
"net/http"
"strconv"
)
// NewPlanRouter returns the http.Handler for the passed in model to route plan methods.
func NewPlanRouter(m *models.Model) http.Handler {
router := chi.NewRouter()
router.Get("/", getAllPlansFunc(m))
router.Post("/", postPlanFunc(m))
router.Get("/{planid}", getPlanByIDFunc(m))
router.Put("/{planid}", putPlanFunc(m))
return router
}
func getAllPlansFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
plans, err := m.Plans(userID)
if err != nil {
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(plans); err != nil {
serverError(w, err)
}
}
}
func getPlanByIDFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
id, err := strconv.Atoi(chi.URLParam(r, "planid"))
if err != nil {
notFoundHandler(w, r)
return
}
// todo get real user id
plan, err := m.Plan(id, userID)
if err != nil {
if models.IsNotFoundError(err) {
notFoundHandler(w, r)
return
}
serverError(w, err)
return
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(plan); err != nil {
serverError(w, err)
}
}
}
type createPlanResponse struct {
CreatedPlan *models.Plan `json:"created_plan"`
ID int64 `json:"id"`
}
func postPlanFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var p models.Plan
err = dec.Decode(&p)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
// Map the fields we allow to be set to the plan to be created.
plan := &models.Plan{PlanDescription: p.PlanDescription, UserID: p.UserID}
id, err := m.AddPlan(plan, userID)
if err != nil {
serverError(w, err)
return
}
plan, err = m.Plan(id, userID)
if err != nil {
serverError(w, err)
return
}
response := &createPlanResponse{
CreatedPlan: plan,
ID: int64(id),
}
w.WriteHeader(http.StatusCreated)
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}
type updatePlanResponse struct {
UpdatedPlan *models.Plan `json:"updated_plan"`
ID int64 `json:"id"`
}
func putPlanFunc(m *models.Model) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, err := tokens.GetUserID(r.Context())
if err != nil {
unauthorizedHandler(w, r)
return
}
id, err := strconv.Atoi(chi.URLParam(r, "planid"))
if err != nil {
notFoundHandler(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var p models.Plan
err = dec.Decode(&p)
if err != nil {
badRequestError(w, err)
return
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
badRequestError(w, err)
return
}
plan := &models.Plan{
PlanDescription: p.PlanDescription,
PlanID: int64(id),
}
err = m.SavePlan(plan, userID)
if err != nil {
serverError(w, err)
return
}
plan, err = m.Plan(id, userID)
if err != nil {
serverError(w, err)
return
}
response := &updatePlanResponse{
UpdatedPlan: plan,
ID: int64(id),
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
serverError(w, err)
}
}
}

220
routes/plans_test.go Normal file
View File

@@ -0,0 +1,220 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
var sampleContext = tokens.GetContextForUserValues(3, "testing")
func TestEmptyPlans(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `[]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestOnePlan(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `[
{
"plan_id": 1,
"plan_description": "2021-01-01",
"user_id": 3
}
]`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestErrorPlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyPlanErrorWriter(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestOnePlanByID(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{
"plan_id": 1,
"plan_description": "2021-01-01",
"user_id": 3
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestErrorPlanByID(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/5", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyPlanErrorWriterByID(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 1, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}
func TestNotFoundPlanByIDText(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/wo", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}
func TestNotFoundPlanByIDEmpty(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(sampleContext, "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
}

View File

@@ -0,0 +1,119 @@
package routes_test
import (
"bytes"
"context"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestEmptyPlanEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestOnePlanEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestOnePlanByIDEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: 3}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/1", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestPureJSONEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_description": "2021-01-01T00:00:00Z",
"plan_id": 1,
"user_id": 3
}`)
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestPutPlanEmptyContext(t *testing.T) {
// set up
assert := assert.New(t)
p := &models.Plan{PlanID: 6, PlanDescription: "sth"}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(context.Background(), "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}

215
routes/post_action_test.go Normal file
View File

@@ -0,0 +1,215 @@
package routes_test
import (
"bytes"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestPureJSONPostAction(t *testing.T) {
// set up
assert := assert.New(t)
compOn, _ := time.Parse("2006-01-02", "2021-01-01")
a := &models.Action{
PlanID: 5,
CompletedOn: &compOn,
EstimatedChunks: 3,
CompletedChunks: 2,
ActionDescription: "here's an action",
}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
data := []byte(`{
"action_description": "here's an action",
"estimated_chunks": 3,
"completed_chunks": 2,
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusCreated, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"created_action": {
"action_description": "here's an action",
"estimated_chunks": 3,
"completed_chunks": 2,
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5,
"action_id": 2,
"user_id": 3
},
"id": 2
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestExtraFieldActionPostJSON(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewActionRouter(m)
data := []byte(`{
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5,
"sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBodyActionPost(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewActionRouter(m)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBodyActionPost(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewActionRouter(m)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorCreateAction(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewActionRouter(m)
a := &models.Action{PlanID: 6}
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrieveCreateAction(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("Model always errors")
router := routes.NewActionRouter(m)
a := &models.Action{PlanID: 6}
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterCreateAction(t *testing.T) {
// set up
assert := assert.New(t)
a := &models.Action{PlanID: 6}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

243
routes/post_plan_test.go Normal file
View File

@@ -0,0 +1,243 @@
package routes_test
import (
"bytes"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCreatePlanRoute(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
userID := 3
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: int64(userID)}
m := getEmptyModel()
m.AddPlan(p, userID)
router := routes.NewPlanRouter(m)
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusCreated, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"created_plan": {
"plan_id": 2,
"plan_description": "2021-01-01",
"user_id": 3
},
"id": 2
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestPureJSON(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
userID := 3
p := &models.Plan{PlanID: 1, PlanDescription: planDescription, UserID: int64(userID)}
m := getEmptyModel()
m.AddPlan(p, userID)
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_description": "2021-01-01",
"plan_id": 1,
"user_id": 3
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusCreated, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"created_plan": {
"plan_id": 2,
"user_id": 3,
"plan_description": "2021-01-01"
},
"id": 2
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestExtraFieldJSON(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
userID := 3
p := &models.Plan{PlanID: 6, PlanDescription: planDescription, UserID: int64(userID)}
m := getEmptyModel()
m.AddPlan(p, userID)
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_description": "2021-01-01",
"plan_id": 5,
"plan_sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBody(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
userID := 3
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, userID)
router := routes.NewPlanRouter(m)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBody(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data := []byte(`{
"plan_description": "2021-01-01",
"plan_id": 5
}, {
"plan_description": "2021-01-01",
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorCreatePlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewPlanRouter(m)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrieveCreatePlan(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("Model always errors")
router := routes.NewPlanRouter(m)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterCreatePlan(t *testing.T) {
// set up
assert := assert.New(t)
planDescription := "2021-01-01"
p := &models.Plan{PlanID: 6, PlanDescription: planDescription}
m := getEmptyModel()
m.AddPlan(p, 3)
router := routes.NewPlanRouter(m)
data, _ := json.Marshal(p)
req, _ := http.NewRequestWithContext(sampleContext, "POST", "/", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

231
routes/put_action_test.go Normal file
View File

@@ -0,0 +1,231 @@
package routes_test
import (
"bytes"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/routes"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestPureJSONPutAction(t *testing.T) {
// set up
assert := assert.New(t)
compOn, _ := time.Parse("2006-01-02", "2021-01-01")
a := &models.Action{
PlanID: 5,
CompletedOn: &compOn,
EstimatedChunks: 1,
CompletedChunks: 1,
ActionDescription: "hn",
}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
data := []byte(`{
"action_description": "here's an action",
"estimated_chunks": 3,
"completed_chunks": 2,
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `{
"updated_action": {
"action_description": "here's an action",
"estimated_chunks": 3,
"completed_chunks": 2,
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5,
"action_id": 1,
"user_id": 3
},
"id": 1
}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestExtraFieldActionPutJSON(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
data := []byte(`{
"completed_on": "2021-01-01T00:00:00Z",
"plan_id": 5,
"sabotage": "omg"
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestEmptyBodyActionPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
data := []byte(``)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestTwoBodyActionPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusBadRequest, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Bad Request`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestBadActionIDPut(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewActionRouter(m)
data := []byte(`{
"plan_id": 5
}, {
"plan_id": 6
}`)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/text", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Not Found`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorUpdateAction(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorModel("Model always errors")
router := routes.NewActionRouter(m)
a := &models.Action{PlanID: 6}
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorOnRetrieveUpdateAction(t *testing.T) {
// set up
assert := assert.New(t)
m := getErrorOnGetModel("Model always errors")
router := routes.NewActionRouter(m)
a := &models.Action{PlanID: 6}
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
// We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp.
expected := `Internal Server Error`
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestErrorWriterUpdateAction(t *testing.T) {
// set up
assert := assert.New(t)
a := &models.Action{PlanID: 6}
m := getEmptyModel()
m.AddAction(a, 3)
router := routes.NewActionRouter(m)
data, _ := json.Marshal(a)
req, _ := http.NewRequestWithContext(sampleContext, "PUT", "/1", bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

View File

@@ -0,0 +1,22 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
)
func getEmptyModel() *models.Model {
str, _ := store.GetInMemoryStore()
m := models.New(str)
return m
}
func getErrorModel(message string) *models.Model {
str := store.GetErrorStore(message, true)
return models.New(str)
}
func getErrorOnGetModel(errorMsg string) *models.Model {
str := store.GetErrorStore(errorMsg, false)
return models.New(str)
}

36
routes/routes.go Normal file
View File

@@ -0,0 +1,36 @@
package routes
import (
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/chi"
"net/http"
)
// NewRouter returns a router powered by the provided model.
func NewRouter(m *models.Model, tok tokens.Toker) http.Handler {
router := chi.NewRouter()
router.MethodNotAllowed(methodNotAllowedHandler)
router.NotFound(notFoundHandler)
router.Group(func(r chi.Router) {
r.Use(tok.Authenticator)
r.Mount("/actions", NewActionRouter(m))
r.Mount("/plans", NewPlanRouter(m))
r.Mount("/me", NewCurrentUserRouter(m))
r.Mount("/currentPlan", NewCurrentPlanRouter(m))
})
router.Mount("/auth", NewAuthRouter(m, tok))
router.Mount("/health", newHealthRouter(m))
router.Get("/ping", ping)
return router
}
func ping(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]string{"ping": "pong"}); err != nil {
serverError(w, err)
}
}

87
routes/routes_test.go Normal file
View File

@@ -0,0 +1,87 @@
package routes_test
import (
"gitea.deepak.science/deepak/gogmagog/routes"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPingHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("GET", "/ping", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusOK, status)
expected := `{"ping": "pong"}`
assert.JSONEq(expected, rr.Body.String())
contentType := rr.Header().Get("Content-Type")
assert.Equal("application/json", contentType)
}
func TestPingPostHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("POST", "/ping", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusMethodNotAllowed, status)
expected := http.StatusText(http.StatusMethodNotAllowed)
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestNotFoundHandler(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("POST", "/null", nil)
rr := httptest.NewRecorder()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusNotFound, status)
expected := http.StatusText(http.StatusNotFound)
assert.Equal(expected, strings.TrimSpace(rr.Body.String()))
}
func TestPingError(t *testing.T) {
// set up
assert := assert.New(t)
m := getEmptyModel()
router := routes.NewRouter(m, tokens.New("whatever"))
req, _ := http.NewRequest("GET", "/ping", nil)
rr := NewBadWriter()
// function under test
router.ServeHTTP(rr, req)
// check results
status := rr.Code
assert.Equal(http.StatusInternalServerError, status)
}

104
store/errorStore.go Normal file
View File

@@ -0,0 +1,104 @@
package store
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
)
func (e *errorStore) SelectActions(userID int) ([]*models.Action, error) {
return nil, e.error
}
func (e *errorStore) SelectActionByID(id int, userID int) (*models.Action, error) {
return nil, e.error
}
func (e *errorStore) InsertAction(action *models.Action, userID int) (int, error) {
if e.errorOnInsert {
return 0, e.error
}
return 0, nil
}
func (e *errorStore) UpdateAction(action *models.Action, userID int) error {
if e.errorOnInsert {
return e.error
}
return nil
}
func (e *errorStore) SelectPlans(userID int) ([]*models.Plan, error) {
return nil, e.error
}
func (e *errorStore) SelectPlanByID(id int, userID int) (*models.Plan, error) {
return nil, e.error
}
func (e *errorStore) InsertPlan(plan *models.Plan, userID int) (int, error) {
if e.errorOnInsert {
return 0, e.error
}
return 0, nil
}
func (e *errorStore) UpdatePlan(plan *models.Plan, userID int) error {
if e.errorOnInsert {
return e.error
}
return nil
}
func (e *errorStore) SelectActionsByPlanID(plan *models.Plan, userID int) ([]*models.Action, error) {
return nil, e.error
}
func (e *errorStore) SelectUserByUsername(name string) (*models.User, error) {
return nil, e.error
}
func (e *errorStore) InsertUser(user *models.User) (int, error) {
if e.errorOnInsert {
return 0, e.error
}
return 0, nil
}
func (e *errorStore) SelectCurrentPlan(userID int) (*models.CurrentPlan, error) {
return nil, e.error
}
func (e *errorStore) InsertCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
if e.errorOnInsert {
return e.error
}
return nil
}
func (e *errorStore) UpdateCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
if e.errorOnInsert {
return e.error
}
return nil
}
func (e *errorStore) ConnectionLive() error {
return e.error
}
type errorStore struct {
error error
errorOnInsert bool
}
// GetErrorStore returns a models.Store that always errors. This is useful for testing purposes.
func GetErrorStore(errorMsg string, errorOnInsert bool) models.Store {
e := &errorStore{error: fmt.Errorf(errorMsg), errorOnInsert: errorOnInsert}
return e
}
// GetErrorStoreForError returns a models.Store that always errors with the provided error.
func GetErrorStoreForError(err error, errorOnInsert bool) models.Store {
e := &errorStore{error: err, errorOnInsert: errorOnInsert}
return e
}

112
store/errorStore_test.go Normal file
View File

@@ -0,0 +1,112 @@
package store_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
func TestErrorActionMethods(t *testing.T) {
assert := assert.New(t)
userID := 2
str := store.GetErrorStore("error message sample", true)
str2 := store.GetErrorStore("error message sample", false)
str3 := store.GetErrorStoreForError(fmt.Errorf("test error"), false)
_, err := str.InsertAction(&models.Action{}, userID)
assert.NotNil(err)
_, err = str2.InsertAction(&models.Action{}, userID)
assert.Nil(err)
_, err = str3.InsertAction(&models.Action{}, userID)
assert.Nil(err)
_, err = str.SelectActionByID(8, userID)
assert.NotNil(err)
_, err = str2.SelectActionByID(8, userID)
assert.NotNil(err)
_, err = str.SelectActions(userID)
assert.NotNil(err)
_, err = str.SelectActionsByPlanID(&models.Plan{}, userID)
assert.NotNil(err)
replacementAction := &models.Action{}
err = str.UpdateAction(replacementAction, userID)
assert.NotNil(err)
err = str2.UpdateAction(replacementAction, userID)
assert.Nil(err)
}
func TestErrorPlanMethods(t *testing.T) {
assert := assert.New(t)
str := store.GetErrorStore("sntahoeu", true)
str2 := store.GetErrorStore("sntahoeu", false)
_, err := str.SelectPlans(3)
assert.NotNil(err)
_, err = str.InsertPlan(&models.Plan{}, 3)
assert.NotNil(err)
_, err = str2.InsertPlan(&models.Plan{}, 3)
assert.Nil(err)
replacementPlan := &models.Plan{}
err = str.UpdatePlan(replacementPlan, 3)
assert.NotNil(err)
err = str2.UpdatePlan(replacementPlan, 3)
assert.Nil(err)
_, err = str.SelectPlanByID(5, 3)
assert.NotNil(err)
}
func TestErrorLive(t *testing.T) {
assert := assert.New(t)
str := store.GetErrorStore("error", true)
err := str.ConnectionLive()
assert.NotNil(err)
}
func TestErrorUserMethods(t *testing.T) {
assert := assert.New(t)
str := store.GetErrorStore("error", true)
str2 := store.GetErrorStore("error", false)
u := &models.User{}
_, err := str.InsertUser(u)
assert.NotNil(err)
_, err = str2.InsertUser(u)
assert.Nil(err)
_, err = str.SelectUserByUsername("snth")
assert.NotNil(err)
}
func TestErrorCurrentPlanMethods(t *testing.T) {
assert := assert.New(t)
str := store.GetErrorStore("error", true)
str2 := store.GetErrorStore("error", false)
cp := &models.CurrentPlan{}
_, err := str.SelectCurrentPlan(1)
assert.NotNil(err)
err = str.InsertCurrentPlan(cp, 1)
assert.NotNil(err)
err = str2.InsertCurrentPlan(cp, 1)
assert.Nil(err)
replace := &models.CurrentPlan{}
err = str.UpdateCurrentPlan(replace, 1)
assert.NotNil(err)
err = str2.UpdateCurrentPlan(replace, 1)
assert.Nil(err)
}

164
store/inmemory.go Normal file
View File

@@ -0,0 +1,164 @@
package store
import (
"database/sql"
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
)
type inMemoryStore struct {
actions []*models.Action
plans []*models.Plan
users []*models.User
currentPlans []*models.CurrentPlan
}
// GetInMemoryStore provides a purely in memory store, for testing purposes only, with no persistence.
func GetInMemoryStore() (models.Store, error) {
return &inMemoryStore{
actions: make([]*models.Action, 0),
plans: make([]*models.Plan, 0),
users: make([]*models.User, 0),
}, nil
}
func (store *inMemoryStore) SelectActions(userID int) ([]*models.Action, error) {
ret := make([]*models.Action, 0)
for _, action := range store.actions {
if int(action.UserID) == userID {
ret = append(ret, action)
}
}
return ret, nil
}
func (store *inMemoryStore) SelectActionsByPlanID(plan *models.Plan, userID int) ([]*models.Action, error) {
ret := make([]*models.Action, 0)
for _, action := range store.actions {
if (int(plan.PlanID) == int(action.PlanID)) && (int(action.UserID) == userID) {
ret = append(ret, action)
}
}
return ret, nil
}
func (store *inMemoryStore) SelectActionByID(id int, userID int) (*models.Action, error) {
for _, action := range store.actions {
if id == int(action.ActionID) && (int(action.UserID) == userID) {
return action, nil
}
}
return nil, sql.ErrNoRows
}
func (store *inMemoryStore) InsertAction(action *models.Action, userID int) (int, error) {
id := len(store.actions) + 1
action.ActionID = int64(id)
action.UserID = int64(userID)
store.actions = append(store.actions, action)
return id, nil
}
func (store *inMemoryStore) UpdateAction(action *models.Action, userID int) error {
currentAction, err := store.SelectActionByID(int(action.ActionID), userID)
if err != nil {
return err
}
currentAction.ActionDescription = action.ActionDescription
currentAction.EstimatedChunks = action.EstimatedChunks
currentAction.CompletedChunks = action.CompletedChunks
currentAction.PlanID = action.PlanID
return nil
}
func (store *inMemoryStore) SelectPlans(userID int) ([]*models.Plan, error) {
ret := make([]*models.Plan, 0)
for _, plan := range store.plans {
if int(plan.UserID) == userID {
ret = append(ret, plan)
}
}
return ret, nil
}
func (store *inMemoryStore) SelectPlanByID(id int, userID int) (*models.Plan, error) {
for _, plan := range store.plans {
if id == int(plan.PlanID) && (userID == int(plan.UserID)) {
return plan, nil
}
}
return nil, sql.ErrNoRows
}
func (store *inMemoryStore) InsertPlan(plan *models.Plan, userID int) (int, error) {
id := len(store.plans) + 1
plan.PlanID = int64(id)
plan.UserID = int64(userID)
store.plans = append(store.plans, plan)
return id, nil
}
func (store *inMemoryStore) UpdatePlan(plan *models.Plan, userID int) error {
currPlan, err := store.SelectPlanByID(int(plan.PlanID), userID)
if err != nil {
return err
}
currPlan.PlanDescription = plan.PlanDescription
return nil
}
func (store *inMemoryStore) SelectCurrentPlan(userID int) (*models.CurrentPlan, error) {
for _, currentPlan := range store.currentPlans {
if userID == int(currentPlan.UserID) {
return currentPlan, nil
}
}
return nil, sql.ErrNoRows
}
func (store *inMemoryStore) InsertCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
_, err := store.SelectCurrentPlan(userID)
if err == nil {
return fmt.Errorf("Can't insert primary plan")
}
// actually impossible, but at this point it must be a not found error.
// if err != sql.ErrNoRows {
// return err
// }
store.currentPlans = append(store.currentPlans, &models.CurrentPlan{PlanID: int64(currentPlan.PlanID), UserID: int64(userID)})
return nil
}
func (store *inMemoryStore) UpdateCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
current, err := store.SelectCurrentPlan(userID)
if err != nil {
return err
}
current.PlanID = currentPlan.PlanID
return nil
}
func (store *inMemoryStore) ConnectionLive() error {
return nil
}
func (store *inMemoryStore) SelectUserByUsername(username string) (*models.User, error) {
for _, user := range store.users {
if username == user.Username {
return user, nil
}
}
return nil, sql.ErrNoRows
}
// inMemoryStore.InsertUser will not enforce unique usernames, which is ok.
func (store *inMemoryStore) InsertUser(user *models.User) (int, error) {
id := len(store.users) + 1
user.UserID = int64(id)
store.users = append(store.users, user)
return id, nil
}

151
store/inmemory_test.go Normal file
View File

@@ -0,0 +1,151 @@
package store_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
func TestInMemoryActionMethods(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
sampleplanid := 8
userID := 10
act := &models.Action{}
a2 := &models.Action{PlanID: sampleplanid}
id, _ := str.InsertAction(act, userID)
assert.EqualValues(1, id)
receivedAction, err := str.SelectActionByID(id, userID)
assert.Nil(err)
assert.EqualValues(act, receivedAction)
_, err = str.SelectActionByID(id, userID+1)
assert.NotNil(err)
allactions, err := str.SelectActions(userID)
assert.Nil(err)
assert.EqualValues(1, len(allactions))
str.InsertAction(a2, userID)
allactions, err = str.SelectActions(userID)
assert.Nil(err)
assert.EqualValues(2, len(allactions))
planactions, err := str.SelectActionsByPlanID(&models.Plan{PlanID: int64(sampleplanid)}, userID)
assert.Nil(err)
assert.EqualValues(1, len(planactions))
assert.Equal(a2, planactions[0])
_, err = str.SelectActionByID(151, userID)
assert.NotNil(err)
sampleDescription := "snth"
replacementAction := &models.Action{ActionID: 1, ActionDescription: sampleDescription}
err = str.UpdateAction(replacementAction, userID)
assert.Nil(err)
assert.Equal(sampleDescription, act.ActionDescription)
replacementAction = &models.Action{ActionID: 1235122, ActionDescription: sampleDescription}
err = str.UpdateAction(replacementAction, userID)
assert.NotNil(err)
}
func TestInMemoryPlanMethods(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
userID := 1
p := &models.Plan{}
plans, err := str.SelectPlans(userID)
assert.Nil(err)
assert.EqualValues(0, len(plans))
id, err := str.InsertPlan(p, userID)
plans, err = str.SelectPlans(userID)
assert.Nil(err)
assert.EqualValues(1, len(plans))
retrievedPlan, err := str.SelectPlanByID(id, userID)
assert.Nil(err)
assert.Equal(retrievedPlan, p)
_, err = str.SelectPlanByID(135135, userID)
assert.NotNil(err)
sampleDescription := "snth"
replacePlan := &models.Plan{PlanID: 1, PlanDescription: sampleDescription}
err = str.UpdatePlan(replacePlan, userID)
assert.Nil(err)
assert.Equal(sampleDescription, p.PlanDescription)
replacePlan = &models.Plan{PlanID: 1235122, PlanDescription: sampleDescription}
err = str.UpdatePlan(replacePlan, userID)
assert.NotNil(err)
}
func TestLive(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
err := str.ConnectionLive()
assert.Nil(err)
}
func TestInMemoryUserMethods(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
uname := "hiimauser"
u := &models.User{Username: uname}
id, err := str.InsertUser(u)
assert.Nil(err)
retrievedUser, err := str.SelectUserByUsername(uname)
assert.Nil(err)
assert.EqualValues(id, retrievedUser.UserID)
_, err = str.SelectUserByUsername("bad username")
assert.NotNil(err)
}
func TestInMemoryCurrentPlanMethods(t *testing.T) {
assert := assert.New(t)
str, _ := store.GetInMemoryStore()
userID := 10
cp1 := &models.CurrentPlan{PlanID: 1}
cp2 := &models.CurrentPlan{PlanID: 2}
err := str.UpdateCurrentPlan(cp1, userID)
assert.NotNil(err)
err = str.InsertCurrentPlan(cp1, userID)
assert.Nil(err)
receivedCp, err := str.SelectCurrentPlan(userID)
assert.Nil(err)
assert.EqualValues(1, receivedCp.PlanID)
_, err = str.SelectCurrentPlan(userID + 1)
assert.NotNil(err)
str.InsertCurrentPlan(cp2, userID)
assert.NotNil(err)
err = str.UpdateCurrentPlan(cp2, userID)
assert.Nil(err)
receivedCp, err = str.SelectCurrentPlan(userID)
assert.Nil(err)
assert.EqualValues(2, receivedCp.PlanID)
}

View File

@@ -1,4 +1,6 @@
DROP TABLE IF EXISTS actions;
DROP TABLE IF EXISTS user_current_plan;
DROP TABLE IF EXISTS plans;
DROP TABLE IF EXISTS users;
DROP FUNCTION IF EXISTS trigger_set_timestamp;

View File

@@ -1,22 +1,39 @@
CREATE TABLE IF NOT EXISTS plans(
plan_id serial PRIMARY KEY,
plan_date DATE NOT NULL,
CREATE TABLE IF NOT EXISTS users(
user_id serial PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
display_name VARCHAR (100) NOT NULL,
password bytea,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS plans(
plan_id serial PRIMARY KEY,
plan_description VARCHAR (500) NOT NULL,
user_id int REFERENCES users(user_id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
UNIQUE (user_id, plan_id)
);
CREATE TABLE IF NOT EXISTS user_current_plan(
user_id int PRIMARY KEY,
plan_id int,
FOREIGN KEY (user_id, plan_id) REFERENCES plans(user_id, plan_id)
);
CREATE TABLE IF NOT EXISTS actions(
action_id serial PRIMARY KEY,
action_description VARCHAR (500) NOT NULL,
user_id int REFERENCES users(user_id),
estimated_chunks SMALLINT,
completed_chunks SMALLINT,
completed_on TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
plan int REFERENCES plans(plan_id)
plan_id int REFERENCES plans(plan_id)
);
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $set_updated$
BEGIN
@@ -34,3 +51,8 @@ CREATE TRIGGER set_updated
BEFORE UPDATE ON plans
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();
CREATE TRIGGER set_updated
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();

248
store/postgres.go Normal file
View File

@@ -0,0 +1,248 @@
package store
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/util"
"github.com/jmoiron/sqlx"
)
type postgresStore struct {
db *sqlx.DB
}
// GetPostgresStore provides a store to back the model based on a postgres connection.
func GetPostgresStore(db *sqlx.DB) (models.Store, error) {
db.MapperFunc(util.ToSnake)
return &postgresStore{db: db}, nil
}
func (store *postgresStore) SelectActions(userID int) ([]*models.Action, error) {
queryString := store.db.Rebind("SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE user_id = ?")
actions := make([]*models.Action, 0)
err := store.db.Select(&actions, queryString, userID)
if err != nil {
return nil, err
}
return actions, nil
}
func (store *postgresStore) SelectActionsByPlanID(plan *models.Plan, userID int) ([]*models.Action, error) {
queryString := store.db.Rebind("SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE plan_id = ? AND user_id = ?")
actions := make([]*models.Action, 0)
err := store.db.Select(&actions, queryString, plan.PlanID, userID)
if err != nil {
return nil, err
}
return actions, nil
}
func (store *postgresStore) SelectActionByID(id int, userID int) (*models.Action, error) {
queryString := store.db.Rebind("SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE action_id = ? AND user_id = ?")
action := models.Action{}
err := store.db.Get(&action, queryString, id, userID)
if err != nil {
return nil, err
}
return &action, nil
}
func (store *postgresStore) InsertAction(action *models.Action, userID int) (int, error) {
queryString := store.db.Rebind(
`INSERT INTO actions (action_description,
user_id,
estimated_chunks,
completed_chunks,
completed_on,
plan_id) VALUES (?, ?, ?, ?, ?, ?) RETURNING action_id`,
)
tx := store.db.MustBegin()
var id int
err := tx.Get(
&id,
queryString,
action.ActionDescription,
userID,
action.EstimatedChunks,
action.CompletedChunks,
action.CompletedOn,
action.PlanID,
)
if err != nil {
tx.Rollback()
return -1, err
}
err = tx.Commit()
if err != nil {
return -1, err
}
return id, nil
}
func (store *postgresStore) UpdateAction(action *models.Action, userID int) error {
query := `UPDATE actions SET
action_description = :action_description,
estimated_chunks = :estimated_chunks,
completed_chunks = :completed_chunks,
completed_on = :completed_on,
plan_id = :plan_id
WHERE action_id = :action_id
AND user_id = :user_id`
tx := store.db.MustBegin()
actionToUse := &models.Action{
ActionDescription: action.ActionDescription,
EstimatedChunks: action.EstimatedChunks,
CompletedChunks: action.CompletedChunks,
CompletedOn: action.CompletedOn,
PlanID: action.PlanID,
ActionID: action.ActionID,
UserID: int64(userID),
}
_, err := store.db.NamedExec(query, actionToUse)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (store *postgresStore) SelectPlans(userID int) ([]*models.Plan, error) {
queryString := store.db.Rebind("SELECT plan_id, plan_description, user_id FROM plans WHERE user_id = ?")
plans := make([]*models.Plan, 0)
err := store.db.Select(&plans, queryString, userID)
if err != nil {
return nil, err
}
return plans, nil
}
func (store *postgresStore) SelectPlanByID(id int, userID int) (*models.Plan, error) {
plan := models.Plan{}
err := store.db.Get(&plan, store.db.Rebind("SELECT plan_id, plan_description, user_id FROM plans WHERE plan_id = ? AND user_id = ?"), id, userID)
if err != nil {
return nil, err
}
return &plan, nil
}
func (store *postgresStore) InsertPlan(plan *models.Plan, userID int) (int, error) {
queryString := store.db.Rebind("INSERT INTO plans (plan_description, user_id) VALUES (?, ?) RETURNING plan_id")
tx := store.db.MustBegin()
var id int
err := tx.Get(&id, queryString, plan.PlanDescription, userID)
if err != nil {
tx.Rollback()
return -1, err
}
err = tx.Commit()
if err != nil {
return -1, err
}
return id, nil
}
func (store *postgresStore) UpdatePlan(plan *models.Plan, userID int) error {
query := `UPDATE plans SET
plan_description = :plan_description
WHERE plan_id = :plan_id
AND user_id = :user_id`
tx := store.db.MustBegin()
planToUse := &models.Plan{
PlanDescription: plan.PlanDescription,
PlanID: plan.PlanID,
UserID: int64(userID),
}
_, err := store.db.NamedExec(query, planToUse)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (store *postgresStore) ConnectionLive() error {
return store.db.Ping()
}
func (store *postgresStore) SelectUserByUsername(username string) (*models.User, error) {
user := models.User{}
err := store.db.Get(&user, store.db.Rebind("SELECT user_id, username, display_name, password FROM users WHERE username = ?"), username)
if err != nil {
return nil, err
}
return &user, nil
}
func (store *postgresStore) InsertUser(user *models.User) (int, error) {
queryString := store.db.Rebind("INSERT INTO users (username, display_name, password) VALUES (?, ?, ?) RETURNING user_id")
tx := store.db.MustBegin()
var id int
err := tx.Get(&id, queryString, user.Username, user.DisplayName, user.Password)
if err != nil {
tx.Rollback()
return -1, err
}
err = tx.Commit()
if err != nil {
return -1, err
}
return id, nil
}
func (store *postgresStore) SelectCurrentPlan(userID int) (*models.CurrentPlan, error) {
pp := models.CurrentPlan{}
queryString := store.db.Rebind(`SELECT user_id, plan_id FROM user_current_plan WHERE user_id = ?`)
err := store.db.Get(&pp, queryString, userID)
if err != nil {
return nil, err
}
return &pp, nil
}
func (store *postgresStore) InsertCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
queryString := store.db.Rebind("INSERT INTO user_current_plan (user_id, plan_id) VALUES (?, ?) RETURNING user_id")
tx := store.db.MustBegin()
var id int
err := tx.Get(&id, queryString, userID, currentPlan.PlanID)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (store *postgresStore) UpdateCurrentPlan(currentPlan *models.CurrentPlan, userID int) error {
query := `UPDATE user_current_plan SET
plan_id = :plan_id
WHERE user_id = :user_id`
tx := store.db.MustBegin()
ppToUse := &models.CurrentPlan{
PlanID: currentPlan.PlanID,
UserID: int64(userID),
}
_, err := store.db.NamedExec(query, ppToUse)
if err != nil {
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,217 @@
package store_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSelectCurrentPlan(t *testing.T) {
assert := assert.New(t)
planIDToUse := 1
userIDToUse := 2
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{"plan_id", "user_id"}).AddRow(planIDToUse, userIDToUse)
mock.ExpectQuery(`^SELECT user_id, plan_id FROM user_current_plan WHERE user_id = \$1`).
WithArgs(userIDToUse).
WillReturnRows(rows)
currPlan, err := str.SelectCurrentPlan(userIDToUse)
assert.Nil(err)
assert.EqualValues(planIDToUse, currPlan.PlanID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSelectCurrentPlanErr(t *testing.T) {
assert := assert.New(t)
userIDToUse := 2
str, mock := getDbMock(t)
mock.ExpectQuery(`^SELECT user_id, plan_id FROM user_current_plan WHERE user_id = \$1`).
WithArgs(userIDToUse).
WillReturnError(fmt.Errorf("example error"))
currPlan, err := str.SelectCurrentPlan(userIDToUse)
assert.NotNil(err)
assert.Nil(currPlan)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertCurrentPlan(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planID := 1
userID := 2
badUserID := 7
cp := &models.CurrentPlan{PlanID: int64(planID), UserID: int64(badUserID)}
rows := sqlmock.NewRows([]string{"userID"}).AddRow(userID)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO user_current_plan \(user_id, plan_id\) VALUES \(\$1, \$2\) RETURNING user_id$`).
WithArgs(userID, planID).
WillReturnRows(rows)
mock.ExpectCommit()
// function under test
err := str.InsertCurrentPlan(cp, userID)
// check results
assert.Nil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertCurrentPlanErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
userID := 2
badUserID := 7
planID := 1
cp := &models.CurrentPlan{PlanID: int64(planID), UserID: int64(badUserID)}
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO user_current_plan \(user_id, plan_id\) VALUES \(\$1, \$2\) RETURNING user_id$`).
WithArgs(userID, planID).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
err := str.InsertCurrentPlan(cp, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertCurrentPlanCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planID := 1
userID := 2
cp := &models.CurrentPlan{PlanID: int64(planID), UserID: int64(userID)}
rows := sqlmock.NewRows([]string{"user_id"}).AddRow(userID)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO user_current_plan \(user_id, plan_id\) VALUES \(\$1, \$2\) RETURNING user_id$`).
WithArgs(userID, planID).
WillReturnRows(rows)
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
err := str.InsertCurrentPlan(cp, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateCurrentPlan(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
userIDToUse := 1
planIDToUse := 2
mock.ExpectBegin()
mock.ExpectExec(`
UPDATE user_current_plan SET
plan_id = \$1
WHERE user_id = \$2`).
WithArgs(planIDToUse, userIDToUse).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// function under test
err := str.UpdateCurrentPlan(&models.CurrentPlan{PlanID: int64(planIDToUse)}, userIDToUse)
// check results
assert.Nil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateCurrentPlanErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
userIDToUse := 1
planIDToUse := 2
mock.ExpectBegin()
mock.ExpectExec(`
UPDATE user_current_plan SET
plan_id = \$1
WHERE user_id = \$2`).
WithArgs(planIDToUse, userIDToUse).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
err := str.UpdateCurrentPlan(&models.CurrentPlan{PlanID: int64(planIDToUse)}, userIDToUse)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateCurrentPlanCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
userIDToUse := 1
planIDToUse := 2
mock.ExpectBegin()
mock.ExpectExec(`
UPDATE user_current_plan SET
plan_id = \$1
WHERE user_id = \$2`).
WithArgs(planIDToUse, userIDToUse).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
err := str.UpdateCurrentPlan(&models.CurrentPlan{PlanID: int64(planIDToUse)}, userIDToUse)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

264
store/postgres_plan_test.go Normal file
View File

@@ -0,0 +1,264 @@
package store_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSelectPlans(t *testing.T) {
assert := assert.New(t)
planDesc := "testing"
idToUse := 1
userIDToUse := 2
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{"plan_id", "plan_description", "user_id"}).AddRow(idToUse, planDesc, userIDToUse)
mock.ExpectQuery(`^SELECT plan_id, plan_description, user_id FROM plans WHERE user_id = \$1`).
WithArgs(userIDToUse).
WillReturnRows(rows)
plans, err := str.SelectPlans(userIDToUse)
assert.Nil(err)
assert.Equal(1, len(plans))
plan := plans[0]
assert.EqualValues(idToUse, plan.PlanID)
assert.Equal(planDesc, plan.PlanDescription)
assert.EqualValues(userIDToUse, plan.UserID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSelectPlanByID(t *testing.T) {
assert := assert.New(t)
planDesc := "tsaoeu"
idToUse := 1
userIDToUse := 2
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{"plan_id", "plan_description", "user_id"}).AddRow(idToUse, planDesc, userIDToUse)
mock.ExpectQuery(`^SELECT plan_id, plan_description, user_id FROM plans WHERE plan_id = \$1 AND user_id = \$2$`).
WithArgs(idToUse, userIDToUse).
WillReturnRows(rows)
plan, err := str.SelectPlanByID(idToUse, userIDToUse)
assert.Nil(err)
assert.EqualValues(idToUse, plan.PlanID)
assert.Equal(planDesc, plan.PlanDescription)
assert.EqualValues(userIDToUse, plan.UserID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertPlan(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
badUserID := 7
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(badUserID)}
idToUse := 8
rows := sqlmock.NewRows([]string{"plan_id"}).AddRow(8)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO plans \(plan_description, user_id\) VALUES \(\$1, \$2\) RETURNING plan_id$`).
WithArgs(planDescription, userID).
WillReturnRows(rows)
mock.ExpectCommit()
// function under test
insertedId, err := str.InsertPlan(plan, userID)
// check results
assert.Nil(err)
assert.EqualValues(idToUse, insertedId)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertPlanErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
badUserID := 7
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(badUserID)}
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO plans \(plan_description, user_id\) VALUES \(\$1, \$2\) RETURNING plan_id$`).
WithArgs(planDescription, userID).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
_, err := str.InsertPlan(plan, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertPlanCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(userID)}
idToUse := 8
rows := sqlmock.NewRows([]string{"plan_id"}).AddRow(idToUse)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO plans \(plan_description, user_id\) VALUES \(\$1, \$2\) RETURNING plan_id$`).
WithArgs(planDescription, userID).
WillReturnRows(rows)
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
_, err := str.InsertPlan(plan, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestErrPlanByID(t *testing.T) {
assert := assert.New(t)
idToUse := 1
str, mock := getDbMock(t)
mock.ExpectQuery(`^SELECT plan_id, plan_description, user_id FROM plans WHERE plan_id = \$1 AND user_id = \$2$`).
WithArgs(idToUse, 8).
WillReturnError(fmt.Errorf("example error"))
plan, err := str.SelectPlanByID(idToUse, 8)
assert.NotNil(err)
assert.Nil(plan)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestErrPlans(t *testing.T) {
// set up tests
assert := assert.New(t)
str, mock := getDbMock(t)
mock.ExpectQuery(`^SELECT plan_id, plan_description, user_id FROM plans WHERE user_id = \$1$`).
WithArgs(8).
WillReturnError(fmt.Errorf("example error"))
// function under test
plans, err := str.SelectPlans(8)
// test results
assert.Nil(plans)
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdatePlan(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
idToUse := 8
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(userID), PlanID: int64(idToUse)}
mock.ExpectBegin()
mock.ExpectExec(`^UPDATE plans SET plan_description = \$1 WHERE plan_id = \$2 AND user_id = \$3$`).
WithArgs(planDescription, idToUse, userID).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// function under test
err := str.UpdatePlan(plan, userID)
// check results
assert.Nil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdatePlanErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
idToUse := 8
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(userID), PlanID: int64(idToUse)}
mock.ExpectBegin()
mock.ExpectExec(`^UPDATE plans SET plan_description = \$1 WHERE plan_id = \$2 AND user_id = \$3$`).
WithArgs(planDescription, idToUse, userID).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
err := str.UpdatePlan(plan, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdatePlanCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
planDescription := "2021-01-01"
userID := 2
idToUse := 8
plan := &models.Plan{PlanDescription: planDescription, UserID: int64(userID), PlanID: int64(idToUse)}
mock.ExpectBegin()
mock.ExpectExec(`^UPDATE plans SET plan_description = \$1 WHERE plan_id = \$2 AND user_id = \$3$`).
WithArgs(planDescription, idToUse, userID).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
err := str.UpdatePlan(plan, userID)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

458
store/postgres_test.go Normal file
View File

@@ -0,0 +1,458 @@
package store_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func getDbMock(t *testing.T) (models.Store, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatalf("got an error creating a stub db. How?: %s", err)
}
str, err := store.GetPostgresStore(sqlx.NewDb(db, "pgx"))
if err != nil {
t.Errorf("Error creating postgres store: %s", err)
}
return str, mock
}
func TestSelectActions(t *testing.T) {
// set up test
assert := assert.New(t)
createTime, _ := time.Parse("2006-01-02", "2020-12-31")
updateTime, _ := time.Parse("2006-01-02", "2021-01-01")
completeTime, _ := time.Parse("2006-01-02", "2021-01-05")
idToUse := 1
estChunks := 5
compChunks := 7
userIDToUse := 13
desc := "Howdy, partner."
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{
"action_id",
"action_description",
"estimated_chunks",
"completed_chunks",
"completed_on",
"created_at",
"updated_at",
"plan_id",
"user_id"}).
AddRow(idToUse, desc, estChunks, compChunks, completeTime, createTime, updateTime, idToUse, userIDToUse).
AddRow(idToUse+1, desc, estChunks, compChunks, completeTime, createTime, updateTime, idToUse, userIDToUse)
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE user_id = \$1$`).
WithArgs(userIDToUse).
WillReturnRows(rows)
// function under test
actions, err := str.SelectActions(userIDToUse)
// test results
assert.Nil(err)
assert.Equal(2, len(actions))
action := actions[0]
assert.EqualValues(idToUse, action.ActionID)
assert.Equal(desc, action.ActionDescription)
assert.Equal(estChunks, action.EstimatedChunks)
assert.Equal(compChunks, action.CompletedChunks)
assert.Equal(completeTime, *action.CompletedOn)
assert.Equal(createTime, *action.CreatedAt)
assert.Equal(updateTime, *action.UpdatedAt)
assert.Equal(idToUse, action.PlanID)
assert.EqualValues(userIDToUse, action.UserID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSelectActionsByPlanID(t *testing.T) {
// set up test
assert := assert.New(t)
createTime, _ := time.Parse("2006-01-02", "2020-12-31")
updateTime, _ := time.Parse("2006-01-02", "2021-01-01")
completeTime, _ := time.Parse("2006-01-02", "2021-01-05")
idToUse := 1
estChunks := 5
compChunks := 7
userIDToUse := 13
desc := "Howdy, partner."
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{
"action_id",
"action_description",
"user_id",
"estimated_chunks",
"completed_chunks",
"completed_on",
"created_at",
"updated_at",
"plan_id"}).
AddRow(idToUse, desc, userIDToUse, estChunks, compChunks, completeTime, createTime, updateTime, idToUse).
AddRow(idToUse+1, desc, userIDToUse, estChunks, compChunks, completeTime, createTime, updateTime, idToUse)
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE plan_id = \$1 AND user_id = \$2$`).
WithArgs(idToUse, userIDToUse).
WillReturnRows(rows)
// function under test
actions, err := str.SelectActionsByPlanID(&models.Plan{PlanID: int64(idToUse)}, userIDToUse)
// test results
assert.Nil(err)
assert.Equal(2, len(actions))
action := actions[0]
assert.EqualValues(idToUse, action.ActionID)
assert.Equal(desc, action.ActionDescription)
assert.Equal(estChunks, action.EstimatedChunks)
assert.Equal(compChunks, action.CompletedChunks)
assert.Equal(completeTime, *action.CompletedOn)
assert.Equal(createTime, *action.CreatedAt)
assert.Equal(updateTime, *action.UpdatedAt)
assert.Equal(idToUse, action.PlanID)
assert.EqualValues(userIDToUse, action.UserID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSelectActionsByPlanIDErr(t *testing.T) {
// set up test
assert := assert.New(t)
idToUse := 1
userIDToUse := 13
str, mock := getDbMock(t)
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE plan_id = \$1 AND user_id = \$2$`).
WithArgs(idToUse, userIDToUse).
WillReturnError(fmt.Errorf("example error"))
// function under test
actions, err := str.SelectActionsByPlanID(&models.Plan{PlanID: int64(idToUse)}, userIDToUse)
// test results
assert.NotNil(err)
assert.Nil(actions)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSelectActionById(t *testing.T) {
// set up test
assert := assert.New(t)
createTime, _ := time.Parse("2006-01-02", "2020-12-31")
updateTime, _ := time.Parse("2006-01-02", "2021-01-01")
completeTime, _ := time.Parse("2006-01-02", "2021-01-05")
idToUse := 1
estChunks := 5
compChunks := 7
userIDToUse := 13
desc := "Howdy, partner."
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{
"action_id",
"action_description",
"user_id",
"estimated_chunks",
"completed_chunks",
"completed_on",
"created_at",
"updated_at",
"plan_id"}).
AddRow(idToUse, desc, userIDToUse, estChunks, compChunks, completeTime, createTime, updateTime, idToUse)
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE action_id = \$1 AND user_id = \$2`).
WithArgs(1, userIDToUse).
WillReturnRows(rows)
// function under test
action, err := str.SelectActionByID(1, userIDToUse)
// test results
assert.Nil(err)
assert.EqualValues(idToUse, action.ActionID)
assert.Equal(desc, action.ActionDescription)
assert.Equal(estChunks, action.EstimatedChunks)
assert.Equal(compChunks, action.CompletedChunks)
assert.Equal(completeTime, *action.CompletedOn)
assert.Equal(createTime, *action.CreatedAt)
assert.Equal(updateTime, *action.UpdatedAt)
assert.Equal(idToUse, action.PlanID)
assert.EqualValues(userIDToUse, action.UserID)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestErrActions(t *testing.T) {
// set up tests
assert := assert.New(t)
str, mock := getDbMock(t)
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE user_id = \$1$`).WithArgs(1).WillReturnError(fmt.Errorf("example error"))
// function under test
actions, err := str.SelectActions(1)
// test results
assert.Nil(actions)
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestErrActionByID(t *testing.T) {
// set up tests
assert := assert.New(t)
str, mock := getDbMock(t)
userIDToUse := 3
mock.ExpectQuery(`^SELECT action_id, action_description, user_id, estimated_chunks, completed_chunks, completed_on, created_at, updated_at, plan_id FROM actions WHERE action_id = \$1 AND user_id = \$2`).WithArgs(1, userIDToUse).WillReturnError(fmt.Errorf("example error"))
// function under test
action, err := str.SelectActionByID(1, userIDToUse)
// test results
assert.Nil(action)
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestConnectionLive(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
mock.ExpectPing()
// perform func under tests
err := str.ConnectionLive()
// results
assert.Nil(err)
}
func TestInsertAction(t *testing.T) {
// setup
assert := assert.New(t)
userIDToUse := 7
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
}
idToUse := 8
rows := sqlmock.NewRows([]string{"action_id"}).AddRow(8)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO actions \(action_description, user_id, estimated_chunks, completed_chunks, completed_on, plan_id\) VALUES \(\$1, \$2, \$3, \$4, \$5, \$6\) RETURNING action_id$`).
WithArgs("testing", userIDToUse, 3, 6, completedOn, 5).
WillReturnRows(rows)
mock.ExpectCommit()
// function under test
insertedId, err := str.InsertAction(action, userIDToUse)
// check results
assert.Nil(err)
assert.EqualValues(idToUse, insertedId)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertActionErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
}
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO actions \(action_description, user_id, estimated_chunks, completed_chunks, completed_on, plan_id\) VALUES \(\$1, \$2, \$3, \$4, \$5, \$6\) RETURNING action_id$`).
WithArgs("testing", 7, 3, 6, completedOn, 5).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
_, err := str.InsertAction(action, 7)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertActionCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
}
idToUse := 8
userIDToUse := 11
rows := sqlmock.NewRows([]string{"plan_id"}).AddRow(idToUse)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO actions \(action_description, user_id, estimated_chunks, completed_chunks, completed_on, plan_id\) VALUES \(\$1, \$2, \$3, \$4, \$5, \$6\) RETURNING action_id$`).
WithArgs("testing", userIDToUse, 3, 6, completedOn, 5).
WillReturnRows(rows)
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
_, err := str.InsertAction(action, userIDToUse)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateAction(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
ActionID: 2,
}
userIDToUse := 31
mock.ExpectBegin()
mock.ExpectExec(`
UPDATE actions SET
action_description = \$1,
estimated_chunks = \$2,
completed_chunks = \$3,
completed_on = \$4,
plan_id = \$5
WHERE action_id = \$6
AND user_id = \$7`).
WithArgs("testing", 3, 6, completedOn, 5, 2, userIDToUse).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// function under test
err := str.UpdateAction(action, userIDToUse)
// check results
assert.Nil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateActionErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
ActionID: 2,
}
mock.ExpectBegin()
mock.ExpectExec("UPDATE actions").
WithArgs("testing", 3, 6, completedOn, 5, 2, 31).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
err := str.UpdateAction(action, 31)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestUpdateActionCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
completedOn, _ := time.Parse("2006-01-02", "2021-01-01")
action := &models.Action{
CompletedOn: &completedOn,
EstimatedChunks: 3,
CompletedChunks: 6,
PlanID: 5,
ActionDescription: "testing",
ActionID: 2,
}
mock.ExpectBegin()
mock.ExpectExec("UPDATE actions").
WithArgs("testing", 3, 6, completedOn, 5, 2, 31).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
err := str.UpdateAction(action, 31)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

150
store/postgres_user_test.go Normal file
View File

@@ -0,0 +1,150 @@
package store_test
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSelectUserByUsername(t *testing.T) {
// set up test
assert := assert.New(t)
id := 1
username := "test"
displayName := "Tom Est"
password := []byte("ABC€")
str, mock := getDbMock(t)
rows := sqlmock.NewRows([]string{
"user_id",
"username",
"display_name",
"password",
}).
AddRow(id, username, displayName, password)
mock.ExpectQuery(`^SELECT user_id, username, display_name, password FROM users WHERE username = \$1`).
WithArgs(username).
WillReturnRows(rows)
// function under test
user, err := str.SelectUserByUsername(username)
// test results
assert.Nil(err)
assert.EqualValues(id, user.UserID)
assert.Equal(username, user.Username)
assert.Equal(displayName, user.DisplayName)
assert.Equal(password, user.Password)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestErrUserByID(t *testing.T) {
assert := assert.New(t)
str, mock := getDbMock(t)
username := "snth"
mock.ExpectQuery(`^SELECT user_id, username, display_name, password FROM users WHERE username = \$1`).WithArgs(username).WillReturnError(fmt.Errorf("example error"))
user, err := str.SelectUserByUsername(username)
assert.NotNil(err)
assert.Nil(user)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertUser(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
username := "test"
displayName := "Tom Est"
password := []byte("ABC€")
usr := &models.User{Username: username, DisplayName: displayName, Password: password}
idToUse := 8
rows := sqlmock.NewRows([]string{"user_id"}).AddRow(8)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO users \(username, display_name, password\) VALUES \(\$1, \$2, \$3\) RETURNING user_id$`).
WithArgs(username, displayName, password).
WillReturnRows(rows)
mock.ExpectCommit()
// function under test
insertedId, err := str.InsertUser(usr)
// check results
assert.Nil(err)
assert.EqualValues(idToUse, insertedId)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertUserErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
username := "test"
displayName := "Tom Est"
password := []byte("ABC€")
usr := &models.User{Username: username, DisplayName: displayName, Password: password}
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO users \(username, display_name, password\) VALUES \(\$1, \$2, \$3\) RETURNING user_id$`).
WithArgs(username, displayName, password).
WillReturnError(fmt.Errorf("example error"))
mock.ExpectRollback()
// function under test
_, err := str.InsertUser(usr)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestInsertUserCommitErr(t *testing.T) {
// setup
assert := assert.New(t)
str, mock := getDbMock(t)
username := "test"
displayName := "Tom Est"
password := []byte("ABC€")
usr := &models.User{Username: username, DisplayName: displayName, Password: password}
idToUse := 8
rows := sqlmock.NewRows([]string{"user_id"}).AddRow(idToUse)
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO users \(username, display_name, password\) VALUES \(\$1, \$2, \$3\) RETURNING user_id$`).
WithArgs(username, displayName, password).
WillReturnRows(rows)
mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example"))
// function under test
_, err := str.InsertUser(usr)
// check results
assert.NotNil(err)
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

77
store/store.go Normal file
View File

@@ -0,0 +1,77 @@
package store
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"log"
"gitea.deepak.science/deepak/gogmagog/config"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
// Blank imports to provide drivers.
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jackc/pgx/v4/stdlib"
)
// GetStore provides a store to back the model based on a postgres connection.
func GetStore(dbConf *config.DBConfig) (models.Store, error) {
if dbConf.Type == "postgres" {
db, err := createPostgresDB(dbConf)
if err != nil {
return nil, err
}
return GetPostgresStore(db)
}
return nil, fmt.Errorf("Unsupported database type: %v", dbConf.Type)
}
func createPostgresDB(dbConf *config.DBConfig) (*sqlx.DB, error) {
connStr := fmt.Sprintf("user=%s password=%s host=%s port=%s database=%s sslmode=disable",
dbConf.User,
dbConf.Password,
dbConf.Host,
dbConf.Port,
dbConf.Database)
tmp, err := sql.Open("pgx", connStr)
if err != nil {
log.Print("Could not connect to database: \n", err)
return nil, err
}
db := sqlx.NewDb(tmp, "pgx")
if err := db.Ping(); err != nil {
log.Print("database ping failed\n", err)
return nil, err
}
driver, err := postgres.WithInstance(db.DB, &postgres.Config{})
if err != nil {
log.Print("Could not create driver for db migration", err)
return nil, err
}
m, err := migrate.NewWithDatabaseInstance("file://store/migrations", "postgres", driver)
if err != nil {
log.Print("Could not perform migration", err)
return nil, err
}
if dbConf.DropOnStart {
log.Print("Going down")
m.Down()
}
if err := m.Up(); err != nil {
if err == migrate.ErrNoChange {
log.Print("No migration needed.")
} else {
log.Printf("An error occurred while syncing the database.. %v", err)
return nil, err
}
} else {
log.Print("Performed database migration")
}
return db, nil
}

View File

@@ -0,0 +1,53 @@
package tokens
import (
"context"
"encoding/json"
"gitea.deepak.science/deepak/gogmagog/models"
"log"
"net/http"
)
type deterministicToker struct{}
// GetDeterministicToker returns a zero security toker for testing purposes.
// Do not use in production.
func GetDeterministicToker() Toker {
return &deterministicToker{}
}
func (d *deterministicToker) EncodeUser(user *models.UserNoPassword) string {
tok := &UserToken{ID: user.UserID, Username: user.Username}
ret, _ := json.Marshal(tok)
return string(ret)
}
func (d *deterministicToker) DecodeTokenString(tokenString string) (*UserToken, error) {
var tok UserToken
err := json.Unmarshal([]byte(tokenString), &tok)
return &tok, err
}
func (d *deterministicToker) Authenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := TokenFromHeader(r)
if tokenString == "" {
log.Print("No valid token found")
unauthorized(w, r)
return
}
userToken, err := d.DecodeTokenString(tokenString)
if err != nil {
log.Printf("Error while verifying token: %s", err)
unauthorized(w, r)
return
}
log.Printf("Got user with ID: [%d]", userToken.ID)
ctx := context.WithValue(r.Context(), userIDCtxKey, userToken.ID)
ctx = context.WithValue(ctx, usernameCtxKey, userToken.Username)
// Authenticated
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,80 @@
package tokens_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"log"
"net/http"
"net/http/httptest"
"testing"
)
var dtMiddlewareURL string = "/"
func dtRequestAuth(header string) *http.Request {
req, _ := http.NewRequest("GET", dtMiddlewareURL, nil)
req.Header.Add(authKey, header)
return req
}
func verifyingHandlerdt(t *testing.T, username string, userID int) http.Handler {
assert := assert.New(t)
toker := tokens.GetDeterministicToker()
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
receivedID, _ := tokens.GetUserID(ctx)
receivedUsername, _ := tokens.GetUsername(ctx)
assert.EqualValues(userID, receivedID)
assert.Equal(username, receivedUsername)
})
return toker.Authenticator(dummyHandler)
}
func TestMiddlewareNoTokendt(t *testing.T) {
assert := assert.New(t)
req := httptest.NewRequest(http.MethodGet, dtMiddlewareURL, nil)
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandlerdt(t, "", 0)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestMiddlewareBadTokendt(t *testing.T) {
assert := assert.New(t)
req := mwRequestAuth("Bearer bad")
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandlerdt(t, "", 0)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestMiddlewareGoodTokendt(t *testing.T) {
assert := assert.New(t)
idToUse := 3
username := "username"
displayName := "display name"
user := &models.UserNoPassword{UserID: int64(idToUse), Username: username, DisplayName: displayName}
toker := tokens.GetDeterministicToker()
validToken := toker.EncodeUser(user)
log.Print(validToken)
req := mwRequestAuth("Bearer " + validToken)
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandlerdt(t, username, idToUse)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusOK, status)
}

88
tokens/middleware.go Normal file
View File

@@ -0,0 +1,88 @@
package tokens
import (
"context"
"fmt"
"log"
"net/http"
"strings"
)
type contextKey struct {
name string
}
var userIDCtxKey = &contextKey{"UserID"}
var usernameCtxKey = &contextKey{"Username"}
func unauthorized(w http.ResponseWriter, r *http.Request) {
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
}
// TokenFromHeader tries to retreive the token string from the
// "Authorization" reqeust header: "Authorization: BEARER T".
func TokenFromHeader(r *http.Request) string {
// Get token from authorization header.
bearer := r.Header.Get("Authorization")
if len(bearer) > 7 && strings.ToUpper(bearer[0:6]) == "BEARER" {
return bearer[7:]
}
return ""
}
func (tok *jwtToker) Authenticator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := TokenFromHeader(r)
if tokenString == "" {
log.Print("No valid token found")
unauthorized(w, r)
return
}
userToken, err := tok.DecodeTokenString(tokenString)
if err != nil {
log.Printf("Error while verifying token: %s", err)
unauthorized(w, r)
return
}
log.Printf("Got user with ID: [%d]", userToken.ID)
ctx := context.WithValue(r.Context(), userIDCtxKey, userToken.ID)
ctx = context.WithValue(ctx, usernameCtxKey, userToken.Username)
// Authenticated
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetUserID is a convenience method that gets the user ID from the context.
// I hate the fact that we're passing user ID on the context, but it is more
// idiomatic Go than any type shenanigans.
func GetUserID(ctx context.Context) (int, error) {
userID, ok := ctx.Value(userIDCtxKey).(int64)
if !ok {
return -1, fmt.Errorf("Could not parse user ID [%s] from context", ctx.Value(userIDCtxKey))
}
return int(userID), nil
}
// SetUserID sets the username field on a context, necessary because the key is an unexported custom type.
func SetUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, userIDCtxKey, int64(id))
}
// GetUsername does something similar to GetUserID.
func GetUsername(ctx context.Context) (string, error) {
username, ok := ctx.Value(usernameCtxKey).(string)
if !ok {
return "", fmt.Errorf("Could not parse username [%s] from context", ctx.Value(usernameCtxKey))
}
return username, nil
}
// GetContextForUserValues is a test helper method that creates a context with user ID set.
func GetContextForUserValues(userID int, username string) context.Context {
ctx := context.WithValue(context.Background(), userIDCtxKey, int64(userID))
return context.WithValue(ctx, usernameCtxKey, username)
}

View File

@@ -0,0 +1,49 @@
package tokens_test
import (
"context"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGoodContext(t *testing.T) {
assert := assert.New(t)
idToUse := 3
username := "username"
ctx := tokens.GetContextForUserValues(idToUse, username)
receivedID, err := tokens.GetUserID(ctx)
assert.Nil(err)
assert.EqualValues(idToUse, receivedID)
receivedUsername, err := tokens.GetUsername(ctx)
assert.Nil(err)
assert.Equal(username, receivedUsername)
}
func TestBadContext(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
_, err := tokens.GetUserID(ctx)
assert.NotNil(err)
_, err = tokens.GetUsername(ctx)
assert.NotNil(err)
}
func TestSetContext(t *testing.T) {
assert := assert.New(t)
idToUse := 3
ctx := tokens.SetUserID(context.Background(), 3)
receivedID, err := tokens.GetUserID(ctx)
assert.Nil(err)
assert.EqualValues(idToUse, receivedID)
}

View File

@@ -0,0 +1,78 @@
package tokens_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
var middlewareURL string = "/"
func mwRequestAuth(header string) *http.Request {
req, _ := http.NewRequest("GET", middlewareURL, nil)
req.Header.Add(authKey, header)
return req
}
func verifyingHandler(t *testing.T, username string, userID int) http.Handler {
assert := assert.New(t)
toker := tokens.New("secret")
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
receivedID, _ := tokens.GetUserID(ctx)
receivedUsername, _ := tokens.GetUsername(ctx)
assert.EqualValues(userID, receivedID)
assert.Equal(username, receivedUsername)
})
return toker.Authenticator(dummyHandler)
}
func TestMiddlewareNoToken(t *testing.T) {
assert := assert.New(t)
req := httptest.NewRequest(http.MethodGet, middlewareURL, nil)
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandler(t, "", 0)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestMiddlewareBadToken(t *testing.T) {
assert := assert.New(t)
req := mwRequestAuth("Bearer bad")
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandler(t, "", 0)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusUnauthorized, status)
}
func TestMiddlewareGoodToken(t *testing.T) {
assert := assert.New(t)
idToUse := 3
username := "username"
displayName := "display name"
user := &models.UserNoPassword{UserID: int64(idToUse), Username: username, DisplayName: displayName}
toker := tokens.New("secret")
validToken := toker.EncodeUser(user)
req := mwRequestAuth("Bearer " + validToken)
rr := httptest.NewRecorder()
middlewareHandler := verifyingHandler(t, username, idToUse)
middlewareHandler.ServeHTTP(rr, req)
status := rr.Code
assert.Equal(http.StatusOK, status)
}

56
tokens/middleware_test.go Normal file
View File

@@ -0,0 +1,56 @@
package tokens_test
import (
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
var (
url = ""
authKey = "Authorization"
)
func requestWithAuth(header string) *http.Request {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add(authKey, header)
return req
}
func TestHeaderParseBasic(t *testing.T) {
assert := assert.New(t)
header := "Bearer testing"
req := requestWithAuth(header)
assert.Equal("testing", tokens.TokenFromHeader(req))
}
func TestHeaderParseNoSpace(t *testing.T) {
assert := assert.New(t)
header := "Bearerxtesting"
req := requestWithAuth(header)
assert.Equal("testing", tokens.TokenFromHeader(req))
}
func TestHeaderParseUnicode(t *testing.T) {
assert := assert.New(t)
header := "Bearer 🌸"
req := requestWithAuth(header)
assert.Equal("🌸", tokens.TokenFromHeader(req))
}
func TestHeaderParseMalformed(t *testing.T) {
assert := assert.New(t)
header := "testing"
req := requestWithAuth(header)
assert.Equal("", tokens.TokenFromHeader(req))
}

86
tokens/tokens.go Normal file
View File

@@ -0,0 +1,86 @@
package tokens
import (
"fmt"
"gitea.deepak.science/deepak/gogmagog/models"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwt"
"net/http"
"time"
)
// Toker represents a tokenizer, capable of encoding and verifying tokens.
type Toker interface {
EncodeUser(user *models.UserNoPassword) string
DecodeTokenString(tokenString string) (*UserToken, error)
Authenticator(http.Handler) http.Handler
}
type jwtToker struct {
tokenAuth *jwtauth.JWTAuth
}
// New returns a default Toker for a given secret key.
func New(key string) Toker {
return &jwtToker{tokenAuth: jwtauth.New("HS256", []byte(key), nil)}
}
func (tok *jwtToker) EncodeUser(user *models.UserNoPassword) string {
claims := map[string]interface{}{
"user_id": user.UserID,
"username": user.Username,
"display_name": user.DisplayName,
"iss": "gogmagog.deepak.science",
"aud": "gogmagog.deepak.science",
}
jwtauth.SetIssuedNow(claims)
jwtauth.SetExpiryIn(claims, 2*time.Hour)
_, tokenString, _ := tok.tokenAuth.Encode(claims)
return tokenString
}
// UserToken represents a decoded jwt token.
type UserToken struct {
ID int64
Username string
}
func (tok *jwtToker) DecodeTokenString(tokenString string) (*UserToken, error) {
token, err := tok.tokenAuth.Decode(tokenString)
if err != nil {
return nil, fmt.Errorf("Error decoding token")
}
// Should never happen, remove soon.
// if token == nil {
// return nil, fmt.Errorf("Token was nil")
// }
err = jwt.Validate(
token,
jwt.WithIssuer("gogmagog.deepak.science"),
jwt.WithAudience("gogmagog.deepak.science"),
)
if err != nil {
return nil, err
}
userIDRaw, ok := token.Get("user_id")
if !ok {
return nil, fmt.Errorf("error finding user_id claim")
}
userID, ok := userIDRaw.(float64)
if !ok {
return nil, fmt.Errorf("Could not parse [%s] as userID", userIDRaw)
}
usernameRaw, ok := token.Get("username")
if !ok {
return nil, fmt.Errorf("error finding username claim")
}
username, ok := usernameRaw.(string)
if !ok {
return nil, fmt.Errorf("Could not parse [%s] as username", usernameRaw)
}
return &UserToken{ID: int64(userID), Username: username}, nil
}

165
tokens/tokens_test.go Normal file
View File

@@ -0,0 +1,165 @@
package tokens_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/tokens"
"github.com/go-chi/jwtauth"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestBasic(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
idToUse := int64(3)
usernameToUse := "test"
usr := &models.UserNoPassword{
UserID: idToUse,
Username: usernameToUse,
DisplayName: "Ted Est III",
}
token := toker.EncodeUser(usr)
userToken, err := toker.DecodeTokenString(token)
assert.Nil(err)
assert.Equal(usernameToUse, userToken.Username)
assert.Equal(idToUse, userToken.ID)
_, err = tokens.New("bad secret").DecodeTokenString(token)
assert.NotNil(err)
}
func getTokenString(claims map[string]interface{}) string {
auth := jwtauth.New("HS256", []byte("secret"), nil)
jwtauth.SetIssuedNow(claims)
jwtauth.SetExpiryIn(claims, 2*time.Hour)
_, tokenString, _ := auth.Encode(claims)
return tokenString
}
func TestDecodeBadIssuer(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
idToUse := 3
username := "test"
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"user_id": int64(idToUse),
"username": username,
"display_name": "display_name",
"iss": gog,
"aud": "bad",
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}
func TestDecodeBadAudience(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
idToUse := 3
username := "test"
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"user_id": int64(idToUse),
"username": username,
"display_name": "display_name",
"iss": "bad",
"aud": gog,
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}
func TestDecodeMissingUserID(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
username := "test"
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"username": username,
"display_name": "display_name",
"iss": gog,
"aud": gog,
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}
func TestDecodeBadUserID(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
username := "test"
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"username": username,
"user_id": "id",
"display_name": "display_name",
"iss": gog,
"aud": gog,
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}
func TestDecodeMissingUsername(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
idToUse := 3
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"user_id": int64(idToUse),
"display_name": "display_name",
"iss": gog,
"aud": gog,
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}
func TestDecodeBadUsername(t *testing.T) {
assert := assert.New(t)
toker := tokens.New("secret")
gog := "gogmagog.deepak.science"
claims := map[string]interface{}{
"username": 5,
"user_id": 3,
"display_name": "display_name",
"iss": gog,
"aud": gog,
}
token := getTokenString(claims)
_, err := toker.DecodeTokenString(token)
assert.NotNil(err)
}