@@ -318,6 +318,121 @@ describe('Record shares namespace (#3587 gap closure)', () => {
318318 } ) ;
319319} ) ;
320320
321+ describe ( 'Sharing rules namespace (#3587 gap closure)' , ( ) => {
322+ it ( 'shares.rules.list pins GET /sharing/rules with filters' , async ( ) => {
323+ const { client, fetchMock } = createMockClient ( { data : [ { name : 'team_leads' } ] } ) ;
324+ const rows = await client . shares . rules . list ( { object : 'lead' , activeOnly : true } ) ;
325+ expect ( String ( fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe (
326+ 'http://localhost:3000/api/v1/sharing/rules?object=lead&activeOnly=true' ,
327+ ) ;
328+ expect ( rows ) . toEqual ( [ { name : 'team_leads' } ] ) ;
329+ } ) ;
330+
331+ it ( 'shares.rules.save pins POST /sharing/rules' , async ( ) => {
332+ const { client, fetchMock } = createMockClient ( { name : 'team_leads' } ) ;
333+ await client . shares . rules . save ( { name : 'team_leads' , object : 'lead' , accessLevel : 'read' } ) ;
334+ const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
335+ expect ( String ( url ) ) . toBe ( 'http://localhost:3000/api/v1/sharing/rules' ) ;
336+ expect ( init . method ) . toBe ( 'POST' ) ;
337+ } ) ;
338+
339+ it ( 'shares.rules.get / delete / evaluate pin the :idOrName routes' , async ( ) => {
340+ const { client, fetchMock } = createMockClient ( { name : 'team_leads' } ) ;
341+ await client . shares . rules . get ( 'team_leads' ) ;
342+ expect ( String ( fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe ( 'http://localhost:3000/api/v1/sharing/rules/team_leads' ) ;
343+ await client . shares . rules . evaluate ( 'team_leads' ) ;
344+ expect ( String ( fetchMock . mock . calls [ 1 ] [ 0 ] ) ) . toBe ( 'http://localhost:3000/api/v1/sharing/rules/team_leads/evaluate' ) ;
345+ expect ( fetchMock . mock . calls [ 1 ] [ 1 ] . method ) . toBe ( 'POST' ) ;
346+
347+ const del = createMockClient ( undefined , 204 ) ;
348+ del . fetchMock . mockResolvedValue ( { ok : true , status : 204 , statusText : 'No Content' , json : async ( ) => { throw new Error ( 'no body' ) ; } , headers : new Headers ( ) } ) ;
349+ const out = await del . client . shares . rules . delete ( 'team_leads' ) ;
350+ expect ( String ( del . fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe ( 'http://localhost:3000/api/v1/sharing/rules/team_leads' ) ;
351+ expect ( out ) . toEqual ( { deleted : true } ) ;
352+ } ) ;
353+ } ) ;
354+
355+ describe ( 'Security explain & global search (#3587 gap closure)' , ( ) => {
356+ it ( 'security.explain pins POST /security/explain with the request body' , async ( ) => {
357+ const { client, fetchMock } = createMockClient ( { allowed : true } ) ;
358+ await client . security . explain ( { object : 'lead' , operation : 'update' , userId : 'u1' , recordId : 'r1' } ) ;
359+ const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
360+ expect ( String ( url ) ) . toBe ( 'http://localhost:3000/api/v1/security/explain' ) ;
361+ expect ( init . method ) . toBe ( 'POST' ) ;
362+ expect ( JSON . parse ( init . body ) ) . toEqual ( { object : 'lead' , operation : 'update' , userId : 'u1' , recordId : 'r1' } ) ;
363+ } ) ;
364+
365+ it ( 'search pins GET /search with q/objects/limit/perObject' , async ( ) => {
366+ const { client, fetchMock } = createMockClient ( { results : [ ] } ) ;
367+ await client . search ( 'acme' , { objects : [ 'lead' , 'account' ] , limit : 20 , perObject : 5 } ) ;
368+ expect ( String ( fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe (
369+ 'http://localhost:3000/api/v1/search?q=acme&objects=lead%2Caccount&limit=20&perObject=5' ,
370+ ) ;
371+ } ) ;
372+ } ) ;
373+
374+ describe ( 'Data actions, email, dataset query, external datasources (#3587 gap closure)' , ( ) => {
375+ it ( 'data.clone pins POST /data/:object/:id/clone and nests overrides' , async ( ) => {
376+ const { client, fetchMock } = createMockClient ( { id : 'new1' } ) ;
377+ await client . data . clone ( 'lead' , 'rec1' , { name : 'Copy of Acme' } ) ;
378+ const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
379+ expect ( String ( url ) ) . toBe ( 'http://localhost:3000/api/v1/data/lead/rec1/clone' ) ;
380+ expect ( init . method ) . toBe ( 'POST' ) ;
381+ expect ( JSON . parse ( init . body ) ) . toEqual ( { overrides : { name : 'Copy of Acme' } } ) ;
382+ } ) ;
383+
384+ it ( 'data.export pins GET /data/:object/export and returns the raw Response' , async ( ) => {
385+ const { client, fetchMock } = createMockClient ( { } ) ;
386+ const res = await client . data . export ( 'lead' , { format : 'xlsx' , limit : 100 , filter : { status : 'open' } , orderby : 'name:asc' , header : false } ) ;
387+ expect ( String ( fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe (
388+ 'http://localhost:3000/api/v1/data/lead/export?format=xlsx&limit=100&filter=%7B%22status%22%3A%22open%22%7D&orderby=name%3Aasc&header=false' ,
389+ ) ;
390+ // A file stream, not a JSON envelope — the raw Response comes back.
391+ expect ( typeof ( res as any ) . json ) . toBe ( 'function' ) ;
392+ } ) ;
393+
394+ it ( 'email.send pins POST /email/send' , async ( ) => {
395+ const { client, fetchMock } = createMockClient ( { status : 'sent' , id : 'm1' } ) ;
396+ await client . email . send ( { to : 'a@example.com' , subject : 'Hello' , text : 'hi' } ) ;
397+ const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
398+ expect ( String ( url ) ) . toBe ( 'http://localhost:3000/api/v1/email/send' ) ;
399+ expect ( init . method ) . toBe ( 'POST' ) ;
400+ } ) ;
401+
402+ it ( 'analytics.queryDataset pins POST /analytics/dataset/query' , async ( ) => {
403+ const { client, fetchMock } = createMockClient ( { rows : [ ] } ) ;
404+ await client . analytics . queryDataset ( { datasetName : 'sales' , selection : { measures : [ 'amount_sum' ] } } ) ;
405+ const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
406+ expect ( String ( url ) ) . toBe ( 'http://localhost:3000/api/v1/analytics/dataset/query' ) ;
407+ expect ( JSON . parse ( init . body ) ) . toEqual ( { datasetName : 'sales' , selection : { measures : [ 'amount_sum' ] } } ) ;
408+ } ) ;
409+
410+ it ( 'datasources.external.* pin the five federation-admin routes' , async ( ) => {
411+ const { client, fetchMock } = createMockClient ( { tables : [ ] } ) ;
412+ await client . datasources . external . listTables ( 'pg_main' , { schema : 'public' } ) ;
413+ expect ( String ( fetchMock . mock . calls [ 0 ] [ 0 ] ) ) . toBe (
414+ 'http://localhost:3000/api/v1/datasources/pg_main/external/tables?schema=public' ,
415+ ) ;
416+ await client . datasources . external . draft ( 'pg_main' , 'customers' ) ;
417+ expect ( String ( fetchMock . mock . calls [ 1 ] [ 0 ] ) ) . toBe (
418+ 'http://localhost:3000/api/v1/datasources/pg_main/external/tables/customers/draft' ,
419+ ) ;
420+ await client . datasources . external . import ( 'pg_main' , 'customers' , { namespace : 'crm' } ) ;
421+ expect ( String ( fetchMock . mock . calls [ 2 ] [ 0 ] ) ) . toBe (
422+ 'http://localhost:3000/api/v1/datasources/pg_main/external/tables/customers/import' ,
423+ ) ;
424+ await client . datasources . external . refreshCatalog ( 'pg_main' ) ;
425+ expect ( String ( fetchMock . mock . calls [ 3 ] [ 0 ] ) ) . toBe (
426+ 'http://localhost:3000/api/v1/datasources/pg_main/external/refresh-catalog' ,
427+ ) ;
428+ await client . datasources . external . validate ( 'pg_main' ) ;
429+ expect ( String ( fetchMock . mock . calls [ 4 ] [ 0 ] ) ) . toBe (
430+ 'http://localhost:3000/api/v1/datasources/pg_main/external/validate' ,
431+ ) ;
432+ for ( let i = 1 ; i <= 4 ; i ++ ) expect ( fetchMock . mock . calls [ i ] [ 1 ] . method ) . toBe ( 'POST' ) ;
433+ } ) ;
434+ } ) ;
435+
321436describe ( 'Approvals namespace (ADR-0019)' , ( ) => {
322437 it ( 'should list approval requests with filters' , async ( ) => {
323438 const { client, fetchMock } = createMockClient ( {
0 commit comments