From 626f02bc69cf7e2a8f2392e9cc73959a310c8221 Mon Sep 17 00:00:00 2001 From: Volodymyr Ishchenko Date: Fri, 20 Jun 2025 16:35:52 +0300 Subject: [PATCH 1/4] consistent custom attributes for HELLO + separate tests set --- src/wampy.js | 3 +- test/custom-attributes-data.js | 208 +++++++++++++++++++ test/custom-attributes-test.js | 352 +++++++++++++++++++++++++++++++++ test/fake-ws-custom-attrs.js | 146 ++++++++++++++ 4 files changed, 708 insertions(+), 1 deletion(-) create mode 100644 test/custom-attributes-data.js create mode 100644 test/custom-attributes-test.js create mode 100644 test/fake-ws-custom-attrs.js diff --git a/src/wampy.js b/src/wampy.js index 9e3fd0b..4a13543 100644 --- a/src/wampy.js +++ b/src/wampy.js @@ -838,10 +838,11 @@ class Wampy { } const messageOptions = { - ...helloCustomDetails, + ...this._extractCustomOptions(helloCustomDetails), ...this._wamp_features, ...(authid ? { authid, authmethods, authextra } : {}), }; + // WAMP SPEC: [HELLO, Realm|string, Details|dict] const encodedMessage = this._encode([WAMP_MSG_SPEC.HELLO, realm, messageOptions]); if (encodedMessage) { diff --git a/test/custom-attributes-data.js b/test/custom-attributes-data.js new file mode 100644 index 0000000..7f16562 --- /dev/null +++ b/test/custom-attributes-data.js @@ -0,0 +1,208 @@ +import { WAMP_MSG_SPEC } from '../src/constants.js'; + +const customAttrsData = [ + // EVENT with custom details for custom.event.topic subscription + { + trigger: { + messageType: WAMP_MSG_SPEC.SUBSCRIBE, + condition: (msg) => msg[3] === 'custom.event.topic' + }, + response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 5001] + }, + { + // Auto-send EVENT after subscription + trigger: { + messageType: WAMP_MSG_SPEC.SUBSCRIBED + }, + response: [WAMP_MSG_SPEC.EVENT, 5001, 9001, { + _publisher_info: 'system_publisher', + _event_priority: 'high', + _timestamp: Date.now() + }, ['event', 'data']], + delay: 5 + }, + + // RESULT with custom details for custom.result.procedure call + { + trigger: { + messageType: WAMP_MSG_SPEC.CALL, + condition: (msg) => msg[3] === 'custom.result.procedure' + }, + response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', { + _execution_time: 42, + _server_version: '2.1.0', + _cache_hit: true + }, ['custom', 'result']] + }, + + // INVOCATION with custom details for custom.invocation.rpc registration + { + trigger: { + messageType: WAMP_MSG_SPEC.REGISTER, + condition: (msg) => msg[3] === 'custom.invocation.rpc' + }, + response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 6001] + }, + { + // Auto-send INVOCATION after registration + trigger: { + messageType: WAMP_MSG_SPEC.REGISTERED + }, + response: [WAMP_MSG_SPEC.INVOCATION, 7001, 6001, { + _caller_info: 'admin_client', + _priority_level: 9, + _auth_context: 'elevated' + }, ['invoke', 'args']], + delay: 5 + }, + + // Test custom attributes in outgoing messages by checking what client sends + { + trigger: { + messageType: WAMP_MSG_SPEC.SUBSCRIBE, + condition: (msg) => { + const options = msg[2]; + // Verify custom attributes are present in SUBSCRIBE options + return options._tracking_id === 'sub_12345' && + options._priority === 'high' && + options._custom_auth === 'bearer_token'; + } + }, + response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 1001] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.PUBLISH, + condition: (msg) => { + const options = msg[2]; + // Verify custom attributes are present in PUBLISH options + return options._tracking_id === 'pub_67890' && + options._priority === 'urgent' && + options._routing_hint === 'datacenter_west'; + } + }, + response: [WAMP_MSG_SPEC.PUBLISHED, 'REQUEST_ID', 2001] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.CALL, + condition: (msg) => { + const options = msg[2]; + // Verify custom attributes are present in CALL options + return options._request_id === 'call_abcdef' && + options._timeout_override === 30000 && + options._auth_context === 'admin_user'; + } + }, + response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['custom', 'call', 'result'] }]] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.CANCEL, + condition: (msg) => { + const options = msg[2]; + // Verify custom attributes are present in CANCEL options + return options._cancel_reason === 'user_requested' && + options._priority === 'immediate' && + options.mode === 'kill'; + } + }, + // CANCEL doesn't get a direct response, but we can verify it was received + response: null + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.REGISTER, + condition: (msg) => { + const options = msg[2]; + // Verify custom attributes are present in REGISTER options + return options._handler_version === '2.1.0' && + options._load_balancing === 'round_robin' && + options._timeout_hint === 5000; + } + }, + response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 3001] + }, + + // Test mixed standard + custom options + { + trigger: { + messageType: WAMP_MSG_SPEC.CALL, + condition: (msg) => { + const options = msg[2]; + // Verify both standard and custom attributes + return options.timeout === 5000 && + options.disclose_me === true && + options._tracking_id === 'mixed_123' && + options._priority === 'high'; + } + }, + response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['mixed', 'options', 'result'] }]] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.PUBLISH, + condition: (msg) => { + const options = msg[2]; + // Verify both standard and custom attributes + return options.exclude_me === false && + options.disclose_me === true && + options._event_type === 'notification' && + options._source_system === 'billing'; + } + }, + response: [WAMP_MSG_SPEC.PUBLISHED, 'REQUEST_ID', 4001] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.SUBSCRIBE, + condition: (msg) => { + const options = msg[2]; + // Verify both standard and custom attributes for prefix subscription + return options.match === 'prefix' && + options._subscription_type === 'live' && + options._filter_level === 'debug'; + } + }, + response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 5002] + }, + + { + trigger: { + messageType: WAMP_MSG_SPEC.REGISTER, + condition: (msg) => { + const options = msg[2]; + // Verify both standard and custom attributes + return options.invoke === 'roundrobin' && + options._handler_type === 'async' && + options._max_concurrency === 10; + } + }, + response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 6002] + }, + + // Test invalid custom attributes (should be filtered out) + { + trigger: { + messageType: WAMP_MSG_SPEC.CALL, + condition: (msg) => { + const options = msg[2]; + // Should only have valid custom attributes, invalid ones filtered + return options._valid_attr === 'valid' && + !options._x && // Too short - should be filtered + !options._X && // Uppercase - should be filtered + !options['_invalid-dash'] && // Contains dash - should be filtered + !options.no_underscore; // No underscore - should be filtered + } + }, + response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['filtered', 'result'] }]] + } +]; + +export default customAttrsData; \ No newline at end of file diff --git a/test/custom-attributes-test.js b/test/custom-attributes-test.js new file mode 100644 index 0000000..926d883 --- /dev/null +++ b/test/custom-attributes-test.js @@ -0,0 +1,352 @@ +import { expect } from 'chai'; +import { Wampy } from '../src/wampy.js'; +import WebSocket from './fake-ws-custom-attrs.js'; + +describe('Wampy.js Custom Attributes (WAMP spec 3.1)', function () { + this.timeout(1000); + + let wampy; + + before(function () { + globalThis.WebSocket = WebSocket; + }); + + beforeEach(function () { + wampy = new Wampy('ws://fake.server.url/ws/', { + realm: 'AppRealm', + ws: WebSocket + }); + }); + + afterEach(async function () { + if (wampy.getOpStatus().code === 0) { + await wampy.disconnect(); + } + }); + + describe('Client-to-Router Messages (Options/Details)', function () { + + // WAMP SPEC: [HELLO, Realm|string, Details|dict] + it('HELLO.Details - supports custom attributes via helloCustomDetails', async function () { + const helloCustomDetails = { + _client_version: '7.2.0', + _tracking_session: 'session_12345', + _environment: 'production' + }; + + const wampyWithCustomHello = new Wampy('ws://fake.server.url/ws/', { + realm: 'AppRealm', + ws: WebSocket, + helloCustomDetails + }); + + // This should work - HELLO uses helloCustomDetails, not _extractCustomOptions + await wampyWithCustomHello.connect(); + expect(wampyWithCustomHello.getOpStatus().code).to.equal(0); + await wampyWithCustomHello.disconnect(); + }); + + // WAMP SPEC: [SUBSCRIBE, Request|id, Options|dict, Topic|uri] + it('SUBSCRIBE.Options - supports custom attributes', async function () { + await wampy.connect(); + + const customOptions = { + _tracking_id: 'sub_12345', + _priority: 'high', + _custom_auth: 'bearer_token' + }; + + const result = await wampy.subscribe('test.topic', function () {}, customOptions); + expect(result).to.have.property('subscriptionId'); + }); + + // WAMP SPEC: [PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list, ArgumentsKw|dict] + it('PUBLISH.Options - supports custom attributes', async function () { + await wampy.connect(); + + const customOptions = { + _tracking_id: 'pub_67890', + _priority: 'urgent', + _routing_hint: 'datacenter_west' + }; + + const result = await wampy.publish('test.topic', 'test payload', customOptions); + expect(result).to.have.property('publicationId'); + }); + + // WAMP SPEC: [CALL, Request|id, Options|dict, Procedure|uri, Arguments|list, ArgumentsKw|dict] + it('CALL.Options - supports custom attributes', async function () { + await wampy.connect(); + + const customOptions = { + _request_id: 'call_abcdef', + _timeout_override: 30000, + _auth_context: 'admin_user' + }; + + const result = await wampy.call('test.procedure', ['arg1'], customOptions); + expect(result).to.have.property('argsList'); + }); + + // WAMP SPEC: [CANCEL, CALL.Request|id, Options|dict] + it('CANCEL.Options - supports custom attributes', async function () { + await wampy.connect(); + + // Start a call first + wampy.call('slow.procedure', []); + const reqId = wampy.getOpStatus().reqId; + + const customOptions = { + mode: 'kill', + _cancel_reason: 'user_requested', + _priority: 'immediate' + }; + + const result = wampy.cancel(reqId, customOptions); + expect(result).to.be.true; + }); + + // WAMP SPEC: [REGISTER, Request|id, Options|dict, Procedure|uri] + it('REGISTER.Options - supports custom attributes', async function () { + await wampy.connect(); + + const customOptions = { + _handler_version: '2.1.0', + _load_balancing: 'round_robin', + _timeout_hint: 5000 + }; + + const result = await wampy.register('test.rpc', function () { return { result: 'ok' }; }, customOptions); + expect(result).to.have.property('registrationId'); + }); + + // WAMP SPEC: [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict] + it('YIELD.Options - supports custom attributes in RPC handler', async function () { + await wampy.connect(); + + await wampy.register('custom.yield.rpc', function ({ result_handler }) { + const customYieldOptions = { + _processing_time: 150, + _cache_hint: 'no_cache', + _result_version: '1.0' + }; + + result_handler({ + argsList: ['custom result'], + options: customYieldOptions + }); + }); + + // This will be tested when an INVOCATION comes in + expect(wampy.getOpStatus().code).to.equal(0); + }); + + // WAMP SPEC: [GOODBYE, Details|dict, Reason|uri] + it('GOODBYE.Details - client-initiated disconnect (no custom attributes needed)', async function () { + await wampy.connect(); + + // GOODBYE is sent by client during disconnect, but doesn't support custom attributes + // This is just to verify the flow works + await wampy.disconnect(); + expect(wampy.getOpStatus().code).to.equal(0); + }); + }); + + describe('Router-to-Client Messages (Details/Options)', function () { + + // WAMP SPEC: [WELCOME, Session|id, Details|dict] + it('WELCOME.Details - receives custom attributes from router', async function () { + await wampy.connect(); + + // WELCOME details are generated by router and passed through automatically + // Our mock doesn't send custom attributes, but in real scenario they would be passed through + expect(wampy.getSessionId()).to.not.be.null; + }); + + // WAMP SPEC: [ABORT, Details|dict, Reason|uri] + it('ABORT.Details - receives custom attributes from router', async function () { + // ABORT is sent by router when rejecting connection + // Would be tested with a router that sends ABORT with custom details + // For now, just verify the mechanism exists + expect(true).to.be.true; // Placeholder - router-generated message + }); + + // WAMP SPEC: [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list, ArgumentsKw|dict] + it('ERROR.Details - receives custom attributes from router', async function () { + // ERROR details are generated by router and passed through automatically + // Would contain custom attributes if router includes them + expect(true).to.be.true; // Placeholder - router-generated message + }); + + // WAMP SPEC: [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentsKw|dict] + it('EVENT.Details - receives custom attributes from router', async function () { + await wampy.connect(); + + let receivedDetails = null; + await wampy.subscribe('custom.event.topic', function ({ details }) { + receivedDetails = details; + }); + + // In a real scenario, router would send EVENT with custom details + // Our mock doesn't auto-send events, but the passthrough mechanism exists + expect(receivedDetails).to.be.null; // No event received yet, which is expected + }); + + // WAMP SPEC: [RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list, YIELD.ArgumentsKw|dict] + it('RESULT.Details - receives custom attributes from router', async function () { + await wampy.connect(); + + const result = await wampy.call('custom.result.procedure', []); + + // The mock should return RESULT with custom details + expect(result.details).to.have.property('_execution_time'); + expect(result.details).to.have.property('_server_version'); + }); + + // WAMP SPEC: [REGISTERED, REGISTER.Request|id, Registration|id] + it('REGISTERED.Details - receives custom attributes from router', async function () { + await wampy.connect(); + + // REGISTERED doesn't have Details|dict in spec, but some routers might extend it + const result = await wampy.register('test.rpc', function () { return { ok: true }; }); + expect(result).to.have.property('registrationId'); + }); + + // WAMP SPEC: [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list, CALL.ArgumentsKw|dict] + it('INVOCATION.Details - receives custom attributes in RPC handler', async function () { + await wampy.connect(); + + let receivedDetails = null; + await wampy.register('custom.invocation.rpc', function ({ details }) { + receivedDetails = details; + return { result: 'processed' }; + }); + + // In a real scenario, router would send INVOCATION with custom details + // Our mock doesn't auto-send invocations, but the passthrough mechanism exists + expect(receivedDetails).to.be.null; // No invocation received yet, which is expected + }); + + // WAMP SPEC: [INTERRUPT, INVOCATION.Request|id, Options|dict] + it('INTERRUPT.Options - receives custom attributes from router', async function () { + // INTERRUPT is sent by router to cancel ongoing RPC invocations + // Would contain custom attributes if router includes them + expect(true).to.be.true; // Placeholder - router-generated message + }); + }); + + describe('Invalid Custom Attributes', function () { + + it('ignores attributes that do not match _[a-z0-9_]{3,} pattern', async function () { + await wampy.connect(); + + const invalidOptions = { + _x: 'too_short', // Too short (< 3 chars after _) + _X: 'uppercase', // Uppercase letter + '_invalid-dash': 'dash', // Contains dash + 'no_underscore': 'none', // No leading underscore + _valid_attr: 'valid' // This should work + }; + + // Should not throw error, just ignore invalid ones + const result = await wampy.call('test.procedure', [], invalidOptions); + expect(result).to.have.property('argsList'); + }); + + it('handles empty or null advanced options gracefully', async function () { + await wampy.connect(); + + // Should work with null/undefined options + const result1 = await wampy.call('test.procedure', [], null); + const result2 = await wampy.call('test.procedure', []); + const result3 = await wampy.call('test.procedure', []); + + expect(result1).to.have.property('argsList'); + expect(result2).to.have.property('argsList'); + expect(result3).to.have.property('argsList'); + }); + }); + + describe('Integration with Standard Options', function () { + + it('combines custom attributes with standard options in CALL', async function () { + await wampy.connect(); + + const mixedOptions = { + timeout: 5000, // Standard option + disclose_me: true, // Standard option + _tracking_id: 'mixed_123', // Custom attribute + _priority: 'high' // Custom attribute + }; + + const result = await wampy.call('test.procedure', ['data'], mixedOptions); + expect(result).to.have.property('argsList'); + }); + + it('combines custom attributes with standard options in PUBLISH', async function () { + await wampy.connect(); + + const mixedOptions = { + exclude_me: false, // Standard option + disclose_me: true, // Standard option + _event_type: 'notification', // Custom attribute + _source_system: 'billing' // Custom attribute + }; + + const result = await wampy.publish('test.topic', { data: 'test' }, mixedOptions); + expect(result).to.have.property('publicationId'); + }); + + it('combines custom attributes with standard options in SUBSCRIBE', async function () { + await wampy.connect(); + + const mixedOptions = { + match: 'prefix', // Standard option + _subscription_type: 'live', // Custom attribute + _filter_level: 'debug' // Custom attribute + }; + + const result = await wampy.subscribe('test.prefix', function () {}, mixedOptions); + expect(result).to.have.property('subscriptionId'); + }); + + it('combines custom attributes with standard options in REGISTER', async function () { + await wampy.connect(); + + const mixedOptions = { + invoke: 'roundrobin', // Standard option + _handler_type: 'async', // Custom attribute + _max_concurrency: 10 // Custom attribute + }; + + const result = await wampy.register('test.rpc', function () { return { ok: true }; }, mixedOptions); + expect(result).to.have.property('registrationId'); + }); + }); + + describe('HELLO Custom Attributes (Special Case)', function () { + + it('HELLO should fail with _extractCustomOptions pattern (expected failure)', async function () { + // This test is expected to FAIL because HELLO doesn't use _extractCustomOptions + // It uses helloCustomDetails instead, which doesn't validate the regex pattern + + const wampyWithInvalidHello = new Wampy('ws://fake.server.url/ws/', { + realm: 'AppRealm', + ws: WebSocket, + helloCustomDetails: { + _x: 'too_short', // This SHOULD be invalid but will pass + _X: 'uppercase', // This SHOULD be invalid but will pass + 'no_underscore': 'test' // This SHOULD be invalid but will pass + } + }); + + // This will succeed even with invalid attributes because HELLO doesn't validate + await wampyWithInvalidHello.connect(); + expect(wampyWithInvalidHello.getOpStatus().code).to.equal(0); + await wampyWithInvalidHello.disconnect(); + + // This test demonstrates that HELLO needs to be updated to use _extractCustomOptions + // for consistent validation across all message types + }); + }); +}); \ No newline at end of file diff --git a/test/fake-ws-custom-attrs.js b/test/fake-ws-custom-attrs.js new file mode 100644 index 0000000..c60f141 --- /dev/null +++ b/test/fake-ws-custom-attrs.js @@ -0,0 +1,146 @@ +import { WAMP_MSG_SPEC } from '../src/constants.js'; +import customAttrsData from './custom-attributes-data.js'; + +let messageQueue = []; +const isDebugMode = false; + +const WebSocket = function (url, protocols) { + this.url = url; + this.protocol = protocols && protocols.length > 0 ? protocols[0] : undefined; + this.readyState = 0; // CONNECTING + this.extensions = ''; + this.bufferedAmount = 0; + this.binaryType = 'arraybuffer'; + + // Reset message queue for each new connection + messageQueue = [...customAttrsData]; + + // Simulate connection opening + setTimeout(() => { + this.readyState = 1; // OPEN + if (this.onopen) { + this.onopen(); + } + }, 1); +}; + +WebSocket.prototype.encode = function (data) { + return JSON.stringify(data); +}; + +WebSocket.prototype.decode = function (data) { + return JSON.parse(data); +}; + +WebSocket.prototype.close = function () { + this.readyState = 3; // CLOSED + if (this.onclose) { + this.onclose(); + } +}; + +WebSocket.prototype.abort = function () { + this.readyState = 3; // CLOSED + if (this.onerror) { + this.onerror(); + } +}; + +WebSocket.prototype.send = function (data) { + const receivedMessage = this.decode(data); + + if (isDebugMode) { + console.log('Mock received:', receivedMessage); + } + + // Find matching response in our data + const response = this.findResponse(receivedMessage); + + if (response) { + // Process the response data + const responseData = this.processResponse(response, receivedMessage); + + if (responseData) { + setTimeout(() => { + if (isDebugMode) { + console.log('Mock sending:', responseData); + } + if (this.onmessage) { + this.onmessage({ data: this.encode(responseData) }); + } + }, response.delay || 1); + } + } +}; + +WebSocket.prototype.findResponse = function (receivedMessage) { + const [messageType, requestId] = receivedMessage; + + // Look for specific responses based on message type and content + for (const item of messageQueue) { + if (item.trigger && item.trigger.messageType === messageType) { + // Check additional conditions + if (!item.trigger.condition || item.trigger.condition(receivedMessage)) { + return item; + } else { + // Condition not met, continue searching + continue; + } + } + } + + // Default responses for standard messages + const defaultResponses = { + [WAMP_MSG_SPEC.HELLO]: { + response: [WAMP_MSG_SPEC.WELCOME, 12345, { + agent: 'Custom Attrs Test Router', + roles: { + broker: { features: {} }, + dealer: { features: { call_canceling: true } } + } + }] + }, + [WAMP_MSG_SPEC.SUBSCRIBE]: { + response: [WAMP_MSG_SPEC.SUBSCRIBED, requestId, Math.floor(Math.random() * 10000)] + }, + [WAMP_MSG_SPEC.PUBLISH]: { + response: [WAMP_MSG_SPEC.PUBLISHED, requestId, Math.floor(Math.random() * 10000)] + }, + [WAMP_MSG_SPEC.CALL]: { + response: [WAMP_MSG_SPEC.RESULT, requestId, {}, ['result']] + }, + [WAMP_MSG_SPEC.REGISTER]: { + response: [WAMP_MSG_SPEC.REGISTERED, requestId, Math.floor(Math.random() * 10000)] + }, + [WAMP_MSG_SPEC.GOODBYE]: { + response: [WAMP_MSG_SPEC.GOODBYE, {}, 'wamp.close.normal'] + } + }; + + return defaultResponses[messageType]; +}; + +WebSocket.prototype.processResponse = function (responseItem, receivedMessage) { + if (responseItem.response) { + const response = [...responseItem.response]; + + // Replace placeholders with actual request ID + if (response[1] === 'REQUEST_ID') { + response[1] = receivedMessage[1]; + } + + return response; + } + + return null; +}; + + + +// Static methods +WebSocket.CONNECTING = 0; +WebSocket.OPEN = 1; +WebSocket.CLOSING = 2; +WebSocket.CLOSED = 3; + +export default WebSocket; \ No newline at end of file From ce8aea080144c9f7bd78482ffbb9d5911e8c3f06 Mon Sep 17 00:00:00 2001 From: Volodymyr Ishchenko Date: Thu, 26 Jun 2025 14:18:55 +0300 Subject: [PATCH 2/4] fix: missing retain option on publish --- src/wampy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wampy.js b/src/wampy.js index 4a13543..7d29e2f 100644 --- a/src/wampy.js +++ b/src/wampy.js @@ -1928,12 +1928,12 @@ class Wampy { acknowledge: true, ...messageOptions, ...(ppt_scheme ? { ppt_scheme } : {}), - ...(ppt_scheme ? { ppt_scheme } : {}), ...(ppt_serializer ? { ppt_serializer } : {}), ...(ppt_cipher ? { ppt_cipher } : {}), ...(ppt_keyid ? { ppt_keyid } : {}), ...(exclude_me ? { exclude_me } : {}), ...(disclose_me ? { disclose_me } : {}), + ...(retain ? { retain } : {}), ...this._extractCustomOptions(advancedOptions) }; From 80881c21b58487392731b3bac3a11e5e03d87ffe Mon Sep 17 00:00:00 2001 From: Volodymyr Ishchenko Date: Sat, 12 Jul 2025 04:41:45 +0300 Subject: [PATCH 3/4] cleanup and forma fake-ws for custom attrs --- test/fake-ws-custom-attrs.js | 98 +++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/test/fake-ws-custom-attrs.js b/test/fake-ws-custom-attrs.js index c60f141..036e641 100644 --- a/test/fake-ws-custom-attrs.js +++ b/test/fake-ws-custom-attrs.js @@ -1,5 +1,5 @@ -import { WAMP_MSG_SPEC } from '../src/constants.js'; -import customAttrsData from './custom-attributes-data.js'; +import { WAMP_MSG_SPEC } from "../src/constants.js"; +import customAttrsData from "./custom-attributes-data.js"; let messageQueue = []; const isDebugMode = false; @@ -7,17 +7,15 @@ const isDebugMode = false; const WebSocket = function (url, protocols) { this.url = url; this.protocol = protocols && protocols.length > 0 ? protocols[0] : undefined; - this.readyState = 0; // CONNECTING - this.extensions = ''; + this.readyState = WebSocket.CONNECTING; + this.extensions = ""; this.bufferedAmount = 0; - this.binaryType = 'arraybuffer'; + this.binaryType = "arraybuffer"; - // Reset message queue for each new connection messageQueue = [...customAttrsData]; - // Simulate connection opening setTimeout(() => { - this.readyState = 1; // OPEN + this.readyState = WebSocket.OPEN; if (this.onopen) { this.onopen(); } @@ -33,14 +31,14 @@ WebSocket.prototype.decode = function (data) { }; WebSocket.prototype.close = function () { - this.readyState = 3; // CLOSED + this.readyState = WebSocket.CLOSED; if (this.onclose) { this.onclose(); } }; WebSocket.prototype.abort = function () { - this.readyState = 3; // CLOSED + this.readyState = WebSocket.CLOSED; if (this.onerror) { this.onerror(); } @@ -48,22 +46,20 @@ WebSocket.prototype.abort = function () { WebSocket.prototype.send = function (data) { const receivedMessage = this.decode(data); - + if (isDebugMode) { - console.log('Mock received:', receivedMessage); + console.log("Mock received:", receivedMessage); } - // Find matching response in our data const response = this.findResponse(receivedMessage); - + if (response) { - // Process the response data const responseData = this.processResponse(response, receivedMessage); - + if (responseData) { setTimeout(() => { if (isDebugMode) { - console.log('Mock sending:', responseData); + console.log("Mock sending:", responseData); } if (this.onmessage) { this.onmessage({ data: this.encode(responseData) }); @@ -75,72 +71,80 @@ WebSocket.prototype.send = function (data) { WebSocket.prototype.findResponse = function (receivedMessage) { const [messageType, requestId] = receivedMessage; - - // Look for specific responses based on message type and content + for (const item of messageQueue) { if (item.trigger && item.trigger.messageType === messageType) { - // Check additional conditions - if (!item.trigger.condition || item.trigger.condition(receivedMessage)) { + if ( !item.trigger.condition || item.trigger.condition(receivedMessage) ) { return item; } else { - // Condition not met, continue searching continue; } } } - - // Default responses for standard messages + const defaultResponses = { [WAMP_MSG_SPEC.HELLO]: { - response: [WAMP_MSG_SPEC.WELCOME, 12345, { - agent: 'Custom Attrs Test Router', - roles: { - broker: { features: {} }, - dealer: { features: { call_canceling: true } } - } - }] + response: [ + WAMP_MSG_SPEC.WELCOME, + 12345, + { + agent: "Custom Attrs Test Router", + roles: { + broker: { features: {} }, + dealer: { features: { call_canceling: true } }, + }, + }, + ], }, [WAMP_MSG_SPEC.SUBSCRIBE]: { - response: [WAMP_MSG_SPEC.SUBSCRIBED, requestId, Math.floor(Math.random() * 10000)] + response: [ + WAMP_MSG_SPEC.SUBSCRIBED, + requestId, + Math.floor(Math.random() * 10000), + ], }, [WAMP_MSG_SPEC.PUBLISH]: { - response: [WAMP_MSG_SPEC.PUBLISHED, requestId, Math.floor(Math.random() * 10000)] + response: [ + WAMP_MSG_SPEC.PUBLISHED, + requestId, + Math.floor(Math.random() * 10000), + ], }, [WAMP_MSG_SPEC.CALL]: { - response: [WAMP_MSG_SPEC.RESULT, requestId, {}, ['result']] + response: [WAMP_MSG_SPEC.RESULT, requestId, {}, ["result"]], }, [WAMP_MSG_SPEC.REGISTER]: { - response: [WAMP_MSG_SPEC.REGISTERED, requestId, Math.floor(Math.random() * 10000)] + response: [ + WAMP_MSG_SPEC.REGISTERED, + requestId, + Math.floor(Math.random() * 10000), + ], }, [WAMP_MSG_SPEC.GOODBYE]: { - response: [WAMP_MSG_SPEC.GOODBYE, {}, 'wamp.close.normal'] - } + response: [WAMP_MSG_SPEC.GOODBYE, {}, "wamp.close.normal"], + }, }; - + return defaultResponses[messageType]; }; WebSocket.prototype.processResponse = function (responseItem, receivedMessage) { if (responseItem.response) { const response = [...responseItem.response]; - - // Replace placeholders with actual request ID - if (response[1] === 'REQUEST_ID') { + + if (response[1] === "REQUEST_ID") { response[1] = receivedMessage[1]; } - + return response; } - + return null; }; - - -// Static methods WebSocket.CONNECTING = 0; WebSocket.OPEN = 1; WebSocket.CLOSING = 2; WebSocket.CLOSED = 3; -export default WebSocket; \ No newline at end of file +export default WebSocket; From 58ea269aee49734449f454a150ad9f2c2ec51584 Mon Sep 17 00:00:00 2001 From: Volodymyr Ishchenko Date: Sat, 12 Jul 2025 05:22:56 +0300 Subject: [PATCH 4/4] removed undoubtfully useless tests + proper formating --- test/custom-attributes-data.js | 204 +++++------ test/custom-attributes-test.js | 625 ++++++++++++++------------------- 2 files changed, 365 insertions(+), 464 deletions(-) diff --git a/test/custom-attributes-data.js b/test/custom-attributes-data.js index 7f16562..cb851e2 100644 --- a/test/custom-attributes-data.js +++ b/test/custom-attributes-data.js @@ -1,61 +1,6 @@ -import { WAMP_MSG_SPEC } from '../src/constants.js'; +import { WAMP_MSG_SPEC } from "../src/constants.js"; const customAttrsData = [ - // EVENT with custom details for custom.event.topic subscription - { - trigger: { - messageType: WAMP_MSG_SPEC.SUBSCRIBE, - condition: (msg) => msg[3] === 'custom.event.topic' - }, - response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 5001] - }, - { - // Auto-send EVENT after subscription - trigger: { - messageType: WAMP_MSG_SPEC.SUBSCRIBED - }, - response: [WAMP_MSG_SPEC.EVENT, 5001, 9001, { - _publisher_info: 'system_publisher', - _event_priority: 'high', - _timestamp: Date.now() - }, ['event', 'data']], - delay: 5 - }, - - // RESULT with custom details for custom.result.procedure call - { - trigger: { - messageType: WAMP_MSG_SPEC.CALL, - condition: (msg) => msg[3] === 'custom.result.procedure' - }, - response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', { - _execution_time: 42, - _server_version: '2.1.0', - _cache_hit: true - }, ['custom', 'result']] - }, - - // INVOCATION with custom details for custom.invocation.rpc registration - { - trigger: { - messageType: WAMP_MSG_SPEC.REGISTER, - condition: (msg) => msg[3] === 'custom.invocation.rpc' - }, - response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 6001] - }, - { - // Auto-send INVOCATION after registration - trigger: { - messageType: WAMP_MSG_SPEC.REGISTERED - }, - response: [WAMP_MSG_SPEC.INVOCATION, 7001, 6001, { - _caller_info: 'admin_client', - _priority_level: 9, - _auth_context: 'elevated' - }, ['invoke', 'args']], - delay: 5 - }, - // Test custom attributes in outgoing messages by checking what client sends { trigger: { @@ -63,12 +8,14 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify custom attributes are present in SUBSCRIBE options - return options._tracking_id === 'sub_12345' && - options._priority === 'high' && - options._custom_auth === 'bearer_token'; - } + return ( + options._tracking_id === "sub_12345" && + options._priority === "high" && + options._custom_auth === "bearer_token" + ); + }, }, - response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 1001] + response: [WAMP_MSG_SPEC.SUBSCRIBED, "REQUEST_ID", 1001], }, { @@ -77,12 +24,14 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify custom attributes are present in PUBLISH options - return options._tracking_id === 'pub_67890' && - options._priority === 'urgent' && - options._routing_hint === 'datacenter_west'; - } + return ( + options._tracking_id === "pub_67890" && + options._priority === "urgent" && + options._routing_hint === "datacenter_west" + ); + }, }, - response: [WAMP_MSG_SPEC.PUBLISHED, 'REQUEST_ID', 2001] + response: [WAMP_MSG_SPEC.PUBLISHED, "REQUEST_ID", 2001], }, { @@ -91,12 +40,19 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify custom attributes are present in CALL options - return options._request_id === 'call_abcdef' && - options._timeout_override === 30000 && - options._auth_context === 'admin_user'; - } + return ( + options._request_id === "call_abcdef" && + options._timeout_override === 30000 && + options._auth_context === "admin_user" + ); + }, }, - response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['custom', 'call', 'result'] }]] + response: [ + WAMP_MSG_SPEC.RESULT, + "REQUEST_ID", + {}, + [{ argsList: ["custom", "call", "result"] }], + ], }, { @@ -105,13 +61,15 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify custom attributes are present in CANCEL options - return options._cancel_reason === 'user_requested' && - options._priority === 'immediate' && - options.mode === 'kill'; - } + return ( + options._cancel_reason === "user_requested" && + options._priority === "immediate" && + options.mode === "kill" + ); + }, }, // CANCEL doesn't get a direct response, but we can verify it was received - response: null + response: null, }, { @@ -120,12 +78,14 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify custom attributes are present in REGISTER options - return options._handler_version === '2.1.0' && - options._load_balancing === 'round_robin' && - options._timeout_hint === 5000; - } + return ( + options._handler_version === "2.1.0" && + options._load_balancing === "round_robin" && + options._timeout_hint === 5000 + ); + }, }, - response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 3001] + response: [WAMP_MSG_SPEC.REGISTERED, "REQUEST_ID", 3001], }, // Test mixed standard + custom options @@ -135,13 +95,20 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify both standard and custom attributes - return options.timeout === 5000 && - options.disclose_me === true && - options._tracking_id === 'mixed_123' && - options._priority === 'high'; - } + return ( + options.timeout === 5000 && + options.disclose_me === true && + options._tracking_id === "mixed_123" && + options._priority === "high" + ); + }, }, - response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['mixed', 'options', 'result'] }]] + response: [ + WAMP_MSG_SPEC.RESULT, + "REQUEST_ID", + {}, + [{ argsList: ["mixed", "options", "result"] }], + ], }, { @@ -150,13 +117,15 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify both standard and custom attributes - return options.exclude_me === false && - options.disclose_me === true && - options._event_type === 'notification' && - options._source_system === 'billing'; - } + return ( + options.exclude_me === false && + options.disclose_me === true && + options._event_type === "notification" && + options._source_system === "billing" + ); + }, }, - response: [WAMP_MSG_SPEC.PUBLISHED, 'REQUEST_ID', 4001] + response: [WAMP_MSG_SPEC.PUBLISHED, "REQUEST_ID", 4001], }, { @@ -165,12 +134,14 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify both standard and custom attributes for prefix subscription - return options.match === 'prefix' && - options._subscription_type === 'live' && - options._filter_level === 'debug'; - } + return ( + options.match === "prefix" && + options._subscription_type === "live" && + options._filter_level === "debug" + ); + }, }, - response: [WAMP_MSG_SPEC.SUBSCRIBED, 'REQUEST_ID', 5002] + response: [WAMP_MSG_SPEC.SUBSCRIBED, "REQUEST_ID", 5002], }, { @@ -179,12 +150,14 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Verify both standard and custom attributes - return options.invoke === 'roundrobin' && - options._handler_type === 'async' && - options._max_concurrency === 10; - } + return ( + options.invoke === "roundrobin" && + options._handler_type === "async" && + options._max_concurrency === 10 + ); + }, }, - response: [WAMP_MSG_SPEC.REGISTERED, 'REQUEST_ID', 6002] + response: [WAMP_MSG_SPEC.REGISTERED, "REQUEST_ID", 6002], }, // Test invalid custom attributes (should be filtered out) @@ -194,15 +167,22 @@ const customAttrsData = [ condition: (msg) => { const options = msg[2]; // Should only have valid custom attributes, invalid ones filtered - return options._valid_attr === 'valid' && - !options._x && // Too short - should be filtered - !options._X && // Uppercase - should be filtered - !options['_invalid-dash'] && // Contains dash - should be filtered - !options.no_underscore; // No underscore - should be filtered - } + return ( + options._valid_attr === "valid" && + !options._x && // Too short - should be filtered + !options._X && // Uppercase - should be filtered + !options["_invalid-dash"] && // Contains dash - should be filtered + !options.no_underscore + ); // No underscore - should be filtered + }, }, - response: [WAMP_MSG_SPEC.RESULT, 'REQUEST_ID', {}, [{ argsList: ['filtered', 'result'] }]] - } + response: [ + WAMP_MSG_SPEC.RESULT, + "REQUEST_ID", + {}, + [{ argsList: ["filtered", "result"] }], + ], + }, ]; -export default customAttrsData; \ No newline at end of file +export default customAttrsData; diff --git a/test/custom-attributes-test.js b/test/custom-attributes-test.js index 926d883..bf54e47 100644 --- a/test/custom-attributes-test.js +++ b/test/custom-attributes-test.js @@ -1,352 +1,273 @@ -import { expect } from 'chai'; -import { Wampy } from '../src/wampy.js'; -import WebSocket from './fake-ws-custom-attrs.js'; - -describe('Wampy.js Custom Attributes (WAMP spec 3.1)', function () { - this.timeout(1000); - - let wampy; - - before(function () { - globalThis.WebSocket = WebSocket; - }); - - beforeEach(function () { - wampy = new Wampy('ws://fake.server.url/ws/', { - realm: 'AppRealm', - ws: WebSocket - }); - }); - - afterEach(async function () { - if (wampy.getOpStatus().code === 0) { - await wampy.disconnect(); - } - }); - - describe('Client-to-Router Messages (Options/Details)', function () { - - // WAMP SPEC: [HELLO, Realm|string, Details|dict] - it('HELLO.Details - supports custom attributes via helloCustomDetails', async function () { - const helloCustomDetails = { - _client_version: '7.2.0', - _tracking_session: 'session_12345', - _environment: 'production' - }; - - const wampyWithCustomHello = new Wampy('ws://fake.server.url/ws/', { - realm: 'AppRealm', - ws: WebSocket, - helloCustomDetails - }); - - // This should work - HELLO uses helloCustomDetails, not _extractCustomOptions - await wampyWithCustomHello.connect(); - expect(wampyWithCustomHello.getOpStatus().code).to.equal(0); - await wampyWithCustomHello.disconnect(); - }); - - // WAMP SPEC: [SUBSCRIBE, Request|id, Options|dict, Topic|uri] - it('SUBSCRIBE.Options - supports custom attributes', async function () { - await wampy.connect(); - - const customOptions = { - _tracking_id: 'sub_12345', - _priority: 'high', - _custom_auth: 'bearer_token' - }; - - const result = await wampy.subscribe('test.topic', function () {}, customOptions); - expect(result).to.have.property('subscriptionId'); - }); - - // WAMP SPEC: [PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list, ArgumentsKw|dict] - it('PUBLISH.Options - supports custom attributes', async function () { - await wampy.connect(); - - const customOptions = { - _tracking_id: 'pub_67890', - _priority: 'urgent', - _routing_hint: 'datacenter_west' - }; - - const result = await wampy.publish('test.topic', 'test payload', customOptions); - expect(result).to.have.property('publicationId'); - }); - - // WAMP SPEC: [CALL, Request|id, Options|dict, Procedure|uri, Arguments|list, ArgumentsKw|dict] - it('CALL.Options - supports custom attributes', async function () { - await wampy.connect(); - - const customOptions = { - _request_id: 'call_abcdef', - _timeout_override: 30000, - _auth_context: 'admin_user' - }; - - const result = await wampy.call('test.procedure', ['arg1'], customOptions); - expect(result).to.have.property('argsList'); - }); - - // WAMP SPEC: [CANCEL, CALL.Request|id, Options|dict] - it('CANCEL.Options - supports custom attributes', async function () { - await wampy.connect(); - - // Start a call first - wampy.call('slow.procedure', []); - const reqId = wampy.getOpStatus().reqId; - - const customOptions = { - mode: 'kill', - _cancel_reason: 'user_requested', - _priority: 'immediate' - }; - - const result = wampy.cancel(reqId, customOptions); - expect(result).to.be.true; - }); - - // WAMP SPEC: [REGISTER, Request|id, Options|dict, Procedure|uri] - it('REGISTER.Options - supports custom attributes', async function () { - await wampy.connect(); - - const customOptions = { - _handler_version: '2.1.0', - _load_balancing: 'round_robin', - _timeout_hint: 5000 - }; - - const result = await wampy.register('test.rpc', function () { return { result: 'ok' }; }, customOptions); - expect(result).to.have.property('registrationId'); - }); - - // WAMP SPEC: [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict] - it('YIELD.Options - supports custom attributes in RPC handler', async function () { - await wampy.connect(); - - await wampy.register('custom.yield.rpc', function ({ result_handler }) { - const customYieldOptions = { - _processing_time: 150, - _cache_hint: 'no_cache', - _result_version: '1.0' - }; - - result_handler({ - argsList: ['custom result'], - options: customYieldOptions - }); - }); - - // This will be tested when an INVOCATION comes in - expect(wampy.getOpStatus().code).to.equal(0); - }); - - // WAMP SPEC: [GOODBYE, Details|dict, Reason|uri] - it('GOODBYE.Details - client-initiated disconnect (no custom attributes needed)', async function () { - await wampy.connect(); - - // GOODBYE is sent by client during disconnect, but doesn't support custom attributes - // This is just to verify the flow works - await wampy.disconnect(); - expect(wampy.getOpStatus().code).to.equal(0); - }); - }); - - describe('Router-to-Client Messages (Details/Options)', function () { - - // WAMP SPEC: [WELCOME, Session|id, Details|dict] - it('WELCOME.Details - receives custom attributes from router', async function () { - await wampy.connect(); - - // WELCOME details are generated by router and passed through automatically - // Our mock doesn't send custom attributes, but in real scenario they would be passed through - expect(wampy.getSessionId()).to.not.be.null; - }); - - // WAMP SPEC: [ABORT, Details|dict, Reason|uri] - it('ABORT.Details - receives custom attributes from router', async function () { - // ABORT is sent by router when rejecting connection - // Would be tested with a router that sends ABORT with custom details - // For now, just verify the mechanism exists - expect(true).to.be.true; // Placeholder - router-generated message - }); - - // WAMP SPEC: [ERROR, REQUEST.Type|int, REQUEST.Request|id, Details|dict, Error|uri, Arguments|list, ArgumentsKw|dict] - it('ERROR.Details - receives custom attributes from router', async function () { - // ERROR details are generated by router and passed through automatically - // Would contain custom attributes if router includes them - expect(true).to.be.true; // Placeholder - router-generated message - }); - - // WAMP SPEC: [EVENT, SUBSCRIBED.Subscription|id, PUBLISHED.Publication|id, Details|dict, PUBLISH.Arguments|list, PUBLISH.ArgumentsKw|dict] - it('EVENT.Details - receives custom attributes from router', async function () { - await wampy.connect(); - - let receivedDetails = null; - await wampy.subscribe('custom.event.topic', function ({ details }) { - receivedDetails = details; - }); - - // In a real scenario, router would send EVENT with custom details - // Our mock doesn't auto-send events, but the passthrough mechanism exists - expect(receivedDetails).to.be.null; // No event received yet, which is expected - }); - - // WAMP SPEC: [RESULT, CALL.Request|id, Details|dict, YIELD.Arguments|list, YIELD.ArgumentsKw|dict] - it('RESULT.Details - receives custom attributes from router', async function () { - await wampy.connect(); - - const result = await wampy.call('custom.result.procedure', []); - - // The mock should return RESULT with custom details - expect(result.details).to.have.property('_execution_time'); - expect(result.details).to.have.property('_server_version'); - }); - - // WAMP SPEC: [REGISTERED, REGISTER.Request|id, Registration|id] - it('REGISTERED.Details - receives custom attributes from router', async function () { - await wampy.connect(); - - // REGISTERED doesn't have Details|dict in spec, but some routers might extend it - const result = await wampy.register('test.rpc', function () { return { ok: true }; }); - expect(result).to.have.property('registrationId'); - }); - - // WAMP SPEC: [INVOCATION, Request|id, REGISTERED.Registration|id, Details|dict, CALL.Arguments|list, CALL.ArgumentsKw|dict] - it('INVOCATION.Details - receives custom attributes in RPC handler', async function () { - await wampy.connect(); - - let receivedDetails = null; - await wampy.register('custom.invocation.rpc', function ({ details }) { - receivedDetails = details; - return { result: 'processed' }; - }); - - // In a real scenario, router would send INVOCATION with custom details - // Our mock doesn't auto-send invocations, but the passthrough mechanism exists - expect(receivedDetails).to.be.null; // No invocation received yet, which is expected - }); - - // WAMP SPEC: [INTERRUPT, INVOCATION.Request|id, Options|dict] - it('INTERRUPT.Options - receives custom attributes from router', async function () { - // INTERRUPT is sent by router to cancel ongoing RPC invocations - // Would contain custom attributes if router includes them - expect(true).to.be.true; // Placeholder - router-generated message - }); - }); - - describe('Invalid Custom Attributes', function () { - - it('ignores attributes that do not match _[a-z0-9_]{3,} pattern', async function () { - await wampy.connect(); - - const invalidOptions = { - _x: 'too_short', // Too short (< 3 chars after _) - _X: 'uppercase', // Uppercase letter - '_invalid-dash': 'dash', // Contains dash - 'no_underscore': 'none', // No leading underscore - _valid_attr: 'valid' // This should work - }; - - // Should not throw error, just ignore invalid ones - const result = await wampy.call('test.procedure', [], invalidOptions); - expect(result).to.have.property('argsList'); - }); - - it('handles empty or null advanced options gracefully', async function () { - await wampy.connect(); - - // Should work with null/undefined options - const result1 = await wampy.call('test.procedure', [], null); - const result2 = await wampy.call('test.procedure', []); - const result3 = await wampy.call('test.procedure', []); - - expect(result1).to.have.property('argsList'); - expect(result2).to.have.property('argsList'); - expect(result3).to.have.property('argsList'); - }); - }); - - describe('Integration with Standard Options', function () { - - it('combines custom attributes with standard options in CALL', async function () { - await wampy.connect(); - - const mixedOptions = { - timeout: 5000, // Standard option - disclose_me: true, // Standard option - _tracking_id: 'mixed_123', // Custom attribute - _priority: 'high' // Custom attribute - }; - - const result = await wampy.call('test.procedure', ['data'], mixedOptions); - expect(result).to.have.property('argsList'); - }); - - it('combines custom attributes with standard options in PUBLISH', async function () { - await wampy.connect(); - - const mixedOptions = { - exclude_me: false, // Standard option - disclose_me: true, // Standard option - _event_type: 'notification', // Custom attribute - _source_system: 'billing' // Custom attribute - }; - - const result = await wampy.publish('test.topic', { data: 'test' }, mixedOptions); - expect(result).to.have.property('publicationId'); - }); - - it('combines custom attributes with standard options in SUBSCRIBE', async function () { - await wampy.connect(); - - const mixedOptions = { - match: 'prefix', // Standard option - _subscription_type: 'live', // Custom attribute - _filter_level: 'debug' // Custom attribute - }; - - const result = await wampy.subscribe('test.prefix', function () {}, mixedOptions); - expect(result).to.have.property('subscriptionId'); - }); - - it('combines custom attributes with standard options in REGISTER', async function () { - await wampy.connect(); - - const mixedOptions = { - invoke: 'roundrobin', // Standard option - _handler_type: 'async', // Custom attribute - _max_concurrency: 10 // Custom attribute - }; - - const result = await wampy.register('test.rpc', function () { return { ok: true }; }, mixedOptions); - expect(result).to.have.property('registrationId'); - }); - }); - - describe('HELLO Custom Attributes (Special Case)', function () { - - it('HELLO should fail with _extractCustomOptions pattern (expected failure)', async function () { - // This test is expected to FAIL because HELLO doesn't use _extractCustomOptions - // It uses helloCustomDetails instead, which doesn't validate the regex pattern - - const wampyWithInvalidHello = new Wampy('ws://fake.server.url/ws/', { - realm: 'AppRealm', - ws: WebSocket, - helloCustomDetails: { - _x: 'too_short', // This SHOULD be invalid but will pass - _X: 'uppercase', // This SHOULD be invalid but will pass - 'no_underscore': 'test' // This SHOULD be invalid but will pass - } - }); - - // This will succeed even with invalid attributes because HELLO doesn't validate - await wampyWithInvalidHello.connect(); - expect(wampyWithInvalidHello.getOpStatus().code).to.equal(0); - await wampyWithInvalidHello.disconnect(); - - // This test demonstrates that HELLO needs to be updated to use _extractCustomOptions - // for consistent validation across all message types - }); - }); -}); \ No newline at end of file +import { expect } from "chai"; +import { Wampy } from "../src/wampy.js"; +import WebSocket from "./fake-ws-custom-attrs.js"; + +describe("Wampy.js Custom Attributes (WAMP spec 3.1)", function () { + this.timeout(1000); + + let wampy; + + before(function () { + globalThis.WebSocket = WebSocket; + }); + + beforeEach(function () { + wampy = new Wampy("ws://fake.server.url/ws/", { + realm: "AppRealm", + ws: WebSocket, + }); + }); + + afterEach(async function () { + if (wampy.getOpStatus().code === 0) { + await wampy.disconnect(); + } + }); + + describe("Client-to-Router Messages (Options/Details)", function () { + // WAMP SPEC: [HELLO, Realm|string, Details|dict] + it("HELLO.Details - supports custom attributes via helloCustomDetails", async function () { + const helloCustomDetails = { + _client_version: "7.2.0", + _tracking_session: "session_12345", + _environment: "production", + }; + + const wampyWithCustomHello = new Wampy("ws://fake.server.url/ws/", { + realm: "AppRealm", + ws: WebSocket, + helloCustomDetails, + }); + + // This should work - HELLO uses helloCustomDetails, not _extractCustomOptions + await wampyWithCustomHello.connect(); + expect(wampyWithCustomHello.getOpStatus().code).to.equal(0); + await wampyWithCustomHello.disconnect(); + }); + + // WAMP SPEC: [SUBSCRIBE, Request|id, Options|dict, Topic|uri] + it("SUBSCRIBE.Options - supports custom attributes", async function () { + await wampy.connect(); + + const customOptions = { + _tracking_id: "sub_12345", + _priority: "high", + _custom_auth: "bearer_token", + }; + + const result = await wampy.subscribe( + "test.topic", + function () {}, + customOptions + ); + expect(result).to.have.property("subscriptionId"); + }); + + // WAMP SPEC: [PUBLISH, Request|id, Options|dict, Topic|uri, Arguments|list, ArgumentsKw|dict] + it("PUBLISH.Options - supports custom attributes", async function () { + await wampy.connect(); + + const customOptions = { + _tracking_id: "pub_67890", + _priority: "urgent", + _routing_hint: "datacenter_west", + }; + + const result = await wampy.publish( + "test.topic", + "test payload", + customOptions + ); + expect(result).to.have.property("publicationId"); + }); + + // WAMP SPEC: [CALL, Request|id, Options|dict, Procedure|uri, Arguments|list, ArgumentsKw|dict] + it("CALL.Options - supports custom attributes", async function () { + await wampy.connect(); + + const customOptions = { + _request_id: "call_abcdef", + _timeout_override: 30000, + _auth_context: "admin_user", + }; + + const result = await wampy.call( + "test.procedure", + ["arg1"], + customOptions + ); + expect(result).to.have.property("argsList"); + }); + + // WAMP SPEC: [CANCEL, CALL.Request|id, Options|dict] + it("CANCEL.Options - supports custom attributes", async function () { + await wampy.connect(); + + // Start a call first + wampy.call("slow.procedure", []); + const reqId = wampy.getOpStatus().reqId; + + const customOptions = { + mode: "kill", + _cancel_reason: "user_requested", + _priority: "immediate", + }; + + const result = wampy.cancel(reqId, customOptions); + expect(result).to.be.true; + }); + + // WAMP SPEC: [REGISTER, Request|id, Options|dict, Procedure|uri] + it("REGISTER.Options - supports custom attributes", async function () { + await wampy.connect(); + + const customOptions = { + _handler_version: "2.1.0", + _load_balancing: "round_robin", + _timeout_hint: 5000, + }; + + const result = await wampy.register( + "test.rpc", + function () { + return { result: "ok" }; + }, + customOptions + ); + expect(result).to.have.property("registrationId"); + }); + + // WAMP SPEC: [YIELD, INVOCATION.Request|id, Options|dict, Arguments|list, ArgumentsKw|dict] + it("YIELD.Options - supports custom attributes in RPC handler", async function () { + await wampy.connect(); + + await wampy.register( + "custom.yield.rpc", + function ({ result_handler }) { + const customYieldOptions = { + _processing_time: 150, + _cache_hint: "no_cache", + _result_version: "1.0", + }; + + result_handler({ + argsList: ["custom result"], + options: customYieldOptions, + }); + } + ); + + // This will be tested when an INVOCATION comes in + expect(wampy.getOpStatus().code).to.equal(0); + }); + }); + + describe("Invalid Custom Attributes", function () { + it("ignores attributes that do not match _[a-z0-9_]{3,} pattern", async function () { + await wampy.connect(); + + const invalidOptions = { + _x: "too_short", // Too short (< 3 chars after _) + _X: "uppercase", // Uppercase letter + "_invalid-dash": "dash", // Contains dash + no_underscore: "none", // No leading underscore + _valid_attr: "valid", // This should work + }; + + // Should not throw error, just ignore invalid ones + const result = await wampy.call( + "test.procedure", + [], + invalidOptions + ); + expect(result).to.have.property("argsList"); + }); + + it("handles empty or null advanced options gracefully", async function () { + await wampy.connect(); + + // Should work with null/undefined options + const result1 = await wampy.call("test.procedure", [], null); + const result2 = await wampy.call("test.procedure", []); + const result3 = await wampy.call("test.procedure", []); + + expect(result1).to.have.property("argsList"); + expect(result2).to.have.property("argsList"); + expect(result3).to.have.property("argsList"); + }); + }); + + describe("Integration with Standard Options", function () { + it("combines custom attributes with standard options in CALL", async function () { + await wampy.connect(); + + const mixedOptions = { + timeout: 5000, // Standard option + disclose_me: true, // Standard option + _tracking_id: "mixed_123", // Custom attribute + _priority: "high", // Custom attribute + }; + + const result = await wampy.call( + "test.procedure", + ["data"], + mixedOptions + ); + expect(result).to.have.property("argsList"); + }); + + it("combines custom attributes with standard options in PUBLISH", async function () { + await wampy.connect(); + + const mixedOptions = { + exclude_me: false, // Standard option + disclose_me: true, // Standard option + _event_type: "notification", // Custom attribute + _source_system: "billing", // Custom attribute + }; + + const result = await wampy.publish( + "test.topic", + { data: "test" }, + mixedOptions + ); + expect(result).to.have.property("publicationId"); + }); + + it("combines custom attributes with standard options in SUBSCRIBE", async function () { + await wampy.connect(); + + const mixedOptions = { + match: "prefix", // Standard option + _subscription_type: "live", // Custom attribute + _filter_level: "debug", // Custom attribute + }; + + const result = await wampy.subscribe( + "test.prefix", + function () {}, + mixedOptions + ); + expect(result).to.have.property("subscriptionId"); + }); + + it("combines custom attributes with standard options in REGISTER", async function () { + await wampy.connect(); + + const mixedOptions = { + invoke: "roundrobin", // Standard option + _handler_type: "async", // Custom attribute + _max_concurrency: 10, // Custom attribute + }; + + const result = await wampy.register( + "test.rpc", + function () { + return { ok: true }; + }, + mixedOptions + ); + expect(result).to.have.property("registrationId"); + }); + }); +});