Initial commit
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Allowlisting gitignore template for GO projects prevents us
|
||||
# from adding various unwanted local files, such as generated
|
||||
# files, developer configurations or IDE-specific files etc.
|
||||
#
|
||||
# Recommended: Go.AllowList.gitignore
|
||||
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# But these files...
|
||||
!.gitignore
|
||||
|
||||
!*.go
|
||||
!go.sum
|
||||
!go.mod
|
||||
|
||||
!README.md
|
||||
!LICENSE
|
||||
|
||||
!Makefile
|
||||
|
||||
!*.sh
|
||||
!*.md
|
||||
|
||||
# ...even if they are in subdirectories
|
||||
!*/
|
||||
33
encrypt.go
Normal file
33
encrypt.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"log"
|
||||
// "unsafe"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// EncryptPassword will encrypt the password and replace the old one. Will return the encrypted password.
|
||||
func (user *User) EncryptPassword() string {
|
||||
if user.Password == "" {
|
||||
log.Println("Nothing to encrypt.")
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
hpass, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
user.Password = string(hpass)
|
||||
|
||||
return user.Password
|
||||
}
|
||||
|
||||
// CheckPassword returns true if passwords match.
|
||||
func (user *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
41
encrypt_test.go
Normal file
41
encrypt_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUser_EncryptPassword(t *testing.T) {
|
||||
u := &User{
|
||||
Password: "1234",
|
||||
}
|
||||
|
||||
encrypted := u.EncryptPassword()
|
||||
|
||||
if u.Password != encrypted {
|
||||
t.Fatal("Failed to encrypt password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_CheckPassword(t *testing.T) {
|
||||
u := &User{
|
||||
Password: "1234",
|
||||
}
|
||||
|
||||
u.EncryptPassword()
|
||||
|
||||
if !u.CheckPassword("1234") {
|
||||
t.Fatal("Failed to check password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_CheckPassword_FalsePositive(t *testing.T) {
|
||||
u := &User{
|
||||
Password: "1234",
|
||||
}
|
||||
|
||||
u.EncryptPassword()
|
||||
|
||||
if u.CheckPassword("!234") {
|
||||
t.Fatal("Should have not accepted password")
|
||||
}
|
||||
}
|
||||
299
persist.go
Normal file
299
persist.go
Normal file
@@ -0,0 +1,299 @@
|
||||
// Package persist offers a convinient way to interact with the database. It takes into consideration multitenancy.
|
||||
package persist
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"reflect"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/commons"
|
||||
"git.gsuntres.com/general/mongo"
|
||||
"git.gsuntres.com/general/structful"
|
||||
)
|
||||
|
||||
type PersistProps struct {
|
||||
MongoSysDb string
|
||||
}
|
||||
|
||||
// Persist is the root object for accessing data.
|
||||
type Persist struct {
|
||||
// IPersist
|
||||
|
||||
SysDb string
|
||||
}
|
||||
|
||||
var fields []reflect.StructField
|
||||
|
||||
var sysDb string
|
||||
|
||||
func NewPersist(props *PersistProps) *Persist {
|
||||
p := &Persist{
|
||||
}
|
||||
|
||||
p.SysDb = props.MongoSysDb
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
var persistOriginal *Persist
|
||||
|
||||
var deferedFuncs map[string]any = make(map[string]any, 0)
|
||||
|
||||
var persist any
|
||||
|
||||
var caseString cases.Caser = cases.Title(language.English)
|
||||
|
||||
type InitProps struct {
|
||||
MongoSysDb string
|
||||
}
|
||||
|
||||
// Init runs on boot, reads system_collections from structful and registers them. No changes after boot are going to be applied.
|
||||
func Init(props *InitProps) {
|
||||
var persistProps PersistProps
|
||||
commons.CopyToStruct(props, &persistProps)
|
||||
persist = NewPersist(&persistProps)
|
||||
|
||||
sysDb = props.MongoSysDb
|
||||
|
||||
persistOriginal = NewPersist(&persistProps)
|
||||
|
||||
// Load structful
|
||||
s := structful.Current()
|
||||
|
||||
filter := map[string]any{}
|
||||
scol, err := s.FilterByGroup("system_collections", filter)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get system collections: %v", err)
|
||||
}
|
||||
|
||||
// TODO(me): WIP
|
||||
// Build a new struct similar to Persist
|
||||
|
||||
origType := reflect.TypeOf(persist)
|
||||
if origType.Kind() == reflect.Ptr {
|
||||
origType = origType.Elem()
|
||||
}
|
||||
|
||||
// 1. Copy all existing fields
|
||||
for i := 0; i < origType.NumField(); i++ {
|
||||
log.Printf("Add existing field: %v", origType.Field(i))
|
||||
fields = append(fields, origType.Field(i))
|
||||
}
|
||||
|
||||
client := mongo.GetMongoClient()
|
||||
// Take system_collection from structful and build the calls.
|
||||
for _, col := range scol {
|
||||
BuildInsertOne(col)
|
||||
BuildFindOne(col)
|
||||
BuildFind(col)
|
||||
BuildGetOne(col)
|
||||
BuildDeleteOne(col)
|
||||
|
||||
client.AddDefinition(col)
|
||||
}
|
||||
|
||||
// 3. Create the new struct type
|
||||
newStructType := reflect.StructOf(fields)
|
||||
|
||||
// 4. Create a new instance
|
||||
newStruct := reflect.New(newStructType).Elem()
|
||||
|
||||
for _, field := range fields {
|
||||
// log.Printf("FIELD %s", field.Name)
|
||||
if actualFunc, ok := deferedFuncs[field.Name]; ok {
|
||||
f := newStruct.FieldByName(field.Name)
|
||||
f.Set(reflect.ValueOf(actualFunc))
|
||||
}
|
||||
}
|
||||
|
||||
persist = newStruct.Addr().Interface()
|
||||
}
|
||||
|
||||
func GetCurrent() any {
|
||||
return persist
|
||||
}
|
||||
|
||||
func Orig() *Persist {
|
||||
return persistOriginal
|
||||
}
|
||||
|
||||
func Call(p any, name string, args... any) []reflect.Value {
|
||||
v := reflect.ValueOf(p)
|
||||
|
||||
// 1. Make sure it's the right type
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
// 2. Get field
|
||||
fn := v.FieldByName(name)
|
||||
|
||||
// 3. Validate
|
||||
if !fn.IsValid() {
|
||||
log.Fatalf("Failed to find %s field", name)
|
||||
}
|
||||
|
||||
if fn.Kind() != reflect.Func {
|
||||
log.Fatalf("%s is not a function", name)
|
||||
}
|
||||
|
||||
if fn.IsNil() {
|
||||
log.Fatalf("%s is nil", name)
|
||||
}
|
||||
|
||||
// 4. Prepare arguments
|
||||
vArgs := make([]reflect.Value, len(args))
|
||||
|
||||
for i, arg := range args {
|
||||
vArgs[i] = reflect.ValueOf(arg)
|
||||
}
|
||||
|
||||
// 5. Call and return results
|
||||
return fn.Call(vArgs)
|
||||
}
|
||||
|
||||
const SAVE_USER_DATA = `
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Fullname": {
|
||||
"type": "string"
|
||||
},
|
||||
"Firstname": {
|
||||
"type": "string"
|
||||
},
|
||||
"Username": {
|
||||
"type": "string",
|
||||
"minLength": 3
|
||||
},
|
||||
"Password": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"Email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["Username", "Password"]
|
||||
}
|
||||
`
|
||||
|
||||
// SaveUser will encrypt the password before saving the user.
|
||||
func (p *Persist) SaveUser(ctx context.Context, data *User) (*User, error) {
|
||||
if valid := commons.Validate(SAVE_USER_DATA, data); valid != nil {
|
||||
return nil, errors.New(fmt.Sprintf("%s", valid.Error()))
|
||||
}
|
||||
|
||||
data.EncryptPassword()
|
||||
|
||||
client := mongo.GetMongoClient()
|
||||
|
||||
dataNew, err := client.InsertOneFromStruct(ctx, p.SysDb, USER_COLLECTION, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, err := bson.Marshal(dataNew)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal into struct
|
||||
var u User
|
||||
err = bson.Unmarshal(d, &u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (p *Persist) CheckUser(ctx context.Context, usernameOrEmail string, password string) (*User, error) {
|
||||
if len(usernameOrEmail) < 3 {
|
||||
return nil, errors.New("username or email too short")
|
||||
}
|
||||
|
||||
client := mongo.GetMongoClient()
|
||||
|
||||
filter := bson.M{
|
||||
"$or": bson.A{
|
||||
bson.M{"username": usernameOrEmail},
|
||||
bson.M{"email": usernameOrEmail},
|
||||
},
|
||||
}
|
||||
|
||||
found, err := client.FindOne(ctx, p.SysDb, USER_COLLECTION, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
if err := mongo.ToStruct(found, &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !user.CheckPassword(password) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
const SAVE_ACCOUNT_DATA = `
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Code": {
|
||||
"type": "string",
|
||||
"minLength": 6
|
||||
},
|
||||
"Owner": {
|
||||
"type": "string",
|
||||
"minLength": 6
|
||||
}
|
||||
},
|
||||
"required": ["Code", "Owner"]
|
||||
}
|
||||
`
|
||||
|
||||
func (p *Persist) SaveAccount(ctx context.Context, data *Account) (*Account, error) {
|
||||
if valid := commons.Validate(SAVE_ACCOUNT_DATA, data); valid != nil {
|
||||
return nil, errors.New(fmt.Sprintf("%s", valid.Error()))
|
||||
}
|
||||
|
||||
client := mongo.GetMongoClient()
|
||||
|
||||
dataNew, err := client.InsertOneFromStruct(ctx, p.SysDb, ACCOUNT_COLLECTION, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var o Account
|
||||
mapstructure.Decode(dataNew, &o)
|
||||
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
func (p *Persist) GetAccountByCode(ctx context.Context, code string) (*Account, error) {
|
||||
client := mongo.GetMongoClient()
|
||||
|
||||
filter := bson.M{"code": code}
|
||||
found, err := client.FindOne(ctx, p.SysDb, ACCOUNT_COLLECTION, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var acc Account
|
||||
if err := mongo.ToStruct(found, &acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &acc, nil
|
||||
}
|
||||
54
persist_deleteone.go
Normal file
54
persist_deleteone.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
)
|
||||
|
||||
func BuildDeleteOne(col map[string]any) {
|
||||
name := col["_name"].(string)
|
||||
singular := col["singular"].(string)
|
||||
|
||||
// prepare input arguments and return results
|
||||
in := []reflect.Type{
|
||||
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||
}
|
||||
out := []reflect.Type{
|
||||
reflect.TypeOf((*error)(nil)).Elem(),
|
||||
}
|
||||
|
||||
// create function signature
|
||||
variadic := false
|
||||
funcType := reflect.FuncOf(in, out, variadic)
|
||||
deleteOneName := fmt.Sprintf("%s%s", "Delete", caseString.String(singular))
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: deleteOneName,
|
||||
Type: funcType,
|
||||
})
|
||||
|
||||
isSystem := false
|
||||
if v, ok := col["system"]; ok {
|
||||
isSystem = v.(bool)
|
||||
}
|
||||
|
||||
mc := mongo.GetMongoClient()
|
||||
|
||||
// we defer function's implementation until we create the actual struct
|
||||
deferedFuncs[deleteOneName] = func(ctx context.Context, filter map[string]any) error {
|
||||
db := "__undefined__"
|
||||
if isSystem {
|
||||
db = sysDb
|
||||
} else {
|
||||
account := ctx.Value("account").(string)
|
||||
if account != "" {
|
||||
db = mc.GetName(account)
|
||||
}
|
||||
}
|
||||
|
||||
return mc.DeleteOne(ctx, db, name, filter)
|
||||
}
|
||||
}
|
||||
58
persist_find.go
Normal file
58
persist_find.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
)
|
||||
|
||||
func BuildFind(col map[string]any) {
|
||||
name := col["_name"].(string)
|
||||
plural := col["plural"].(string)
|
||||
|
||||
// prepare input arguments and return results
|
||||
in := []reflect.Type{
|
||||
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||
reflect.TypeOf((*int64)(nil)).Elem(),
|
||||
}
|
||||
out := []reflect.Type{
|
||||
reflect.TypeOf(bson.M{}),
|
||||
reflect.TypeOf((*error)(nil)).Elem(),
|
||||
}
|
||||
|
||||
// create function signature
|
||||
variadic := false
|
||||
funcType := reflect.FuncOf(in, out, variadic)
|
||||
funcName := fmt.Sprintf("%s%s", "Find", caseString.String(plural))
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: funcName,
|
||||
Type: funcType,
|
||||
})
|
||||
|
||||
isSystem := false
|
||||
if v, ok := col["system"]; ok {
|
||||
isSystem = v.(bool)
|
||||
}
|
||||
|
||||
mc := mongo.GetMongoClient()
|
||||
|
||||
// we defer function's implementation until we create the actual struct
|
||||
deferedFuncs[funcName] = func(ctx context.Context, filter map[string]any, limit int64) (bson.M, error) {
|
||||
db := "__undefined__"
|
||||
if isSystem {
|
||||
db = sysDb
|
||||
} else {
|
||||
account := ctx.Value("account").(string)
|
||||
if account != "" {
|
||||
db = mc.GetName(account)
|
||||
}
|
||||
}
|
||||
|
||||
return mc.Find(ctx, db, name, filter, limit)
|
||||
}
|
||||
}
|
||||
57
persist_findone.go
Normal file
57
persist_findone.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
)
|
||||
|
||||
func BuildFindOne(col map[string]any) {
|
||||
name := col["_name"].(string)
|
||||
singular := col["singular"].(string)
|
||||
|
||||
// prepare input arguments and return results
|
||||
in := []reflect.Type{
|
||||
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||
}
|
||||
out := []reflect.Type{
|
||||
reflect.TypeOf(bson.M{}),
|
||||
reflect.TypeOf((*error)(nil)).Elem(),
|
||||
}
|
||||
|
||||
// create function signature
|
||||
variadic := false
|
||||
funcType := reflect.FuncOf(in, out, variadic)
|
||||
funcName := fmt.Sprintf("%s%s", "Find", caseString.String(singular))
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: funcName,
|
||||
Type: funcType,
|
||||
})
|
||||
|
||||
isSystem := false
|
||||
if v, ok := col["system"]; ok {
|
||||
isSystem = v.(bool)
|
||||
}
|
||||
|
||||
mc := mongo.GetMongoClient()
|
||||
|
||||
// we defer function's implementation until we create the actual struct
|
||||
deferedFuncs[funcName] = func(ctx context.Context, filter map[string]any) (bson.M, error) {
|
||||
db := "__undefined__"
|
||||
if isSystem {
|
||||
db = sysDb
|
||||
} else {
|
||||
account := ctx.Value("account").(string)
|
||||
if account != "" {
|
||||
db = mc.GetName(account)
|
||||
}
|
||||
}
|
||||
|
||||
return mc.FindOne(ctx, db, name, filter)
|
||||
}
|
||||
}
|
||||
57
persist_getone.go
Normal file
57
persist_getone.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
)
|
||||
|
||||
func BuildGetOne(col map[string]any) {
|
||||
name := col["_name"].(string)
|
||||
singular := col["singular"].(string)
|
||||
|
||||
// prepare input arguments and return results
|
||||
in := []reflect.Type{
|
||||
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||
}
|
||||
out := []reflect.Type{
|
||||
reflect.TypeOf(bson.M{}),
|
||||
reflect.TypeOf((*error)(nil)).Elem(),
|
||||
}
|
||||
|
||||
// create function signature
|
||||
variadic := false
|
||||
funcType := reflect.FuncOf(in, out, variadic)
|
||||
funcName := fmt.Sprintf("%s%s", "Get", caseString.String(singular))
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: funcName,
|
||||
Type: funcType,
|
||||
})
|
||||
|
||||
isSystem := false
|
||||
if v, ok := col["system"]; ok {
|
||||
isSystem = v.(bool)
|
||||
}
|
||||
|
||||
mc := mongo.GetMongoClient()
|
||||
|
||||
// we defer function's implementation until we create the actual struct
|
||||
deferedFuncs[funcName] = func(ctx context.Context, data map[string]any) (bson.M, error) {
|
||||
db := "__undefined__"
|
||||
if isSystem {
|
||||
db = sysDb
|
||||
} else {
|
||||
account := ctx.Value("account").(string)
|
||||
if account != "" {
|
||||
db = mc.GetName(account)
|
||||
}
|
||||
}
|
||||
|
||||
return mc.GetOne(ctx, db, name, data)
|
||||
}
|
||||
}
|
||||
57
persist_insertone.go
Normal file
57
persist_insertone.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
)
|
||||
|
||||
func BuildInsertOne(col map[string]any) {
|
||||
name := col["_name"].(string)
|
||||
singular := col["singular"].(string)
|
||||
|
||||
// prepare input arguments and return results
|
||||
in := []reflect.Type{
|
||||
reflect.TypeOf((*context.Context)(nil)).Elem(),
|
||||
reflect.TypeOf((*map[string]any)(nil)).Elem(),
|
||||
}
|
||||
out := []reflect.Type{
|
||||
reflect.TypeOf(bson.M{}),
|
||||
reflect.TypeOf((*error)(nil)).Elem(),
|
||||
}
|
||||
|
||||
// create function signature
|
||||
variadic := false
|
||||
funcType := reflect.FuncOf(in, out, variadic)
|
||||
insertOneName := fmt.Sprintf("%s%s", "Insert", caseString.String(singular))
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: insertOneName,
|
||||
Type: funcType,
|
||||
})
|
||||
|
||||
isSystem := false
|
||||
if v, ok := col["system"]; ok {
|
||||
isSystem = v.(bool)
|
||||
}
|
||||
|
||||
mc := mongo.GetMongoClient()
|
||||
|
||||
// we defer function's implementation until we create the actual struct
|
||||
deferedFuncs[insertOneName] = func(ctx context.Context, data map[string]any) (bson.M, error) {
|
||||
db := "__undefined__"
|
||||
if isSystem {
|
||||
db = sysDb
|
||||
} else {
|
||||
account := ctx.Value("account").(string)
|
||||
if account != "" {
|
||||
db = mc.GetName(account)
|
||||
}
|
||||
}
|
||||
|
||||
return mc.InsertOne(ctx, db, name, data)
|
||||
}
|
||||
}
|
||||
194
persist_test.go
Normal file
194
persist_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package persist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
"git.gsuntres.com/general/mongo"
|
||||
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("RUN_INTEGRATION") == "" {
|
||||
fmt.Println("Skipping package tests: RUN_INTEGRATION is missing")
|
||||
os.Exit(0) // Exit with success, but no tests ran
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
user := "admin"
|
||||
pass := "1"
|
||||
|
||||
// 1. Setup: Start the MongoDB container
|
||||
mongoContainer, err := mongodb.Run(ctx, "mongo:8",
|
||||
testcontainers.WithEnv(map[string]string{
|
||||
"MONGO_INITDB_ROOT_USERNAME": user,
|
||||
"MONGO_INITDB_ROOT_PASSWORD": pass,
|
||||
}),)
|
||||
if err != nil {
|
||||
panic("failed to start container")
|
||||
}
|
||||
|
||||
// 2. Get the connection string dynamically
|
||||
endpoint, _ := mongoContainer.ConnectionString(ctx)
|
||||
|
||||
mongo.Start(&mongo.MongoStartProps{
|
||||
MongoUri: endpoint,
|
||||
MongoUser: user,
|
||||
MongoPass: pass,
|
||||
})
|
||||
|
||||
// 3. Run tests
|
||||
code := m.Run()
|
||||
|
||||
// 4. Teardown: Clean up resources
|
||||
mongo.Stop()
|
||||
_ = testcontainers.TerminateContainer(mongoContainer)
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestSaveUser_Valid(t *testing.T) {
|
||||
p := NewPersist(&PersistProps{
|
||||
MongoSysDb: "test_boxtep_sys",
|
||||
})
|
||||
|
||||
data := &User{
|
||||
Fullname: "King Long Too",
|
||||
Firstname: "Too",
|
||||
Username: "toolong",
|
||||
Password: "mypass",
|
||||
Email: "too@long.test",
|
||||
}
|
||||
|
||||
user, err := p.SaveUser(context.TODO(), data)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Should have saved user %#v", err)
|
||||
}
|
||||
|
||||
data.Id = user.Id
|
||||
|
||||
dataMap, _ := mongo.ToMap(data)
|
||||
userMap, _ := mongo.ToMap(user)
|
||||
|
||||
mongo.AssertSubset(t, dataMap, userMap, "Should have been equal")
|
||||
}
|
||||
|
||||
func TestSaveUser_RequireUsername(t *testing.T) {
|
||||
p := NewPersist(&PersistProps{
|
||||
MongoSysDb: "test_boxtep_sys",
|
||||
})
|
||||
|
||||
data := &User{
|
||||
Fullname: "King Long Too",
|
||||
Firstname: "Too",
|
||||
Password: "mypass",
|
||||
Email: "too@long.test",
|
||||
}
|
||||
|
||||
_, err := p.SaveUser(context.TODO(), data)
|
||||
|
||||
if err == nil || err.Error() != `properties/Username: value "" too short for "minLength" argument 3` {
|
||||
t.Fatal("Should require username")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUser_RequirePassword(t *testing.T) {
|
||||
p := NewPersist(&PersistProps{
|
||||
MongoSysDb: "test_boxtep_sys",
|
||||
})
|
||||
|
||||
data := &User{
|
||||
Fullname: "King Long Too",
|
||||
Firstname: "Too",
|
||||
Username: "toolong",
|
||||
Email: "too@long.test",
|
||||
}
|
||||
|
||||
_, err := p.SaveUser(context.TODO(), data)
|
||||
|
||||
if err == nil || err.Error() != `properties/Password: value "" too short for "minLength" argument 1` {
|
||||
t.Fatal("Should require password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUser_UsesSysDb(t *testing.T) {
|
||||
db := "test_boxtep_sys"
|
||||
p := NewPersist(&PersistProps{
|
||||
MongoSysDb: db,
|
||||
})
|
||||
|
||||
data := &User{
|
||||
Fullname: "King Long Too",
|
||||
Firstname: "Too",
|
||||
Username: "toolong",
|
||||
Password: "1",
|
||||
Email: "too@long.test",
|
||||
}
|
||||
|
||||
_, err := p.SaveUser(context.TODO(), data)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Should have saved user %#v", err)
|
||||
}
|
||||
|
||||
// check if user is saved
|
||||
client := mongo.GetMongoClient()
|
||||
|
||||
collection := client.GetCollection(db, USER_COLLECTION)
|
||||
|
||||
var found bson.M
|
||||
filter := bson.M{"username": "toolong"}
|
||||
if err := collection.FindOne(context.Background(), filter).Decode(&found); err != nil {
|
||||
t.Fatalf("Should have found user %#v", err)
|
||||
}
|
||||
|
||||
delete(found, "_id")
|
||||
delete(found, "password")
|
||||
delete(found, "created_at")
|
||||
delete(found, "updated_at")
|
||||
|
||||
d, _ := mongo.ToMap(data)
|
||||
|
||||
mongo.AssertSubset(t, found, d, "Should have been equal")
|
||||
}
|
||||
|
||||
func TestAuthUser_UsingUsername(t *testing.T) {
|
||||
db := "test_boxtep_sys"
|
||||
p := NewPersist(&PersistProps{
|
||||
MongoSysDb: db,
|
||||
})
|
||||
|
||||
data := &User{
|
||||
Fullname: "King Long Too",
|
||||
Firstname: "Too",
|
||||
Username: "toolong_test1",
|
||||
Password: "1",
|
||||
Email: "too@long.test",
|
||||
}
|
||||
|
||||
_, err := p.SaveUser(context.TODO(), data)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Should have saved user %#v", err)
|
||||
}
|
||||
|
||||
user, err := p.CheckUser(context.TODO(), "toolong_test1", "1")
|
||||
if err != nil {
|
||||
t.Fatalf("Should have checked %#v", err)
|
||||
}
|
||||
|
||||
d, _ := mongo.ToMap(data)
|
||||
u, _ := mongo.ToMap(user)
|
||||
|
||||
delete(u, "_id")
|
||||
|
||||
mongo.AssertSubset(t, u, d, "Wrong user")
|
||||
}
|
||||
23
type.go
Normal file
23
type.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package persist
|
||||
|
||||
const USER_COLLECTION = "user"
|
||||
|
||||
// User can use [User.EncryptPassword] to encrypt the password
|
||||
type User struct {
|
||||
Id string `bson:"_id" json:"id"`
|
||||
Fullname string `bson:"fullname" json:"fullname"`
|
||||
Firstname string `bson:"firstname" json:"firstname"`
|
||||
Username string `bson:"username" json:"username"`
|
||||
Password string `bson:"password" json:"password"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
Roles []string `bson:"roles" json:"roles"`
|
||||
Credentials map[string]any `bson:"credentials" json:"credentials"`
|
||||
}
|
||||
|
||||
const ACCOUNT_COLLECTION = "account"
|
||||
// Deprecate: use core.BusinessAccount
|
||||
type Account struct {
|
||||
Id string `bson:"_id" json:"id"`
|
||||
Code string `bson:"code" json:"code"`
|
||||
Owner string `bson:"owner" json:"owner"`
|
||||
}
|
||||
Reference in New Issue
Block a user