Files
app/test/test.organization.node.ts

303 lines
10 KiB
TypeScript

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();