//go:build integration // +build integration package indexable import ( "context" "testing" "time" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mongodb" "github.com/testcontainers/testcontainers-go/wait" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/zap" ) func setupTestDB(t *testing.T) (repository.Repository, func()) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() mongoContainer, err := mongodb.Run(ctx, "mongo:latest", mongodb.WithUsername("root"), mongodb.WithPassword("password"), testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")), ) require.NoError(t, err, "failed to start MongoDB container") mongoURI, err := mongoContainer.ConnectionString(ctx) require.NoError(t, err, "failed to get MongoDB connection string") clientOptions := options.Client().ApplyURI(mongoURI) client, err := mongo.Connect(ctx, clientOptions) require.NoError(t, err, "failed to connect to MongoDB") db := client.Database("testdb") repo := repository.CreateMongoRepository(db, "projects") cleanup := func() { disconnect(ctx, t, client) terminate(ctx, t, mongoContainer) } return repo, cleanup } func disconnect(ctx context.Context, t *testing.T, client *mongo.Client) { if err := client.Disconnect(ctx); err != nil { t.Logf("failed to disconnect from MongoDB: %v", err) } } func terminate(ctx context.Context, t *testing.T, container testcontainers.Container) { if err := container.Terminate(ctx); err != nil { t.Logf("failed to terminate MongoDB container: %v", err) } } func TestIndexableDB_Reorder(t *testing.T) { repo, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() organizationRef := primitive.NewObjectID() logger := zap.NewNop() // Create test projects with different indices projects := []*model.Project{ { ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organizationRef, }, }, Describable: model.Describable{Name: "Project A"}, Indexable: model.Indexable{Index: 0}, Mnemonic: "A", State: model.ProjectStateActive, }, }, { ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organizationRef, }, }, Describable: model.Describable{Name: "Project B"}, Indexable: model.Indexable{Index: 1}, Mnemonic: "B", State: model.ProjectStateActive, }, }, { ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organizationRef, }, }, Describable: model.Describable{Name: "Project C"}, Indexable: model.Indexable{Index: 2}, Mnemonic: "C", State: model.ProjectStateActive, }, }, { ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organizationRef, }, }, Describable: model.Describable{Name: "Project D"}, Indexable: model.Indexable{Index: 3}, Mnemonic: "D", State: model.ProjectStateActive, }, }, } // Insert projects into database for _, project := range projects { project.ID = primitive.NewObjectID() err := repo.Insert(ctx, project, nil) require.NoError(t, err) } // Create helper functions for Project type createEmpty := func() *model.Project { return &model.Project{} } getIndexable := func(p *model.Project) *model.Indexable { return &p.Indexable } indexableDB := NewIndexableDB(repo, logger, createEmpty, getIndexable) t.Run("Reorder_NoChange", func(t *testing.T) { // Test reordering to the same position (should be no-op) err := indexableDB.Reorder(ctx, projects[1].ID, 1, repository.Query()) require.NoError(t, err) // Verify indices haven't changed var result model.Project err = repo.Get(ctx, projects[0].ID, &result) require.NoError(t, err) assert.Equal(t, 0, result.Index) err = repo.Get(ctx, projects[1].ID, &result) require.NoError(t, err) assert.Equal(t, 1, result.Index) }) t.Run("Reorder_MoveDown", func(t *testing.T) { // Move Project A (index 0) to index 2 err := indexableDB.Reorder(ctx, projects[0].ID, 2, repository.Query()) require.NoError(t, err) // Verify the reordering: // Project A should now be at index 2 // Project B should be at index 0 // Project C should be at index 1 // Project D should remain at index 3 var result model.Project // Check Project A (moved to index 2) err = repo.Get(ctx, projects[0].ID, &result) require.NoError(t, err) assert.Equal(t, 2, result.Index) // Check Project B (shifted to index 0) err = repo.Get(ctx, projects[1].ID, &result) require.NoError(t, err) assert.Equal(t, 0, result.Index) // Check Project C (shifted to index 1) err = repo.Get(ctx, projects[2].ID, &result) require.NoError(t, err) assert.Equal(t, 1, result.Index) // Check Project D (unchanged) err = repo.Get(ctx, projects[3].ID, &result) require.NoError(t, err) assert.Equal(t, 3, result.Index) }) t.Run("Reorder_MoveUp", func(t *testing.T) { // Reset indices for this test for i, project := range projects { project.Index = i err := repo.Update(ctx, project) require.NoError(t, err) } // Move Project C (index 2) to index 0 err := indexableDB.Reorder(ctx, projects[2].ID, 0, repository.Query()) require.NoError(t, err) // Verify the reordering: // Project C should now be at index 0 // Project A should be at index 1 // Project B should be at index 2 // Project D should remain at index 3 var result model.Project // Check Project C (moved to index 0) err = repo.Get(ctx, projects[2].ID, &result) require.NoError(t, err) assert.Equal(t, 0, result.Index) // Check Project A (shifted to index 1) err = repo.Get(ctx, projects[0].ID, &result) require.NoError(t, err) assert.Equal(t, 1, result.Index) // Check Project B (shifted to index 2) err = repo.Get(ctx, projects[1].ID, &result) require.NoError(t, err) assert.Equal(t, 2, result.Index) // Check Project D (unchanged) err = repo.Get(ctx, projects[3].ID, &result) require.NoError(t, err) assert.Equal(t, 3, result.Index) }) t.Run("Reorder_WithFilter", func(t *testing.T) { // Reset indices for this test for i, project := range projects { project.Index = i err := repo.Update(ctx, project) require.NoError(t, err) } // Test reordering with organization filter orgFilter := repository.OrgFilter(organizationRef) err := indexableDB.Reorder(ctx, projects[0].ID, 2, orgFilter) require.NoError(t, err) // Verify the reordering worked with filter var result model.Project err = repo.Get(ctx, projects[0].ID, &result) require.NoError(t, err) assert.Equal(t, 2, result.Index) }) } func TestIndexableDB_EdgeCases(t *testing.T) { repo, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() organizationRef := primitive.NewObjectID() logger := zap.NewNop() // Create a single project for edge case testing project := &model.Project{ ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organizationRef, }, }, Describable: model.Describable{Name: "Test Project"}, Indexable: model.Indexable{Index: 0}, Mnemonic: "TEST", State: model.ProjectStateActive, }, } project.ID = primitive.NewObjectID() err := repo.Insert(ctx, project, nil) require.NoError(t, err) // Create helper functions for Project type createEmpty := func() *model.Project { return &model.Project{} } getIndexable := func(p *model.Project) *model.Indexable { return &p.Indexable } indexableDB := NewIndexableDB(repo, logger, createEmpty, getIndexable) t.Run("Reorder_SingleItem", func(t *testing.T) { // Test reordering a single item (should work but have no effect) err := indexableDB.Reorder(ctx, project.ID, 0, repository.Query()) require.NoError(t, err) var result model.Project err = repo.Get(ctx, project.ID, &result) require.NoError(t, err) assert.Equal(t, 0, result.Index) }) t.Run("Reorder_InvalidObjectID", func(t *testing.T) { // Test reordering with an invalid object ID invalidID := primitive.NewObjectID() err := indexableDB.Reorder(ctx, invalidID, 1, repository.Query()) require.Error(t, err) // Should fail because object doesn't exist }) }