feat(proxies): CM-9 backend — polymorphic point-of-contact + resolver
- proxies table (migration 0095, port_id cascade), one per client/interest/yacht - service: get/set(upsert)/clear + resolveEffectiveProxy (yacht → interest → client precedence), port-scoped with entity-in-port guard - per-entity sub-resource routes (/clients|interests|yachts/[id]/proxy) reusing each entity's existing view/edit permission (no new permission resource) - 3 integration tests (CRUD/upsert, tenant guard, resolution precedence) Backend only — ProxyCard UI on the 3 detail pages to follow. tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
src/app/api/v1/clients/[id]/proxy/route.ts
Normal file
8
src/app/api/v1/clients/[id]/proxy/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
|
import { makeProxyHandlers } from '@/lib/api/proxy-route-handlers';
|
||||||
|
|
||||||
|
const { getHandler, putHandler, deleteHandler } = makeProxyHandlers('client');
|
||||||
|
|
||||||
|
export const GET = withAuth(withPermission('clients', 'view', getHandler));
|
||||||
|
export const PUT = withAuth(withPermission('clients', 'edit', putHandler));
|
||||||
|
export const DELETE = withAuth(withPermission('clients', 'edit', deleteHandler));
|
||||||
8
src/app/api/v1/interests/[id]/proxy/route.ts
Normal file
8
src/app/api/v1/interests/[id]/proxy/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
|
import { makeProxyHandlers } from '@/lib/api/proxy-route-handlers';
|
||||||
|
|
||||||
|
const { getHandler, putHandler, deleteHandler } = makeProxyHandlers('interest');
|
||||||
|
|
||||||
|
export const GET = withAuth(withPermission('interests', 'view', getHandler));
|
||||||
|
export const PUT = withAuth(withPermission('interests', 'edit', putHandler));
|
||||||
|
export const DELETE = withAuth(withPermission('interests', 'edit', deleteHandler));
|
||||||
8
src/app/api/v1/yachts/[id]/proxy/route.ts
Normal file
8
src/app/api/v1/yachts/[id]/proxy/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
|
import { makeProxyHandlers } from '@/lib/api/proxy-route-handlers';
|
||||||
|
|
||||||
|
const { getHandler, putHandler, deleteHandler } = makeProxyHandlers('yacht');
|
||||||
|
|
||||||
|
export const GET = withAuth(withPermission('yachts', 'view', getHandler));
|
||||||
|
export const PUT = withAuth(withPermission('yachts', 'edit', putHandler));
|
||||||
|
export const DELETE = withAuth(withPermission('yachts', 'edit', deleteHandler));
|
||||||
56
src/lib/api/proxy-route-handlers.ts
Normal file
56
src/lib/api/proxy-route-handlers.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* CM-9: shared GET/PUT/DELETE handlers for the per-entity proxy sub-resource
|
||||||
|
* (`/api/v1/{clients|interests|yachts}/[id]/proxy`). Each entity's route.ts
|
||||||
|
* binds these with its own permission resource so we reuse existing
|
||||||
|
* clients/interests/yachts gating instead of a new permission.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { type RouteHandler } from '@/lib/api/helpers';
|
||||||
|
import { parseBody } from '@/lib/api/route-helpers';
|
||||||
|
import { errorResponse } from '@/lib/errors';
|
||||||
|
import { clearProxy, getProxy, setProxy } from '@/lib/services/proxies.service';
|
||||||
|
import { setProxySchema, type ProxyEntityType } from '@/lib/validators/proxies';
|
||||||
|
|
||||||
|
export function makeProxyHandlers(entityType: ProxyEntityType) {
|
||||||
|
const getHandler: RouteHandler = async (req, ctx, params) => {
|
||||||
|
try {
|
||||||
|
const proxy = await getProxy(ctx.portId, entityType, params.id!);
|
||||||
|
return NextResponse.json({ data: proxy });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const putHandler: RouteHandler = async (req, ctx, params) => {
|
||||||
|
try {
|
||||||
|
const body = await parseBody(req, setProxySchema);
|
||||||
|
const proxy = await setProxy(ctx.portId, entityType, params.id!, body, {
|
||||||
|
userId: ctx.userId,
|
||||||
|
portId: ctx.portId,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
});
|
||||||
|
return NextResponse.json({ data: proxy });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteHandler: RouteHandler = async (req, ctx, params) => {
|
||||||
|
try {
|
||||||
|
await clearProxy(ctx.portId, entityType, params.id!, {
|
||||||
|
userId: ctx.userId,
|
||||||
|
portId: ctx.portId,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
});
|
||||||
|
return new NextResponse(null, { status: 204 });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getHandler, putHandler, deleteHandler };
|
||||||
|
}
|
||||||
24
src/lib/db/migrations/0095_proxies.sql
Normal file
24
src/lib/db/migrations/0095_proxies.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- 0095_proxies.sql
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- CM-9: per-entity point-of-contact ("proxy") attachable to a client, interest,
|
||||||
|
-- or yacht. At most one per entity; outbound comms resolve the most specific
|
||||||
|
-- via yacht → interest → client. entity_id is polymorphic (no FK; validated in
|
||||||
|
-- the service against the right table). Idempotent — safe to re-run.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS proxies (
|
||||||
|
id text PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||||
|
port_id text NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
|
||||||
|
entity_type text NOT NULL,
|
||||||
|
entity_id text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
email text,
|
||||||
|
phone text,
|
||||||
|
relationship text,
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_proxies_entity ON proxies(port_id, entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_proxies_entity ON proxies(entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_proxies_port ON proxies(port_id);
|
||||||
@@ -10,6 +10,9 @@ export * from './clients';
|
|||||||
// Client groups (CM-1 - mailing/segment groups)
|
// Client groups (CM-1 - mailing/segment groups)
|
||||||
export * from './client-groups';
|
export * from './client-groups';
|
||||||
|
|
||||||
|
// Proxies / points-of-contact (CM-9 - polymorphic across client/interest/yacht)
|
||||||
|
export * from './proxies';
|
||||||
|
|
||||||
// Companies
|
// Companies
|
||||||
export * from './companies';
|
export * from './companies';
|
||||||
|
|
||||||
|
|||||||
48
src/lib/db/schema/proxies.ts
Normal file
48
src/lib/db/schema/proxies.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Proxies / points-of-contact (CM-9).
|
||||||
|
*
|
||||||
|
* A `proxy` is a designated contact person who acts on behalf of an entity.
|
||||||
|
* Polymorphic: attachable to a `client` (default), an `interest` (per-deal
|
||||||
|
* override), or a `yacht` (per-vessel override). At most one proxy per entity
|
||||||
|
* (unique index). Outbound comms resolve the most specific proxy via the chain
|
||||||
|
* yacht → interest → client (see resolveEffectiveProxy in proxies.service).
|
||||||
|
*
|
||||||
|
* `entity_id` is polymorphic (no FK) — validated against the right table in the
|
||||||
|
* service, same pattern as polymorphic yacht ownership / notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { index, pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
import { ports } from './ports';
|
||||||
|
|
||||||
|
export const proxies = pgTable(
|
||||||
|
'proxies',
|
||||||
|
{
|
||||||
|
id: text('id')
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
|
portId: text('port_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => ports.id, { onDelete: 'cascade' }),
|
||||||
|
/** 'client' | 'interest' | 'yacht' */
|
||||||
|
entityType: text('entity_type').notNull(),
|
||||||
|
entityId: text('entity_id').notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
email: text('email'),
|
||||||
|
phone: text('phone'),
|
||||||
|
/** Free-form relationship label, e.g. broker / spouse / assistant / legal. */
|
||||||
|
relationship: text('relationship'),
|
||||||
|
notes: text('notes'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
// At most one proxy per entity.
|
||||||
|
uniqueIndex('uniq_proxies_entity').on(table.portId, table.entityType, table.entityId),
|
||||||
|
index('idx_proxies_entity').on(table.entityType, table.entityId),
|
||||||
|
index('idx_proxies_port').on(table.portId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Proxy = typeof proxies.$inferSelect;
|
||||||
|
export type NewProxy = typeof proxies.$inferInsert;
|
||||||
164
src/lib/services/proxies.service.ts
Normal file
164
src/lib/services/proxies.service.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* CM-9: proxy / point-of-contact service.
|
||||||
|
*
|
||||||
|
* A proxy is a designated contact attached to a client, interest, or yacht
|
||||||
|
* (one per entity). `resolveEffectiveProxy` picks the most specific for an
|
||||||
|
* outbound-comms context via the chain yacht → interest → client. All
|
||||||
|
* operations are port-scoped; the entity is verified to belong to the port.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { clients, interests, proxies, yachts } from '@/lib/db/schema';
|
||||||
|
import type { Proxy } from '@/lib/db/schema';
|
||||||
|
import { NotFoundError } from '@/lib/errors';
|
||||||
|
import type { ProxyEntityType, SetProxyInput } from '@/lib/validators/proxies';
|
||||||
|
|
||||||
|
const norm = (v?: string | null): string | null => {
|
||||||
|
const t = v?.trim();
|
||||||
|
return t ? t : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function assertEntityInPort(
|
||||||
|
entityType: ProxyEntityType,
|
||||||
|
entityId: string,
|
||||||
|
portId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
let exists = false;
|
||||||
|
if (entityType === 'client') {
|
||||||
|
const [r] = await db
|
||||||
|
.select({ id: clients.id })
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.id, entityId), eq(clients.portId, portId)))
|
||||||
|
.limit(1);
|
||||||
|
exists = !!r;
|
||||||
|
} else if (entityType === 'interest') {
|
||||||
|
const [r] = await db
|
||||||
|
.select({ id: interests.id })
|
||||||
|
.from(interests)
|
||||||
|
.where(and(eq(interests.id, entityId), eq(interests.portId, portId)))
|
||||||
|
.limit(1);
|
||||||
|
exists = !!r;
|
||||||
|
} else {
|
||||||
|
const [r] = await db
|
||||||
|
.select({ id: yachts.id })
|
||||||
|
.from(yachts)
|
||||||
|
.where(and(eq(yachts.id, entityId), eq(yachts.portId, portId)))
|
||||||
|
.limit(1);
|
||||||
|
exists = !!r;
|
||||||
|
}
|
||||||
|
if (!exists) throw new NotFoundError(`${entityType} not found in this port`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProxy(
|
||||||
|
portId: string,
|
||||||
|
entityType: ProxyEntityType,
|
||||||
|
entityId: string,
|
||||||
|
): Promise<Proxy | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(proxies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(proxies.portId, portId),
|
||||||
|
eq(proxies.entityType, entityType),
|
||||||
|
eq(proxies.entityId, entityId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setProxy(
|
||||||
|
portId: string,
|
||||||
|
entityType: ProxyEntityType,
|
||||||
|
entityId: string,
|
||||||
|
data: SetProxyInput,
|
||||||
|
meta: AuditMeta,
|
||||||
|
): Promise<Proxy> {
|
||||||
|
await assertEntityInPort(entityType, entityId, portId);
|
||||||
|
const next = {
|
||||||
|
name: data.name.trim(),
|
||||||
|
email: norm(data.email),
|
||||||
|
phone: norm(data.phone),
|
||||||
|
relationship: norm(data.relationship),
|
||||||
|
notes: norm(data.notes),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
const [row] = await db
|
||||||
|
.insert(proxies)
|
||||||
|
.values({ portId, entityType, entityId, ...next })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [proxies.portId, proxies.entityType, proxies.entityId],
|
||||||
|
set: next,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
if (!row) throw new NotFoundError('Failed to save proxy');
|
||||||
|
void createAuditLog({
|
||||||
|
...meta,
|
||||||
|
action: 'update',
|
||||||
|
entityType: `proxy_${entityType}`,
|
||||||
|
entityId,
|
||||||
|
newValue: toAuditJson(next),
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearProxy(
|
||||||
|
portId: string,
|
||||||
|
entityType: ProxyEntityType,
|
||||||
|
entityId: string,
|
||||||
|
meta: AuditMeta,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.delete(proxies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(proxies.portId, portId),
|
||||||
|
eq(proxies.entityType, entityType),
|
||||||
|
eq(proxies.entityId, entityId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
void createAuditLog({
|
||||||
|
...meta,
|
||||||
|
action: 'delete',
|
||||||
|
entityType: `proxy_${entityType}`,
|
||||||
|
entityId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectiveProxy {
|
||||||
|
proxy: Proxy;
|
||||||
|
/** Which level the proxy was resolved from. */
|
||||||
|
source: ProxyEntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the most specific proxy for an outbound-comms context.
|
||||||
|
* Precedence: yacht override → interest override → client default.
|
||||||
|
* Returns null when no proxy is set anywhere in the chain (caller falls back
|
||||||
|
* to the client themselves).
|
||||||
|
*/
|
||||||
|
export async function resolveEffectiveProxy(args: {
|
||||||
|
portId: string;
|
||||||
|
clientId?: string | null;
|
||||||
|
interestId?: string | null;
|
||||||
|
yachtId?: string | null;
|
||||||
|
}): Promise<EffectiveProxy | null> {
|
||||||
|
const { portId, clientId, interestId, yachtId } = args;
|
||||||
|
if (yachtId) {
|
||||||
|
const p = await getProxy(portId, 'yacht', yachtId);
|
||||||
|
if (p) return { proxy: p, source: 'yacht' };
|
||||||
|
}
|
||||||
|
if (interestId) {
|
||||||
|
const p = await getProxy(portId, 'interest', interestId);
|
||||||
|
if (p) return { proxy: p, source: 'interest' };
|
||||||
|
}
|
||||||
|
if (clientId) {
|
||||||
|
const p = await getProxy(portId, 'client', clientId);
|
||||||
|
if (p) return { proxy: p, source: 'client' };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
17
src/lib/validators/proxies.ts
Normal file
17
src/lib/validators/proxies.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/** CM-9: proxy / point-of-contact. */
|
||||||
|
|
||||||
|
export const PROXY_ENTITY_TYPES = ['client', 'interest', 'yacht'] as const;
|
||||||
|
export type ProxyEntityType = (typeof PROXY_ENTITY_TYPES)[number];
|
||||||
|
|
||||||
|
export const setProxySchema = z.object({
|
||||||
|
name: z.string().trim().min(1, 'Name is required').max(200),
|
||||||
|
// Loose contact fields — empty strings are normalised to null in the service.
|
||||||
|
email: z.string().trim().max(320).nullish(),
|
||||||
|
phone: z.string().trim().max(50).nullish(),
|
||||||
|
relationship: z.string().trim().max(100).nullish(),
|
||||||
|
notes: z.string().trim().max(2000).nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SetProxyInput = z.infer<typeof setProxySchema>;
|
||||||
94
tests/integration/proxies.test.ts
Normal file
94
tests/integration/proxies.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user