Added test suites for all current models

This commit is contained in:
🐙PiperYxzzy
2022-05-01 20:49:03 +02:00
parent 5f85a5800e
commit 6e7b30be0a
12 changed files with 400 additions and 27 deletions

View File

@@ -6,7 +6,6 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/yxzzy-wtf/gin-gonic-prepack/database" "github.com/yxzzy-wtf/gin-gonic-prepack/database"
"github.com/yxzzy-wtf/gin-gonic-prepack/models" "github.com/yxzzy-wtf/gin-gonic-prepack/models"
@@ -103,7 +102,7 @@ func UserVerify() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
verifyJwt, _ := c.GetQuery("verify") verifyJwt, _ := c.GetQuery("verify")
claims, err := parseJwt(verifyJwt, models.UserHmac) claims, err := util.ParseJwt(verifyJwt, models.UserHmac)
if err != nil || claims["role"] != "verify" { if err != nil || claims["role"] != "verify" {
fmt.Println("bad claim or role not 'verify'", err) fmt.Println("bad claim or role not 'verify'", err)
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
@@ -175,7 +174,7 @@ func UserResetForgottenPassword() gin.HandlerFunc {
return return
} }
claims, err := parseJwt(resetVals.Token, models.UserHmac) claims, err := util.ParseJwt(resetVals.Token, models.UserHmac)
if err != nil || claims["role"] != "reset" { if err != nil || claims["role"] != "reset" {
fmt.Println("bad claim or role not 'reset'", err) fmt.Println("bad claim or role not 'reset'", err)
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
@@ -245,7 +244,7 @@ func AdminLogin() gin.HandlerFunc {
} }
} }
func genericAuth(expectedRole string) gin.HandlerFunc { func genericAuth(expectedRole string, hmac []byte) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
tokenStr := c.GetHeader(JwtHeader) tokenStr := c.GetHeader(JwtHeader)
if tokenStr == "" { if tokenStr == "" {
@@ -253,7 +252,7 @@ func genericAuth(expectedRole string) gin.HandlerFunc {
return return
} }
claims, err := parseJwt(tokenStr, models.UserHmac) claims, err := util.ParseJwt(tokenStr, hmac)
if err != nil { if err != nil {
if strings.HasPrefix(err.Error(), "token ") || err.Error() == "signature is invalid" { if strings.HasPrefix(err.Error(), "token ") || err.Error() == "signature is invalid" {
c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: err.Error()}) c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: err.Error()})
@@ -279,27 +278,11 @@ func genericAuth(expectedRole string) gin.HandlerFunc {
} }
func UserAuth() gin.HandlerFunc { func UserAuth() gin.HandlerFunc {
return genericAuth("user") return genericAuth("user", models.UserHmac)
} }
func AdminAuth() gin.HandlerFunc { func AdminAuth() gin.HandlerFunc {
return genericAuth("admin") return genericAuth("admin", models.AdminHmac)
}
func parseJwt(tokenStr string, hmac []byte) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("bad signing method %v", token.Header["alg"])
}
return hmac, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
} else {
return jwt.MapClaims{}, err
}
} }
func Doot() gin.HandlerFunc { func Doot() gin.HandlerFunc {

View File

@@ -0,0 +1,7 @@
package core
import "testing"
func TestUserSignup(t *testing.T) {
}

2
go.mod
View File

@@ -5,6 +5,7 @@ go 1.18
require github.com/gin-gonic/gin v1.7.7 require github.com/gin-gonic/gin v1.7.7
require ( require (
github.com/boombuler/barcode v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
@@ -20,6 +21,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.12 // indirect github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pquerna/otp v1.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
golang.org/x/sys v0.0.0-20220429121018-84afa8d3f7b3 // indirect golang.org/x/sys v0.0.0-20220429121018-84afa8d3f7b3 // indirect

5
go.sum
View File

@@ -1,3 +1,6 @@
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -55,6 +58,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -16,7 +16,7 @@ type Admin struct {
const adminJwtDuration = time.Hour * 2 const adminJwtDuration = time.Hour * 2
var adminHmac = util.GenerateHmac() var AdminHmac = util.GenerateHmac()
func (a *Admin) GetJwt() (string, int) { func (a *Admin) GetJwt() (string, int) {
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
@@ -26,7 +26,7 @@ func (a *Admin) GetJwt() (string, int) {
"role": "admin", "role": "admin",
}) })
jstr, err := j.SignedString(adminHmac) jstr, err := j.SignedString(AdminHmac)
if err != nil { if err != nil {
// we should ALWAYS be able to build and sign a str // we should ALWAYS be able to build and sign a str
panic(err) panic(err)

41
models/admin_test.go Normal file
View File

@@ -0,0 +1,41 @@
package models
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/yxzzy-wtf/gin-gonic-prepack/util"
)
func TestAdminGetJwt(t *testing.T) {
a := Admin{}
a.Uid = uuid.New()
jwtToken, maxAge := a.GetJwt()
if maxAge != int(time.Hour.Seconds()*2) {
t.Errorf("issued token with incorrect max age, expected %vs but was %vs", time.Hour.Seconds()*2, maxAge)
}
testClaims, err := util.ParseJwt(jwtToken, AdminHmac)
if err != nil {
t.Errorf("tried to parse valid token but got error %v", err)
}
if testClaims["sub"] != a.Uid.String() {
t.Errorf("`sub` value of %v does not match expected of %v", testClaims["sub"], a.Uid)
}
if testClaims["role"] != "admin" {
t.Errorf("`role` value of %v does not match expected of `admin`", testClaims["role"])
}
if _, exists := testClaims["iat"]; !exists {
t.Errorf("`iat` does not exist in jwt")
}
if _, exists := testClaims["exp"]; !exists {
t.Errorf("`exp` does not exist in jwt")
}
}

View File

@@ -2,8 +2,10 @@ package models
import ( import (
"errors" "errors"
"strings"
"time" "time"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -16,17 +18,29 @@ type Auth struct {
} }
func (a *Auth) SetPassword(pass string) error { func (a *Auth) SetPassword(pass string) error {
if len(pass) < 12 {
return errors.New("password too short")
}
if strings.Contains(strings.ToLower(pass), "password") {
return errors.New("contains phrase 'password'")
}
passHash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) passHash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
a.PasswordHash = string(passHash) a.PasswordHash = string(passHash)
return nil return nil
} }
func (a *Auth) Login(pass string, tfCode string) (error, bool) { func (a *Auth) Login(pass string, tfCode string) (error, bool) {
return a.login(pass, tfCode, time.Now())
}
func (a *Auth) login(pass string, tfCode string, stamp time.Time) (error, bool) {
if err := a.CheckPassword(pass); err != nil { if err := a.CheckPassword(pass); err != nil {
return err, false return err, false
} }
if err := a.ValidateTwoFactor(tfCode, time.Now()); err != nil { if err := a.ValidateTwoFactor(tfCode, stamp); err != nil {
return err, true return err, true
} }
@@ -51,7 +65,14 @@ func (a *Auth) ValidateTwoFactor(tfCode string, stamp time.Time) error {
//TODO two factor //TODO two factor
if len(tfCode) == 6 { if len(tfCode) == 6 {
// Test 2FA // Test 2FA
return errors.New("2FA invalid") expect, err := totp.GenerateCode(a.TwoFactorSecret, stamp)
if err != nil {
return errors.New("could not process 2fa")
}
if expect == tfCode {
return nil
}
return errors.New("2fa invalid")
} else { } else {
// May be a renewal code // May be a renewal code
return errors.New("unlock invalid") return errors.New("unlock invalid")

137
models/auth_test.go Normal file
View File

@@ -0,0 +1,137 @@
package models
import (
"testing"
"time"
)
func TestBadPasswords(t *testing.T) {
a := Auth{}
if err := a.SetPassword("short"); err.Error() != "password too short" {
t.Errorf("allowed short password")
}
if err := a.SetPassword("tqr9wyfPassword9k8rwcd"); err.Error() != "contains phrase 'password'" {
t.Errorf("allowed password containing the word 'password'")
}
if err := a.SetPassword("qc2q2fn34dqifqu23j7dp0"); err != nil {
t.Errorf("rejected acceptable password")
}
}
func TestSettingPassword(t *testing.T) {
a := Auth{}
if a.PasswordHash != "" {
t.Errorf("passwordhash comes with default value")
}
a.SetPassword("This-q2o37rcfy2ij34tgjwi374f3w")
ph := a.PasswordHash
if ph == "" {
t.Errorf("passwordhash was not set")
}
a.SetPassword("Different-q2o37rcfy2ij34tgjwi374f3w")
if ph == a.PasswordHash {
t.Errorf("password hashes are the same across different passwords")
}
}
func TestPasswordFlow(t *testing.T) {
a := Auth{}
a.SetPassword("Base-w894t7yw9xj8fxh834dr32")
if err := a.CheckPassword("Incorrect-w894t7yw9xj8fxh834dr32"); err == nil {
t.Errorf("did not fail when provided the wrong password")
}
if err := a.CheckPassword("Base-w894t7yw9xj8fxh834dr32"); err != nil {
t.Errorf("failed when provided the right password")
}
a.SetPassword("Secondary-w894t7yw9xj8fxh834dr32")
if err := a.CheckPassword("Base-w894t7yw9xj8fxh834dr32"); err == nil {
t.Errorf("did not fail when provided the original password")
}
if err := a.CheckPassword("Secondary-w894t7yw9xj8fxh834dr32"); err != nil {
t.Errorf("failed when provided the correct updated password")
}
}
func TestTwoFactorWhenNotSet(t *testing.T) {
a := Auth{}
if err := a.ValidateTwoFactor("ZZZZZZ", time.Now()); err == nil {
t.Errorf("no 2fa set up but code provided, should get err")
}
if err := a.ValidateTwoFactor("", time.Now()); err != nil {
t.Errorf("no code give but no 2fa set up, should not have errored")
}
}
func TestTwoFactor(t *testing.T) {
a := Auth{}
a.TwoFactorSecret = "AAAAAAAAAAAAAAAA"
testTime := time.Date(2022, 1, 5, 18, 0, 0, 0, time.UTC)
expected := "566833"
if err := a.ValidateTwoFactor("000000", testTime); err == nil {
t.Errorf("accepted invalid token")
}
if err := a.ValidateTwoFactor(expected, testTime); err != nil {
t.Errorf("rejected expected token at T0 of period")
}
if err := a.ValidateTwoFactor(expected, testTime.Add(29*time.Second)); err != nil {
t.Errorf("rejected expected token at T29 of period")
}
if err := a.ValidateTwoFactor(expected, testTime.Add(35*time.Second)); err == nil {
t.Errorf("accepted valid token at T35 of period (token is from last period)")
}
}
func TestCombinedLogin(t *testing.T) {
a := Auth{}
a.SetPassword("q2ricy2rqi3c4r23rcou")
a.TwoFactorSecret = "AAAAAAAAAAAAAAAA"
testTime := time.Date(2022, 1, 5, 18, 0, 0, 0, time.UTC)
expected := "566833"
err, show := a.login("q2ricy2rqi3c4r23rcou", expected, testTime)
if err == nil || err.Error() != "not yet verified" {
t.Errorf("validated login of unverified user")
}
if !show {
t.Errorf("unverified is an acceptable message to show, did not indicate true")
}
err, show = a.login("q2ricy2rqi3c4r23rcou", "000000", testTime)
if err == nil || err.Error() != "2fa invalid" {
t.Errorf("validated incorrect 2fa code")
}
if !show {
t.Errorf("bad 2fa is an acceptable message to show, did not indicate true")
}
err, show = a.login("bad", "000000", testTime)
if err == nil {
t.Errorf("validated bad password")
}
if show {
t.Errorf("bad passwrd not an acceptable message to show, but indicated true")
}
a.Verified = true
err, _ = a.login("q2ricy2rqi3c4r23rcou", expected, testTime)
if err != nil {
t.Errorf("failed good login")
}
}

54
models/base_test.go Normal file
View File

@@ -0,0 +1,54 @@
package models
import (
"testing"
"time"
"github.com/google/uuid"
)
func TestOnCreateNewUUID(t *testing.T) {
now := time.Now()
b := Base{}
b.BeforeCreate(nil)
if b.Uid == uuid.Nil {
t.Errorf("did not generate uuid BeforeCreate")
}
if b.Created.IsZero() {
t.Errorf("did not set created time")
}
if !b.Created.After(now) {
t.Errorf("created date should be after %v, was %v", now, b.Created)
}
if !b.Updated.IsZero() {
t.Errorf("updated date already set to %v", b.Updated)
}
if !b.Deleted.IsZero() {
t.Errorf("deleted date already set to %v", b.Updated)
}
}
func TestOnSaveUpdateDate(t *testing.T) {
now := time.Now()
b := Base{}
b.BeforeSave(nil)
if !b.Updated.After(now) {
t.Errorf("updated date should be updated to after %v, is %v", now, b.Updated)
}
}
func TestDeleteSetsTime(t *testing.T) {
now := time.Now()
b := Base{}
b.Delete()
if !b.Deleted.After(now) {
t.Errorf("updated date should be updated to after %v, is %v", now, b.Updated)
}
}

11
models/tenanted_test.go Normal file
View File

@@ -0,0 +1,11 @@
package models
import "testing"
func TestCannotCreateUntenanted(t *testing.T) {
tnt := Tenanted{}
if err := tnt.BeforeCreate(nil); err == nil {
t.Errorf("allowed creation of Tenanted model without Tenant value")
}
}

95
models/user_test.go Normal file
View File

@@ -0,0 +1,95 @@
package models
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/yxzzy-wtf/gin-gonic-prepack/util"
)
func TestUserGetJwt(t *testing.T) {
u := User{}
u.Uid = uuid.New()
jwtToken, maxAge := u.GetJwt()
if maxAge != int(time.Hour.Seconds()*24) {
t.Errorf("issued token with incorrect max age, expected %vs but was %vs", time.Hour.Seconds()*24, maxAge)
}
testClaims, err := util.ParseJwt(jwtToken, UserHmac)
if err != nil {
t.Errorf("tried to parse valid token but got error %v", err)
}
if testClaims["sub"] != u.Uid.String() {
t.Errorf("`sub` value of %v does not match expected of %v", testClaims["sub"], u.Uid)
}
if testClaims["role"] != "user" {
t.Errorf("`role` value of %v does not match expected of `user`", testClaims["role"])
}
if _, exists := testClaims["iat"]; !exists {
t.Errorf("`iat` does not exist in jwt")
}
if _, exists := testClaims["exp"]; !exists {
t.Errorf("`exp` does not exist in jwt")
}
}
func TestUserGetVerifyJwt(t *testing.T) {
u := User{}
u.Uid = uuid.New()
jwtToken := u.GetVerificationJwt()
testClaims, err := util.ParseJwt(jwtToken, UserHmac)
if err != nil {
t.Errorf("tried to parse valid token but got error %v", err)
}
if testClaims["sub"] != u.Uid.String() {
t.Errorf("`sub` value of %v does not match expected of %v", testClaims["sub"], u.Uid)
}
if testClaims["role"] != "verify" {
t.Errorf("`role` value of %v does not match expected of `verify`", testClaims["role"])
}
if _, exists := testClaims["iat"]; !exists {
t.Errorf("`iat` does not exist in jwt")
}
if _, exists := testClaims["exp"]; !exists {
t.Errorf("`exp` does not exist in jwt")
}
}
func TestUserGetResetJwt(t *testing.T) {
u := User{}
u.Uid = uuid.New()
jwtToken := u.GetResetPasswordJwt()
testClaims, err := util.ParseJwt(jwtToken, UserHmac)
if err != nil {
t.Errorf("tried to parse valid token but got error %v", err)
}
if testClaims["sub"] != u.Uid.String() {
t.Errorf("`sub` value of %v does not match expected of %v", testClaims["sub"], u.Uid)
}
if testClaims["role"] != "reset" {
t.Errorf("`role` value of %v does not match expected of `reset`", testClaims["role"])
}
if _, exists := testClaims["iat"]; !exists {
t.Errorf("`iat` does not exist in jwt")
}
if _, exists := testClaims["exp"]; !exists {
t.Errorf("`exp` does not exist in jwt")
}
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"github.com/golang-jwt/jwt"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -33,3 +34,19 @@ func SendEmail(title string, body string, recipient string) {
//TODO //TODO
fmt.Println("Send", title, body, "to", recipient) fmt.Println("Send", title, body, "to", recipient)
} }
func ParseJwt(tokenStr string, hmac []byte) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("bad signing method %v", token.Header["alg"])
}
return hmac, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
} else {
return jwt.MapClaims{}, err
}
}