Files
pn-new-crm/tests/integration/client-relationship-port-isolation.test.ts
Matt Ciaccio ba89b61b3f
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 1m17s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped
fix(security): port-scope clientId/berthId/yachtId on interests + clientRelationships
Pass-6 findings — both MEDIUM cross-tenant FK injection.

- interests.service: createInterest/updateInterest/linkBerth accepted
  clientId/berthId/yachtId from the request body without verifying the
  referenced row belongs to the caller's port. getInterestById joins
  clients/berths/yachtTags on these FKs without a port filter, so a
  port-A caller could splice a foreign-port id and surface that
  tenant's clientName, mooringNumber, or yacht ownership on read.
  New assertInterestFksInPort helper guards all three surfaces.

- clients.service.createRelationship: accepted clientBId from the
  body without a port check; the relationship list endpoint joins
  clients without filtering by port, so the foreign client's name
  + email would render in the relationships tab. Now verifies
  clientBId belongs to portId and rejects self-relationships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 04:14:09 +02:00

75 lines
2.7 KiB
TypeScript

/**
* clients.service.createRelationship — tenant-FK validation tests.
*
* Covers the fix that requires both clientAId (the URL id) and clientBId
* (the body id) to belong to the caller's port. The list endpoint joins
* clientRelationships → clients without a port filter, so a foreign-port
* clientBId would surface that client's name in the relationship payload.
*/
import { describe, it, expect, beforeAll } from 'vitest';
describe('clients.service — createRelationship port isolation', () => {
let createRelationship: typeof import('@/lib/services/clients.service').createRelationship;
let makePort: typeof import('../helpers/factories').makePort;
let makeClient: typeof import('../helpers/factories').makeClient;
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;
beforeAll(async () => {
const svc = await import('@/lib/services/clients.service');
createRelationship = svc.createRelationship;
const factories = await import('../helpers/factories');
makePort = factories.makePort;
makeClient = factories.makeClient;
makeAuditMeta = factories.makeAuditMeta;
});
it('rejects createRelationship when clientBId belongs to a foreign port', async () => {
const portA = await makePort();
const portB = await makePort();
const localClient = await makeClient({ portId: portA.id });
const foreignClient = await makeClient({ portId: portB.id });
await expect(
createRelationship(
localClient.id,
portA.id,
{ clientBId: foreignClient.id, relationshipType: 'family' },
makeAuditMeta({ portId: portA.id }),
),
).rejects.toThrow(/clientBId not found in this port/);
});
it('rejects creating a self-relationship', async () => {
const portA = await makePort();
const localClient = await makeClient({ portId: portA.id });
await expect(
createRelationship(
localClient.id,
portA.id,
{ clientBId: localClient.id, relationshipType: 'family' },
makeAuditMeta({ portId: portA.id }),
),
).rejects.toThrow(/cannot have a relationship to themselves/);
});
it('accepts createRelationship when both clients are in the same port', async () => {
const portA = await makePort();
const clientA = await makeClient({ portId: portA.id });
const clientB = await makeClient({ portId: portA.id });
const rel = await createRelationship(
clientA.id,
portA.id,
{ clientBId: clientB.id, relationshipType: 'family' },
makeAuditMeta({ portId: portA.id }),
);
expect(rel.clientAId).toBe(clientA.id);
expect(rel.clientBId).toBe(clientB.id);
expect(rel.portId).toBe(portA.id);
});
});