feat(app): wire dashboard administration flows
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { AppConnection } from '../ts/reception/classes.appconnection.js';
|
||||
import { AppConnectionManager } from '../ts/reception/classes.appconnectionmanager.js';
|
||||
import { User } from '../ts/reception/classes.user.js';
|
||||
|
||||
const createTestAppConnectionManager = (optionsArg: {
|
||||
allowedScopes?: string[];
|
||||
grantedScopes?: string[];
|
||||
} = {}) => {
|
||||
const activities: Array<{ userId: string; action: string; description: string; metadata?: any }> = [];
|
||||
const alerts: Array<{ eventType: string; organizationId?: string; relatedEntityId?: string }> = [];
|
||||
|
||||
const user = new User();
|
||||
user.id = 'user-1';
|
||||
user.data = {
|
||||
name: 'Admin User',
|
||||
username: 'admin@example.com',
|
||||
email: 'admin@example.com',
|
||||
status: 'active',
|
||||
connectedOrgs: ['org-1'],
|
||||
};
|
||||
|
||||
const app = {
|
||||
id: 'app-1',
|
||||
type: 'global',
|
||||
data: {
|
||||
name: 'Finance App',
|
||||
oauthCredentials: {
|
||||
allowedScopes: optionsArg.allowedScopes || ['openid', 'roles', 'billing'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const organization = {
|
||||
id: 'org-1',
|
||||
data: {
|
||||
name: 'Lossless GmbH',
|
||||
slug: 'lossless',
|
||||
},
|
||||
checkIfUserIsAdmin: async () => true,
|
||||
};
|
||||
|
||||
const connection = new AppConnection();
|
||||
connection.id = 'connection-1';
|
||||
connection.data = {
|
||||
organizationId: organization.id,
|
||||
appId: app.id,
|
||||
appType: 'global',
|
||||
status: 'active',
|
||||
connectedAt: Date.now(),
|
||||
connectedByUserId: user.id,
|
||||
grantedScopes: optionsArg.grantedScopes || ['openid', 'roles', 'billing'],
|
||||
roleMappings: [],
|
||||
};
|
||||
connection.save = async () => undefined;
|
||||
|
||||
const reception = {
|
||||
db: { smartdataDb: {} },
|
||||
typedrouter: { addTypedRouter: () => undefined },
|
||||
organizationmanager: {
|
||||
COrganization: {
|
||||
getInstance: async () => organization,
|
||||
},
|
||||
getAvailableRoleKeys: async () => ['owner', 'admin', 'viewer', 'finance'],
|
||||
validateRoleKey: (roleKeyArg: string) => roleKeyArg.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
||||
},
|
||||
appManager: {
|
||||
getAppById: async () => app,
|
||||
},
|
||||
activityLogManager: {
|
||||
logActivity: async (userId: string, action: string, description: string, metadata?: any) => {
|
||||
activities.push({ userId, action, description, metadata });
|
||||
},
|
||||
},
|
||||
alertManager: {
|
||||
createAlertsForEvent: async (options: { eventType: string; organizationId?: string; relatedEntityId?: string }) => {
|
||||
alerts.push(options);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const manager = new AppConnectionManager(reception);
|
||||
(manager as any).CAppConnection = {
|
||||
getInstance: async () => connection,
|
||||
};
|
||||
|
||||
return {
|
||||
manager,
|
||||
user,
|
||||
connection,
|
||||
activities,
|
||||
alerts,
|
||||
};
|
||||
};
|
||||
|
||||
tap.test('rejects app role mappings with unsupported app scopes', async () => {
|
||||
const { manager, user, connection, activities } = createTestAppConnectionManager({
|
||||
allowedScopes: ['openid', 'roles'],
|
||||
grantedScopes: ['openid', 'roles', 'billing'],
|
||||
});
|
||||
|
||||
await expect(manager.updateAppRoleMappings({
|
||||
user,
|
||||
organizationId: 'org-1',
|
||||
appId: 'app-1',
|
||||
roleMappings: [{
|
||||
orgRoleKey: 'finance',
|
||||
appRoles: [],
|
||||
permissions: [],
|
||||
scopes: ['billing'],
|
||||
}],
|
||||
})).rejects.toThrow();
|
||||
|
||||
expect(connection.data.roleMappings).toEqual([]);
|
||||
expect(activities).toEqual([]);
|
||||
});
|
||||
|
||||
tap.test('rejects app role mappings with ungranted connection scopes', async () => {
|
||||
const { manager, user, connection, activities } = createTestAppConnectionManager({
|
||||
allowedScopes: ['openid', 'roles', 'billing'],
|
||||
grantedScopes: ['openid', 'roles'],
|
||||
});
|
||||
|
||||
await expect(manager.updateAppRoleMappings({
|
||||
user,
|
||||
organizationId: 'org-1',
|
||||
appId: 'app-1',
|
||||
roleMappings: [{
|
||||
orgRoleKey: 'finance',
|
||||
appRoles: [],
|
||||
permissions: [],
|
||||
scopes: ['billing'],
|
||||
}],
|
||||
})).rejects.toThrow();
|
||||
|
||||
expect(connection.data.roleMappings).toEqual([]);
|
||||
expect(activities).toEqual([]);
|
||||
});
|
||||
|
||||
tap.test('updates app role mappings and writes audit activity', async () => {
|
||||
const { manager, user, connection, activities, alerts } = createTestAppConnectionManager();
|
||||
|
||||
await manager.updateAppRoleMappings({
|
||||
user,
|
||||
organizationId: 'org-1',
|
||||
appId: 'app-1',
|
||||
roleMappings: [{
|
||||
orgRoleKey: ' Finance ',
|
||||
appRoles: ['accountant', 'accountant', ''],
|
||||
permissions: ['invoices:read'],
|
||||
scopes: ['billing'],
|
||||
}],
|
||||
});
|
||||
|
||||
expect(connection.data.roleMappings).toEqual([{
|
||||
orgRoleKey: 'finance',
|
||||
appRoles: ['accountant'],
|
||||
permissions: ['invoices:read'],
|
||||
scopes: ['billing'],
|
||||
}]);
|
||||
expect(activities[0].action).toEqual('org_app_role_mappings_updated');
|
||||
expect(activities[0].metadata.targetId).toEqual(connection.id);
|
||||
expect(alerts[0].eventType).toEqual('org_app_role_mappings_updated');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
+94
-1
@@ -1,16 +1,21 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { App } from '../ts/reception/classes.app.js';
|
||||
import { AppConnection } from '../ts/reception/classes.appconnection.js';
|
||||
import { OidcAccessToken } from '../ts/reception/classes.oidcaccesstoken.js';
|
||||
import { OidcAuthorizationCode } from '../ts/reception/classes.oidcauthorizationcode.js';
|
||||
import { OidcManager } from '../ts/reception/classes.oidcmanager.js';
|
||||
import { OidcRefreshToken } from '../ts/reception/classes.oidcrefreshtoken.js';
|
||||
import { OidcUserConsent } from '../ts/reception/classes.oidcuserconsent.js';
|
||||
import { Role } from '../ts/reception/classes.role.js';
|
||||
import { User } from '../ts/reception/classes.user.js';
|
||||
|
||||
const createTestOidcManager = () => {
|
||||
const createTestOidcManager = (receptionOverridesArg: Record<string, any> = {}) => {
|
||||
const oidcManager = new OidcManager({
|
||||
db: { smartdataDb: {} },
|
||||
typedrouter: { addTypedRouter: () => undefined },
|
||||
options: { baseUrl: 'https://idp.example' },
|
||||
...receptionOverridesArg,
|
||||
} as any);
|
||||
void oidcManager.stop();
|
||||
return oidcManager;
|
||||
@@ -205,4 +210,92 @@ tap.test('prepares OAuth authorization as ready when consent already exists', as
|
||||
await oidcManager.stop();
|
||||
});
|
||||
|
||||
tap.test('includes connected app role mappings in roles-scope claims', async () => {
|
||||
const user = new User();
|
||||
user.id = 'user-1';
|
||||
user.data = {
|
||||
name: 'Finance User',
|
||||
username: 'finance-user',
|
||||
email: 'finance@example.com',
|
||||
status: 'active',
|
||||
connectedOrgs: ['org-1'],
|
||||
};
|
||||
|
||||
const role = new Role();
|
||||
role.id = 'role-1';
|
||||
role.data = {
|
||||
userId: user.id,
|
||||
organizationId: 'org-1',
|
||||
roles: ['finance'],
|
||||
};
|
||||
|
||||
const app = new App();
|
||||
app.id = 'app-1';
|
||||
app.type = 'global';
|
||||
app.data = {
|
||||
name: 'Accounting',
|
||||
description: 'Accounting app',
|
||||
logoUrl: '',
|
||||
appUrl: 'https://accounting.example',
|
||||
category: 'finance',
|
||||
isActive: true,
|
||||
createdAt: Date.now(),
|
||||
createdByUserId: 'admin-1',
|
||||
oauthCredentials: {
|
||||
clientId: 'client-1',
|
||||
clientSecretHash: 'secret-hash',
|
||||
redirectUris: ['https://accounting.example/callback'],
|
||||
allowedScopes: ['openid', 'roles'],
|
||||
grantTypes: ['authorization_code'],
|
||||
},
|
||||
};
|
||||
|
||||
const connection = new AppConnection();
|
||||
connection.id = 'connection-1';
|
||||
connection.data = {
|
||||
organizationId: 'org-1',
|
||||
appId: app.id,
|
||||
appType: 'global',
|
||||
status: 'active',
|
||||
connectedAt: Date.now(),
|
||||
connectedByUserId: 'admin-1',
|
||||
grantedScopes: ['openid', 'roles'],
|
||||
roleMappings: [{
|
||||
orgRoleKey: 'finance',
|
||||
appRoles: ['accountant'],
|
||||
permissions: ['invoices:read'],
|
||||
scopes: ['billing'],
|
||||
}],
|
||||
};
|
||||
|
||||
const oidcManager = createTestOidcManager({
|
||||
userManager: {
|
||||
CUser: {
|
||||
getInstance: async () => user,
|
||||
},
|
||||
},
|
||||
roleManager: {
|
||||
getAllRolesForUser: async () => [role],
|
||||
},
|
||||
appManager: {
|
||||
CApp: {
|
||||
getInstances: async () => [app],
|
||||
},
|
||||
},
|
||||
appConnectionManager: {
|
||||
CAppConnection: {
|
||||
getInstances: async () => [connection],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const claims = await (oidcManager as any).getUserClaims(user.id, ['roles'], 'client-1');
|
||||
|
||||
expect(claims.app_roles).toEqual(['accountant']);
|
||||
expect(claims.app_permissions).toEqual(['invoices:read']);
|
||||
expect(claims.app_scopes).toEqual(['billing']);
|
||||
|
||||
await oidcManager.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { AppConnection } from '../ts/reception/classes.appconnection.js';
|
||||
import { BillingPlan } from '../ts/reception/classes.billingplan.js';
|
||||
import { Organization } from '../ts/reception/classes.organization.js';
|
||||
import { OrganizationManager } from '../ts/reception/classes.organizationmanager.js';
|
||||
import { Role } from '../ts/reception/classes.role.js';
|
||||
import { User } from '../ts/reception/classes.user.js';
|
||||
import { UserInvitation } from '../ts/reception/classes.userinvitation.js';
|
||||
|
||||
const getNestedValue = (targetArg: any, pathArg: string) => {
|
||||
return pathArg.split('.').reduce((currentArg, keyArg) => currentArg?.[keyArg], targetArg);
|
||||
};
|
||||
|
||||
const matchesQuery = (targetArg: any, queryArg: Record<string, any>) => {
|
||||
return Object.entries(queryArg).every(([keyArg, valueArg]) => {
|
||||
const currentValue = getNestedValue(targetArg, keyArg);
|
||||
if (valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg)) {
|
||||
return Object.entries(valueArg).every(([nestedKeyArg, nestedValueArg]) => currentValue?.[nestedKeyArg] === nestedValueArg);
|
||||
}
|
||||
return currentValue === valueArg;
|
||||
});
|
||||
};
|
||||
|
||||
const attachPersistence = <TDoc extends { id: string; save?: () => Promise<void>; delete?: () => Promise<void> }>(
|
||||
docArg: TDoc,
|
||||
mapArg: Map<string, TDoc>
|
||||
) => {
|
||||
docArg.save = async () => {
|
||||
mapArg.set(docArg.id, docArg);
|
||||
};
|
||||
docArg.delete = async () => {
|
||||
mapArg.delete(docArg.id);
|
||||
};
|
||||
mapArg.set(docArg.id, docArg);
|
||||
return docArg;
|
||||
};
|
||||
|
||||
const createTestOrganizationManager = () => {
|
||||
const organizations = new Map<string, Organization>();
|
||||
const roles = new Map<string, Role>();
|
||||
const users = new Map<string, User>();
|
||||
const appConnections = new Map<string, AppConnection>();
|
||||
const invitations = new Map<string, UserInvitation>();
|
||||
const billingPlans = new Map<string, BillingPlan>();
|
||||
const activities: Array<{ userId: string; action: string; description: string }> = [];
|
||||
const alerts: Array<{ eventType: string; organizationId?: string }> = [];
|
||||
|
||||
const getInstancesFromMap = async <TDoc>(mapArg: Map<string, TDoc>, queryArg: Record<string, any> = {}) => {
|
||||
return Array.from(mapArg.values()).filter((docArg) => matchesQuery(docArg, queryArg));
|
||||
};
|
||||
|
||||
const reception = {
|
||||
db: { smartdataDb: {} },
|
||||
typedrouter: { addTypedRouter: () => undefined },
|
||||
roleManager: {
|
||||
getRoleForUserAndOrg: async (userArg: User, organizationArg: Organization) => {
|
||||
return Array.from(roles.values()).find((roleArg) => roleArg.data.userId === userArg.id && roleArg.data.organizationId === organizationArg.id) || null;
|
||||
},
|
||||
getAllRolesForOrg: async (organizationIdArg: string) => {
|
||||
return Array.from(roles.values()).filter((roleArg) => roleArg.data.organizationId === organizationIdArg);
|
||||
},
|
||||
},
|
||||
userManager: {
|
||||
CUser: {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(users.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
},
|
||||
},
|
||||
activityLogManager: {
|
||||
logActivity: async (userId: string, action: string, description: string) => {
|
||||
activities.push({ userId, action, description });
|
||||
},
|
||||
},
|
||||
alertManager: {
|
||||
createAlertsForEvent: async (optionsArg: { eventType: string; organizationId?: string }) => {
|
||||
alerts.push(optionsArg);
|
||||
return [];
|
||||
},
|
||||
},
|
||||
appConnectionManager: {
|
||||
CAppConnection: {
|
||||
getInstances: async (queryArg: Record<string, any>) => getInstancesFromMap(appConnections, queryArg),
|
||||
},
|
||||
},
|
||||
userInvitationManager: {
|
||||
CUserInvitation: {
|
||||
getInstances: async (queryArg: Record<string, any>) => getInstancesFromMap(invitations, queryArg),
|
||||
},
|
||||
},
|
||||
billingPlanManager: {
|
||||
CBillingPlan: {
|
||||
getInstances: async (queryArg: Record<string, any>) => getInstancesFromMap(billingPlans, queryArg),
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const manager = new OrganizationManager(reception);
|
||||
(manager as any).COrganization = {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(organizations.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
getInstances: async (queryArg: Record<string, any>) => getInstancesFromMap(organizations, queryArg),
|
||||
};
|
||||
|
||||
return {
|
||||
manager,
|
||||
organizations,
|
||||
roles,
|
||||
users,
|
||||
appConnections,
|
||||
invitations,
|
||||
billingPlans,
|
||||
activities,
|
||||
alerts,
|
||||
};
|
||||
};
|
||||
|
||||
const addUser = (usersArg: Map<string, User>, idArg: string, emailArg: string, connectedOrgsArg: string[] = []) => {
|
||||
const user = new User();
|
||||
user.id = idArg;
|
||||
user.data = {
|
||||
name: emailArg,
|
||||
username: emailArg,
|
||||
email: emailArg,
|
||||
status: 'active',
|
||||
connectedOrgs: connectedOrgsArg,
|
||||
};
|
||||
return attachPersistence(user, usersArg);
|
||||
};
|
||||
|
||||
const addOrganization = (organizationsArg: Map<string, Organization>) => {
|
||||
const organization = new Organization();
|
||||
organization.id = 'org-1';
|
||||
organization.data = {
|
||||
name: 'Lossless GmbH',
|
||||
slug: 'lossless',
|
||||
billingPlanId: 'billing-1',
|
||||
roleIds: ['role-owner', 'role-member'],
|
||||
};
|
||||
return attachPersistence(organization, organizationsArg);
|
||||
};
|
||||
|
||||
const addRole = (rolesArg: Map<string, Role>, idArg: string, userIdArg: string, rolesValueArg: string[]) => {
|
||||
const role = new Role();
|
||||
role.id = idArg;
|
||||
role.data = {
|
||||
userId: userIdArg,
|
||||
organizationId: 'org-1',
|
||||
roles: rolesValueArg,
|
||||
};
|
||||
return attachPersistence(role, rolesArg);
|
||||
};
|
||||
|
||||
tap.test('updates organization settings only with audited confirmation', async () => {
|
||||
const { manager, organizations, roles, users, activities, alerts } = createTestOrganizationManager();
|
||||
const owner = addUser(users, 'owner-1', 'owner@example.com', ['org-1']);
|
||||
addOrganization(organizations);
|
||||
addRole(roles, 'role-owner', owner.id, ['owner']);
|
||||
|
||||
await expect(manager.updateOrganizationWithAudit({
|
||||
user: owner,
|
||||
organizationId: 'org-1',
|
||||
name: 'Lossless Updated',
|
||||
slug: 'lossless-updated',
|
||||
confirmationText: 'wrong',
|
||||
})).rejects.toThrow();
|
||||
|
||||
const updatedOrganization = await manager.updateOrganizationWithAudit({
|
||||
user: owner,
|
||||
organizationId: 'org-1',
|
||||
name: 'Lossless Updated',
|
||||
slug: 'lossless-updated',
|
||||
confirmationText: 'lossless',
|
||||
});
|
||||
|
||||
expect(updatedOrganization.data.name).toEqual('Lossless Updated');
|
||||
expect(updatedOrganization.data.slug).toEqual('lossless-updated');
|
||||
expect(activities[0].action).toEqual('org_updated');
|
||||
expect(alerts[0].eventType).toEqual('org_updated');
|
||||
});
|
||||
|
||||
tap.test('deletes organization dependencies only with audited owner confirmation', async () => {
|
||||
const { manager, organizations, roles, users, appConnections, invitations, billingPlans, activities, alerts } = createTestOrganizationManager();
|
||||
const owner = addUser(users, 'owner-1', 'owner@example.com', ['org-1']);
|
||||
const member = addUser(users, 'member-1', 'member@example.com', ['org-1']);
|
||||
addOrganization(organizations);
|
||||
addRole(roles, 'role-owner', owner.id, ['owner']);
|
||||
addRole(roles, 'role-member', member.id, ['viewer']);
|
||||
|
||||
const appConnection = new AppConnection();
|
||||
appConnection.id = 'connection-1';
|
||||
appConnection.data = {
|
||||
organizationId: 'org-1',
|
||||
appId: 'app-1',
|
||||
appType: 'global',
|
||||
status: 'active',
|
||||
connectedAt: Date.now(),
|
||||
connectedByUserId: owner.id,
|
||||
grantedScopes: ['openid'],
|
||||
};
|
||||
attachPersistence(appConnection, appConnections);
|
||||
|
||||
const invitation = new UserInvitation();
|
||||
invitation.id = 'invitation-1';
|
||||
invitation.data = {
|
||||
email: 'invite@example.com',
|
||||
token: 'token',
|
||||
status: 'pending',
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 1000,
|
||||
organizationRefs: [{
|
||||
organizationId: 'org-1',
|
||||
invitedByUserId: owner.id,
|
||||
invitedAt: Date.now(),
|
||||
roles: ['viewer'],
|
||||
}],
|
||||
};
|
||||
attachPersistence(invitation, invitations);
|
||||
|
||||
const billingPlan = new BillingPlan();
|
||||
billingPlan.id = 'billing-1';
|
||||
billingPlan.data.organizationId = 'org-1';
|
||||
attachPersistence(billingPlan, billingPlans);
|
||||
|
||||
await expect(manager.deleteOrganizationWithAudit({
|
||||
user: owner,
|
||||
organizationId: 'org-1',
|
||||
confirmationText: 'delete wrong',
|
||||
})).rejects.toThrow();
|
||||
|
||||
await manager.deleteOrganizationWithAudit({
|
||||
user: owner,
|
||||
organizationId: 'org-1',
|
||||
confirmationText: 'delete lossless',
|
||||
});
|
||||
|
||||
expect(organizations.size).toEqual(0);
|
||||
expect(roles.size).toEqual(0);
|
||||
expect(appConnections.size).toEqual(0);
|
||||
expect(billingPlans.size).toEqual(0);
|
||||
expect(invitation.data.status).toEqual('cancelled');
|
||||
expect(owner.data.connectedOrgs).toEqual([]);
|
||||
expect(member.data.connectedOrgs).toEqual([]);
|
||||
expect(activities[0].action).toEqual('org_deleted');
|
||||
expect(alerts[0].eventType).toEqual('org_deleted');
|
||||
});
|
||||
|
||||
tap.test('manages custom role definitions and cleans assignments and mappings on delete', async () => {
|
||||
const { manager, organizations, roles, users, appConnections } = createTestOrganizationManager();
|
||||
const owner = addUser(users, 'owner-1', 'owner@example.com', ['org-1']);
|
||||
const member = addUser(users, 'member-1', 'member@example.com', ['org-1']);
|
||||
const organization = addOrganization(organizations);
|
||||
addRole(roles, 'role-owner', owner.id, ['owner']);
|
||||
const memberRole = addRole(roles, 'role-member', member.id, ['viewer', 'finance']);
|
||||
|
||||
const roleDefinitions = await manager.upsertOrgRoleDefinition({
|
||||
user: owner,
|
||||
organizationId: organization.id,
|
||||
roleDefinition: {
|
||||
key: 'finance',
|
||||
name: 'Finance',
|
||||
description: 'Finance team access',
|
||||
},
|
||||
});
|
||||
expect(roleDefinitions).toHaveLength(1);
|
||||
expect(roleDefinitions[0].key).toEqual('finance');
|
||||
expect(await manager.assertRoleKeysAreValid(organization.id, ['finance'])).toEqual(['finance']);
|
||||
|
||||
const appConnection = new AppConnection();
|
||||
appConnection.id = 'connection-1';
|
||||
appConnection.data = {
|
||||
organizationId: organization.id,
|
||||
appId: 'app-1',
|
||||
appType: 'global',
|
||||
status: 'active',
|
||||
connectedAt: Date.now(),
|
||||
connectedByUserId: owner.id,
|
||||
grantedScopes: ['openid'],
|
||||
roleMappings: [{
|
||||
orgRoleKey: 'finance',
|
||||
appRoles: ['accountant'],
|
||||
permissions: ['invoices:read'],
|
||||
scopes: ['billing'],
|
||||
}],
|
||||
};
|
||||
attachPersistence(appConnection, appConnections);
|
||||
|
||||
await manager.deleteOrgRoleDefinition({
|
||||
user: owner,
|
||||
organizationId: organization.id,
|
||||
roleKey: 'finance',
|
||||
confirmationText: 'delete role finance',
|
||||
});
|
||||
|
||||
expect(organization.data.roleDefinitions).toEqual([]);
|
||||
expect(memberRole.data.roles).toEqual(['viewer']);
|
||||
expect(appConnection.data.roleMappings).toEqual([]);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user