diff --git a/app/gen-server/entity/Group.ts b/app/gen-server/entity/Group.ts index 63fc747311..4b83e4297e 100644 --- a/app/gen-server/entity/Group.ts +++ b/app/gen-server/entity/Group.ts @@ -5,6 +5,8 @@ import {User} from "./User"; @Entity({name: 'groups'}) export class Group extends BaseEntity { + public static readonly ROLE_TYPE = 'role'; + public static readonly RESOURCE_USERS_TYPE = 'resource users'; @PrimaryGeneratedColumn() public id: number; @@ -30,4 +32,16 @@ export class Group extends BaseEntity { @OneToOne(type => AclRule, aclRule => aclRule.group) public aclRule: AclRule; + + + @Column({type: String, enum: [Group.ROLE_TYPE, Group.RESOURCE_USERS_TYPE], default: Group.ROLE_TYPE, + // Disabling nullable and select is necessary for the code to be run with older versions of the database. + // Especially it is required for testing the migrations. + nullable: true, + // We must set select to false because of older migrations (like 1556726945436-Billing.ts) + // which does not expect a type column at this moment. + select: false}) + public type: typeof Group.ROLE_TYPE | typeof Group.RESOURCE_USERS_TYPE = Group.ROLE_TYPE; } + + diff --git a/app/gen-server/lib/homedb/GroupsManager.ts b/app/gen-server/lib/homedb/GroupsManager.ts index 0257f4ff2c..172c715bc0 100644 --- a/app/gen-server/lib/homedb/GroupsManager.ts +++ b/app/gen-server/lib/homedb/GroupsManager.ts @@ -2,13 +2,18 @@ import * as roles from "app/common/roles"; import { AclRule } from "app/gen-server/entity/AclRule"; import { Document } from "app/gen-server/entity/Document"; import { Group } from "app/gen-server/entity/Group"; -import { GroupDescriptor, NonGuestGroup, Resource } from "app/gen-server/lib/homedb/Interfaces"; +import { GroupWithMembersDescriptor, NonGuestGroup, + Resource, RoleGroupDescriptor, RunInTransaction } from "app/gen-server/lib/homedb/Interfaces"; import { Organization } from "app/gen-server/entity/Organization"; import { Permissions } from 'app/gen-server/lib/Permissions'; import { User } from "app/gen-server/entity/User"; import { Workspace } from "app/gen-server/entity/Workspace"; import { EntityManager } from "typeorm"; +import { UsersManager } from "./UsersManager"; +import { ApiError } from "app/common/ApiError"; + +export type GroupTypes = typeof Group.ROLE_TYPE | typeof Group.RESOURCE_USERS_TYPE; /** * Class responsible for Groups and Roles Management. @@ -18,18 +23,18 @@ import { EntityManager } from "typeorm"; */ export class GroupsManager { // All groups. - public get defaultGroups(): GroupDescriptor[] { + public get defaultGroups(): RoleGroupDescriptor[] { return this._defaultGroups; } // Groups whose permissions are inherited from parent resource to child resources. - public get defaultBasicGroups(): GroupDescriptor[] { + public get defaultBasicGroups(): RoleGroupDescriptor[] { return this._defaultGroups .filter(_grpDesc => _grpDesc.nestParent); } // Groups that are common to all resources. - public get defaultCommonGroups(): GroupDescriptor[] { + public get defaultCommonGroups(): RoleGroupDescriptor[] { return this._defaultGroups .filter(_grpDesc => !_grpDesc.orgOnly); } @@ -91,7 +96,7 @@ export class GroupsManager { * TODO: app/common/roles already contains an ordering of the default roles. Usage should * be consolidated. */ - private readonly _defaultGroups: GroupDescriptor[] = [{ + private readonly _defaultGroups: RoleGroupDescriptor[] = [{ name: roles.OWNER, permissions: Permissions.OWNER, nestParent: true @@ -114,6 +119,8 @@ export class GroupsManager { orgOnly: true }]; + public constructor (private _usersManager: UsersManager, private _runInTransaction: RunInTransaction) {} + /** * Helper for adjusting acl inheritance rules. Given an array of top-level groups from the * resource of interest, and an array of inherited groups belonging to the parent resource, @@ -272,4 +279,78 @@ export class GroupsManager { } return roles.getEffectiveRole(maxInheritedRole); } + + public async createGroup(groupDescriptor: GroupWithMembersDescriptor, optManager?: EntityManager) { + return await this._runInTransaction(optManager, async (manager) => { + const group = Group.create({ + type: groupDescriptor.type, + name: groupDescriptor.name, + memberUsers: await this._usersManager.getUsersByIds(groupDescriptor.memberUsers ?? [], manager), + memberGroups: await this._getGroupsByIds(groupDescriptor.memberGroups ?? [], manager), + }); + return await manager.save(group); + }); + } + + public async updateGroup( + id: number, groupDescriptor: Partial, optManager?: EntityManager + ) { + return await this._runInTransaction(optManager, async (manager) => { + const updatedProperties = Group.create({ + type: groupDescriptor.type, + name: groupDescriptor.name, + memberUsers: groupDescriptor.memberUsers ? + await this._usersManager.getUsersByIds(groupDescriptor.memberUsers, manager) : [], + memberGroups: groupDescriptor.memberGroups ? + await this._getGroupsByIds(groupDescriptor.memberGroups, manager) : [], + }); + const existingGroup = await this.getGroupWithMembersById(id, manager); + if (!existingGroup) { + throw new ApiError(`Group with id ${id} not found`, 404); + } + const group = Group.merge(existingGroup, updatedProperties); + return await manager.save(group); + }); + } + + public getGroupsWithMembers(type: GroupTypes, mamager?: EntityManager): Promise { + return this._runInTransaction(mamager, async (manager: EntityManager) => { + return this._getGroupByTypeQueryBuilder(manager) + .where('groups.type = :type', {type}) + .getMany(); + }); + } + + public async getGroupWithMembersById(groupId: number, optManager?: EntityManager): Promise { + return await this._runInTransaction(optManager, async (manager) => { + return await this._getGroupByTypeQueryBuilder(manager) + .andWhere('groups.id = :groupId', {groupId}) + .getOne(); + }); + } + + /** + * Returns a Promise for an array of User entites for the given userIds. + */ + private async _getGroupsByIds(groupIds: number[], optManager?: EntityManager): Promise { + if (groupIds.length === 0) { + return []; + } + return await this._runInTransaction(optManager, async (manager) => { + const queryBuilder = manager.createQueryBuilder() + .select('groups') + .from(Group, 'groups') + .where('groups.id IN (:...groupIds)', {groupIds}); + return await queryBuilder.getMany(); + }); + } + + private _getGroupByTypeQueryBuilder(manager: EntityManager) { + return manager.createQueryBuilder() + .select('groups') + .addSelect('groups.type') + .from(Group, 'groups') + .leftJoinAndSelect('groups.memberUsers', 'memberUsers') + .leftJoinAndSelect('groups.memberGroups', 'memberGroups'); + } } diff --git a/app/gen-server/lib/homedb/HomeDBManager.ts b/app/gen-server/lib/homedb/HomeDBManager.ts index d4593ebda4..3252f0a3b0 100644 --- a/app/gen-server/lib/homedb/HomeDBManager.ts +++ b/app/gen-server/lib/homedb/HomeDBManager.ts @@ -46,12 +46,13 @@ import { AvailableUsers, DocumentAccessChanges, GetUserOptions, - GroupDescriptor, + GroupWithMembersDescriptor, NonGuestGroup, OrgAccessChanges, PreviousAndCurrent, QueryResult, Resource, + RoleGroupDescriptor, UserProfileChange, WorkspaceAccessChanges, } from 'app/gen-server/lib/homedb/Interfaces'; @@ -88,7 +89,7 @@ import { WhereExpressionBuilder } from "typeorm"; import {v4 as uuidv4} from "uuid"; -import { GroupsManager } from './GroupsManager'; +import { GroupsManager, GroupTypes } from './GroupsManager'; // Support transactions in Sqlite in async code. This is a monkey patch, affecting // the prototypes of various TypeORM classes. @@ -253,7 +254,7 @@ export type BillingOptions = Partial { const org = this.unwrapQueryResult(await this.getOrg(scope, orgKey, transaction)); + if (!org.billingAccount.isManager && scope.userId !== this._usersManager.getPreviewerUserId() && // The special permit (used for the support user) allows access to the billing account. scope.specialPermit?.org !== orgKey) { @@ -3067,6 +3069,18 @@ export class HomeDBManager extends EventEmitter { return query.getOne(); } + public async createGroup(groupDescriptor: GroupWithMembersDescriptor, optManager?: EntityManager) { + return this._groupsManager.createGroup(groupDescriptor, optManager); + } + + public getGroupsWithMembers(type: GroupTypes, manager?: EntityManager): Promise { + return this._groupsManager.getGroupsWithMembers(type, manager); + } + + public getGroupWithMembersById(id: number, manager?: EntityManager): Promise { + return this._groupsManager.getGroupWithMembersById(id, manager); + } + private _installConfig( key: ConfigKey, { manager }: { manager?: EntityManager } diff --git a/app/gen-server/lib/homedb/Interfaces.ts b/app/gen-server/lib/homedb/Interfaces.ts index bb125f9b4b..a3c75aaa13 100644 --- a/app/gen-server/lib/homedb/Interfaces.ts +++ b/app/gen-server/lib/homedb/Interfaces.ts @@ -8,6 +8,7 @@ import { User } from "app/gen-server/entity/User"; import { Workspace } from "app/gen-server/entity/Workspace"; import { EntityManager } from "typeorm"; +import { GroupTypes } from "./GroupsManager"; export interface QueryResult { status: number; @@ -61,13 +62,20 @@ export interface OrgAccessChanges { accessChanges: Omit; } -export interface GroupDescriptor { +export interface RoleGroupDescriptor { readonly name: roles.Role; readonly permissions: number; readonly nestParent: boolean; readonly orgOnly?: boolean; } +export interface GroupWithMembersDescriptor { + readonly type: GroupTypes; + readonly name: string; + readonly memberUsers?: number[]; + readonly memberGroups?: number[]; +} + interface AccessChanges { publicAccess: roles.NonGuestRole | null; maxInheritedAccess: roles.BasicRole | null; diff --git a/app/gen-server/lib/homedb/UsersManager.ts b/app/gen-server/lib/homedb/UsersManager.ts index 6a0f431c84..5115aafcd5 100644 --- a/app/gen-server/lib/homedb/UsersManager.ts +++ b/app/gen-server/lib/homedb/UsersManager.ts @@ -725,12 +725,13 @@ export class UsersManager { if (userIds.length === 0) { return []; } - const manager = optManager || new EntityManager(this._connection); - const queryBuilder = manager.createQueryBuilder() - .select('users') - .from(User, 'users') - .where('users.id IN (:...userIds)', {userIds}); - return await queryBuilder.getMany(); + return await this._runInTransaction(optManager, async (manager) => { + const queryBuilder = manager.createQueryBuilder() + .select('users') + .from(User, 'users') + .where('users.id IN (:...userIds)', {userIds}); + return await queryBuilder.getMany(); + }); } /** diff --git a/app/gen-server/migration/1734097274107-GroupTypes.ts b/app/gen-server/migration/1734097274107-GroupTypes.ts new file mode 100644 index 0000000000..42a9bbe750 --- /dev/null +++ b/app/gen-server/migration/1734097274107-GroupTypes.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; +import { Group } from "app/gen-server/entity/Group"; + +export class GroupTypes1734097274107 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const newColumn = new TableColumn({ + name: 'type', + type: 'varchar', + enum: [Group.ROLE_TYPE, Group.RESOURCE_USERS_TYPE], + comment: `If the type is ${Group.ROLE_TYPE}, the group is meant to assign a role to ` + + 'users for a resource (document, workspace or org).' + + '\n\n' + + `If the type is "${Group.RESOURCE_USERS_TYPE}", the group is meant to gather users together ` + + 'so they can be granted the same role to some resources (hence this name).', + isNullable: true, // Make it not nullable after setting the roles for existing groups + }); + + await queryRunner.addColumn('groups', newColumn); + + await queryRunner.manager + .query('UPDATE groups SET type = $1', [Group.ROLE_TYPE]); + + newColumn.isNullable = false; + + await queryRunner.changeColumn('groups', newColumn.name, newColumn); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('groups', 'type'); + } +} + diff --git a/app/server/lib/dbUtils.ts b/app/server/lib/dbUtils.ts index c540954aae..1966ff0205 100644 --- a/app/server/lib/dbUtils.ts +++ b/app/server/lib/dbUtils.ts @@ -83,24 +83,34 @@ export async function getOrCreateConnection(): Promise { } export async function runMigrations(connection: Connection) { - // on SQLite, migrations fail if we don't temporarily disable foreign key - // constraint checking. This is because for sqlite typeorm copies each - // table and rebuilds it from scratch for each schema change. - // Also, we need to disable foreign key constraint checking outside of any - // transaction, or it has no effect. - const sqlite = connection.driver.options.type === 'sqlite'; - if (sqlite) { await connection.query("PRAGMA foreign_keys = OFF;"); } - await connection.runMigrations({ transaction: "all" }); - if (sqlite) { await connection.query("PRAGMA foreign_keys = ON;"); } + return await withSqliteForeignKeyConstraintDisabled(connection, async () => { + await connection.runMigrations({ transaction: "all" }); + }); } export async function undoLastMigration(connection: Connection) { + return await withSqliteForeignKeyConstraintDisabled(connection, async () => { + await connection.transaction(async tr => { + await tr.connection.undoLastMigration(); + }); + }); +} + +// on SQLite, migrations fail if we don't temporarily disable foreign key +// constraint checking. This is because for sqlite typeorm copies each +// table and rebuilds it from scratch for each schema change. +// Also, we need to disable foreign key constraint checking outside of any +// transaction, or it has no effect. +export async function withSqliteForeignKeyConstraintDisabled( + connection: Connection, cb: () => Promise +): Promise { const sqlite = connection.driver.options.type === 'sqlite'; if (sqlite) { await connection.query("PRAGMA foreign_keys = OFF;"); } - await connection.transaction(async tr => { - await tr.connection.undoLastMigration(); - }); - if (sqlite) { await connection.query("PRAGMA foreign_keys = ON;"); } + try { + return await cb(); + } finally { + if (sqlite) { await connection.query("PRAGMA foreign_keys = ON;"); } + } } // Replace the old janky ormconfig.js file, which was always a source of diff --git a/test/gen-server/lib/homedb/GroupsManager.ts b/test/gen-server/lib/homedb/GroupsManager.ts new file mode 100644 index 0000000000..dd688e0c02 --- /dev/null +++ b/test/gen-server/lib/homedb/GroupsManager.ts @@ -0,0 +1,139 @@ +import { assert } from 'chai'; +import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager'; +import { EnvironmentSnapshot } from 'test/server/testUtils'; +import { createInitialDb, removeConnection, setUpDB } from 'test/gen-server/seed'; +import { Group } from 'app/gen-server/entity/Group'; +import omit from 'lodash/omit'; +import { User } from 'app/gen-server/entity/User'; + +describe("GroupsManager", function () { + this.timeout('3m'); + let env: EnvironmentSnapshot; + let db: HomeDBManager; + + before(async function () { + env = new EnvironmentSnapshot(); + process.env.TEST_CLEAN_DATABASE = 'true'; + setUpDB(this); + db = new HomeDBManager(); + await createInitialDb(); + await db.connect(); + await db.initializeSpecialIds(); + }); + + after(async function () { + env?.restore(); + await removeConnection(); + }); + + function sanitizeUserPropertiesForMembership(user: User) { + return omit(user, 'logins', 'personalOrg'); + } + + describe('createGroup()', function () { + it('should create a new resource users group', async function () { + const chimpy = (await db.getExistingUserByLogin('chimpy@getgrist.com'))!; + const group = await db.createGroup({ + name: 'test-creategroup', + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [chimpy.id], + }); + assert.equal(group.name, 'test-creategroup'); + assert.equal(group.type, 'resource users'); + assert.deepEqual(group.memberUsers, [sanitizeUserPropertiesForMembership(chimpy)]); + }); + + it('should create a new resource users group with groupMembers', async function () { + const kiwi = (await db.getExistingUserByLogin('kiwi@getgrist.com'))!; + const innerGroup = await db.createGroup({ + name: 'test-creategroup-innerGroup', + type: Group.RESOURCE_USERS_TYPE, + }); + const group = await db.createGroup({ + name: 'test-creategroup-with-groupMembers', + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [kiwi.id], + memberGroups: [innerGroup.id], + }); + assert.equal(group.name, 'test-creategroup-with-groupMembers'); + assert.equal(group.type, 'resource users'); + assert.deepEqual(group.memberUsers, [sanitizeUserPropertiesForMembership(kiwi)]); + assert.equal(group.memberGroups.length, 1); + assert.equal(group.memberGroups[0].name, innerGroup.name); + }); + }); + + describe('getGroupsWithMembers()', function () { + it('should return groups and members for roles', async function () { + const groups = await db.getGroupsWithMembers(Group.ROLE_TYPE); + assert.isNotEmpty(groups, 'should return roles'); + const groupsNames = new Set(groups.map(group => group.name)); + assert.deepEqual(groupsNames, new Set(['owners', 'editors', 'viewers', 'guests', 'members'])); + assert.isTrue(groups.some(g => g.memberUsers.length > 0), "memberUsers should be populated"); + assert.isTrue(groups.some(g => g.memberGroups.length > 0), "memberGroups should be populated"); + assert.isTrue(groups.every(g => g.type === Group.ROLE_TYPE), 'some groups retrieved are not of type ' + + Group.ROLE_TYPE); + }); + + it('should return groups and members for resource users', async function () { + const chimpy = (await db.getExistingUserByLogin('chimpy@getgrist.com'))!; + const kiwi = (await db.getExistingUserByLogin('kiwi@getgrist.com'))!; + const innerGroupName = 'test-getGroupsWithMembers-inner'; + const groupName = 'test-getGroupsWithMembers'; + + const innerGroup = await db.createGroup({ + name: innerGroupName, + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [kiwi.id], + }); + await db.createGroup({ + name: groupName, + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [chimpy.id], + memberGroups: [innerGroup.id] + }); + const groups = await db.getGroupsWithMembers(Group.RESOURCE_USERS_TYPE); + assert.isTrue(groups.every(g => g.type === Group.RESOURCE_USERS_TYPE), + `some groups retrieved are not of type "${Group.RESOURCE_USERS_TYPE}"`); + const group = groups.find(g => g.name === groupName); + assert.exists(group, 'group not found'); + assert.deepEqual(group!.memberUsers, [sanitizeUserPropertiesForMembership(chimpy)]); + assert.exists(groups.find(g => g.name === innerGroupName), 'inner group not found'); + }); + }); + + describe('getGroupWithMembersById()', function () { + it('should return null when the group is not found', async function () { + const nonExistingGroup = await db.getGroupWithMembersById(999); + assert.isNull(nonExistingGroup); + }); + + it('should return a group and with its members given an ID', async function () { + // FIXME: factorize this piece of code + const chimpy = (await db.getExistingUserByLogin('chimpy@getgrist.com'))!; + const kiwi = (await db.getExistingUserByLogin('kiwi@getgrist.com'))!; + const innerGroupName = 'test-getGroupWithMembers-inner'; + const groupName = 'test-getGroupWithMembers'; + + const innerGroup = await db.createGroup({ + name: innerGroupName, + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [kiwi.id], + }); + const createdGroup = await db.createGroup({ + name: groupName, + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [chimpy.id], + memberGroups: [innerGroup.id] + }); + + const group = (await db.getGroupWithMembersById(createdGroup.id))!; + assert.exists(group, 'group not found'); + assert.equal(group.name, groupName); + assert.equal(group.type, Group.RESOURCE_USERS_TYPE); + assert.deepEqual(group.memberUsers, [sanitizeUserPropertiesForMembership(chimpy)]); + assert.equal(group.memberGroups.length, 1); + assert.equal(group.memberGroups[0].name, innerGroup.name); + }); + }); +}); diff --git a/test/gen-server/migrations.ts b/test/gen-server/migrations.ts index 2795fd2529..558c8230aa 100644 --- a/test/gen-server/migrations.ts +++ b/test/gen-server/migrations.ts @@ -49,6 +49,9 @@ import {ActivationEnabled1722529827161 import {Configs1727747249153 as Configs} from 'app/gen-server/migration/1727747249153-Configs'; import {LoginsEmailsIndex1729754662550 as LoginsEmailsIndex} from 'app/gen-server/migration/1729754662550-LoginsEmailIndex'; +import {GroupTypes1734097274107 + as GroupTypes} from 'app/gen-server/migration/1734097274107-GroupTypes'; +import { withSqliteForeignKeyConstraintDisabled } from "app/server/lib/dbUtils"; const home: HomeDBManager = new HomeDBManager(); @@ -58,7 +61,7 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart, DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID, Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures, - UserLastConnection, ActivationEnabled, Configs, LoginsEmailsIndex]; + UserLastConnection, ActivationEnabled, Configs, LoginsEmailsIndex, GroupTypes]; // Assert that the "members" acl rule and group exist (or not). function assertMembersGroup(org: Organization, exists: boolean) { @@ -150,55 +153,55 @@ describe('migrations', function() { it('can correctly switch display_email column to non-null with data', async function() { this.timeout(60000); - const sqlite = home.connection.driver.options.type === 'sqlite'; - // sqlite migrations need foreign keys turned off temporarily - if (sqlite) { await home.connection.query("PRAGMA foreign_keys = OFF;"); } - const runner = home.connection.createQueryRunner(); - for (const migration of migrations) { - await (new migration()).up(runner); - } - await addSeedData(home.connection); - // migrate back until just before display_email column added, so we have no - // display_emails - for (const migration of migrations.slice().reverse()) { - await (new migration()).down(runner); - if (migration.name === DisplayEmail.name) { break; } - } - // now check DisplayEmail and DisplayEmailNonNull succeed with data in the db. - await (new DisplayEmail()).up(runner); - await (new DisplayEmailNonNull()).up(runner); - if (sqlite) { await home.connection.query("PRAGMA foreign_keys = ON;"); } + return withSqliteForeignKeyConstraintDisabled(home.connection, async () => { + const runner = home.connection.createQueryRunner(); + for (const migration of migrations) { + await (new migration()).up(runner); + } + await addSeedData(home.connection); + // migrate back until just before display_email column added, so we have no + // display_emails + for (const migration of migrations.slice().reverse()) { + await (new migration()).down(runner); + if (migration.name === DisplayEmail.name) { break; } + } + // now check DisplayEmail and DisplayEmailNonNull succeed with data in the db. + await (new DisplayEmail()).up(runner); + await (new DisplayEmailNonNull()).up(runner); + }); }); // a test to ensure the TeamMember migration works on databases with existing content it('can perform TeamMember migration with seed data set', async function() { this.timeout(30000); - const runner = home.connection.createQueryRunner(); - // Perform full up migration and add the seed data. - for (const migration of migrations) { - await (new migration()).up(runner); - } - await addSeedData(home.connection); - const initAclCount = await getAclRowCount(runner); - const initGroupCount = await getGroupRowCount(runner); - - // Assert that members groups are present to start. - for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); } - - // Perform down TeamMembers migration with seed data and assert members groups are removed. - await (new TeamMembers()).down(runner); - const downMigratedOrgs = await getAllOrgs(runner); - for (const org of downMigratedOrgs) { assertMembersGroup(org, false); } - // Assert that the correct number of ACLs and groups were removed. - assert.equal(await getAclRowCount(runner), initAclCount - downMigratedOrgs.length); - assert.equal(await getGroupRowCount(runner), initGroupCount - downMigratedOrgs.length); - - // Perform up TeamMembers migration with seed data and assert members groups are added. - await (new TeamMembers()).up(runner); - for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); } - // Assert that the correct number of ACLs and groups were re-added. - assert.equal(await getAclRowCount(runner), initAclCount); - assert.equal(await getGroupRowCount(runner), initGroupCount); + return await withSqliteForeignKeyConstraintDisabled(home.connection, async () => { + const runner = home.connection.createQueryRunner(); + // Perform full up migration and add the seed data. + for (const migration of migrations) { + await (new migration()).up(runner); + } + await addSeedData(home.connection); + const initAclCount = await getAclRowCount(runner); + const initGroupCount = await getGroupRowCount(runner); + + // Assert that members groups are present to start. + for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); } + + // Perform down TeamMembers migration with seed data and assert members groups are removed. + await (new TeamMembers()).down(runner); + const downMigratedOrgs = await getAllOrgs(runner); + for (const org of downMigratedOrgs) { assertMembersGroup(org, false); } + // Assert that the correct number of ACLs and groups were removed. + assert.equal(await getAclRowCount(runner), initAclCount - downMigratedOrgs.length); + assert.equal(await getGroupRowCount(runner), initGroupCount - downMigratedOrgs.length); + + // Perform up TeamMembers migration with seed data and assert members groups are added. + await (new TeamMembers()).up(runner); + for (const org of (await getAllOrgs(runner))) { assertMembersGroup(org, true); } + // Assert that the correct number of ACLs and groups were re-added. + assert.equal(await getAclRowCount(runner), initAclCount); + assert.equal(await getGroupRowCount(runner), initGroupCount); + }); }); });