Compare commits

...

100 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
ea20a8cc3c Adding permission change to jenkinsfile
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 09:56:26 -06:00
4801411b1d Adds do.sh build script
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-29 09:42:33 -06:00
0a6caeffc5 Removing error check that can never be forced
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-28 17:02:07 -06:00
538e23708a adds error test for config
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-28 16:53:39 -06:00
8cc2de5c2a adds test for default values of config 2020-12-28 16:47:37 -06:00
da50161a92 Makes config testable
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-28 16:30:10 -06:00
3977d395f3 adding blank tests so all packages represented
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-28 08:55:07 -06:00
6a3cdb1877 deleting static as unnecessary for now 2020-12-27 20:13:56 -06:00
ff0a8b97be Adds junit and coverage to jenkinsfile 2020-12-27 20:07:46 -06:00
403295b2b4 makes lint fail the build
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-27 19:34:38 -06:00
1c00a06c10 Fix lint issues 2020-12-27 19:33:54 -06:00
4b8fd5d78a adds lint to jenkinsfile
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good
2020-12-27 19:16:21 -06:00
f947dd2912 Adds additional models, slightly more tests 2020-12-27 19:14:27 -06:00
90 changed files with 7459 additions and 8388 deletions

39
Jenkinsfile vendored
View File

@@ -6,11 +6,21 @@ pipeline {
environment {
GO115MODULE = 'on'
CGO_ENABLED = 0
GOPATH = '/tmp/gopath'
}
stages {
stage('Pre Test') {
steps {
echo 'Installing dependencies'
sh 'go version'
sh 'go get -u golang.org/x/lint/golint'
echo 'Setting permission for build script'
sh "chmod +x do.sh"
}
}
stage('Compile') {
steps {
sh 'go build'
sh './do.sh build'
}
}
stage('Test') {
@@ -18,13 +28,35 @@ pipeline {
stage('go vet') {
steps {
echo 'Running vetting'
sh 'go vet .'
sh './do.sh _vet'
}
}
stage('test') {
environment {
PATH="${env.PATH}:${env.GOPATH}/bin"
}
steps {
echo 'Running test'
sh 'go test -v ./...'
sh './do.sh _test'
sh "go get github.com/tebeka/go2xunit"
sh "go2xunit < tests.out -output tests.xml"
junit "tests.xml"
// convert coverage
sh "go get github.com/t-yuki/gocover-cobertura"
sh "gocover-cobertura < coverage.out > coverage.xml"
cobertura coberturaReportFile: 'coverage.xml'
}
}
stage('lint') {
environment {
PATH="${env.PATH}:${env.GOPATH}/bin"
}
steps {
sh './do.sh _lint'
}
}
}
@@ -35,7 +67,6 @@ pipeline {
always {
mail (bcc: '',
body: "Project: ${env.JOB_NAME} <br>Build Number: ${env.BUILD_NUMBER} <br> Build URL: ${env.BUILD_URL}", cc: '', charset: 'UTF-8', from: 'jenkins@jenkins.deepak.science', mimeType: 'text/html', replyTo: 'dmallubhotla+jenkins@gmail.com', subject: "${env.JOB_NAME} #${env.BUILD_NUMBER}: Build ${currentBuild.currentResult}", to: "dmallubhotla+ci@gmail.com")
}
}
}

View File

@@ -0,0 +1,5 @@
app:
environment: "missingfield"
db:
type: "typical"

12
config/config-sample.yaml Normal file
View File

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

View File

@@ -2,44 +2,71 @@ package config
import (
"log"
"os"
"github.com/spf13/viper"
)
type Key string
const (
Environment Key = `app.environment`
Port Key = `app.port`
DBType Key = `db.type`
DBHost Key = `db.host`
DBPort Key = `db.port`
DBUser Key = `db.user`
DBPassword Key = `db.password`
DBDatabase Key = `db.database`
)
func (k Key) GetString() string {
return viper.GetString(string(k))
// AppConfig represents the config flags for the base application.
type AppConfig struct {
Environment string
Port string
Timezone string
}
func InitDefault() {
viper.SetDefault(string(Environment), "local")
viper.SetDefault(string(Port), "8080")
// DBConfig is the config for the DB connection.
type DBConfig struct {
Type string
Host string
Port string
User string
Password string
Database string
DropOnStart bool
}
func Init() {
log.Print("Initialising config...")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
// Conf represents the overall configuration of the application.
type Conf struct {
App AppConfig
Db DBConfig
}
err := viper.ReadInConfig()
if err != nil {
log.Fatal("Could not read config file: \n", err)
os.Exit(1)
func createDefaultConf() *Conf {
cnf := &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",
DropOnStart: false,
},
}
return cnf
}
// GetConf returns config values
func GetConf(filename string) (*Conf, error) {
cnf := createDefaultConf()
vv := viper.New()
vv.SetConfigName(filename)
vv.SetConfigType("yaml")
vv.AddConfigPath(".")
err := vv.ReadInConfig()
if err != nil {
log.Print("Could not load config file: \n", err)
return nil, err
}
vv.Unmarshal(&cnf)
return cnf, nil
}

58
config/config_test.go Normal file
View File

@@ -0,0 +1,58 @@
package config_test
import (
"github.com/stretchr/testify/assert"
"testing"
"gitea.deepak.science/deepak/gogmagog/config"
)
func TestSample(t *testing.T) {
assert := assert.New(t)
conf, err := config.GetConf("config-sample")
assert.Nil(err)
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)
assert.Equal("aeihn", dbConf.Host)
assert.Equal("1234", dbConf.Port)
assert.Equal("USER", dbConf.User)
assert.Equal("PASSWORD", dbConf.Password)
assert.Equal("g2", dbConf.Database)
assert.True(dbConf.DropOnStart)
}
func TestDefault(t *testing.T) {
assert := assert.New(t)
conf, err := config.GetConf("config-missing-fields")
assert.Nil(err)
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)
assert.Equal("localhost", dbConf.Host)
assert.Equal("5432", dbConf.Port)
assert.Equal("<user>", dbConf.User)
assert.Equal("<password>", dbConf.Password)
assert.Equal("gogmagog", dbConf.Database)
assert.False(dbConf.DropOnStart)
}
func TestMissingFile(t *testing.T) {
assert := assert.New(t)
conf, err := config.GetConf("non-existent")
assert.Nil(conf)
assert.NotNil(err)
}

View File

@@ -1,3 +0,0 @@
DROP TABLE IF EXISTS actions;
DROP FUNCTION IF EXISTS trigger_set_timestamp;

View File

@@ -1,20 +0,0 @@
CREATE TABLE IF NOT EXISTS actions(
id serial PRIMARY KEY,
description VARCHAR (500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $set_updated$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$set_updated$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated
BEFORE UPDATE ON actions
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();

View File

@@ -1,69 +0,0 @@
package db
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"log"
"os"
"gitea.deepak.science/deepak/gogmagog/config"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jackc/pgx/v4/stdlib"
"gitea.deepak.science/deepak/gogmagog/models"
)
type postgresStore struct {
db *sqlx.DB
}
func GetStore() *postgresStore {
if config.DBType.GetString() != "postgres" {
log.Fatalf("Unsupported database type: " + config.DBType.GetString())
os.Exit(1)
}
connStr := fmt.Sprintf("user=%s password=%s host=%s port=%s database=%s sslmode=disable",
config.DBUser.GetString(),
config.DBPassword.GetString(),
config.DBHost.GetString(),
config.DBPort.GetString(),
config.DBDatabase.GetString())
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)
}
return &postgresStore{db: db}
}
func (store *postgresStore) SelectActions() ([]*models.Action, error) {
actions := make([]*models.Action, 0)
err := store.db.Select(&actions, "SELECT id, description, created_at AS createdat, updated_at AS updatedat FROM actions")
if err != nil {
return nil, err
}
return actions, nil
}

50
do.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Do - The Simplest Build Tool on Earth.
# Documentation and examples see https://github.com/8gears/do
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
}
test() {
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() {
go test -v -coverprofile=coverage.out -covermode count ./... | tee tests.out
}
_testhtml() {
_test && go tool cover -html=coverage.out
}
_lint() {
golint -set_exit_status ./...
}
_vet() {
go vet ./...
}
all() {
fmt && test && build
}
"$@" # <- execute the task
[ "$#" -gt 0 ] || printf "Usage:\n\t./do.sh %s\n" "($(compgen -A function | grep '^[^_]' | paste -sd '|' -))"

6
go.mod
View File

@@ -3,8 +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
)

47
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=
@@ -399,6 +431,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@@ -510,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=
@@ -570,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=
@@ -581,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=
@@ -623,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=
@@ -696,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=
@@ -710,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=
@@ -720,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=
@@ -741,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=

56
main.go
View File

@@ -2,54 +2,48 @@ 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"
)
func main() {
// Config
config.Init()
port := config.Port.GetString()
env := config.Environment.GetString()
conf, err := config.GetConf("config")
if err != nil {
log.Println("Could not get config", err)
os.Exit(1)
}
log.Print("Running server on " + port)
log.Print("App environment is " + env)
appConf := conf.App
port := appConf.Port
env := appConf.Environment
// DB
store := db.GetStore()
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")
}
http.Handle("/static/", http.FileServer(http.Dir("public")))
router := routes.NewRouter(m, tokens.New("my secret"))
http.HandleFunc("/actions/", HandleAction)
log.Println("Running server on " + port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func ShowCompleteActions(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.URL.Path))
}
func HandleAction(w http.ResponseWriter, r *http.Request) {
var message string
id := r.URL.Path[len("/actions/"):]
if r.Method == "GET" {
message = "Get the action " + id
} else {
message = r.Method + " the action " + id
}
w.Write([]byte(message))
}
func Hello() string {
return "Hello, world!\n"
http.ListenAndServe(":"+port, router)
log.Println("App environment is " + env)
}

View File

@@ -1,10 +0,0 @@
package main
import "testing"
func TestHello(t *testing.T) {
want := "Hello, world!\n"
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}

View File

@@ -4,13 +4,36 @@ import (
"time"
)
// Action represents a single action item.
type Action struct {
ID int64
Description 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"`
}
func (m *Model) Actions() ([]*Action, error) {
return m.SelectActions()
// Actions returns all actions from the model.
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,13 +1,43 @@
package models
type store interface {
SelectActions() ([]*Action, error)
import (
"fmt"
)
// Store represents the backing store.
type Store interface {
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.
type Model struct {
store
Store
}
func New(store store) *Model {
return &Model{store: store}
// New creates an instance of the model using the passed in store.
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()
}

91
models/models_test.go Normal file
View File

@@ -0,0 +1,91 @@
package models_test
import (
"gitea.deepak.science/deepak/gogmagog/models"
"gitea.deepak.science/deepak/gogmagog/store"
"github.com/stretchr/testify/assert"
"testing"
)
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}
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(1, len(actions))
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)
}

34
models/plan.go Normal file
View File

@@ -0,0 +1,34 @@
package models
// Plan represents a single day's agenda of actions.
type Plan struct {
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(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)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
.navbar-fixed-top + .sidebar-trigger .sidebar-toggle,
.navbar-fixed-bottom + .sidebar-trigger .sidebar-toggle,
.navbar-fixed-top + .sidebar-trigger + .sidebar-trigger .sidebar-toggle,
.navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-toggle {
position: fixed;
z-index: 1032;
}
.navbar-fixed-top + .sidebar-trigger .sidebar-wrapper,
.navbar-fixed-bottom + .sidebar-trigger .sidebar-wrapper,
.navbar-fixed-top + .sidebar-trigger + .sidebar-trigger .sidebar-wrapper,
.navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-wrapper {
z-index: 1033;
}
.navbar-fixed-bottom + .sidebar-trigger .sidebar-toggle,
.navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger .sidebar-toggle {
top: inherit;
bottom: 0;
}
.navbar-fixed-top + .container,
.navbar-fixed-top + .container-fluid,
.navbar-fixed-top + .sidebar-trigger + .container,
.navbar-fixed-top + .sidebar-trigger + .container-fluid,
.navbar-fixed-top + .sidebar-trigger + .sidebar-trigger + .container,
.navbar-fixed-top + .sidebar-trigger + .sidebar-trigger + .container-fluid {
margin-top: 70px;
}
.navbar-fixed-bottom + .container,
.navbar-fixed-bottom + .container-fluid,
.navbar-fixed-bottom + .sidebar-trigger + .container,
.navbar-fixed-bottom + .sidebar-trigger + .container-fluid,
.navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger + .container,
.navbar-fixed-bottom + .sidebar-trigger + .sidebar-trigger + .container-fluid {
margin-bottom: 70px;
}
@media (min-width: 992px) {
.sidebar-force-open:not(.sidebar-right) + .container,
.sidebar-force-open:not(.sidebar-right) + .sidebar-trigger + .container {
padding-left: 225px;
}
.sidebar-force-open:not(.sidebar-right) + .container-fluid,
.sidebar-force-open:not(.sidebar-right) + .sidebar-trigger + .container-fluid {
margin-left: 210px;
}
.sidebar-force-open.sidebar-right + .container,
.sidebar-force-open.sidebar-right + .sidebar-trigger + .container {
padding-right: 225px;
}
.sidebar-force-open.sidebar-right + .container-fluid,
.sidebar-force-open.sidebar-right + .sidebar-trigger + .container-fluid {
margin-right: 210px;
}
}

View File

@@ -1,336 +0,0 @@
.sidebar-toggle {
position: absolute;
top: 0;
left: 0;
height: 51px;
cursor: pointer;
margin: 0;
padding: 17px 20px 17px 0;
}
.sidebar-toggle .glyphicon{
color:white;
}
.sidebar-toggle > i {
font-size: 18px;
margin: 0 0 0 -5px;
-webkit-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
}
.sidebar-toggle > .fa-sidebar-toggle:before {
content: "\f0c9";
}
.sidebar-toggle:hover > i,
.sidebar-toggle.sidebar-toggle-opened > i {
margin-left: -9px;
}
.sidebar-toggle + .navbar-brand > img {
margin-left: 10px;
}
.sidebar-togglable .sidebar-toggle {
display: block;
}
.sidebar-wrapper {
position: fixed;
top: 0;
overflow: hidden;
bottom: 0;
width: 210px;
cursor: default;
-moz-user-select: -moz-none;
user-select: none;
-webkit-transform: translate3d(-210px, 0px, 0px);
transform: translate3d(-210px, 0px, 0px);
}
.sidebar-wrapper.sidebar-ready {
-webkit-transition: -webkit-transform 0.2s;
transition: transform 0.2s;
}
.sidebar-wrapper.sidebar-open {
-webkit-transform: translate3d(0px, 0px, 0px);
transform: translate3d(0px, 0px, 0px);
}
.sidebar-wrapper .sidebar-scroller {
position: absolute;
overflow-x: hidden;
overflow-y: scroll;
left: 0;
right: -18px;
height: 100%;
}
.sidebar-wrapper .sidebar-menu {
margin: 0;
padding: 0;
overflow-x: hidden;
list-style: none;
text-align: left;
font-size: 14px;
}
.sidebar-wrapper .sidebar-menu ul {
padding: 0;
margin: 0;
}
.sidebar-wrapper .sidebar-menu li {
display: block;
}
.sidebar-wrapper .sidebar-group > span,
.sidebar-wrapper .sidebar-item > a {
display: block;
height: 100%;
padding: 12px 15px;
text-decoration: none;
}
.sidebar-wrapper .sidebar-item > a.active {
border-left: 5px solid;
padding: 12px 15px 12px 10px;
}
.sidebar-wrapper .sidebar-item.sidebar-item-mini {
font-size: 0.7em;
}
.sidebar-wrapper .sidebar-item.sidebar-item-mini > a {
padding-top: 7px;
padding-bottom: 7px;
}
.sidebar-wrapper .sidebar-group {
margin-top: 20px;
}
.sidebar-wrapper .sidebar-group:first-child {
margin-top: 0;
}
.sidebar-wrapper .sidebar-group > span {
font-family: inherit;
font-size: 24px;
border-bottom: 1px solid transparent;
}
.sidebar-wrapper .sidebar-group.sticky-header {
position: absolute;
height: auto;
top: 0;
left: 0;
right: 0;
z-index: 1;
margin-top: 0;
}
.sidebar-wrapper .sidebar-group + .sidebar-item {
margin-top: 32px;
border-top: 1px solid transparent;
}
.sidebar-wrapper .sidebar-item > a {
cursor: pointer;
}
.sidebar-swipe {
position: fixed;
z-index: 1001;
width: 20px;
left: 0;
top: 0;
bottom: 0;
-moz-user-select: -moz-none;
user-select: none;
}
.sidebar-open + .sidebar-swipe,
.sidebar-force-open .sidebar-swipe {
left: 210px;
}
.sidebar-trigger .sidebar-toggle {
z-index: 1002;
}
.sidebar-trigger .sidebar-wrapper {
z-index: 1003;
}
.sidebar-wrapper.sidebar-default {
color: #222222;
background-color: #ffffff;
}
.sidebar-wrapper.sidebar-default.sidebar-open,
.sidebar-wrapper.sidebar-default.sidebar-dragging {
-webkit-box-shadow: 3px 0 4px rgba(0, 0, 0, 0.18);
box-shadow: 3px 0 4px rgba(0, 0, 0, 0.18);
}
.sidebar-wrapper.sidebar-default .sidebar-group > span {
color: #dbdbdb;
border-bottom-color: #eeeeee;
}
.sidebar-wrapper.sidebar-default .sidebar-group.sticky-header > span {
background-color: #ffffff;
}
.sidebar-wrapper.sidebar-default .sidebar-group + .sidebar-item {
border-top-color: #eeeeee;
}
.sidebar-wrapper.sidebar-default .sidebar-item > a {
color: #222222;
background-color: #ffffff;
}
.sidebar-wrapper.sidebar-default .sidebar-item > a:hover,
.sidebar-wrapper.sidebar-default .sidebar-item > a:focus {
color: #333333;
background-color: #eeeeee;
}
.sidebar-wrapper.sidebar-default .sidebar-item > a.active {
color: #337ab7;
background-color: #ffffff;
}
.sidebar-wrapper.sidebar-default .sidebar-item.sidebar-item-mini > a {
color: #747474;
}
.sidebar-wrapper.sidebar-default .hammer-scrollbar {
background-color: #555555;
}
.sidebar-wrapper.sidebar-inverse {
color: #9d9d9d;
background-color: #2a3542;
}
.sidebar-wrapper.sidebar-inverse.sidebar-open,
.sidebar-wrapper.sidebar-inverse.sidebar-dragging {
-webkit-box-shadow: 3px 0 4px rgba(0, 0, 0, 0.32);
box-shadow: 3px 0 4px rgba(0, 0, 0, 0.32);
}
.sidebar-wrapper.sidebar-inverse .sidebar-group > span {
color: #46586e;
border-bottom-color: #344252;
}
.sidebar-wrapper.sidebar-inverse .sidebar-group.sticky-header > span {
background-color: #2a3542;
}
.sidebar-wrapper.sidebar-inverse .sidebar-group + .sidebar-item {
border-top-color: #344252;
}
.sidebar-wrapper.sidebar-inverse .sidebar-item > a {
color: #9d9d9d;
background-color: #2a3542;
}
.sidebar-wrapper.sidebar-inverse .sidebar-item > a:hover,
.sidebar-wrapper.sidebar-inverse .sidebar-item > a:focus {
color: #ffffff;
background-color: #344252;
}
.sidebar-wrapper.sidebar-inverse .sidebar-item > a.active {
color: #dfecf6;
background-color: #2a3542;
}
.sidebar-wrapper.sidebar-inverse .sidebar-item.sidebar-item-mini > a {
color: #6d85a2;
}
.sidebar-wrapper.sidebar-inverse .hammer-scrollbar {
background-color: #e4e8ed;
}
.navbar-default + .sidebar-trigger .sidebar-toggle > i,
.navbar-default + .sidebar-trigger + .sidebar-trigger .sidebar-toggle > i {
color: #888888;
}
.navbar-default + .sidebar-trigger .sidebar-toggle:hover > i,
.navbar-default + .sidebar-trigger + .sidebar-trigger .sidebar-toggle:hover > i {
color: #333333;
}
.navbar-inverse + .sidebar-trigger .sidebar-toggle > i,
.navbar-inverse + .sidebar-trigger + .sidebar-trigger .sidebar-toggle > i {
color: #ffffff;
}
.navbar-inverse + .sidebar-trigger .sidebar-toggle:hover > i,
.navbar-inverse + .sidebar-trigger + .sidebar-trigger .sidebar-toggle:hover > i {
color: #ffffff;
}
.sidebar-right .sidebar-toggle {
left: auto;
right: 0;
padding-left: 20px;
padding-right: 0;
}
.sidebar-right .sidebar-toggle > i {
margin-left: 0;
margin-right: -5px;
}
.sidebar-right .sidebar-toggle:hover > i,
.sidebar-right .sidebar-toggle.sidebar-toggle-opened > i {
margin-right: -9px;
}
.sidebar-right .sidebar-wrapper {
right: 0;
-webkit-transform: translate3d(210px, 0px, 0px);
transform: translate3d(210px, 0px, 0px);
}
.sidebar-right .sidebar-wrapper.sidebar-open {
-webkit-transform: translate3d(0px, 0px, 0px);
transform: translate3d(0px, 0px, 0px);
}
.sidebar-right .sidebar-wrapper.sidebar-default.sidebar-open,
.sidebar-right .sidebar-wrapper.sidebar-default.sidebar-dragging {
-webkit-box-shadow: -3px 0 4px rgba(0, 0, 0, 0.18);
box-shadow: -3px 0 4px rgba(0, 0, 0, 0.18);
}
.sidebar-right .sidebar-wrapper.sidebar-inverse.sidebar-open,
.sidebar-right .sidebar-wrapper.sidebar-inverse.sidebar-dragging {
-webkit-box-shadow: -3px 0 4px rgba(0, 0, 0, 0.32);
box-shadow: -3px 0 4px rgba(0, 0, 0, 0.32);
}
.sidebar-right .sidebar-wrapper .sidebar-item > a.active {
border-left: inherit;
border-right: 5px solid;
padding: 12px 10px 12px 15px;
}
.sidebar-right .sidebar-swipe {
left: auto;
right: 0;
}
.sidebar-right .sidebar-open + .sidebar-swipe,
.sidebar-right .sidebar-force-open .sidebar-swipe {
right: 210px;
}
@media (max-width: 767px) {
.sidebar-wrapper {
width: 80%;
-webkit-transform: translate3d(-100%, 0px, 0px);
transform: translate3d(-100%, 0px, 0px);
}
.sidebar-open + .sidebar-swipe,
.sidebar-force-open .sidebar-open + .sidebar-swipe {
left: 80%;
}
.sidebar-force-open .sidebar-swipe {
left: 0;
}
.sidebar-right .sidebar-toggle {
padding-left: 7px;
}
.sidebar-right .sidebar-wrapper {
-webkit-transform: translate3d(100%, 0px, 0px);
transform: translate3d(100%, 0px, 0px);
}
.sidebar-right .sidebar-open + .sidebar-swipe {
left: auto;
right: 80%;
}
.sidebar-right .sidebar-force-open .sidebar-swipe {
left: auto;
right: 0;
}
}
@media (max-width: 991px) {
.sidebar-force-open .sidebar-wrapper:not(.sidebar-open) + .sidebar-swipe {
left: 0;
}
}
@media (min-width: 992px) {
.sidebar-trigger.sidebar-locked .sidebar-toggle {
display: none;
}
.sidebar-trigger.sidebar-locked .sidebar-wrapper {
margin-top: 51px;
}
.sidebar-wrapper.sidebar-open-init {
-webkit-transform: translate3d(0px, 0px, 0px);
transform: translate3d(0px, 0px, 0px);
}
.sidebar-force-open .sidebar-wrapper.sidebar-open,
.sidebar-force-open.sidebar-right .sidebar-wrapper.sidebar-open,
.sidebar-force-open .sidebar-wrapper.sidebar-dragging,
.sidebar-force-open.sidebar-right .sidebar-wrapper.sidebar-dragging {
-webkit-box-shadow: none;
box-shadow: none;
}
}
.sidebar-wrapper {
-ms-touch-action: none;
}
.sidebar-swipe {
-ms-touch-action: none;
}

View File

@@ -1,412 +0,0 @@
/*
omninotesweb main stylesheet
=============
Author: Suraj Patil
Updated: January 13, 2014
Notes: omninotesweb
*/
/*--------------------------------------
Layout
-------------------------------------- */
.commentslist {
background-color: #f5f5f5;
}
.comment {
padding-left: 25px;
padding-right: 25px;
padding-top: 15px;
}
ul{
list-style-type: none;
}
.timestamp{
color: #aaa;
}
.badge{
background-color: #1a78c9;
margin-right:10px;
float:right;
}
.center{
text-align:center;
}
.navbar-brand:hover{
color:white;
}
.footer{
height:30px;
width:auto;
text-align:center;
}
textarea {
resize: vertical;
}
.floating-action-icon{
position: fixed;
bottom:40px;
right:30px;
z-index:101;
width: 56px;
height: 56px;
font-size: 20px;
border-radius: 35px;
box-shadow:0 0 4px #111;
padding-top:10px !important;
}
.notification{
width: -moz-fit-content;
position: fixed;
right: 10px;
max-width: 400px;
padding: 12px;
background-color: #F2FCFF;
box-shadow: 1px 0px 9px 0px rgba(27, 123, 216, 0.4);
border: 1px solid #5596CE;
border-radius: 10px;
border-radius: 10px;
z-index: 1100;
max-height: 90px;
margin-top: 20px;
min-width: 200px;
}
#btnMessage {
padding: 1px 12px;
height: 30px;
text-align: center;
float: right;
}
/*--------------------------------------
Navbar
-------------------------------------- */
.mainHeader{
background-color: #3f51b5;
border-radius:0px;
}
.navbar-brand{
margin-left: 25px;
font-size:1.5em;
color:white;
max-width: 2000px;
}
.btn-action{
background-color: #3f51b5;
}
#icons{
text-align:right;
float: right;
height: 50%;
line-height: 52px;
float:right;
margin-top: 10px;
}
nav .glyphicon{
color:white;
}
nav .glyphicon:hover{
color:black;
}
.sr-only{
display:block;
width:20px;
height:3px;
background-color: white;
margin-bottom: 4px;
}
.hidden{
display:none;
}
/*--------------------------------------
NotesFeed
-------------------------------------- */
.overdue {
color: red;
margin-top: 1px;
font-size: 12px;
padding-top: -100px;
position: absolute;
padding-left: 5px;
}
.noteHeading {
font-weight:900;
font-size:17px;
color:#666666;
margin-bottom:0px;
padding-bottom:5px;
}
hr{
margin: 0;
padding: 0;
}
.noteHeading a {
font-weight:400;
font-size:11px;
}
.noteContent {
padding-top: 7px;
font-size: 0.9em;
display: none;
}
.noteContent img {
width: 90%;
padding-left: 10%;
}
.toggle{
cursor: pointer;
margin-top:-30px;
float:right;
}
.notefooter{
color:#aaa;
height: 20px;
}
/* These are the classes that are going to be applied: */
.column {
float: left;
}
.note{
display: block;
margin: 4px;
margin-bottom: 6px;
padding: 15px;
background-color: white;
box-shadow:1px 1px 2px 1px #7D7470;
border-radius:4px 4px 4px 4px;
height: auto;
}
.note:hover{
border-bottom: 2px solid black;
}
.menu a {
text-decoration: none;
}
.menu li{
margin-top: -10px;
list-style: none;
float: right;
padding: 5px;
height: 15px;
}
/*--------------------------------------
Navigation drawer
-------------------------------------- */
.list-group-item{
border:none;
}
.sidebar-item h5{
margin-left:15px;
display: block;
}
.sidebar-item .glyphicon:hover{
color:black;
}
.col-sm-2 a.list-group-item:hover,a.list-group-item:focus{
text-decoration:none;
background-color: transparent;
}
body{
background-color: #f5f5f5;
overflow-x:hidden;
overflow-y:auto;
margin-top:70px;
}
.nav-item{
padding-left: 10px;
}
.col-sm-2{
background-color: white;
}
.badge{
background-color: #7D8EF0;
margin-right:10px;
float:right;
}
/* --------------------------------------
Global Styles
-------------------------------------- */
body{
width:100%;
}
.main-content{
color:black;
}
.highlight{
background-color: #72CBFF;
}
/* --------------------------------------
Media Queries
-------------------------------------- */
/* Portrait & landscape phone */
@media (max-width: 480px) {
body{
margin-top:105px;
}
.timeline {
overflow: hidden;
width: 100%;
}
#icons{
text-align:center;
padding:0;
height: 50%;
line-height: 52px;
float:right;
margin-top: 10px;
}
.mainHeader h3{
padding:0px;
text-align:center;
}
.note-sm{
height:150px;
}
.floating-action-icon{
position: fixed;
bottom:35px;
right:20px;
z-index:101;
width: 50px;
height: 50px;
padding: 10px 10px;
font-size: 24px;
border-radius: 25px;
text-align:center;
}
.modal-dialog{
width:240px;
}
}
/* Landscape phone to portrait tablet */
@media (max-width: 768px) and (min-width:481px) {
#icons{
text-align:center;
padding:0;
height: 50%;
line-height: 52px;
float:right;
margin-top: 10px;
}
body{
margin-top:105px;
}
.timeline {
overflow: hidden;
width: 100%;
}
.mainHeader h3{
padding:0px;
text-align:center;
}
.note-lg{
height:360px;
/*change later*/
}
.modal-dialog{
width:250px;
}
}
/* Large desktop */
@media (max-width: 1400px) and (min-width:769px) {
.timeline {
overflow: hidden;
width: 55%;
margin-left:20%;
}
}
/*--------------------------------------
Sidebar
-------------------------------------- */
.sidebar-toggle{
left:10px !important;
}
.sidebar-wrapper.sidebar-default.sidebar-open, .sidebar-wrapper.sidebar-default.sidebar-dragging{
width:260px;
}
/*Modal dialog*/
.modal-footer {
padding: 10px 15px 15px;
margin:0;
border:none;
}
.modal-body{
padding: 20px 20px 10px 20px;
}
.modal{
overflow: hidden;
}

View File

@@ -1,175 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph />
<glyph />
<glyph unicode="&#xd;" />
<glyph unicode=" " />
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-63 -32t-65.5 -67t-50 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-207 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447z" />
<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
<glyph unicode="&#xe023;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM500 500v400h100v-300h200v-100h-300z" />
<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
<glyph unicode="&#xe026;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM350 600h150v300h200v-300h150l-250 -300z" />
<glyph unicode="&#xe027;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM350 600h150v-300h200v300h150l-250 300z" />
<glyph unicode="&#xe028;" d="M0 25v475l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
<glyph unicode="&#xe029;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 601q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM500 397v401l297 -200z" />
<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v275v25q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 108 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
<glyph unicode="&#xe041;" d="M1 700v475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
<glyph unicode="&#xe042;" d="M2 700v475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391q67 -181 82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140 l116 -317h-340z" />
<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 13.5t-49 14v71h471q76 0 145.5 -37.5t115 -111.5t45.5 -167q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129 q0 -84 -59 -156.5t-142 -111t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
<glyph unicode="&#xe062;" d="M219 725q0 -116 60 -249q65 -114 158.5 -231.5t154.5 -178.5l61 -61q22 25 59.5 69t132 167t163.5 231q70 142 70 258q0 117 -57.5 218.5t-156.5 161t-216 59.5q-116 0 -215 -61t-156.5 -163.5t-57.5 -219.5zM431 752q0 92 64.5 157t156.5 65t157 -65t65 -157t-65 -156.5 t-157 -64.5t-156.5 64.5t-64.5 156.5z" />
<glyph unicode="&#xe063;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5v854q-177 0 -302 -125t-125 -302z " />
<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM994 1015l114 -113l113 113l-21 85l-92 28z" />
<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l566 567l-136 137l-430 -431l-147 147z" />
<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
<glyph unicode="&#xe079;" d="M136 550v1l551 550l198 -197l-352 -353l352 -353l-198 -198z" />
<glyph unicode="&#xe080;" d="M315 198l198 -198l552 550l-1 1l-551 550l-198 -197l353 -353z" />
<glyph unicode="&#xe081;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
<glyph unicode="&#xe082;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM300 500h600v200h-600v-200z" />
<glyph unicode="&#xe083;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM247 459l212 -212l141 141l141 -141l213 212l-142 141l142 142l-213 212 l-141 -142l-141 142l-212 -212l141 -142z" />
<glyph unicode="&#xe084;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
<glyph unicode="&#xe085;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM364 700h143q4 0 11.5 -1t11 -0.5t6.5 3t3 8.5t1 11t3.5 8.5t3.5 6t5.5 4 t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26q0 -21 -4.5 -27.5t-26.5 -21.5q-5 -1 -12.5 -3.5t-27 -13.5t-34 -27t-26.5 -46.5t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 46t31 69t14 93.5q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5 t-53.5 -74.5t-19 -114zM500 300h200v100h-200v-100z" />
<glyph unicode="&#xe086;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200 v100h-200v-100z" />
<glyph unicode="&#xe087;" d="M0 500v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h6h165h32v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206 h200v-206q149 48 201 206h-201v200h200q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210z" />
<glyph unicode="&#xe088;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM384 465l135 135l-135 135l81 81l135 -135l135 135l81 -81l-135 -135l135 -135l-81 -81l-135 136l-135 -136z" />
<glyph unicode="&#xe089;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125 -301.5t302 -125.5t302 125.5t125 301.5 q0 177 -125 302t-302 125t-302 -125t-125 -302zM350 537l113 113l87 -87l204 204l113 -113l-317 -317z" />
<glyph unicode="&#xe090;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -119 66 -225l586 587q-105 65 -225 65q-177 0 -302 -125 t-125 -302zM381 235q104 -62 219 -62q177 0 302 125.5t125 301.5q0 117 -62 219z" />
<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
<glyph unicode="&#xe095;" d="M0 200q5 105 27 193t68 167t113 135t166.5 91.5t225.5 42.5v271l600 -453l-600 -448v301q-94 -2 -182.5 -20t-170.5 -52.5t-147 -92.5t-100 -135z" />
<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -142l-295 -294l129 -130h-400z" />
<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100z M100 0h400v400h-400v-400zM200 900q-3 0 14 48t35 96l18 47l214 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-4 -23 -14 -51.5t-20 -49t-24 -49.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5 t30.5 2.5t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5 t8 -43t6 -39.5t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM119 600q37 -48 65.5 -82.5t84 -93t118.5 -100t126 -60.5l37 141 q-107 18 -178.5 101.5t-71.5 193.5q0 85 46 158q-97 -83 -227 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59.5 69t-43.5 68l-15 26zM780 161l38 145q22 15 45 34t45.5 43.5t40.5 44.5t40.5 49.5t34 44.5t32 44t24.5 34q-83 113 -139 175l38 146 q68 -54 132.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
<glyph unicode="&#xe112;" d="M100 600v200h300v-250v-26v-55.5t3.5 -50t11 -47.5t22 -37t35.5 -31.5t53.5 -18t74.5 -7.5t74.5 8t53.5 18.5t35.5 32t22 38t11 48t3.5 49.5v54v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5 t-89 96.5t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
<glyph unicode="&#xe113;" d="M-1 410l198 -198l353 353l353 -353l198 198l-550 552z" />
<glyph unicode="&#xe114;" d="M99 797l551 -551l550 551l-198 198l-353 -352l-352 352z" />
<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-199l300 -283l299 283h-200v600h-796z" />
<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-300v-50q0 -21 -14.5 -35.5 t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23 t-167.5 -37t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
<glyph unicode="&#xe125;" d="M0 200v600h200v-600h-200zM300 275v400q0 37 20 63l145 196l96 198q14 28 38 48t51 20h50q39 0 69.5 -40.5t30.5 -84.5v-150l-28 -125h328q39 0 69.5 -40.5t30.5 -84.5v-100q0 -43 -29 -74l-238 -344q-37 -57 -83 -57h-250q-7 0 -41.5 25t-66.5 50l-31 25h-61 q-100 0 -100 75z" />
<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63z" />
<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275q0 -13 83 -94t90 -81h341q15 0 28.5 19.5t20.5 41.5l130 339h107q84 0 138.5 39t54.5 111t-53.5 110t-138.5 38h-302l85 121q11 15 10.5 34t-13.5 32l-110 112q-22 22 -53 6l-362 -230q-6 -4 -15.5 -10.5t-25 -26t-15.5 -36.5v-525z M408 289v503l339 236l86 -83l-147 -183q-17 -23 -5 -47q2 -3 4 -5.5t4 -4t5.5 -2.5t5 -1.5t6 -1t6.5 -0.5h7.5h6.5h457q22 0 30.5 -25t-0.5 -50t-30 -25h-203q-15 0 -28.5 -20t-19.5 -41l-131 -339h-293z" />
<glyph unicode="&#xe128;" d="M-101 651q0 -72 55 -111t139 -39h107l130 -339q6 -21 19.5 -41t29.5 -20h341q8 0 94 80.5t86 93.5v526q0 17 -15 35.5t-30 27.5l-15 10l-365 230q-32 14 -54 -6l-109 -113q-13 -13 -13.5 -32t10.5 -34l85 -121q-101 1 -302 1q-85 0 -139 -38t-54 -110zM-1 601v100h476 h6.5h7.5t6.5 0.5t6.5 1t5.5 1.5t5 2.5l4 4t3.5 5.5q13 24 -5 46l-145 184l87 83l343 -237v-502l-107 -89h-293l-131 339q-6 20 -19.5 40.5t-28.5 20.5h-222zM1000 201v600h200v-600h-200z" />
<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-203q0 -15 20 -28.5t41 -19.5l339 -131v-293l-89 -100h-503zM400 0v200h600v-200h-600z" />
<glyph unicode="&#xe130;" d="M1 585q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM76 565l237 339h503 l89 -100v-294l-340 -130q-21 -7 -40.5 -20.5t-19.5 -28.5v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
<glyph unicode="&#xe131;" d="M22 600q0 157 77.5 290.5t210.5 210.5t290 77t290 -77t210.5 -210.5t77.5 -290.5t-77.5 -290t-210.5 -210.5t-290 -77.5t-290 77.5t-210.5 210.5t-77.5 290zM298 500h300v-194l402 294l-402 299v-198h-300v-201z" />
<glyph unicode="&#xe132;" d="M22 600q0 157 77.5 290.5t210.5 210.5t290 77t290 -77t210.5 -210.5t77.5 -290.5t-77.5 -290t-210.5 -210.5t-290 -77.5t-290 77.5t-210.5 210.5t-77.5 290zM200 600l400 -294v194h300v201h-298v198z" />
<glyph unicode="&#xe133;" d="M22 600q0 157 77.5 290.5t210.5 210.5t290 77t290 -77t210.5 -210.5t77.5 -290.5t-77.5 -290t-210.5 -210.5t-290 -77.5t-290 77.5t-210.5 210.5t-77.5 290zM302 599h197v-300h201v300h194l-294 401z" />
<glyph unicode="&#xe134;" d="M22 600q0 157 77.5 290.5t210.5 210.5t290 77t290 -77t210.5 -210.5t77.5 -290.5t-77.5 -290t-210.5 -210.5t-290 -77.5t-290 77.5t-210.5 210.5t-77.5 290zM306 602l294 -402l298 402h-197v300h-201v-300h-194z" />
<glyph unicode="&#xe135;" d="M24 600q0 154 78 287t211 211t287 78t287 -78t211 -211t78 -287t-78 -287t-211 -211t-287 -78t-287 78t-211 211t-78 287zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60q12 0 23 -5.5t23 -15t20 -13.5q16 -8 34 -15t40 -14.5t34 -12.5q22 -8 53 -31.5 t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49.5t20.5 62.5q-14 9 -37.5 9t-35.5 7q-14 8 -49 15.5t-52 18.5q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12 q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5 t34 22.5q-6 17 10 36q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23q-20 -3 -37 1q-16 -13 -37.5 -21.5t-34 -12t-44 -8.5t-38.5 -6 q-15 -3 -45.5 0.5t-45.5 -2.5q-22 -8 -52.5 -27t-33.5 -34q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -17 8 -41.5t16.5 -44.5t9.5 -24q-9 2 -39.5 6t-52 10t-37.5 16z" />
<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
<glyph unicode="&#xe141;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM173 600q0 -176 125.5 -301.5t301.5 -125.5q177 0 302 125.5t125 301.5 q0 177 -125 302t-302 125q-176 0 -301.5 -125t-125.5 -302zM291 655q0 23 16 39t38 16q23 0 39 -16t16 -39t-16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM513 609q0 32 21 56.5 t52 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-16 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-62 25.5t-26 61.5zM800 655q0 23 16 39t39 16q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16t-16 39z " />
<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36 q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60l517 511 q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-39 -23 -111 -95l-512 -512q-68 -68 -81 -163z" />
<glyph unicode="&#xe143;" d="M99 785q0 64 28 122.5t73 100t104.5 64t119 20.5t120 -38.5t105.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100t27 -122.5q0 -70 -55.5 -151.5t-129.5 -151t-182.5 -181t-182.5 -212.5q-62 85 -145.5 174t-150 152.5t-127 127t-94 124.5t-33.5 118z M229 785q0 -31 29.5 -75t64.5 -80.5t97 -97.5q17 -16 25 -24q101 -98 204 -217q85 97 209 219q127 125 163 171q48 62 48 104q0 78 -53.5 132.5t-120.5 54.5q-85 0 -147 -91l-102 -147l-97 150q-58 88 -141 88q-68 0 -123.5 -55.5t-55.5 -131.5z" />
<glyph unicode="&#xe144;" d="M57 353q0 -95 66 -159l141 -142q66 -66 159 -66t159 66l283 283q66 66 66 159t-66 159l-141 141q-3 4 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q3 -3 9.5 -9t8.5 -8 l106 105l-212 212l389 389l247 -247l-95 -96l17 -17q47 -47 78 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -36 9 -60t31 -38t36 -19.5t47 -13.5q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5zM700 237 q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,80 +0,0 @@
/*
This is a javascript file for omninotesweb
============
Author: Suraj patil
Updated: January 2015
keyCode: n-110
*/
$(document).ready(function(){
/*on() is used instead of click because click can be used only on static elements, and on() is to be used when you add
elements dynamically*/
$('[data-toggle="tooltip"]').tooltip();
//
// $('.items').openOnHover(function(){
// alert();
// });
$('.floating-action-icon-add').click(function(){
$('#addNoteModal').modal('show');
});
$('#editCatFrmBtn').click(function(){
$('#EditForm').toggleClass('hidden')
});
$('#searchFormBtn').click(function(){
$('#SearchForm').toggleClass('hidden')
});
$('#toggleAddFileGrp').click(function(){
$('#file-group').toggleClass('hidden');
$('#toggleAddFileGrp').addClass('hidden') ;
});
$("#noti").click(
function(){
this.fadeOut();
}
);
if ($('#actlMsg').html()==''){
$('.notification').addClass('hidden');
} else {
$('.notification').fadeOut(9000);
}
$('.btnMessage').click(function(){$('.notification').fadeOut()})
/*$( document ).keypress(
function(event){
if ( event.which == 110 ) { //bind the 'n' key to add note
$('#addNoteModal').modal('show');
}
if (event.which==109 ) { //binds the 'm' key to show the navigation drawer
$('.sidebar-toggle').click();
}
}
);*/
$("#addNoteBtn").on("click", function() {
this.preventDefaults();
var task_id = $("#task-id").val();
$.ajax({
url: "/tasks/" + task_id,
type: "POST",
data: {'title':'randome note', 'content':'this and that'}
}).done(function(res, status) {
console.log(status, res);
var response = res
$("#timeline").append(response)
});
});
$('.toggle').click(function(){
$(this).next().toggle();
});
});

View File

@@ -1,732 +0,0 @@
/*
* This file is part of the Sonatra package.
*
* (c) François Pluchino <francois.pluchino@sonatra.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*global jQuery*/
/*global window*/
/*global navigator*/
/*global document*/
/*global CSSMatrix*/
/*global WebKitCSSMatrix*/
/*global MSCSSMatrix*/
/*global Hammer*/
/*global Sidebar*/
/**
* @param {jQuery} $
*
* @typedef {Sidebar} Sidebar
*/
(function ($) {
'use strict';
/**
* Check if is a mobile device.
*
* @returns {boolean}
*
* @private
*/
function mobileCheck() {
return Boolean(navigator.userAgent.match(/Android|iPhone|iPad|iPod|IEMobile|BlackBerry|Opera Mini/i));
}
/**
* Binding actions of keyboard.
*
* @param {jQuery.Event|Event} event
*
* @typedef {Sidebar} Event.data The sidebar instance
*
* @private
*/
function keyboardAction(event) {
if (!event instanceof jQuery.Event || event.data.options.disabledKeyboard) {
return;
}
var self = event.data,
kbe = self.options.keyboardEvent;
if (event.shiftKey === kbe.shiftKey
&& event.ctrlKey === kbe.ctrlKey
&& event.altKey === kbe.altKey
&& event.keyCode === kbe.keyCode) {
self.toggle(event);
}
}
/**
* Checks if the window width is wider than the minimum width defined in
* options.
*
* @param {Sidebar} self The sidebar instance
*
* @returns {boolean}
*
* @private
*/
function isOverMinWidth(self) {
var scrollbarWidth = 'scrollbarWidth',
isOver = false,
$window = $(window),
windowWidth = $window.innerWidth(),
widthNoScroll,
inner,
outer;
if ($('body').height() > $window.innerHeight()) {
if (null === self.scrollbarWidth) {
inner = document.createElement('div');
outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.width = '100px';
document.body.appendChild(outer);
widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';
inner.style.width = '100%';
outer.appendChild(inner);
self[scrollbarWidth] = widthNoScroll - inner.offsetWidth;
outer.parentNode.removeChild(outer);
}
windowWidth += self[scrollbarWidth];
}
if (windowWidth >= self.options.minLockWidth) {
isOver = true;
}
return isOver;
}
/**
* Close the sidebar since external action.
*
* @param {Event} event The event
*
* @typedef {Sidebar} Event.data The sidebar instance
*
* @private
*/
function closeExternal(event) {
var self = event.data,
$target = $(event.currentTarget.activeElement);
if ((self.isLocked() && isOverMinWidth(self)) || $(event.target).parents('.' + self.options.classWrapper).size() > 0 || $target.parents('.' + self.options.classWrapper).size() > 0 || $target.hasClass('sidebar-swipe')) {
return;
}
event.stopPropagation();
event.preventDefault();
if (isOverMinWidth(self)) {
self.close();
} else {
self.forceClose();
}
}
/**
* Close the sidebar or reopen the locked sidebar on window resize event.
*
* @param {Event} event The event
*
* @typedef {Sidebar} Event.data The sidebar instance
*
* @private
*/
function onResizeWindow(event) {
var self = event.data;
if (isOverMinWidth(self) && self.isLocked()) {
self.forceOpen();
return;
}
closeExternal(event);
}
/**
* Get the sidebar wrapper position.
*
* @param {jQuery} $target
*
* @returns {number} The Y axis position
*
* @private
*/
function getWrapperPosition($target) {
var transformCss = $target.css('transform'),
transform = {e: 0, f: 0},
reMatrix,
match;
if (transformCss) {
if ('function' === typeof CSSMatrix) {
transform = new CSSMatrix(transformCss);
} else if ('function' === typeof WebKitCSSMatrix) {
transform = new WebKitCSSMatrix(transformCss);
} else if ('function' === typeof MSCSSMatrix) {
transform = new MSCSSMatrix(transformCss);
} else {
reMatrix = /matrix\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/;
match = transformCss.match(reMatrix);
if (match) {
transform.e = parseInt(match[1], 10);
transform.f = parseInt(match[2], 10);
}
}
}
return transform.e;
}
/**
* Cleans the hammer configuration on the wrapper element.
*
* @param {Sidebar} self The sidebar instance
*
* @private
*/
function cleanHammer(self) {
self.$wrapper.removeData('drap-start-position');
self.$wrapper.css('-webkit-transition', '');
self.$wrapper.css('transition', '');
self.$wrapper.css('-webkit-transform', '');
self.$wrapper.css('transform', '');
self.$wrapper.removeClass(self.options.classOnDragging);
delete self.dragStartPosition;
}
/**
* Action of "on drag" hammer event.
*
* @param {Sidebar} self The sidebar instance
* @param {Event} event The hammer event
*
* @private
*/
function onDrag(self, event) {
var dragStartPosition = 'dragStartPosition',
horizontal;
if (undefined !== self.hammerScroll) {
self.hammerScroll.onDrag(event);
}
if ((Hammer.DIRECTION_LEFT !== event.gesture.direction && Hammer.DIRECTION_RIGHT !== event.gesture.direction)
|| (self.options.locked && isOverMinWidth(self))) {
return;
}
if (undefined === self.dragStartPosition) {
self[dragStartPosition] = getWrapperPosition(self.$wrapper);
}
horizontal = Math.round(self.dragStartPosition + event.gesture.deltaX);
if ((Sidebar.POSITION_LEFT === self.getPosition() && horizontal > 0) || (Sidebar.POSITION_RIGHT === self.getPosition() && horizontal < 0)) {
horizontal = 0;
}
self.$wrapper.addClass(self.options.classOnDragging);
self.$wrapper.css('-webkit-transition', 'none');
self.$wrapper.css('transition', 'none');
self.$wrapper.css('-webkit-transform', 'translate3d(' + horizontal + 'px, 0px, 0px)');
self.$wrapper.css('transform', 'translate3d(' + horizontal + 'px, 0px, 0px)');
}
/**
* Action of "on drag end" hammer event.
*
* @param {Sidebar} self The sidebar instance
* @param {Event} event The hammer event
*
* @private
*/
function onDragEnd(self, event) {
var closeGesture = Sidebar.POSITION_LEFT,
openGesture = Sidebar.POSITION_RIGHT;
if (undefined !== self.hammerScroll) {
self.hammerScroll.onDragEnd(event);
}
cleanHammer(self);
if (Math.abs(event.gesture.deltaX) <= (self.$wrapper.innerWidth() / 4)) {
return;
}
if (Sidebar.POSITION_RIGHT === self.getPosition()) {
closeGesture = Sidebar.POSITION_RIGHT;
openGesture = Sidebar.POSITION_LEFT;
}
if (self.isOpen() && closeGesture === event.gesture.direction) {
self.forceClose();
} else if (openGesture === event.gesture.direction) {
if (self.isOpen() && isOverMinWidth(self) && $.inArray(self.options.forceToggle, [true, 'always']) >= 0) {
self.forceOpen();
} else if (isOverMinWidth(self) && 'always' === self.options.forceToggle) {
self.forceOpen();
} else {
self.open();
}
}
}
/**
* Init the hammer instance.
*
* @param {Sidebar} self The sidebar instance
*
* @private
*/
function initHammer(self) {
if (!Hammer) {
return;
}
var swipe = '$swipe',
hammer = 'hammer',
hammerScroll = 'hammerScroll';
if ($.fn.hammerScroll) {
self[hammerScroll] = $('.sidebar-scroller', self.$wrapper).hammerScroll({
contentWrapperClass: 'sidebar-scroller-content',
eventDelegated: true,
hammerStickyHeader: self.options.sidebarStickyHeader,
scrollbar: self.options.hammerScrollbar,
scrollbarInverse: Sidebar.POSITION_RIGHT === self.options.position
}).data('st.hammerscroll');
}
self[swipe] = $('<div id="sidebar-swipe' + self.guid + '" class="sidebar-swipe"></div>').appendTo(self.$element);
self[swipe].on('mouseover', function (event) {
event.stopPropagation();
event.preventDefault();
});
self[hammer] = new Hammer(self.$element[0], {
tap: false,
transform: false,
release: false,
hold: false,
swipe: false,
drag_block_horizontal: true,
drag_block_vertical: $.fn.hammerScroll,
drag_lock_to_axis: false,
drag_min_distance: 5
})
.on('drag', function (event) {
onDrag(self, event);
})
.on('dragend', function (event) {
onDragEnd(self, event);
});
}
/**
* Destroy the hammer configuration.
*
* @param {Sidebar} self The sidebar instance
*
* @private
*/
function destroyHammer(self) {
if (!Hammer) {
return;
}
self.$swipe.off('mouseover');
self.$element.remove(self.$swipe);
}
// SIDEBAR CLASS DEFINITION
// ========================
/**
* @constructor
*
* @param {string|elements|object|jQuery} element
* @param {object} options
*
* @this Sidebar
*/
var Sidebar = function (element, options) {
this.guid = jQuery.guid;
this.options = $.extend({}, Sidebar.DEFAULTS, options);
this.$element = $(element);
this.$toggle = $('.' + this.options.classToggle, this.$element);
this.$wrapper = $('.' + this.options.classWrapper, this.$element);
this.eventType = mobileCheck() ? 'touchstart' : 'click';
this.scrollbarWidth = null;
this.hammer = undefined;
this.hammerScroll = undefined;
this.$swipe = undefined;
this.$element.attr('data-sidebar', 'true');
var $findToggle;
if (Sidebar.POSITION_RIGHT !== this.options.position) {
this.options.position = Sidebar.POSITION_LEFT;
} else {
this.$element.addClass('sidebar-right');
}
if (this.$element.hasClass('sidebar-right')) {
this.options.position = Sidebar.POSITION_RIGHT;
}
if (this.options.locked) {
this.options.forceToggle = 'always';
this.$element.css('-webkit-transition', 'none');
this.$element.css('transition', 'none');
this.$wrapper.css('-webkit-transition', 'none');
this.$wrapper.css('transition', 'none');
this.$element.addClass(this.options.classLocked);
this.$element.addClass(this.options.classForceOpen);
this.$wrapper.addClass(this.options.classOpen + '-init');
}
if (!mobileCheck() && this.options.openOnHover && null === this.options.toggleId) {
this.$element.on('mouseover.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.open, this));
this.$element.on('mouseout.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.close, this));
}
if (null !== this.options.toggleId) {
$findToggle = $('#' + this.options.toggleId);
if (1 === $findToggle.size()) {
this.$toggle.remove();
this.$toggle = $findToggle;
}
} else {
this.$element.addClass('sidebar-togglable');
}
this.$toggle.on(this.eventType + '.st.sidebar' + this.guid, null, this, Sidebar.prototype.toggle);
$(window).on('keyup.st.sidebar' + this.guid, null, this, keyboardAction);
$(window).on('resize.st.sidebar' + this.guid, null, this, onResizeWindow);
if (this.$wrapper.hasClass(this.options.classOpen + '-init')) {
if (isOverMinWidth(this)) {
this.$wrapper.addClass(this.options.classOpen);
} else {
this.$wrapper.removeClass(this.options.classOpen);
}
this.$wrapper.removeClass(this.options.classOpen + '-init');
}
if (this.$wrapper.hasClass(this.options.classOpen)) {
$(document).on(this.eventType + '.st.sidebar' + this.guid, null, this, closeExternal);
}
if (this.options.sidebarStickyHeader && $.fn.stickyHeader && !$.fn.hammerScroll) {
this.stickyHeader = $('.sidebar-scroller', this.$wrapper).stickyHeader().data('st.stickyheader');
}
initHammer(this);
this.$element.css('-webkit-transition', '');
this.$element.css('transition', '');
this.$wrapper.css('-webkit-transition', '');
this.$wrapper.css('transition', '');
this.$wrapper.addClass('sidebar-ready');
},
old;
/**
* Defaults options.
*
* @type {object}
*/
Sidebar.DEFAULTS = {
classToggle: 'sidebar-toggle',
classWrapper: 'sidebar-wrapper',
classOpen: 'sidebar-open',
classLocked: 'sidebar-locked',
classForceOpen: 'sidebar-force-open',
classOnDragging: 'sidebar-dragging',
openOnHover: false,
forceToggle: false,//false, true, 'always'
locked: false,
position: Sidebar.POSITION_LEFT,//left, right
minLockWidth: 992,
toggleId: null,
sidebarStickyHeader: false,
hammerScrollbar: true,
disabledKeyboard: false,
keyboardEvent: {
ctrlKey: true,
shiftKey: false,
altKey: true,
keyCode: 'S'.charCodeAt(0)
}
};
/**
* Left position.
*
* @type {string}
*/
Sidebar.POSITION_LEFT = 'left';
/**
* Right position.
*
* @type {string}
*/
Sidebar.POSITION_RIGHT = 'right';
/**
* Get sidebar position.
*
* @returns {string} The position (left or right)
*
* @this Sidebar
*/
Sidebar.prototype.getPosition = function () {
return this.options.position;
};
/**
* Checks if sidebar is locked (always open).
*
* @returns {boolean}
*
* @this Sidebar
*/
Sidebar.prototype.isLocked = function () {
return this.options.locked;
};
/**
* Checks if sidebar is locked (always open).
*
* @returns {boolean}
*
* @this Sidebar
*/
Sidebar.prototype.isOpen = function () {
return this.$wrapper.hasClass(this.options.classOpen);
};
/**
* Checks if sidebar is fully opened.
*
* @return {boolean}
*
* @this Sidebar
*/
Sidebar.prototype.isFullyOpened = function () {
return this.$element.hasClass(this.options.classForceOpen);
};
/**
* Force open the sidebar.
*
* @this Sidebar
*/
Sidebar.prototype.forceOpen = function () {
if (this.isOpen() && this.isFullyOpened()) {
return;
}
this.$element.addClass(this.options.classForceOpen);
this.open();
this.$toggle.removeClass(this.options.classToggle + '-opened');
};
/**
* Force close the sidebar.
*
* @this Sidebar
*/
Sidebar.prototype.forceClose = function () {
if (!this.isOpen() || (this.isLocked() && isOverMinWidth(this))) {
return;
}
this.$element.removeClass(this.options.classForceOpen);
this.close();
};
/**
* Open the sidebar.
*
* @this Sidebar
*/
Sidebar.prototype.open = function () {
if (this.isOpen()) {
return;
}
$('[data-sidebar=true]').sidebar('forceClose');
this.$wrapper.addClass(this.options.classOpen);
this.$toggle.addClass(this.options.classToggle + '-opened');
$(document).on(this.eventType + '.st.sidebar' + this.guid, null, this, closeExternal);
};
/**
* Close open the sidebar.
*
* @this Sidebar
*/
Sidebar.prototype.close = function () {
if (!this.isOpen() || (this.isFullyOpened() && isOverMinWidth(this))) {
return;
}
this.$wrapper.removeClass(this.options.classOpen);
this.$toggle.removeClass(this.options.classToggle + '-opened');
$(document).off(this.eventType + '.st.sidebar' + this.guid, closeExternal);
};
/**
* Toggle the sidebar ("close, "open", "force open").
*
* @param {jQuery.Event|Event} [event]
*
* @typedef {Sidebar} Event.data The sidebar instance
*
* @this Sidebar
*/
Sidebar.prototype.toggle = function (event) {
var self = (undefined !== event) ? event.data : this,
$target,
$parents;
if (event) {
$target = $(event.target);
$parents = $target.parents('.' + self.options.classWrapper);
event.stopPropagation();
if ($target.hasClass(self.options.classToggle) || $target.parents('.' + self.options.classToggle).size() > 0) {
event.preventDefault();
}
if ($parents.size() > 0 || $target.hasClass('sidebar-swipe')) {
return;
}
}
if (self.isOpen()) {
if (self.isFullyOpened()) {
self.forceClose();
} else if (isOverMinWidth(self) && $.inArray(self.options.forceToggle, [true, 'always']) >= 0) {
self.forceOpen();
} else {
self.close();
}
} else if (isOverMinWidth(self) && 'always' === self.options.forceToggle) {
self.forceOpen();
} else {
self.open();
}
};
/**
* Destroy instance.
*
* @this Sidebar
*/
Sidebar.prototype.destroy = function () {
if (!mobileCheck()) {
this.$element.off('mouseover.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.open, this));
this.$element.off('mouseout.st.sidebar' + this.guid, $.proxy(Sidebar.prototype.close, this));
}
$(document).off(this.eventType + '.st.sidebar' + this.guid, closeExternal);
$(window).off('resize.st.sidebar' + this.guid, onResizeWindow);
this.$toggle.off(this.eventType + '.st.sidebar' + this.guid, Sidebar.prototype.toggle);
$(window).off('keyup.st.sidebar' + this.guid, keyboardAction);
destroyHammer(this);
if (undefined !== this.stickyHeader) {
this.stickyHeader.destroy();
}
this.$element.removeData('st.sidebar');
};
// SIDEBAR PLUGIN DEFINITION
// =========================
function Plugin(option, value) {
return this.each(function () {
var $this = $(this),
data = $this.data('st.sidebar'),
options = typeof option === 'object' && option;
if (!data && option === 'destroy') {
return;
}
if (!data) {
$this.data('st.sidebar', (data = new Sidebar(this, options)));
}
if (typeof option === 'string') {
data[option](value);
}
});
}
old = $.fn.sidebar;
$.fn.sidebar = Plugin;
$.fn.sidebar.Constructor = Sidebar;
// SIDEBAR NO CONFLICT
// ===================
$.fn.sidebar.noConflict = function () {
$.fn.sidebar = old;
return this;
};
// SIDEBAR DATA-API
// ================
$(window).on('load', function () {
$('[data-sidebar="true"]').each(function () {
var $this = $(this);
Plugin.call($this, $this.data());
});
});
}(jQuery));

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

@@ -0,0 +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

@@ -0,0 +1,58 @@
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_id int REFERENCES plans(plan_id)
);
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $set_updated$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$set_updated$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated
BEFORE UPDATE ON actions
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();
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)
}

19
util/snake.go Normal file
View File

@@ -0,0 +1,19 @@
package util
import "unicode"
// ToSnake converts CamelCase to snake_case.
func ToSnake(in string) string {
runes := []rune(in)
length := len(runes)
var out []rune
for i := 0; i < length; i++ {
if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
out = append(out, '_')
}
out = append(out, unicode.ToLower(runes[i]))
}
return string(out)
}

69
util/snake_test.go Normal file
View File

@@ -0,0 +1,69 @@
package util_test
import (
"strings"
"testing"
"gitea.deepak.science/deepak/gogmagog/util"
)
// from https://github.com/paultyng/snake
type SnakeTest struct {
input string
output string
}
var tests = []SnakeTest{
{"a", "a"},
{"snake", "snake"},
{"A", "a"},
{"ID", "id"},
{"MOTD", "motd"},
{"Snake", "snake"},
{"SnakeTest", "snake_test"},
{"SnakeID", "snake_id"},
{"SnakeIDGoogle", "snake_id_google"},
{"LinuxMOTD", "linux_motd"},
{"OMGWTFBBQ", "omgwtfbbq"},
{"omg_wtf_bbq", "omg_wtf_bbq"},
{"APIResponse", "api_response"},
}
func TestToSnake(t *testing.T) {
for _, test := range tests {
if util.ToSnake(test.input) != test.output {
t.Errorf(`ToSnake("%s"), wanted "%s", got \%s"`, test.input, test.output, util.ToSnake(test.input))
}
}
}
var benchmarks = []string{
"a",
"snake",
"A",
"Snake",
"SnakeTest",
"SnakeID",
"SnakeIDGoogle",
"LinuxMOTD",
"OMGWTFBBQ",
"omg_wtf_bbq",
"APIResponse",
}
func BenchmarkToSnake(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, input := range benchmarks {
util.ToSnake(input)
}
}
}
func BenchmarkToLower(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, input := range benchmarks {
strings.ToLower(input)
}
}
}