Files
crawlab/core/models/service/base_service_test.go

461 lines
15 KiB
Go

package service_test
import (
"context"
"testing"
"time"
"github.com/apex/log"
"github.com/crawlab-team/crawlab/core/models/models"
"github.com/crawlab-team/crawlab/core/mongo"
"github.com/crawlab-team/crawlab/core/models/service"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type TestModel struct {
any `collection:"testmodels"`
Id primitive.ObjectID `bson:"_id,omitempty"`
models.BaseModel `bson:",inline"`
Name string `bson:"name"`
}
// TestModelBase represents a base model for testing embedded struct scenarios
type TestModelBase struct {
any `collection:"testbase"`
Id primitive.ObjectID `bson:"_id,omitempty"`
models.BaseModel `bson:",inline"`
BaseName string `bson:"base_name"`
}
// TestModelDTO represents a DTO that embeds TestModelBase (similar to SpiderDTO pattern)
type TestModelDTO struct {
TestModelBase `json:",inline" bson:",inline"`
ExtraField string `json:"extra_field,omitempty" bson:"extra_field,omitempty"`
}
// TestModelNestedDTO represents a deeply nested DTO structure
type TestModelNestedDTO struct {
TestModelDTO `json:",inline" bson:",inline"`
NestedField string `json:"nested_field,omitempty" bson:"nested_field,omitempty"`
}
// TestModelNonStandardPosition tests Priority 3: collection tag not on first field
type TestModelNonStandardPosition struct {
SomeField string `bson:"some_field"`
AnotherField string `bson:"another_field" collection:"non_standard_collection"`
models.BaseModel `bson:",inline"`
}
// TestModelEmbeddedInSecondPosition tests Priority 3: embedded struct with collection tag in non-first position
type TestModelWithCollectionTag struct {
any `collection:"embedded_collection"`
models.BaseModel `bson:",inline"`
EmbeddedName string `bson:"embedded_name"`
}
type TestModelEmbeddedInSecondPosition struct {
FirstField string `bson:"first_field"`
TestModelWithCollectionTag `bson:",inline"` // Embedded struct in second position
ThirdField string `bson:"third_field"`
}
func setupTestDB() {
viper.Set("mongo.db", "testdb")
}
func teardownTestDB() {
db := mongo.GetMongoDb("testdb")
err := db.Drop(context.Background())
if err != nil {
log.Errorf("dropping test db error: %v", err)
return
}
log.Infof("dropped test db")
}
func TestModelService(t *testing.T) {
setupTestDB()
defer teardownTestDB()
t.Run("GetById", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "GetById Test"}
id, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetById(id)
require.Nil(t, err)
assert.Equal(t, testModel.Name, result.Name)
})
t.Run("GetOne", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "GetOne Test"}
_, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetOne(bson.M{"name": "GetOne Test"}, nil)
require.Nil(t, err)
assert.Equal(t, testModel.Name, result.Name)
})
t.Run("GetMany", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModels := []TestModel{
{Name: "GetMany Test 1"},
{Name: "GetMany Test 2"},
}
_, err := svc.InsertMany(testModels)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
results, err := svc.GetMany(bson.M{"name": bson.M{"$regex": "^GetMany Test"}}, nil)
require.Nil(t, err)
assert.Equal(t, 2, len(results))
})
t.Run("InsertOne", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "InsertOne Test"}
id, err := svc.InsertOne(testModel)
require.Nil(t, err)
assert.NotEqual(t, primitive.NilObjectID, id)
})
t.Run("InsertMany", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModels := []TestModel{
{Name: "InsertMany Test 1"},
{Name: "InsertMany Test 2"},
}
ids, err := svc.InsertMany(testModels)
require.Nil(t, err)
assert.Equal(t, 2, len(ids))
})
t.Run("UpdateById", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "UpdateById Test"}
id, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
update := bson.M{"$set": bson.M{"name": "UpdateById Test New Name"}}
err = svc.UpdateById(id, update)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetById(id)
require.Nil(t, err)
assert.Equal(t, "UpdateById Test New Name", result.Name)
})
t.Run("UpdateOne", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "UpdateOne Test"}
_, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
update := bson.M{"$set": bson.M{"name": "UpdateOne Test New Name"}}
err = svc.UpdateOne(bson.M{"name": "UpdateOne Test"}, update)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetOne(bson.M{"name": "UpdateOne Test New Name"}, nil)
require.Nil(t, err)
assert.Equal(t, "UpdateOne Test New Name", result.Name)
})
t.Run("UpdateMany", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModels := []TestModel{
{Name: "UpdateMany Test 1"},
{Name: "UpdateMany Test 2"},
}
_, err := svc.InsertMany(testModels)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
update := bson.M{"$set": bson.M{"name": "UpdateMany Test New Name"}}
err = svc.UpdateMany(bson.M{"name": bson.M{"$regex": "^UpdateMany Test"}}, update)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
results, err := svc.GetMany(bson.M{"name": "UpdateMany Test New Name"}, nil)
require.Nil(t, err)
assert.Equal(t, 2, len(results))
})
t.Run("DeleteById", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "DeleteById Test"}
id, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
err = svc.DeleteById(id)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetById(id)
assert.NotNil(t, err)
assert.Nil(t, result)
})
t.Run("DeleteOne", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModel := TestModel{Name: "DeleteOne Test"}
_, err := svc.InsertOne(testModel)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
err = svc.DeleteOne(bson.M{"name": "DeleteOne Test"})
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
result, err := svc.GetOne(bson.M{"name": "DeleteOne Test"}, nil)
assert.NotNil(t, err)
assert.Nil(t, result)
})
t.Run("DeleteMany", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModels := []TestModel{
{Name: "DeleteMany Test 1"},
{Name: "DeleteMany Test 2"},
}
_, err := svc.InsertMany(testModels)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
err = svc.DeleteMany(bson.M{"name": bson.M{"$regex": "^DeleteMany Test"}})
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
results, err := svc.GetMany(bson.M{"name": bson.M{"$regex": "^DeleteMany Test"}}, nil)
require.Nil(t, err)
assert.Equal(t, 0, len(results))
})
t.Run("Count", func(t *testing.T) {
svc := service.NewModelService[TestModel]()
testModels := []TestModel{
{Name: "Count Test 1"},
{Name: "Count Test 2"},
}
_, err := svc.InsertMany(testModels)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
total, err := svc.Count(bson.M{"name": bson.M{"$regex": "^Count Test"}})
require.Nil(t, err)
assert.Equal(t, 2, total)
})
}
func TestEmbeddedStructHandling(t *testing.T) {
setupTestDB()
defer teardownTestDB()
t.Run("GetCollectionName for embedded struct DTO", func(t *testing.T) {
// Test that GetCollectionName correctly handles embedded struct (DTO pattern)
collectionName := service.GetCollectionName[TestModelDTO]()
assert.Equal(t, "testbase", collectionName, "Should extract collection name from embedded struct")
})
t.Run("GetCollectionName for deeply nested embedded struct", func(t *testing.T) {
// Test deeply nested embedded struct
collectionName := service.GetCollectionName[TestModelNestedDTO]()
assert.Equal(t, "testbase", collectionName, "Should extract collection name from deeply nested embedded struct")
})
t.Run("GetCollectionName for regular struct", func(t *testing.T) {
// Test that regular struct (non-embedded) still works
collectionName := service.GetCollectionName[TestModel]()
assert.Equal(t, "testmodels", collectionName, "Should get collection name from regular struct")
collectionName = service.GetCollectionName[TestModelBase]()
assert.Equal(t, "testbase", collectionName, "Should get collection name from base struct")
})
t.Run("GetCollectionName follows Crawlab pattern", func(t *testing.T) {
// Test that the function correctly identifies the Crawlab pattern:
// First field is `any` with collection tag (no field name)
// This matches the actual pattern used in Spider, Node, Project, etc.
// TestModel follows: any `collection:"testmodels"`
collectionName := service.GetCollectionName[TestModel]()
assert.Equal(t, "testmodels", collectionName, "Should extract collection from first anonymous any field")
// TestModelBase follows: any `collection:"testbase"`
collectionName = service.GetCollectionName[TestModelBase]()
assert.Equal(t, "testbase", collectionName, "Should extract collection from first anonymous any field")
})
t.Run("ModelService with embedded struct DTO", func(t *testing.T) {
// Test that ModelService works correctly with embedded struct DTO
svc := service.NewModelService[TestModelDTO]()
assert.NotNil(t, svc, "Should create service for DTO")
// Verify the service uses the correct collection
col := svc.GetCol()
assert.NotNil(t, col, "Should have collection")
// The collection name should be extracted from the embedded struct
assert.Equal(t, "testbase", col.GetName(), "Should use collection name from embedded struct")
})
t.Run("CRUD operations with embedded struct DTO", func(t *testing.T) {
// Test actual CRUD operations with DTO
svc := service.NewModelService[TestModelDTO]()
testDTO := TestModelDTO{
TestModelBase: TestModelBase{BaseName: "Base Test"},
ExtraField: "Extra Test",
}
// Insert
id, err := svc.InsertOne(testDTO)
require.Nil(t, err)
assert.NotEqual(t, primitive.NilObjectID, id)
time.Sleep(100 * time.Millisecond)
// Get by ID
result, err := svc.GetById(id)
require.Nil(t, err)
assert.Equal(t, "Base Test", result.BaseName)
assert.Equal(t, "Extra Test", result.ExtraField)
// Update
update := bson.M{"$set": bson.M{"extra_field": "Updated Extra"}}
err = svc.UpdateById(id, update)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
// Verify update
updated, err := svc.GetById(id)
require.Nil(t, err)
assert.Equal(t, "Updated Extra", updated.ExtraField)
// Delete
err = svc.DeleteById(id)
require.Nil(t, err)
time.Sleep(100 * time.Millisecond)
// Verify deletion
deleted, err := svc.GetById(id)
assert.NotNil(t, err)
assert.Nil(t, deleted)
})
t.Run("GetCollection function with embedded struct", func(t *testing.T) {
// Test GetCollection function directly
col := service.GetCollection[TestModelDTO]()
assert.NotNil(t, col, "Should get collection for DTO")
assert.Equal(t, "testbase", col.GetName(), "Should use collection name from embedded struct")
// Test with regular struct
col = service.GetCollection[TestModel]()
assert.NotNil(t, col, "Should get collection for regular struct")
assert.Equal(t, "testmodels", col.GetName(), "Should use collection name from struct")
})
t.Run("Multiple DTO instances share same collection", func(t *testing.T) {
// Test that both base model and DTO use the same collection
baseSvc := service.NewModelService[TestModelBase]()
dtoSvc := service.NewModelService[TestModelDTO]()
baseCol := baseSvc.GetCol()
dtoCol := dtoSvc.GetCol()
assert.Equal(t, baseCol.GetName(), dtoCol.GetName(), "Base and DTO should use same collection")
assert.Equal(t, "testbase", baseCol.GetName(), "Should use correct collection name")
})
t.Run("Priority 3: Collection tag on non-first field", func(t *testing.T) {
// Test Priority 3: fallback to check remaining fields for collection tag
// TestModelNonStandardPosition has collection tag on second field
collectionName := service.GetCollectionName[TestModelNonStandardPosition]()
assert.Equal(t, "non_standard_collection", collectionName, "Should find collection tag on non-first field")
})
t.Run("Priority 3: Embedded struct in non-first position", func(t *testing.T) {
// Test Priority 3: embedded struct with collection tag in non-first position
// TestModelEmbeddedInSecondPosition has embedded struct as second field
collectionName := service.GetCollectionName[TestModelEmbeddedInSecondPosition]()
assert.Equal(t, "embedded_collection", collectionName, "Should find collection tag in embedded struct at non-first position")
})
t.Run("Priority 3: CRUD operations with non-standard position", func(t *testing.T) {
// Test that ModelService works with non-standard collection tag position
svc := service.NewModelService[TestModelNonStandardPosition]()
testModel := TestModelNonStandardPosition{
SomeField: "Test Field",
AnotherField: "Another Field",
}
// Insert
id, err := svc.InsertOne(testModel)
require.Nil(t, err)
assert.NotEqual(t, primitive.NilObjectID, id)
time.Sleep(100 * time.Millisecond)
// Get by ID
result, err := svc.GetById(id)
require.Nil(t, err)
assert.Equal(t, "Test Field", result.SomeField)
assert.Equal(t, "Another Field", result.AnotherField)
// Verify the service uses the correct collection
col := svc.GetCol()
assert.Equal(t, "non_standard_collection", col.GetName(), "Should use collection from non-first field")
// Delete
err = svc.DeleteById(id)
require.Nil(t, err)
})
t.Run("Priority order verification", func(t *testing.T) {
// This test verifies that priorities work as expected:
// Priority 1: First field collection tag
// Priority 2: First field embedded struct
// Priority 3: Other fields
// TestModel has collection tag on first field (Priority 1)
collectionName := service.GetCollectionName[TestModel]()
assert.Equal(t, "testmodels", collectionName, "Priority 1: First field collection tag")
// TestModelDTO has embedded struct as first field (Priority 2)
collectionName = service.GetCollectionName[TestModelDTO]()
assert.Equal(t, "testbase", collectionName, "Priority 2: First field embedded struct")
// TestModelNonStandardPosition has collection tag on second field (Priority 3)
collectionName = service.GetCollectionName[TestModelNonStandardPosition]()
assert.Equal(t, "non_standard_collection", collectionName, "Priority 3: Non-first field collection tag")
// TestModelEmbeddedInSecondPosition has embedded struct in second position (Priority 3)
collectionName = service.GetCollectionName[TestModelEmbeddedInSecondPosition]()
assert.Equal(t, "embedded_collection", collectionName, "Priority 3: Embedded struct in non-first position")
})
}