//go:build integration // +build integration package organizationdb import ( "context" "errors" "testing" "github.com/tech/sendico/pkg/db/internal/mongo/commentdb" "github.com/tech/sendico/pkg/db/internal/mongo/projectdb" "github.com/tech/sendico/pkg/db/internal/mongo/reactiondb" "github.com/tech/sendico/pkg/db/internal/mongo/statusdb" "github.com/tech/sendico/pkg/db/internal/mongo/taskdb" "github.com/tech/sendico/pkg/db/repository/builder" "github.com/tech/sendico/pkg/db/template" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/modules/mongodb" "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 setupSetArchivedTestDB(t *testing.T) (*OrganizationDB, *projectDBAdapter, *taskdb.TaskDB, *commentdb.CommentDB, *reactiondb.ReactionDB, func()) { ctx := context.Background() // Start MongoDB container mongodbContainer, err := mongodb.Run(ctx, "mongo:latest") require.NoError(t, err) // Get connection string endpoint, err := mongodbContainer.Endpoint(ctx, "") require.NoError(t, err) // Connect to MongoDB client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://"+endpoint)) require.NoError(t, err) db := client.Database("test_organization_setarchived") logger := zap.NewNop() // Create mock enforcer and policy DB mockEnforcer := &mockSetArchivedEnforcer{} mockPolicyDB := &mockSetArchivedPolicyDB{} mockPGroupDB := &mockSetArchivedPGroupDB{} // Create databases // We need to create a projectDB first, but we'll create a temporary one for organizationDB creation // Create temporary taskDB and statusDB for the temporary projectDB // Create temporary reactionDB and commentDB for the temporary taskDB tempReactionDB, err := reactiondb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db) require.NoError(t, err) tempCommentDB, err := commentdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db, tempReactionDB) require.NoError(t, err) tempTaskDB, err := taskdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db, tempCommentDB, tempReactionDB) require.NoError(t, err) tempStatusDB, err := statusdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db) require.NoError(t, err) tempProjectDB, err := projectdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, tempTaskDB, tempStatusDB, db) require.NoError(t, err) // Create adapter for organizationDB creation tempProjectDBAdapter := &projectDBAdapter{ ProjectDB: tempProjectDB, taskDB: tempTaskDB, commentDB: tempCommentDB, reactionDB: tempReactionDB, statusDB: tempStatusDB, } organizationDB, err := Create(ctx, logger, mockEnforcer, mockPolicyDB, tempProjectDBAdapter, mockPGroupDB, db) require.NoError(t, err) var projectDB *projectdb.ProjectDB var taskDB *taskdb.TaskDB var commentDB *commentdb.CommentDB var reactionDB *reactiondb.ReactionDB var statusDB *statusdb.StatusDB // Create databases in dependency order reactionDB, err = reactiondb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db) require.NoError(t, err) commentDB, err = commentdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db, reactionDB) require.NoError(t, err) taskDB, err = taskdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db, commentDB, reactionDB) require.NoError(t, err) statusDB, err = statusdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, db) require.NoError(t, err) projectDB, err = projectdb.Create(ctx, logger, mockEnforcer, mockPolicyDB, taskDB, statusDB, db) require.NoError(t, err) // Create adapter for the actual projectDB projectDBAdapter := &projectDBAdapter{ ProjectDB: projectDB, taskDB: taskDB, commentDB: commentDB, reactionDB: reactionDB, statusDB: statusDB, } cleanup := func() { client.Disconnect(context.Background()) mongodbContainer.Terminate(ctx) } return organizationDB, projectDBAdapter, taskDB, commentDB, reactionDB, cleanup } // projectDBAdapter adapts projectdb.ProjectDB to project.DB interface for testing type projectDBAdapter struct { *projectdb.ProjectDB taskDB *taskdb.TaskDB commentDB *commentdb.CommentDB reactionDB *reactiondb.ReactionDB statusDB *statusdb.StatusDB } // DeleteCascade implements the project.DB interface func (a *projectDBAdapter) DeleteCascade(ctx context.Context, projectRef primitive.ObjectID) error { // Call the concrete implementation return a.ProjectDB.DeleteCascade(ctx, projectRef) } // SetArchived implements the project.DB interface func (a *projectDBAdapter) SetArchived(ctx context.Context, accountRef, organizationRef, projectRef primitive.ObjectID, archived, cascade bool) error { // Use the stored dependencies for the concrete implementation return a.ProjectDB.SetArchived(ctx, accountRef, organizationRef, projectRef, archived, cascade) } // List implements the project.DB interface func (a *projectDBAdapter) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Project, error) { return a.ProjectDB.List(ctx, accountRef, organizationRef, primitive.NilObjectID, cursor) } // Previews implements the project.DB interface func (a *projectDBAdapter) Previews(ctx context.Context, accountRef, organizationRef primitive.ObjectID, projectRefs []primitive.ObjectID, cursor *model.ViewCursor, assigneeRefs, reporterRefs []primitive.ObjectID) ([]model.ProjectPreview, error) { return a.ProjectDB.Previews(ctx, accountRef, organizationRef, projectRefs, cursor, assigneeRefs, reporterRefs) } // DeleteProject implements the project.DB interface func (a *projectDBAdapter) DeleteProject(ctx context.Context, accountRef, organizationRef, projectRef primitive.ObjectID, migrateToRef *primitive.ObjectID) error { // Call the concrete implementation with the organizationRef return a.ProjectDB.DeleteProject(ctx, accountRef, organizationRef, projectRef, migrateToRef) } // RemoveTagFromProjects implements the project.DB interface func (a *projectDBAdapter) RemoveTagFromProjects(ctx context.Context, accountRef, organizationRef, tagRef primitive.ObjectID) error { // Call the concrete implementation return a.ProjectDB.RemoveTagFromProjects(ctx, accountRef, organizationRef, tagRef) } // Mock implementations for SetArchived testing type mockSetArchivedEnforcer struct{} func (m *mockSetArchivedEnforcer) Enforce(ctx context.Context, permissionRef, accountRef, orgRef, objectRef primitive.ObjectID, action model.Action) (bool, error) { return true, nil } func (m *mockSetArchivedEnforcer) EnforceBatch(ctx context.Context, objectRefs []model.PermissionBoundStorable, accountRef primitive.ObjectID, action model.Action) (map[primitive.ObjectID]bool, error) { // Allow all objects for testing result := make(map[primitive.ObjectID]bool) for _, obj := range objectRefs { result[*obj.GetID()] = true } return result, nil } func (m *mockSetArchivedEnforcer) GetRoles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, error) { return nil, nil } func (m *mockSetArchivedEnforcer) GetPermissions(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, []model.Permission, error) { return nil, nil, nil } type mockSetArchivedPolicyDB struct{} func (m *mockSetArchivedPolicyDB) Create(ctx context.Context, policy *model.PolicyDescription) error { return nil } func (m *mockSetArchivedPolicyDB) Get(ctx context.Context, policyRef primitive.ObjectID, result *model.PolicyDescription) error { return merrors.ErrNoData } func (m *mockSetArchivedPolicyDB) InsertMany(ctx context.Context, objects []*model.PolicyDescription) error { return nil } func (m *mockSetArchivedPolicyDB) Update(ctx context.Context, policy *model.PolicyDescription) error { return nil } func (m *mockSetArchivedPolicyDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error { return nil } func (m *mockSetArchivedPolicyDB) Delete(ctx context.Context, policyRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedPolicyDB) DeleteMany(ctx context.Context, filter builder.Query) error { return nil } func (m *mockSetArchivedPolicyDB) FindOne(ctx context.Context, filter builder.Query, result *model.PolicyDescription) error { return merrors.ErrNoData } func (m *mockSetArchivedPolicyDB) ListIDs(ctx context.Context, query builder.Query) ([]primitive.ObjectID, error) { return nil, nil } func (m *mockSetArchivedPolicyDB) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) { return nil, nil } func (m *mockSetArchivedPolicyDB) Collection() string { return "" } func (m *mockSetArchivedPolicyDB) All(ctx context.Context, organizationRef primitive.ObjectID) ([]model.PolicyDescription, error) { return nil, nil } func (m *mockSetArchivedPolicyDB) Policies(ctx context.Context, refs []primitive.ObjectID) ([]model.PolicyDescription, error) { return nil, nil } func (m *mockSetArchivedPolicyDB) GetBuiltInPolicy(ctx context.Context, resourceType mservice.Type, policy *model.PolicyDescription) error { return nil } func (m *mockSetArchivedPolicyDB) DeleteCascade(ctx context.Context, policyRef primitive.ObjectID) error { return nil } type mockSetArchivedPGroupDB struct{} func (m *mockSetArchivedPGroupDB) Create(ctx context.Context, accountRef, organizationRef primitive.ObjectID, pgroup *model.PriorityGroup) error { return nil } func (m *mockSetArchivedPGroupDB) InsertMany(ctx context.Context, accountRef, organizationRef primitive.ObjectID, objects []*model.PriorityGroup) error { return nil } func (m *mockSetArchivedPGroupDB) Get(ctx context.Context, accountRef, pgroupRef primitive.ObjectID, result *model.PriorityGroup) error { return merrors.ErrNoData } func (m *mockSetArchivedPGroupDB) Update(ctx context.Context, accountRef primitive.ObjectID, pgroup *model.PriorityGroup) error { return nil } func (m *mockSetArchivedPGroupDB) Delete(ctx context.Context, accountRef, pgroupRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedPGroupDB) DeleteCascadeAuth(ctx context.Context, accountRef, pgroupRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedPGroupDB) Patch(ctx context.Context, accountRef, pgroupRef primitive.ObjectID, patch builder.Patch) error { return nil } func (m *mockSetArchivedPGroupDB) PatchMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, patch builder.Patch) (int, error) { return 0, nil } func (m *mockSetArchivedPGroupDB) Unprotected() template.DB[*model.PriorityGroup] { return nil } func (m *mockSetArchivedPGroupDB) ListIDs(ctx context.Context, action model.Action, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) { return nil, nil } func (m *mockSetArchivedPGroupDB) All(ctx context.Context, organizationRef primitive.ObjectID, limit, offset *int64) ([]model.PriorityGroup, error) { return nil, nil } func (m *mockSetArchivedPGroupDB) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.PriorityGroup, error) { return nil, nil } func (m *mockSetArchivedPGroupDB) DeleteCascade(ctx context.Context, statusRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedPGroupDB) SetArchived(ctx context.Context, accountRef, organizationRef, statusRef primitive.ObjectID, archived, cascade bool) error { return nil } func (m *mockSetArchivedPGroupDB) Reorder(ctx context.Context, accountRef, priorityGroupRef primitive.ObjectID, oldIndex, newIndex int) error { return nil } // Mock project DB for statusdb creation type mockSetArchivedProjectDB struct{} func (m *mockSetArchivedProjectDB) Create(ctx context.Context, accountRef, organizationRef primitive.ObjectID, project *model.Project) error { return nil } func (m *mockSetArchivedProjectDB) Get(ctx context.Context, accountRef, projectRef primitive.ObjectID, result *model.Project) error { return merrors.ErrNoData } func (m *mockSetArchivedProjectDB) Update(ctx context.Context, accountRef primitive.ObjectID, project *model.Project) error { return nil } func (m *mockSetArchivedProjectDB) Delete(ctx context.Context, accountRef, projectRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) DeleteCascadeAuth(ctx context.Context, accountRef, projectRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error { return nil } func (m *mockSetArchivedProjectDB) PatchMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, patch builder.Patch) (int, error) { return 0, nil } func (m *mockSetArchivedProjectDB) Unprotected() template.DB[*model.Project] { return nil } func (m *mockSetArchivedProjectDB) ListIDs(ctx context.Context, action model.Action, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) { return nil, nil } func (m *mockSetArchivedProjectDB) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Project, error) { return nil, nil } func (m *mockSetArchivedProjectDB) Previews(ctx context.Context, accountRef, organizationRef primitive.ObjectID, projectRefs []primitive.ObjectID, cursor *model.ViewCursor, assigneeRefs, reporterRefs []primitive.ObjectID) ([]model.ProjectPreview, error) { return nil, nil } func (m *mockSetArchivedProjectDB) DeleteProject(ctx context.Context, accountRef, organizationRef, projectRef primitive.ObjectID, migrateToRef *primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) DeleteCascade(ctx context.Context, projectRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) SetArchived(ctx context.Context, accountRef, organizationRef, projectRef primitive.ObjectID, archived, cascade bool) error { return nil } func (m *mockSetArchivedProjectDB) All(ctx context.Context, organizationRef primitive.ObjectID, limit, offset *int64) ([]model.Project, error) { return nil, nil } func (m *mockSetArchivedProjectDB) Reorder(ctx context.Context, accountRef, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error { return nil } func (m *mockSetArchivedProjectDB) AddTag(ctx context.Context, accountRef, objectRef, tagRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) RemoveTag(ctx context.Context, accountRef, objectRef, tagRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) RemoveTags(ctx context.Context, accountRef, organizationRef, tagRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) AddTags(ctx context.Context, accountRef, objectRef primitive.ObjectID, tagRefs []primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) SetTags(ctx context.Context, accountRef, objectRef primitive.ObjectID, tagRefs []primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) RemoveAllTags(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { return nil } func (m *mockSetArchivedProjectDB) GetTags(ctx context.Context, accountRef, objectRef primitive.ObjectID) ([]primitive.ObjectID, error) { return nil, nil } func (m *mockSetArchivedProjectDB) HasTag(ctx context.Context, accountRef, objectRef, tagRef primitive.ObjectID) (bool, error) { return false, nil } func (m *mockSetArchivedProjectDB) FindByTag(ctx context.Context, accountRef, tagRef primitive.ObjectID) ([]*model.Project, error) { return nil, nil } func (m *mockSetArchivedProjectDB) FindByTags(ctx context.Context, accountRef primitive.ObjectID, tagRefs []primitive.ObjectID) ([]*model.Project, error) { return nil, nil } func TestOrganizationDB_SetArchived(t *testing.T) { organizationDB, projectDBAdapter, taskDB, commentDB, reactionDB, cleanup := setupSetArchivedTestDB(t) defer cleanup() ctx := context.Background() accountRef := primitive.NewObjectID() t.Run("SetArchived_OrganizationWithProjectsTasksCommentsAndReactions_Cascade", func(t *testing.T) { // Create an organization using unprotected DB organization := &model.Organization{ OrganizationBase: model.OrganizationBase{ Describable: model.Describable{Name: "Test Organization for Archive"}, TimeZone: "UTC", }, } organization.ID = primitive.NewObjectID() err := organizationDB.Create(ctx, accountRef, organization.ID, organization) require.NoError(t, err) // Create a project for the organization using unprotected DB project := &model.Project{ ProjectBase: model.ProjectBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organization.ID, }, }, Describable: model.Describable{Name: "Test Project"}, Indexable: model.Indexable{Index: 0}, Mnemonic: "TEST", State: model.ProjectStateActive, }, } project.ID = primitive.NewObjectID() err = projectDBAdapter.Unprotected().Create(ctx, project) require.NoError(t, err) // Create a task for the project using unprotected DB task := &model.Task{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organization.ID, }, }, Describable: model.Describable{Name: "Test Task for Archive"}, ProjectRef: project.ID, } task.ID = primitive.NewObjectID() err = taskDB.Unprotected().Create(ctx, task) require.NoError(t, err) // Create comments for the task using unprotected DB comment := &model.Comment{ CommentBase: model.CommentBase{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organization.ID, }, }, AuthorRef: accountRef, TaskRef: task.ID, Content: "Test Comment for Archive", }, } comment.ID = primitive.NewObjectID() err = commentDB.Unprotected().Create(ctx, comment) require.NoError(t, err) // Create reaction for the comment using unprotected DB reaction := &model.Reaction{ PermissionBound: model.PermissionBound{ OrganizationBoundBase: model.OrganizationBoundBase{ OrganizationRef: organization.ID, }, }, Type: "like", AuthorRef: accountRef, CommentRef: comment.ID, } reaction.ID = primitive.NewObjectID() err = reactionDB.Unprotected().Create(ctx, reaction) require.NoError(t, err) // Verify all entities are not archived initially var retrievedOrganization model.Organization err = organizationDB.Get(ctx, accountRef, organization.ID, &retrievedOrganization) require.NoError(t, err) assert.False(t, retrievedOrganization.IsArchived()) var retrievedProject model.Project err = projectDBAdapter.Unprotected().Get(ctx, project.ID, &retrievedProject) require.NoError(t, err) assert.False(t, retrievedProject.IsArchived()) var retrievedTask model.Task err = taskDB.Unprotected().Get(ctx, task.ID, &retrievedTask) require.NoError(t, err) assert.False(t, retrievedTask.IsArchived()) var retrievedComment model.Comment err = commentDB.Unprotected().Get(ctx, comment.ID, &retrievedComment) require.NoError(t, err) assert.False(t, retrievedComment.IsArchived()) // Archive organization with cascade err = organizationDB.SetArchived(ctx, accountRef, organization.ID, true, true) require.NoError(t, err) // Verify all entities are archived due to cascade err = organizationDB.Get(ctx, accountRef, organization.ID, &retrievedOrganization) require.NoError(t, err) assert.True(t, retrievedOrganization.IsArchived()) err = projectDBAdapter.Unprotected().Get(ctx, project.ID, &retrievedProject) require.NoError(t, err) assert.True(t, retrievedProject.IsArchived()) err = taskDB.Unprotected().Get(ctx, task.ID, &retrievedTask) require.NoError(t, err) assert.True(t, retrievedTask.IsArchived()) err = commentDB.Unprotected().Get(ctx, comment.ID, &retrievedComment) require.NoError(t, err) assert.True(t, retrievedComment.IsArchived()) // Verify reaction still exists (reactions don't support archiving) var retrievedReaction model.Reaction err = reactionDB.Unprotected().Get(ctx, reaction.ID, &retrievedReaction) require.NoError(t, err) // Unarchive organization with cascade err = organizationDB.SetArchived(ctx, accountRef, organization.ID, false, true) require.NoError(t, err) // Verify all entities are unarchived err = organizationDB.Get(ctx, accountRef, organization.ID, &retrievedOrganization) require.NoError(t, err) assert.False(t, retrievedOrganization.IsArchived()) err = projectDBAdapter.Unprotected().Get(ctx, project.ID, &retrievedProject) require.NoError(t, err) assert.False(t, retrievedProject.IsArchived()) err = taskDB.Unprotected().Get(ctx, task.ID, &retrievedTask) require.NoError(t, err) assert.False(t, retrievedTask.IsArchived()) err = commentDB.Unprotected().Get(ctx, comment.ID, &retrievedComment) require.NoError(t, err) assert.False(t, retrievedComment.IsArchived()) // Clean up err = reactionDB.Unprotected().Delete(ctx, reaction.ID) require.NoError(t, err) err = commentDB.Unprotected().Delete(ctx, comment.ID) require.NoError(t, err) err = taskDB.Unprotected().Delete(ctx, task.ID) require.NoError(t, err) err = projectDBAdapter.Unprotected().Delete(ctx, project.ID) require.NoError(t, err) err = organizationDB.Delete(ctx, accountRef, organization.ID) require.NoError(t, err) }) t.Run("SetArchived_NonExistentOrganization", func(t *testing.T) { // Try to archive non-existent organization nonExistentID := primitive.NewObjectID() err := organizationDB.SetArchived(ctx, accountRef, nonExistentID, true, true) assert.Error(t, err) // Could be either no data or access denied error depending on the permission system assert.True(t, errors.Is(err, merrors.ErrNoData) || errors.Is(err, merrors.ErrAccessDenied)) }) }