resource/meta_test.go

Summary

Maintainability
F
3 days
Test Coverage
package resource_test

import (
    "database/sql/driver"
    "fmt"
    "net/http"
    "reflect"
    "testing"
    "time"

    "github.com/jinzhu/gorm"
    "github.com/qor/admin"
    "github.com/qor/publish2"
    "github.com/qor/qor"
    "github.com/qor/qor/resource"
    testutils "github.com/qor/qor/test/utils"
    "github.com/qor/qor/utils"
    "github.com/qor/sorting"
)

func format(value interface{}) string {
    return fmt.Sprint(utils.Indirect(reflect.ValueOf(value)).Interface())
}

func checkMeta(record interface{}, meta *resource.Meta, value interface{}, t *testing.T, expectedValues ...string) {
    var (
        context       = &qor.Context{DB: testutils.GetTestDB()}
        metaValue     = &resource.MetaValue{Name: meta.Name, Value: value}
        expectedValue = fmt.Sprint(value)
    )

    for _, v := range expectedValues {
        expectedValue = v
    }

    meta.PreInitialize()
    meta.Initialize()

    if meta.Setter != nil {
        meta.Setter(record, metaValue, context)
        if context.HasError() {
            t.Errorf("No error should happen, but got %v", context.Errors)
        }

        result := meta.Valuer(record, context)
        if resultValuer, ok := result.(driver.Valuer); ok {
            if v, err := resultValuer.Value(); err == nil {
                result = v
            }
        }

        if format(result) != expectedValue {
            t.Errorf("Wrong value, should be %v, but got %v", expectedValue, format(result))
        }
    } else {
        t.Errorf("No setter generated for meta %v", meta.Name)
    }
}

func TestStringMetaValuerAndSetter(t *testing.T) {
    user := &struct {
        Name  string
        Name2 *string
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Name",
        BaseResource: res,
    }

    checkMeta(user, meta, "hello world", t)

    meta2 := &resource.Meta{
        Name:         "Name2",
        BaseResource: res,
    }

    checkMeta(user, meta2, "hello world2", t)
}

func TestIntMetaValuerAndSetter(t *testing.T) {
    user := &struct {
        Age  int
        Age2 uint
        Age3 *int8
        Age4 *uint8
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Age",
        BaseResource: res,
    }

    checkMeta(user, meta, 18, t)

    meta2 := &resource.Meta{
        Name:         "Age2",
        BaseResource: res,
    }

    checkMeta(user, meta2, "28", t)

    meta3 := &resource.Meta{
        Name:         "Age3",
        BaseResource: res,
    }

    checkMeta(user, meta3, 38, t)

    meta4 := &resource.Meta{
        Name:         "Age4",
        BaseResource: res,
    }

    checkMeta(user, meta4, "48", t)
}

func TestFloatMetaValuerAndSetter(t *testing.T) {
    user := &struct {
        Age  float64
        Age2 *float64
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Age",
        BaseResource: res,
    }

    checkMeta(user, meta, 18.5, t)

    meta2 := &resource.Meta{
        Name:         "Age2",
        BaseResource: res,
    }

    checkMeta(user, meta2, "28.5", t)
}

func TestBoolMetaValuerAndSetter(t *testing.T) {
    user := &struct {
        Actived  bool
        Actived2 *bool
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Actived",
        BaseResource: res,
    }

    checkMeta(user, meta, "true", t)

    meta2 := &resource.Meta{
        Name:         "Actived2",
        BaseResource: res,
    }

    checkMeta(user, meta2, "true", t)

    meta3 := &resource.Meta{
        Name:         "Actived",
        BaseResource: res,
    }

    checkMeta(user, meta3, "", t, "false")

    meta4 := &resource.Meta{
        Name:         "Actived2",
        BaseResource: res,
    }

    checkMeta(user, meta4, "f", t, "false")
}

type scanner struct {
    Body string
}

func (s *scanner) Scan(value interface{}) error {
    s.Body = fmt.Sprint(value)
    return nil
}

func (s scanner) Value() (driver.Value, error) {
    return s.Body, nil
}

func TestScannerMetaValuerAndSetter(t *testing.T) {
    user := &struct {
        Scanner scanner
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Scanner",
        BaseResource: res,
    }

    checkMeta(user, meta, "scanner", t)
}

func TestSliceMetaValuerAndSetter(t *testing.T) {
    t.Skip()

    user := &struct {
        Names  []string
        Names2 []*string
        Names3 *[]string
        Names4 []*string
    }{}

    res := resource.New(user)

    meta := &resource.Meta{
        Name:         "Names",
        BaseResource: res,
    }

    checkMeta(user, meta, []string{"name1", "name2"}, t)

    meta2 := &resource.Meta{
        Name:         "Names2",
        BaseResource: res,
    }

    checkMeta(user, meta2, []string{"name1", "name2"}, t)

    meta3 := &resource.Meta{
        Name:         "Names3",
        BaseResource: res,
    }

    checkMeta(user, meta3, []string{"name1", "name2"}, t)

    meta4 := &resource.Meta{
        Name:         "Names4",
        BaseResource: res,
    }

    checkMeta(user, meta4, []string{"name1", "name2"}, t)
}

type Collection struct {
    gorm.Model

    Name string

    Products       []Product `gorm:"many2many:collection_products;association_autoupdate:false"`
    ProductsSorter sorting.SortableCollection

    LunchProducts       []*Product `gorm:"many2many:collection_lunch_products;association_autoupdate:false"`
    LunchProductsSorter sorting.SortableCollection

    TagID uint
    Tag   Tag
}

type Tag struct {
    gorm.Model

    Name string
}

type Manager struct {
    gorm.Model

    Name string

    publish2.Version
}

type CollectionWithVersion struct {
    gorm.Model

    publish2.Version
    publish2.Schedule

    Name string

    Products       []ProductWithVersion `gorm:"many2many:collection_with_version_product_with_versions;association_autoupdate:false"`
    ProductsSorter sorting.SortableCollection

    ManagerID          uint
    ManagerVersionName string
    Manager            Manager
}

type CollectionWithVersionAndUint64PrimaryKey struct {
    ID        uint64 `gorm:"primary_key"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time `sql:"index"`

    publish2.Version
    publish2.Schedule

    Name string

    Products       []ProductWithVersion `gorm:"many2many:collection_with_version_product_with_versions;association_autoupdate:false"`
    ProductsSorter sorting.SortableCollection

    ManagerID          uint
    ManagerVersionName string
    Manager            Manager
}

func (coll *CollectionWithVersion) AssignVersionName(db *gorm.DB) {
    var count int
    name := time.Now().Format("2006-01-02")
    db.Model(&CollectionWithVersion{}).Where("id = ? AND version_name like ?", coll.ID, name+"%").Count(&count)
    coll.VersionName = fmt.Sprintf("%s-v%v", name, count+1)
}

func (coll *CollectionWithVersionAndUint64PrimaryKey) AssignVersionName(db *gorm.DB) {
    var count int
    name := time.Now().Format("2006-01-02")
    db.Model(&CollectionWithVersionAndUint64PrimaryKey{}).Where("id = ? AND version_name like ?", coll.ID, name+"%").Count(&count)
    coll.VersionName = fmt.Sprintf("%s-v%v", name, count+1)
}

type ProductWithVersion struct {
    gorm.Model

    publish2.Schedule
    publish2.Version

    resource.CompositePrimaryKeyField

    Name string
}

type Product struct {
    gorm.Model

    Name string
}

func WithoutVersion(db *gorm.DB) *gorm.DB {
    return db.Set(admin.DisableCompositePrimaryKeyMode, "on").Set(publish2.VersionMode, publish2.VersionMultipleMode).Set(publish2.ScheduleMode, publish2.ModeOff)
}

func updateVersionPriority() func(scope *gorm.Scope) {
    return func(scope *gorm.Scope) {
        if field, ok := scope.FieldByName("VersionPriority"); ok {
            createdAtField, _ := scope.FieldByName("CreatedAt")
            createdAt := createdAtField.Field.Interface().(time.Time)

            versionNameField, _ := scope.FieldByName("VersionName")
            versionName := versionNameField.Field.Interface().(string)

            versionPriority := fmt.Sprintf("%v_%v", createdAt.UTC().Format(time.RFC3339), versionName)
            field.Set(versionPriority)
        }
    }
}

func TestMany2ManyRelation(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductsMeta(t, db, "Products")
    // lunchProduct is to cover the pointer association like []*Product
    lunchProductsMeta := setupProductsMeta(t, db, "LunchProducts")

    p1 := Product{Name: "p1"}
    p2 := Product{Name: "p2"}
    p3 := Product{Name: "p3"}
    p4 := Product{Name: "p4"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2).Error)
    testutils.AssertNoErr(t, db.Save(&p3).Error)
    testutils.AssertNoErr(t, db.Save(&p4).Error)

    record := Collection{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    if len(record.Products) != 0 {
        t.Fatal("smoke test")
    }
    ctx := &qor.Context{DB: db}

    metaValue := &resource.MetaValue{Name: productsMeta.Name, Value: []string{fmt.Sprintf("%d", p1.ID), fmt.Sprintf("%d", p2.ID)}}
    lunchProductMetaValue := &resource.MetaValue{Name: lunchProductsMeta.Name, Value: []string{fmt.Sprintf("%d", p1.ID), fmt.Sprintf("%d", p2.ID)}}

    productsMeta.Setter(&record, metaValue, ctx)
    lunchProductsMeta.Setter(&record, lunchProductMetaValue, ctx)

    testutils.AssertNoErr(t, db.Preload("Products").Preload("LunchProducts").Find(&record).Error)
    if len(record.Products) != 2 {
        t.Error("products not set to collection")
    }
    if len(record.LunchProducts) != 2 {
        t.Error("products not set to collection")
    }
}

func setupProductsMeta(t *testing.T, db *gorm.DB, metaName string) resource.Meta {
    testutils.ResetDBTables(db, &Collection{}, &Product{})

    adm := admin.New(&qor.Config{DB: db.Set(publish2.ScheduleMode, publish2.ModeOff)})
    c := adm.AddResource(&Collection{})

    productsMeta := resource.Meta{
        Name:         metaName,
        FieldName:    metaName,
        BaseResource: c,
        Config: &admin.SelectManyConfig{
            Collection: func(value interface{}, ctx *qor.Context) (results [][]string) {
                if c, ok := value.(*Collection); ok {
                    var products []Product
                    ctx.GetDB().Model(c).Related(&products, "Products")

                    for _, product := range products {
                        results = append(results, []string{fmt.Sprintf("%v", product.ID), product.Name})
                    }
                }
                return
            },
        },
    }

    var scope = &gorm.Scope{Value: c.Value}
    var getField = func(fields []*gorm.StructField, name string) *gorm.StructField {
        for _, field := range fields {
            if field.Name == name || field.DBName == name {
                return field
            }
        }
        return nil
    }

    productsMeta.FieldStruct = getField(scope.GetStructFields(), productsMeta.FieldName)

    if err := productsMeta.Initialize(); err != nil {
        t.Fatal(err)
    }

    return productsMeta
}

func TestValuer(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductsMeta(t, db, "Products")

    p1 := Product{Name: "p1"}
    p2 := Product{Name: "p2"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2).Error)

    record := Collection{Name: "test"}
    record.Products = []Product{p1, p2}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    ctx := &qor.Context{DB: db}
    result := productsMeta.Valuer(&record, ctx)
    if len(result.([]Product)) != 2 {
        t.Error("valuer doesn't return correct value")
    }
}

func setupProductWithVersionMeta(t *testing.T, db *gorm.DB) resource.Meta {
    registerVersionNameCallback(db)
    publish2.RegisterCallbacks(db)
    testutils.ResetDBTables(db, &CollectionWithVersion{}, &ProductWithVersion{})

    adm := admin.New(&qor.Config{DB: db.Set(publish2.ScheduleMode, publish2.ModeOff)})
    c := adm.AddResource(&CollectionWithVersion{})

    productsMeta := resource.Meta{
        Name:         "Products",
        FieldName:    "Products",
        BaseResource: c,
    }

    var scope = &gorm.Scope{Value: c.Value}
    var getField = func(fields []*gorm.StructField, name string) *gorm.StructField {
        for _, field := range fields {
            if field.Name == name || field.DBName == name {
                return field
            }
        }
        return nil
    }

    productsMeta.FieldStruct = getField(scope.GetStructFields(), productsMeta.FieldName)

    if err := productsMeta.Initialize(); err != nil {
        t.Fatal(err)
    }

    return productsMeta
}

func TestValuer_WithVersion(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductWithVersionMeta(t, db)

    p1 := ProductWithVersion{Name: "p1"}
    p2_v1 := ProductWithVersion{Name: "p2"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2_v1).Error)
    p2_v2 := ProductWithVersion{Name: "p2"}
    p2_v2.ID = p2_v1.ID
    testutils.AssertNoErr(t, db.Save(&p2_v2).Error)

    record := CollectionWithVersion{Name: "test"}
    record.Products = []ProductWithVersion{p1, p2_v2}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    ctx := &qor.Context{DB: db}

    coll := CollectionWithVersion{}
    testutils.AssertNoErr(t, db.Find(&coll).Error)
    result := productsMeta.Valuer(&coll, ctx)

    associatedProducts := result.([]ProductWithVersion)
    if len(associatedProducts) != 2 {
        t.Error("valuer doesn't return correct value")
    }

    i := 0
    for _, p := range associatedProducts {
        if p.ID == p1.ID && p.CompositePrimaryKey == fmt.Sprintf("%d%s%s", p1.ID, resource.CompositePrimaryKeySeparator, p1.GetVersionName()) {
            i += 1
        }

        if p.ID == p2_v2.ID && p.GetVersionName() == p2_v2.GetVersionName() && p.CompositePrimaryKey == fmt.Sprintf("%d%s%s", p2_v2.ID, resource.CompositePrimaryKeySeparator, p2_v2.GetVersionName()) {
            i += 1
        }
    }

    if i != 2 {
        t.Error("valuer doesn't return correct version of products")
    }
}

func TestSetCompositePrimaryKey(t *testing.T) {
    db := testutils.GetTestDB()

    p1 := ProductWithVersion{Name: "p1"}
    p1.SetVersionName("v1")
    testutils.AssertNoErr(t, db.Save(&p1).Error)

    record := CollectionWithVersion{Name: "test"}
    record.SetVersionName("v1")
    record.Products = []ProductWithVersion{p1}
    testutils.AssertNoErr(t, db.Save(&record).Error)

    scope := db.NewScope(record)

    f, _ := scope.FieldByName("Products")
    resource.SetCompositePrimaryKey(f)

    if record.Products[0].CompositePrimaryKey != fmt.Sprintf("%d%s%s", p1.ID, resource.CompositePrimaryKeySeparator, p1.GetVersionName()) {
        t.Error("composite primary key not set to the 'has_many' record")
    }
}

func TestFieldIsStructAndHasVersion(t *testing.T) {
    record := CollectionWithVersion{Name: "test"}

    field := utils.Indirect(reflect.ValueOf(record)).FieldByName("Products")

    if hasVersion := resource.FieldIsStructAndHasVersion(field); !hasVersion {
        t.Error("field has version but not detected")
    }

    recordWithNoVersion := Collection{Name: "test"}

    fieldWithNoVersion := utils.Indirect(reflect.ValueOf(recordWithNoVersion)).FieldByName("Products")

    if hasVersion := resource.FieldIsStructAndHasVersion(fieldWithNoVersion); hasVersion {
        t.Error("field has no version but detected")
    }
}

// By default, qor publish2 select records by MAX(version_priority). to make it work with older version user need to define its own valuer
func TestValuer_WithVersionWithNotMaxVersionPriority(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductWithVersionMeta(t, db)
    productsMeta.Valuer = func(value interface{}, ctx *qor.Context) interface{} {
        coll := value.(*CollectionWithVersion)
        if err := ctx.GetDB().Set("publish:version:mode", "multiple").Preload("Products").Find(coll).Error; err == nil {
            prods := []ProductWithVersion{}
            for _, p := range coll.Products {
                p.CompositePrimaryKeyField.CompositePrimaryKey = resource.GenCompositePrimaryKey(p.ID, p.GetVersionName())
                prods = append(prods, p)
            }
            return prods
        }

        return ""
    }

    p1 := ProductWithVersion{Name: "p1"}
    p2_v1 := ProductWithVersion{Name: "p2"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2_v1).Error)
    p2_v2 := ProductWithVersion{Name: "p2"}
    p2_v2.ID = p2_v1.ID
    testutils.AssertNoErr(t, db.Save(&p2_v2).Error)
    // Here v3 has the MAX version_priority but the collection is linked with v2
    p2_v3 := ProductWithVersion{Name: "p3"}
    p2_v3.ID = p2_v1.ID
    testutils.AssertNoErr(t, db.Save(&p2_v3).Error)

    record := CollectionWithVersion{Name: "test"}
    record.Products = []ProductWithVersion{p1, p2_v2}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    ctx := &qor.Context{DB: db}

    coll := CollectionWithVersion{}
    testutils.AssertNoErr(t, db.Find(&coll).Error)
    result := productsMeta.Valuer(&coll, ctx)

    associatedProducts := result.([]ProductWithVersion)
    if len(associatedProducts) != 2 {
        t.Error("valuer doesn't return correct value")
    }

    i := 0
    for _, p := range associatedProducts {
        if p.ID == p1.ID && p.CompositePrimaryKey == fmt.Sprintf("%d%s%s", p1.ID, resource.CompositePrimaryKeySeparator, p1.GetVersionName()) {
            i += 1
        }

        if p.ID == p2_v2.ID && p.GetVersionName() == p2_v2.GetVersionName() && p.CompositePrimaryKey == fmt.Sprintf("%d%s%s", p2_v2.ID, resource.CompositePrimaryKeySeparator, p2_v2.GetVersionName()) {
            i += 1
        }
    }

    if i != 2 {
        t.Error("valuer doesn't return correct version of products")
    }
}

func TestManyToManyRelation_WithVersion(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductWithVersionMeta(t, db)

    p1 := ProductWithVersion{Name: "p1"}
    p2_v1 := ProductWithVersion{Name: "p2"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2_v1).Error)
    p2_v2 := ProductWithVersion{Name: "p2"}
    p2_v2.ID = p2_v1.ID
    testutils.AssertNoErr(t, db.Save(&p2_v2).Error)

    record := CollectionWithVersion{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    ctx := &qor.Context{DB: db}
    metaValue := &resource.MetaValue{Name: productsMeta.Name, Value: []string{
        fmt.Sprintf("%d%s%s", p1.ID, resource.CompositePrimaryKeySeparator, p1.GetVersionName()),
        fmt.Sprintf("%d%s%s", p2_v2.ID, resource.CompositePrimaryKeySeparator, p2_v2.GetVersionName()),
    }}

    productsMeta.Setter(&record, metaValue, ctx)

    testutils.AssertNoErr(t, db.Preload("Products").Find(&record).Error)
    if len(record.Products) != 2 {
        t.Error("products not set to collection")
    }

    hasCorrectVersion := false
    for _, p := range record.Products {
        if p.ID == p2_v2.ID && p.GetVersionName() == p2_v2.VersionName {
            hasCorrectVersion = true
        }
    }

    if !hasCorrectVersion {
        t.Error("p2 is not associated with collection with correct version")
    }

    metaValue1 := &resource.MetaValue{Name: productsMeta.Name, Value: []string{}}
    productsMeta.Setter(&record, metaValue1, ctx)

    testutils.AssertNoErr(t, db.Preload("Products").Find(&record).Error)
    if len(record.Products) != 0 {
        t.Error("products not set to 0")
    }
}

func TestHandleBelongsTo(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &Collection{}, &Tag{})

    ctx := &qor.Context{DB: db}
    var scope = &gorm.Scope{Value: &Collection{}}
    fieldStruct := getField(scope.GetStructFields(), "Tag")
    relationship := fieldStruct.Relationship

    t1 := Tag{Name: "t1"}
    t2 := Tag{Name: "t2"}
    testutils.AssertNoErr(t, db.Save(&t1).Error)
    testutils.AssertNoErr(t, db.Save(&t2).Error)
    c := Collection{Name: "test", TagID: t2.ID}

    record := reflect.ValueOf(&c).Elem()
    field := reflect.ValueOf(&t1).Elem()

    // belongs_to object not changed
    samePrimaryKeys := []string{fmt.Sprintf("%d", t1.ID)}
    resource.HandleBelongsTo(ctx, record, field, relationship, samePrimaryKeys)
    // The function only set value to field, not the record.
    if field.Interface().(Tag).ID != t1.ID {
        t.Error("tag id changed")
    }

    // Set belongs_to to nil
    emptyPrimaryKeys := []string{}
    resource.HandleBelongsTo(ctx, record, field, relationship, emptyPrimaryKeys)
    // The function would set TagID instead of Tag object during emptyPrimaryKeys.
    // So use TagID as the check condition
    if record.Interface().(Collection).TagID != 0 {
        t.Error("tag id not set to 0")
    }

    // Set belongs_to to a new tag
    primaryKeys := []string{fmt.Sprintf("%d", t2.ID)}
    resource.HandleBelongsTo(ctx, record, field, relationship, primaryKeys)

    if field.Interface().(Tag).ID != t2.ID {
        t.Error("new tag id not set")
    }
}

func TestHandleVersioningBelongsTo(t *testing.T) {
    db := testutils.GetTestDB()
    registerVersionNameCallback(db)
    testutils.ResetDBTables(db, &CollectionWithVersion{}, &Manager{})

    ctx := &qor.Context{DB: db}
    var scope = &gorm.Scope{Value: &CollectionWithVersion{}}
    fieldStruct := getField(scope.GetStructFields(), "Manager")
    relationship := fieldStruct.Relationship

    m1 := Manager{Name: "m1"}
    m2 := Manager{Name: "m2"}
    testutils.AssertNoErr(t, db.Save(&m1).Error)
    m1_v2 := m1
    m1_v2.SetVersionName("v2")
    testutils.AssertNoErr(t, db.Save(&m1_v2).Error)
    testutils.AssertNoErr(t, db.Save(&m2).Error)
    c := CollectionWithVersion{Name: "test", ManagerID: m1.ID, ManagerVersionName: m1.GetVersionName()}

    record := reflect.ValueOf(&c).Elem()
    field := reflect.ValueOf(&m1).Elem()

    // Not changed
    samePrimaryKeys := []string{resource.GenCompositePrimaryKey(m1.ID, m1.GetVersionName())}
    resource.HandleVersioningBelongsTo(ctx, record, field, relationship, samePrimaryKeys, true)
    // The function only set value to field, not the record.
    if field.Interface().(Manager).ID != m1.ID {
        t.Error("product id changed")
    }

    // Set belongs_to to nil
    emptyPrimaryKeys := []string{}
    resource.HandleVersioningBelongsTo(ctx, record, field, relationship, emptyPrimaryKeys, true)
    // The function would set TagID instead of Tag object during emptyPrimaryKeys.
    // So use TagID as the check condition
    if r := record.Interface().(CollectionWithVersion); r.ManagerID != 0 || r.ManagerVersionName != "" {
        t.Error("manager not set to zero value")
    }

    // Set belongs_to to a new version of the same manager
    primaryKeysOfDiffVersion := []string{resource.GenCompositePrimaryKey(m1_v2.ID, m1_v2.GetVersionName())}
    resource.HandleVersioningBelongsTo(ctx, record, field, relationship, primaryKeysOfDiffVersion, true)
    if f := field.Interface().(Manager); f.ID != m1_v2.ID || f.GetVersionName() != m1_v2.GetVersionName() {
        t.Error("new version manager not set")
    }

    // Set belongs_to to a new manager
    primaryKeys := []string{resource.GenCompositePrimaryKey(m2.ID, m2.GetVersionName())}
    resource.HandleVersioningBelongsTo(ctx, record, field, relationship, primaryKeys, true)
    if f := field.Interface().(Manager); f.ID != m2.ID || f.GetVersionName() != m2.GetVersionName() {
        t.Error("new manager not set")
    }

    // Set belongs_to to a new manager, but the application doesn't implement the composite primary key
    // so we treat it as a normal belongs_to which means only process an ID as the foreign key
    singlePrimaryKeys := []string{fmt.Sprintf("%d", m1.ID)}
    resource.HandleVersioningBelongsTo(ctx, record, field, relationship, singlePrimaryKeys, true)
    // since we can't predict which version of record the SQL would return with a single ID(no version_name)
    // so just to detect the ID is correct here
    if f := field.Interface().(Manager); f.ID != m1.ID {
        t.Error("new manager not set")
    }
}

func TestBelongsToRelation(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &Collection{}, &Product{}, &Tag{})

    adm := admin.New(&qor.Config{DB: db.Set(publish2.ScheduleMode, publish2.ModeOff)})
    c := adm.AddResource(&Collection{})

    tagMeta := resource.Meta{
        Name:         "Tag",
        FieldName:    "Tag",
        BaseResource: c,
    }

    testutils.AssertNoErr(t, tagMeta.PreInitialize())
    testutils.AssertNoErr(t, tagMeta.Initialize())

    t1 := Tag{Name: "t1"}
    t2 := Tag{Name: "t2"}
    testutils.AssertNoErr(t, db.Save(&t1).Error)
    testutils.AssertNoErr(t, db.Save(&t2).Error)

    record := Collection{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    // resource.SetupSetter(&tagMeta, "Tag", record)
    ctx := &qor.Context{DB: db}
    metaValue := &resource.MetaValue{Name: tagMeta.Name, Value: []string{fmt.Sprintf("%d", t1.ID)}}

    tagMeta.Setter(&record, metaValue, ctx)
    testutils.AssertNoErr(t, db.Save(&record).Error)

    if record.TagID != t1.ID {
        t.Error("tag not set to collection")
    }

    metaValue1 := &resource.MetaValue{Name: tagMeta.Name, Value: []string{fmt.Sprintf("%d", t2.ID)}}
    tagMeta.Setter(&record, metaValue1, ctx)
    testutils.AssertNoErr(t, db.Save(&record).Error)

    if record.TagID != t2.ID {
        t.Error("tag not set to collection")
    }
}

func getField(fields []*gorm.StructField, name string) *gorm.StructField {
    for _, field := range fields {
        if field.Name == name || field.DBName == name {
            return field
        }
    }

    return nil
}

func TestBelongsToWithVersionRelation(t *testing.T) {
    db := testutils.GetTestDB()
    registerVersionNameCallback(db)
    testutils.ResetDBTables(db, &CollectionWithVersion{}, &ProductWithVersion{}, &Manager{})

    adm := admin.New(&qor.Config{DB: db.Set(publish2.ScheduleMode, publish2.ModeOff)})
    c := adm.AddResource(&CollectionWithVersion{})

    managerMeta := resource.Meta{
        Name:         "Manager",
        FieldName:    "Manager",
        BaseResource: c,
    }

    var scope = &gorm.Scope{Value: c.Value}

    managerMeta.FieldStruct = getField(scope.GetStructFields(), managerMeta.FieldName)
    if err := managerMeta.Initialize(); err != nil {
        t.Fatal(err)
    }

    m1 := Manager{Name: "Manager1"}
    testutils.AssertNoErr(t, db.Save(&m1).Error)

    record := CollectionWithVersion{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    ctx := &qor.Context{DB: db}
    metaValue := &resource.MetaValue{Name: managerMeta.Name, Value: []string{resource.GenCompositePrimaryKey(m1.ID, m1.GetVersionName())}}

    managerMeta.Setter(&record, metaValue, ctx)
    // Setter only sets Manager to record, we need save it explicitly in the test.
    testutils.AssertNoErr(t, db.Save(&record).Error)

    if record.ManagerID != m1.ID || record.ManagerVersionName != m1.GetVersionName() {
        t.Error("manager not set to collection")
    }
}

func registerVersionNameCallback(db *gorm.DB) {
    db.Callback().Create().Before("gorm:begin_transaction").Register("publish2:versions", func(scope *gorm.Scope) {
        if field, ok := scope.FieldByName("VersionName"); ok {
            if !field.IsBlank {
                return
            }

            name := time.Now().Format("2006-01-02")

            idField, _ := scope.FieldByName("ID")
            var id uint64
            idUint, ok := idField.Field.Interface().(uint)
            if !ok {
                id64, ok := idField.Field.Interface().(uint64)
                if !ok {
                    panic("ID filed must be uint or uint64")
                }
                id = id64
            } else {
                id = uint64(idUint)
            }

            var count int
            scope.DB().Table(scope.TableName()).Unscoped().Scopes(WithoutVersion).Where("id = ? AND version_name like ?", id, name+"%").Count(&count)

            versionName := fmt.Sprintf("%s-v%v", name, count+1)
            field.Set(versionName)
        }
    })

    db.Callback().Create().After("gorm:begin_transaction").Register("publish2:version_priority", updateVersionPriority())
}

//  Test assigning associations when creating new version. the associations should assign to correct version after save
func TestAssigningAssociationsOnNewVersion(t *testing.T) {
    db := testutils.GetTestDB()
    productsMeta := setupProductWithVersionMeta(t, db)

    p1 := ProductWithVersion{Name: "p1"}
    p2_v1 := ProductWithVersion{Name: "p2"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2_v1).Error)
    p2_v2 := ProductWithVersion{Name: "p2"}
    p2_v2.ID = p2_v1.ID
    testutils.AssertNoErr(t, db.Save(&p2_v2).Error)

    record := CollectionWithVersion{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)

    newVersionCollection := CollectionWithVersion{Name: "test-v2"}
    newVersionCollection.ID = record.ID

    formValues := map[string][]string{"QorResource.VersionName": {}}
    ctx := &qor.Context{DB: db, Request: &http.Request{Form: formValues}}
    metaValue := &resource.MetaValue{Name: productsMeta.Name, Value: []string{
        fmt.Sprintf("%d%s%s", p1.ID, resource.CompositePrimaryKeySeparator, p1.GetVersionName()),
        fmt.Sprintf("%d%s%s", p2_v2.ID, resource.CompositePrimaryKeySeparator, p2_v2.GetVersionName()),
    }}

    productsMeta.Setter(&newVersionCollection, metaValue, ctx)
    // For new version, the object will not be saved inside setter, so we have to call save explicitly
    testutils.AssertNoErr(t, db.Save(&newVersionCollection).Error)

    testutils.AssertNoErr(t, db.Preload("Products").Find(&newVersionCollection).Error)
    if len(newVersionCollection.Products) != 2 {
        t.Error("products not set to collection")
    }

    hasCorrectVersion := false
    for _, p := range newVersionCollection.Products {
        if p.ID == p2_v2.ID && p.GetVersionName() == p2_v2.VersionName {
            hasCorrectVersion = true
        }
    }

    if !hasCorrectVersion {
        t.Error("p2 is not associated with collection with correct version")
    }
}
func TestSwitchRecordToNewVersionIfNeeded(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &CollectionWithVersion{})
    registerVersionNameCallback(db)

    record := CollectionWithVersion{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    oldVersionName := record.VersionName

    formValues := map[string][]string{"QorResource.VersionName": {}}
    ctx := &qor.Context{DB: db, Request: &http.Request{Form: formValues}}

    newRecord := resource.SwitchRecordToNewVersionIfNeeded(ctx, record)

    if newRecord.(CollectionWithVersion).VersionName == oldVersionName {
        t.Error("new version name is not assigned to record")
    }
}

func TestSwitchRecordToNewVersionIfNeeded_Uint64ID(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &CollectionWithVersionAndUint64PrimaryKey{})
    registerVersionNameCallback(db)

    record := CollectionWithVersionAndUint64PrimaryKey{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    oldVersionName := record.VersionName

    formValues := map[string][]string{"QorResource.VersionName": {}}
    ctx := &qor.Context{DB: db, Request: &http.Request{Form: formValues}}

    newRecord := resource.SwitchRecordToNewVersionIfNeeded(ctx, record)
    if newRecord.(CollectionWithVersionAndUint64PrimaryKey).VersionName == oldVersionName {
        t.Error("new version name is not assigned to record")
    }
}
func TestSwitchRecordToNewVersionIfNeeded_EditExistingVersion(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &CollectionWithVersion{})
    registerVersionNameCallback(db)

    record := CollectionWithVersion{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    oldVersionName := record.VersionName

    formValues := map[string][]string{"QorResource.VersionName": []string{oldVersionName}}
    ctx := &qor.Context{DB: db, Request: &http.Request{Form: formValues}}

    newRecord := resource.SwitchRecordToNewVersionIfNeeded(ctx, record)

    if newRecord.(CollectionWithVersion).VersionName != oldVersionName {
        t.Error("new version name is assigned to record when it shouldn't ")
    }
}

type Athlete struct {
    gorm.Model

    publish2.Version
    publish2.Schedule

    Name string
}

func TestSwitchRecordToNewVersionIfNeeded_WithNoAssignVersionMethod(t *testing.T) {
    db := testutils.GetTestDB()
    testutils.ResetDBTables(db, &Athlete{})
    registerVersionNameCallback(db)

    record := Athlete{Name: "test"}
    testutils.AssertNoErr(t, db.Save(&record).Error)
    oldVersionName := record.VersionName

    formValues := map[string][]string{"QorResource.VersionName": {}}
    ctx := &qor.Context{DB: db, Request: &http.Request{Form: formValues}}

    newRecord := resource.SwitchRecordToNewVersionIfNeeded(ctx, record)

    if newRecord.(Athlete).VersionName != oldVersionName {
        t.Error("new version name is assigned to record")
    }
}

func TestCollectPrimaryKeys(t *testing.T) {
    r1 := []resource.CompositePrimaryKeyStruct{{
        ID:          1,
        VersionName: "2020-09-14-v1",
    }, {
        ID:          2,
        VersionName: "2020-09-14-v3",
    }}

    cases := []struct {
        desc       string
        input      []string
        output     []resource.CompositePrimaryKeyStruct
        errContent string
    }{
        {"standard operation", []string{"1^|^2020-09-14-v1", "2^|^2020-09-14-v3"}, r1, ""},
        {"incorrect composite key format", []string{"1^|2020-09-14-v1", "2^|^2020-09-14-v3"}, []resource.CompositePrimaryKeyStruct{}, "metaValue is not for composite primary key"},
        {"incorrect id format", []string{"x^|^2020-09-14-v1", "2^|^2020-09-14-v3"}, []resource.CompositePrimaryKeyStruct{}, "composite primary key has incorrect id x"},
    }
    for _, c := range cases {
        t.Run(c.desc, func(t *testing.T) {
            result, err := resource.CollectPrimaryKeys(c.input)
            errStr := ""
            if err != nil {
                errStr = err.Error()
            }

            if errStr != c.errContent {
                t.Fatal("error expected to be nil but not get nil")
            }

            if got, want := result, c.output; len(want) != len(got) {
                t.Errorf("expect to have %v but got %v", want, got)
            }
        })
    }
}

func TestHandleVersionedManyToMany(t *testing.T) {
    db := testutils.GetTestDB()
    registerVersionNameCallback(db)
    testutils.ResetDBTables(db, &CollectionWithVersion{}, &ProductWithVersion{})
    ctx := &qor.Context{DB: db}

    record := CollectionWithVersion{Name: "test"}
    record.SetVersionName("v1")
    p1 := ProductWithVersion{Name: "p1"}
    p2_v1 := ProductWithVersion{Name: "p2"}
    p3_v1 := ProductWithVersion{Name: "p3"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2_v1).Error)
    testutils.AssertNoErr(t, db.Save(&p3_v1).Error)

    record.Products = []ProductWithVersion{p1}
    testutils.AssertNoErr(t, db.Save(&record).Error)

    field := utils.Indirect(reflect.ValueOf(&record)).FieldByName("Products")

    r1 := []resource.CompositePrimaryKeyStruct{{
        ID:          p2_v1.ID,
        VersionName: p2_v1.GetVersionName(),
    }, {
        ID:          p3_v1.ID,
        VersionName: p3_v1.GetVersionName(),
    }}

    resource.HandleVersionedManyToMany(ctx, field, r1)

    result := field.Interface().([]ProductWithVersion)
    if len(result) != 2 {
        t.Error("products not set to field")
    }

    if !IncludeGivenObj(result, p3_v1.ID) || !IncludeGivenObj(result, p3_v1.GetVersionName()) || !IncludeGivenObj(result, p2_v1.ID) || !IncludeGivenObj(result, p2_v1.GetVersionName()) {
        t.Error("products not set to field correctly")
    }
}

func IncludeGivenObj(result []ProductWithVersion, target interface{}) bool {
    flag := false

    for _, v := range result {
        if id, ok := target.(uint); ok {
            if v.ID == id {
                flag = true
                break
            }
        }

        if versionName, ok := target.(string); ok {
            if v.GetVersionName() == versionName {
                flag = true
                break
            }
        }
    }

    return flag
}

func TestHandleNormalManyToMany(t *testing.T) {
    db := testutils.GetTestDB()
    registerVersionNameCallback(db)
    testutils.ResetDBTables(db, &Collection{}, &Product{})
    ctx := &qor.Context{DB: db}

    record := Collection{Name: "test"}
    p1 := Product{Name: "p1"}
    p2 := Product{Name: "p2"}
    p3 := Product{Name: "p3"}
    testutils.AssertNoErr(t, db.Save(&p1).Error)
    testutils.AssertNoErr(t, db.Save(&p2).Error)
    testutils.AssertNoErr(t, db.Save(&p3).Error)

    record.Products = []Product{p1}
    testutils.AssertNoErr(t, db.Save(&record).Error)

    field := utils.Indirect(reflect.ValueOf(&record)).FieldByName("Products")

    metaValue := &resource.MetaValue{Name: "Products", Value: []string{fmt.Sprintf("%d", p2.ID), fmt.Sprintf("%d", p3.ID)}}

    resource.HandleNormalManyToMany(ctx, field, metaValue, false, nil)

    result := field.Interface().([]Product)
    if len(result) != 2 {
        t.Error("products not set to field")
    }

    if result[0].ID != p2.ID || result[1].ID != p3.ID {
        t.Error("products not set to field correctly")
    }

    zeroMetaValue := &resource.MetaValue{Name: "Products", Value: []string{}}
    resource.HandleNormalManyToMany(ctx, field, zeroMetaValue, false, nil)
    result1 := field.Interface().([]Product)
    if len(result1) != 0 {
        t.Error("product not set to blank")
    }
}