95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
|
|
/**
|
||
|
|
* CM-9: proxy service — per-entity CRUD/upsert, tenant guard, and the
|
||
|
|
* yacht → interest → client resolution precedence.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
import { createInterest } from '@/lib/services/interests.service';
|
||
|
|
import {
|
||
|
|
clearProxy,
|
||
|
|
getProxy,
|
||
|
|
resolveEffectiveProxy,
|
||
|
|
setProxy,
|
||
|
|
} from '@/lib/services/proxies.service';
|
||
|
|
import { makeAuditMeta, makeClient, makePort, makeYacht } from '../helpers/factories';
|
||
|
|
|
||
|
|
describe('proxies.service (CM-9)', () => {
|
||
|
|
it('sets, reads, upserts and clears a client proxy', async () => {
|
||
|
|
const port = await makePort();
|
||
|
|
const meta = makeAuditMeta({ portId: port.id });
|
||
|
|
const client = await makeClient({ portId: port.id });
|
||
|
|
|
||
|
|
expect(await getProxy(port.id, 'client', client.id)).toBeNull();
|
||
|
|
|
||
|
|
const p = await setProxy(
|
||
|
|
port.id,
|
||
|
|
'client',
|
||
|
|
client.id,
|
||
|
|
{ name: 'Broker Bob', email: 'bob@example.com' },
|
||
|
|
meta,
|
||
|
|
);
|
||
|
|
expect(p.name).toBe('Broker Bob');
|
||
|
|
expect(p.email).toBe('bob@example.com');
|
||
|
|
|
||
|
|
// Upsert: one proxy per entity — setting again updates the same row.
|
||
|
|
const p2 = await setProxy(
|
||
|
|
port.id,
|
||
|
|
'client',
|
||
|
|
client.id,
|
||
|
|
{ name: 'Broker Bob', email: '', phone: '+100' },
|
||
|
|
meta,
|
||
|
|
);
|
||
|
|
expect(p2.id).toBe(p.id);
|
||
|
|
expect(p2.email).toBeNull(); // empty string normalised to null
|
||
|
|
expect(p2.phone).toBe('+100');
|
||
|
|
|
||
|
|
await clearProxy(port.id, 'client', client.id, meta);
|
||
|
|
expect(await getProxy(port.id, 'client', client.id)).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects an entity from a foreign port', async () => {
|
||
|
|
const portA = await makePort();
|
||
|
|
const portB = await makePort();
|
||
|
|
const meta = makeAuditMeta({ portId: portA.id });
|
||
|
|
const foreign = await makeClient({ portId: portB.id });
|
||
|
|
await expect(setProxy(portA.id, 'client', foreign.id, { name: 'X' }, meta)).rejects.toThrow(
|
||
|
|
/not found in this port/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('resolves the most specific proxy: yacht → interest → client', async () => {
|
||
|
|
const port = await makePort();
|
||
|
|
const meta = makeAuditMeta({ portId: port.id });
|
||
|
|
const client = await makeClient({ portId: port.id });
|
||
|
|
const interest = await createInterest(
|
||
|
|
port.id,
|
||
|
|
{ clientId: client.id, pipelineStage: 'enquiry', tagIds: [], reminderEnabled: false },
|
||
|
|
meta,
|
||
|
|
);
|
||
|
|
const yacht = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: client.id });
|
||
|
|
|
||
|
|
await setProxy(port.id, 'client', client.id, { name: 'Client PoC' }, meta);
|
||
|
|
await setProxy(port.id, 'interest', interest.id, { name: 'Deal PoC' }, meta);
|
||
|
|
await setProxy(port.id, 'yacht', yacht.id, { name: 'Vessel PoC' }, meta);
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
portId: port.id,
|
||
|
|
clientId: client.id,
|
||
|
|
interestId: interest.id,
|
||
|
|
yachtId: yacht.id,
|
||
|
|
};
|
||
|
|
expect((await resolveEffectiveProxy(ctx))?.source).toBe('yacht');
|
||
|
|
|
||
|
|
await clearProxy(port.id, 'yacht', yacht.id, meta);
|
||
|
|
expect((await resolveEffectiveProxy(ctx))?.source).toBe('interest');
|
||
|
|
|
||
|
|
await clearProxy(port.id, 'interest', interest.id, meta);
|
||
|
|
const eff = await resolveEffectiveProxy(ctx);
|
||
|
|
expect(eff?.source).toBe('client');
|
||
|
|
expect(eff?.proxy.name).toBe('Client PoC');
|
||
|
|
|
||
|
|
await clearProxy(port.id, 'client', client.id, meta);
|
||
|
|
expect(await resolveEffectiveProxy(ctx)).toBeNull();
|
||
|
|
});
|
||
|
|
});
|