@itentialopensource/adapter-metaswitch 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
 
2
+ ## 1.2.4 [07-01-2026]
3
+
4
+ * Changes made at 2026.07.01_14:17PM
5
+
6
+ See merge request itentialopensource/adapters/adapter-metaswitch!51
7
+
8
+ ---
9
+
10
+ ## 1.2.3 [06-17-2026]
11
+
12
+ * fix: Add namespace prefix detection for OriginHost element
13
+
14
+ See merge request itentialopensource/adapters/adapter-metaswitch!48
15
+
16
+ ---
17
+
2
18
  ## 1.2.2 [06-16-2026]
3
19
 
4
20
  * Changes made at 2026.06.16_11:41AM
package/adapter.js CHANGED
@@ -100,6 +100,7 @@ class Metaswitch extends AdapterBaseCl {
100
100
  // Exclude SOAP utility methods from workflow functions
101
101
  myIgnore.push('wrapBodyInSoapEnvelope');
102
102
  myIgnore.push('getSoapNamespaces');
103
+ myIgnore.push('collectNamespaceDeclarations');
103
104
  myIgnore.push('buildOriginHost');
104
105
  myIgnore.push('injectOriginHost');
105
106
  myIgnore.push('escapeXml');
@@ -683,7 +684,7 @@ class Metaswitch extends AdapterBaseCl {
683
684
  }
684
685
 
685
686
  // Build OriginHost with credentials from adapter properties
686
- const originHostResult = this.buildOriginHost();
687
+ const originHostResult = this.buildOriginHost(body);
687
688
  if (originHostResult.error) {
688
689
  return { error: originHostResult.error };
689
690
  }
@@ -691,8 +692,9 @@ class Metaswitch extends AdapterBaseCl {
691
692
  // Inject OriginHost into the body XML (append before any closing tags)
692
693
  const bodyWithOriginHost = this.injectOriginHost(body, originHostResult.originHost);
693
694
 
694
- // Determine namespace based on API type
695
- const namespaces = this.getSoapNamespaces(apiType);
695
+ // Collect all namespace declarations needed, scanning the body for every
696
+ // prefix in use so nothing is silently dropped from the final envelope
697
+ const namespaces = this.collectNamespaceDeclarations(bodyWithOriginHost, apiType);
696
698
 
697
699
  // Construct SOAP envelope with minimal header (Metaswitch pattern)
698
700
  const soapEnvelope = `<soapenv:Envelope ${namespaces}>
@@ -729,46 +731,213 @@ class Metaswitch extends AdapterBaseCl {
729
731
  return `${commonNamespaces} ${apiNamespaces[apiType] || apiNamespaces.EAS}`;
730
732
  }
731
733
 
734
+ /**
735
+ * @function collectNamespaceDeclarations
736
+ * @summary Scans an XML body for every namespace prefix in use and returns the
737
+ * complete set of xmlns declarations needed on the SOAP envelope.
738
+ *
739
+ * Resolution order for each prefix:
740
+ * 1. Inline declaration already present in the body (xmlns:prefix="uri")
741
+ * 2. API-type-aware mapping for the sh: prefix
742
+ * 3. Known fallback table for common XML prefixes (xsd, xsi, wsa, …)
743
+ * The soapenv prefix is always included regardless of body content.
744
+ *
745
+ * @param {string} body - XML body content to scan
746
+ * @param {string} apiType - API type (EAS, NSeries, Metaview, NWSAP)
747
+ * @returns {string} Space-separated xmlns declaration string
748
+ */
749
+ // eslint-disable-next-line class-methods-use-this
750
+ collectNamespaceDeclarations(body, apiType = 'EAS') {
751
+ // -----------------------------------------------------------------------
752
+ // Metaswitch API-type-specific namespace table.
753
+ // EAS URIs are sourced from ~/Desktop/EAS_WebServices documentation
754
+ // (ShService.wsdl, userData.xsd, serviceData.xsd, Documentation.xsd).
755
+ // NSeries / Metaview / NWSAP follow the same sub-path convention.
756
+ //
757
+ // Prefix legend (all map to one of the four Metaswitch URI families):
758
+ // sh, impl, intf, tns1, m → base SOAP service namespace
759
+ // user, u → userdata sub-namespace (userData.xsd)
760
+ // ser, s → servicedata sub-namespace (serviceData.xsd)
761
+ // doc, d → documentation sub-namespace (Documentation.xsd)
762
+ // -----------------------------------------------------------------------
763
+ const apiNamespaces = {
764
+ EAS: {
765
+ sh: 'http://www.metaswitch.com/sdp/soap/sh',
766
+ impl: 'http://www.metaswitch.com/sdp/soap/sh',
767
+ intf: 'http://www.metaswitch.com/sdp/soap/sh',
768
+ tns1: 'http://www.metaswitch.com/sdp/soap/sh',
769
+ m: 'http://www.metaswitch.com/sdp/soap/sh',
770
+ user: 'http://www.metaswitch.com/sdp/soap/sh/userdata',
771
+ u: 'http://www.metaswitch.com/sdp/soap/sh/userdata',
772
+ ser: 'http://www.metaswitch.com/sdp/soap/sh/servicedata',
773
+ s: 'http://www.metaswitch.com/sdp/soap/sh/servicedata',
774
+ doc: 'http://www.metaswitch.com/sdp/soap/sh/documentation',
775
+ d: 'http://www.metaswitch.com/sdp/soap/sh/documentation'
776
+ },
777
+ NSeries: {
778
+ sh: 'http://www.metaswitch.com/nsrs/soap/sh',
779
+ impl: 'http://www.metaswitch.com/nsrs/soap/sh',
780
+ intf: 'http://www.metaswitch.com/nsrs/soap/sh',
781
+ tns1: 'http://www.metaswitch.com/nsrs/soap/sh',
782
+ m: 'http://www.metaswitch.com/nsrs/soap/sh',
783
+ user: 'http://www.metaswitch.com/nsrs/soap/sh/userdata',
784
+ u: 'http://www.metaswitch.com/nsrs/soap/sh/userdata',
785
+ ser: 'http://www.metaswitch.com/nsrs/soap/sh/servicedata',
786
+ s: 'http://www.metaswitch.com/nsrs/soap/sh/servicedata',
787
+ doc: 'http://www.metaswitch.com/nsrs/soap/sh/documentation',
788
+ d: 'http://www.metaswitch.com/nsrs/soap/sh/documentation'
789
+ },
790
+ Metaview: {
791
+ sh: 'http://www.metaswitch.com/ems/soap/sh',
792
+ impl: 'http://www.metaswitch.com/ems/soap/sh',
793
+ intf: 'http://www.metaswitch.com/ems/soap/sh',
794
+ tns1: 'http://www.metaswitch.com/ems/soap/sh',
795
+ m: 'http://www.metaswitch.com/ems/soap/sh',
796
+ user: 'http://www.metaswitch.com/ems/soap/sh/userdata',
797
+ u: 'http://www.metaswitch.com/ems/soap/sh/userdata',
798
+ ser: 'http://www.metaswitch.com/ems/soap/sh/servicedata',
799
+ s: 'http://www.metaswitch.com/ems/soap/sh/servicedata',
800
+ doc: 'http://www.metaswitch.com/ems/soap/sh/documentation',
801
+ d: 'http://www.metaswitch.com/ems/soap/sh/documentation'
802
+ },
803
+ NWSAP: {
804
+ sh: 'http://www.metaswitch.com/srb/soap/sh',
805
+ impl: 'http://www.metaswitch.com/srb/soap/sh',
806
+ intf: 'http://www.metaswitch.com/srb/soap/sh',
807
+ tns1: 'http://www.metaswitch.com/srb/soap/sh',
808
+ m: 'http://www.metaswitch.com/srb/soap/sh',
809
+ user: 'http://www.metaswitch.com/srb/soap/sh/userdata',
810
+ u: 'http://www.metaswitch.com/srb/soap/sh/userdata',
811
+ ser: 'http://www.metaswitch.com/srb/soap/sh/servicedata',
812
+ s: 'http://www.metaswitch.com/srb/soap/sh/servicedata',
813
+ doc: 'http://www.metaswitch.com/srb/soap/sh/documentation',
814
+ d: 'http://www.metaswitch.com/srb/soap/sh/documentation'
815
+ }
816
+ };
817
+
818
+ // -----------------------------------------------------------------------
819
+ // Standard W3C / SOAP namespaces — identical for all API types.
820
+ // Sources: W3C specifications, OASIS WSS, Apache SOAP.
821
+ // -----------------------------------------------------------------------
822
+ const standardNamespaces = {
823
+ // XML MIME types (W3C) — also always emitted on the envelope because
824
+ // Metaswitch declares it even when no element uses the prefix directly
825
+ xm: 'http://www.w3.org/2005/05/xmlmime',
826
+ xmime: 'http://www.w3.org/2005/05/xmlmime',
827
+ // XML Schema (W3C)
828
+ xsd: 'http://www.w3.org/2001/XMLSchema',
829
+ xs: 'http://www.w3.org/2001/XMLSchema',
830
+ // XML Schema instance (W3C)
831
+ xsi: 'http://www.w3.org/2001/XMLSchema-instance',
832
+ // WS-Addressing (W3C)
833
+ wsa: 'http://www.w3.org/2005/08/addressing',
834
+ // WS-Security utility (OASIS)
835
+ wsu: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
836
+ // WSDL / SOAP binding
837
+ wsdl: 'http://schemas.xmlsoap.org/wsdl/',
838
+ wsdlsoap: 'http://schemas.xmlsoap.org/wsdl/soap/',
839
+ apachesoap: 'http://xml.apache.org/xml-soap'
840
+ };
841
+
842
+ // Step 1: harvest any URIs already declared inline in the body.
843
+ // Inline declarations are authoritative and override every table entry.
844
+ const declaredInBody = {};
845
+ (body.match(/xmlns:(\w+)="([^"]+)"/g) || []).forEach((decl) => {
846
+ const [, prefix, uri] = decl.match(/xmlns:(\w+)="([^"]+)"/) || [];
847
+ if (prefix) {
848
+ declaredInBody[prefix] = uri;
849
+ }
850
+ });
851
+
852
+ // Step 2: collect every prefix used in element names.
853
+ const usedPrefixes = {};
854
+ (body.match(/<\/?(\w+):[A-Za-z]/g) || []).forEach((token) => {
855
+ const [, prefix] = token.match(/(\w+):/) || [];
856
+ if (prefix) {
857
+ usedPrefixes[prefix] = true;
858
+ }
859
+ });
860
+
861
+ // Resolves a prefix: inline body first, then API table, then standard table.
862
+ const apiNs = apiNamespaces[apiType] || apiNamespaces.EAS;
863
+ const resolveUri = (prefix) => declaredInBody[prefix] || apiNs[prefix] || standardNamespaces[prefix];
864
+
865
+ // Step 3: build the declaration list; emit() deduplicates.
866
+ const emitted = {};
867
+ const declarations = [];
868
+ const emit = (prefix) => {
869
+ const uri = resolveUri(prefix);
870
+ if (!emitted[prefix] && uri) {
871
+ declarations.push(`xmlns:${prefix}="${uri}"`);
872
+ emitted[prefix] = true;
873
+ }
874
+ };
875
+
876
+ // Always-present: soapenv (structural envelope requirement).
877
+ declarations.push('xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"');
878
+ emitted.soapenv = true;
879
+
880
+ // Emit every prefix the body actually uses (element names + inline declarations).
881
+ const allBodyPrefixes = { ...usedPrefixes };
882
+ Object.keys(declaredInBody).forEach((p) => {
883
+ allBodyPrefixes[p] = true;
884
+ });
885
+ Object.keys(allBodyPrefixes).forEach(emit);
886
+
887
+ // Always-present: xm — Metaswitch includes it on every envelope even when
888
+ // no element in the body uses the prefix directly (e.g. for attachment MIME types).
889
+ emit('xm');
890
+
891
+ return declarations.join(' ');
892
+ }
893
+
732
894
  /**
733
895
  * @function buildOriginHost
734
896
  * @summary Builds OriginHost XML element with credentials from adapter properties
735
- * Following Metaswitch pattern: server@domain?clientVersion=X.X&adminName=XXX&password=YYY
897
+ * Following Metaswitch pattern: host?clientVersion=X.X&adminName=XXX&password=YYY
736
898
  *
899
+ * @param {string} body - The XML body to detect namespace prefix from
737
900
  * @returns {object} Object containing the OriginHost XML or error
738
901
  * @returns {string} returns.originHost - The OriginHost XML element
739
902
  * @returns {Error} returns.error - Error object if credentials are missing
740
903
  */
741
- buildOriginHost() {
904
+ buildOriginHost(body) {
742
905
  const meth = 'adapter-buildOriginHost';
743
906
  const origin = `${this.id}-${meth}`;
744
907
  log.trace(origin);
745
908
 
746
909
  try {
747
910
  // Get credentials and connection info from adapter properties
911
+ // Properties are at top level: this.allProps.host, this.allProps.domain, etc.
748
912
  const auth = this.allProps.authentication || {};
749
- const conn = this.allProps.properties || {};
750
913
 
751
914
  // Validate required credentials
752
915
  if (!auth.username || !auth.password) {
916
+ log.error(`${origin}: Missing authentication credentials - username: ${!!auth.username}, password: ${!!auth.password}`);
753
917
  return { error: new Error('Missing username or password in adapter authentication configuration') };
754
918
  }
755
919
 
756
- // Build OriginHost value following Metaswitch sample pattern
757
- // Format: server@domain?clientVersion=X.X&adminName=XXX&password=YYY&ignoreSequenceNumber=true
758
- const server = conn.host || 'server';
759
- const domain = conn.domain || 'domain';
760
- const clientVersion = conn.clientVersion || '1.0';
920
+ // Build OriginHost value following working Metaswitch pattern
921
+ // Format: host?clientVersion=X.X&adminName=XXX&password=YYY&ignoreSequenceNumber=true
922
+ const host = this.allProps.host || 'server';
923
+ const clientVersion = this.allProps.clientVersion || '1.0';
924
+
925
+ log.debug(`${origin}: Using host=${host}, clientVersion=${clientVersion}, username=${auth.username}`);
761
926
 
762
927
  // URL-encode credentials to handle special characters
763
928
  const adminName = encodeURIComponent(auth.username);
764
929
  const password = encodeURIComponent(auth.password);
765
930
 
766
- const originHostValue = `${server}@${domain}?clientVersion=${clientVersion}&adminName=${adminName}&password=${password}&ignoreSequenceNumber=true`;
931
+ const originHostValue = `${host}?clientVersion=${clientVersion}&adminName=${adminName}&password=${password}&ignoreSequenceNumber=true`;
932
+
933
+ // Detect namespace prefix from body (e.g., sh:ShPull -> use sh:OriginHost)
934
+ const nsMatch = body.match(/<(\w+):(ShPull|ShUpdate|ShSubs|ShNotif)/);
935
+ const nsPrefix = nsMatch ? `${nsMatch[1]}:` : '';
767
936
 
768
- // Return as XML element (will be inserted into SOAP body)
769
- const originHostXml = `<OriginHost>${this.escapeXml(originHostValue)}</OriginHost>`;
937
+ // Return as XML element with matching namespace prefix
938
+ const originHostXml = `<${nsPrefix}OriginHost>${this.escapeXml(originHostValue)}</${nsPrefix}OriginHost>`;
770
939
 
771
- log.debug(`${origin}: Built OriginHost for ${server}@${domain} with user ${auth.username}`);
940
+ log.debug(`${origin}: Built OriginHost for ${host} with user ${auth.username} (namespace: ${nsPrefix || 'none'})`);
772
941
 
773
942
  return { originHost: originHostXml };
774
943
  } catch (ex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itentialopensource/adapter-metaswitch",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "This adapter integrates with system described as: Metaswitch.",
5
5
  "main": "adapter.js",
6
6
  "wizardVersion": "3.12.1",
@@ -46,14 +46,14 @@
46
46
  "author": "Itential",
47
47
  "homepage": "https://gitlab.com/itentialopensource/adapters/adapter-metaswitch#readme",
48
48
  "dependencies": {
49
- "@itentialopensource/adapter-utils": "6.1.22",
49
+ "@itentialopensource/adapter-utils": "6.1.23",
50
50
  "acorn": "8.16.0",
51
51
  "ajv": "8.20.0",
52
- "axios": "1.16.0",
52
+ "axios": "1.18.1",
53
53
  "commander": "11.1.0",
54
- "fs-extra": "11.3.5",
54
+ "fs-extra": "11.3.6",
55
55
  "json-query": "2.2.2",
56
- "mocha": "12.0.0-beta-10",
56
+ "mocha": "12.0.0-rc.1",
57
57
  "mocha-param": "2.0.1",
58
58
  "mongodb": "4.17.2",
59
59
  "ping": "0.4.4",
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "1.2.0",
2
+ "version": "1.2.1",
3
3
  "configLines": 4722,
4
4
  "scriptLines": 2523,
5
- "codeLines": 2750,
6
- "testLines": 4416,
7
- "testCases": 200,
8
- "totalCodeLines": 9689,
5
+ "codeLines": 2919,
6
+ "testLines": 4606,
7
+ "testCases": 211,
8
+ "totalCodeLines": 10048,
9
9
  "wfTasks": 29
10
10
  }
@@ -0,0 +1,30 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ ### Jun 4, 2026
7
+
8
+ | ID | Time | T | Title | Read |
9
+ |----|------|---|-------|------|
10
+ | #2327 | 12:06 PM | 🔵 | Entity Method Implementation Locations Identified | ~619 |
11
+
12
+ ### Jun 11, 2026
13
+
14
+ | ID | Time | T | Title | Read |
15
+ |----|------|---|-------|------|
16
+ | #2411 | 9:53 AM | ✅ | Replaced WS-Security Tests with OriginHost Builder and Injector Tests | ~614 |
17
+ | #2410 | " | 🔄 | Updated getSoapNamespaces Test to Verify WS-Security Removal | ~403 |
18
+ | #2409 | " | ✅ | Updated Unit Tests for OriginHost Authentication Pattern | ~535 |
19
+ | #2408 | 9:52 AM | 🔄 | Updated Unit Test for OriginHost Authentication Pattern | ~176 |
20
+
21
+ ### Jun 15, 2026
22
+
23
+ | ID | Time | T | Title | Read |
24
+ |----|------|---|-------|------|
25
+ | #2447 | 11:17 AM | 🔴 | OriginHost format corrected to match working implementation | ~334 |
26
+ | #2443 | 10:57 AM | 🟣 | Authentication Refactoring Work Completed with All Tests Passing | ~341 |
27
+ | #2440 | 10:22 AM | 🔴 | Property access and namespace detection fixes complete | ~306 |
28
+ | #2437 | 10:21 AM | 🔴 | Updated Test Assertion to Match Namespaced OriginHost Element | ~334 |
29
+ | #2429 | 10:10 AM | 🟣 | Added Namespace Prefix Detection to buildOriginHost Method | ~384 |
30
+ </claude-mem-context>
@@ -280,10 +280,10 @@ describe('[unit] Metaswitch Adapter Test', () => {
280
280
  assert.notEqual(null, packageDotJson.dependencies);
281
281
  assert.notEqual('', packageDotJson.dependencies);
282
282
  assert.equal('8.20.0', packageDotJson.dependencies.ajv);
283
- assert.equal('1.16.0', packageDotJson.dependencies.axios);
283
+ assert.equal('1.18.1', packageDotJson.dependencies.axios);
284
284
  assert.equal('11.1.0', packageDotJson.dependencies.commander);
285
- assert.equal('11.3.5', packageDotJson.dependencies['fs-extra']);
286
- assert.equal('12.0.0-beta-10', packageDotJson.dependencies.mocha);
285
+ assert.equal('11.3.6', packageDotJson.dependencies['fs-extra']);
286
+ assert.equal('12.0.0-rc.1', packageDotJson.dependencies.mocha);
287
287
  assert.equal('2.0.1', packageDotJson.dependencies['mocha-param']);
288
288
  assert.equal('0.4.4', packageDotJson.dependencies.ping);
289
289
  assert.equal('1.4.10', packageDotJson.dependencies['readline-sync']);
@@ -1496,7 +1496,7 @@ describe('[unit] Metaswitch Adapter Test', () => {
1496
1496
  assert.equal(result.payload.includes('<soapenv:Envelope'), true);
1497
1497
  assert.equal(result.payload.includes('<soapenv:Header/>'), true);
1498
1498
  assert.equal(result.payload.includes('<soapenv:Body>'), true);
1499
- assert.equal(result.payload.includes('<OriginHost>'), true);
1499
+ assert.equal(result.payload.includes('<sh:OriginHost>'), true);
1500
1500
  assert.equal(result.payload.includes('adminName='), true);
1501
1501
  assert.equal(result.payload.includes('password='), true);
1502
1502
  assert.equal(result.payload.includes('clientVersion='), true);
@@ -1612,8 +1612,31 @@ describe('[unit] Metaswitch Adapter Test', () => {
1612
1612
  assert.equal(result.payload.includes('<wsse:Security'), false);
1613
1613
  assert.equal(result.payload.includes('<wsse:UsernameToken>'), false);
1614
1614
 
1615
- // Should have OriginHost with credentials instead
1616
- assert.equal(result.payload.includes('<OriginHost>'), true);
1615
+ // Should have OriginHost with credentials and matching namespace prefix
1616
+ assert.equal(result.payload.includes('<sh:OriginHost>'), true);
1617
+ done();
1618
+ } catch (error) {
1619
+ log.error(`Test Failure: ${error}`);
1620
+ done(error);
1621
+ }
1622
+ }).timeout(attemptTimeout);
1623
+
1624
+ it('should include all namespace declarations from a multi-namespace payload', (done) => {
1625
+ try {
1626
+ // Payload that uses xsi and xsd prefixes in addition to sh
1627
+ const xmlPayload = '<sh:ShPull xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><UserIdentity xsi:type="xsd:string">12345</UserIdentity></sh:ShPull>';
1628
+ const result = a.wrapBodyInSoapEnvelope(xmlPayload, 'EAS');
1629
+
1630
+ assert.equal(result.error, undefined);
1631
+ assert.notEqual(result.payload, undefined);
1632
+ // Core envelope structure
1633
+ assert.equal(result.payload.includes('<soapenv:Envelope'), true);
1634
+ assert.equal(result.payload.includes('xmlns:soapenv='), true);
1635
+ // sh: namespace pulled from apiType
1636
+ assert.equal(result.payload.includes('xmlns:sh="http://www.metaswitch.com/sdp/soap/sh"'), true);
1637
+ // Additional namespaces from the payload must survive into the envelope
1638
+ assert.equal(result.payload.includes('xmlns:xsi='), true);
1639
+ assert.equal(result.payload.includes('xmlns:xsd='), true);
1617
1640
  done();
1618
1641
  } catch (error) {
1619
1642
  log.error(`Test Failure: ${error}`);
@@ -1681,6 +1704,147 @@ describe('[unit] Metaswitch Adapter Test', () => {
1681
1704
  }).timeout(attemptTimeout);
1682
1705
  });
1683
1706
 
1707
+ describe('#collectNamespaceDeclarations', () => {
1708
+ it('should have a collectNamespaceDeclarations function', (done) => {
1709
+ try {
1710
+ assert.equal(true, typeof a.collectNamespaceDeclarations === 'function');
1711
+ done();
1712
+ } catch (error) {
1713
+ log.error(`Test Failure: ${error}`);
1714
+ done(error);
1715
+ }
1716
+ }).timeout(attemptTimeout);
1717
+
1718
+ it('should always include soapenv declaration', (done) => {
1719
+ try {
1720
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1721
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1722
+
1723
+ assert.equal(result.includes('xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'), true);
1724
+ done();
1725
+ } catch (error) {
1726
+ log.error(`Test Failure: ${error}`);
1727
+ done(error);
1728
+ }
1729
+ }).timeout(attemptTimeout);
1730
+
1731
+ it('should resolve sh: namespace per apiType when used in body', (done) => {
1732
+ try {
1733
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1734
+ const apiTypes = [
1735
+ { type: 'EAS', base: 'http://www.metaswitch.com/sdp/soap/sh' },
1736
+ { type: 'NSeries', base: 'http://www.metaswitch.com/nsrs/soap/sh' },
1737
+ { type: 'Metaview', base: 'http://www.metaswitch.com/ems/soap/sh' },
1738
+ { type: 'NWSAP', base: 'http://www.metaswitch.com/srb/soap/sh' }
1739
+ ];
1740
+
1741
+ apiTypes.forEach(({ type, base }) => {
1742
+ const result = a.collectNamespaceDeclarations(body, type);
1743
+ assert.equal(result.includes(`xmlns:sh="${base}"`), true);
1744
+ });
1745
+ done();
1746
+ } catch (error) {
1747
+ log.error(`Test Failure: ${error}`);
1748
+ done(error);
1749
+ }
1750
+ }).timeout(attemptTimeout);
1751
+
1752
+ it('should resolve user and ser from static table when detected in body', (done) => {
1753
+ try {
1754
+ // Body that uses user: and ser: element prefixes
1755
+ const body = '<sh:ShUpdate><user:Sh-Data><ser:MetaSphereData/></user:Sh-Data></sh:ShUpdate>';
1756
+ const apiTypes = [
1757
+ { type: 'EAS', base: 'http://www.metaswitch.com/sdp/soap/sh' },
1758
+ { type: 'NSeries', base: 'http://www.metaswitch.com/nsrs/soap/sh' },
1759
+ { type: 'Metaview', base: 'http://www.metaswitch.com/ems/soap/sh' },
1760
+ { type: 'NWSAP', base: 'http://www.metaswitch.com/srb/soap/sh' }
1761
+ ];
1762
+
1763
+ apiTypes.forEach(({ type, base }) => {
1764
+ const result = a.collectNamespaceDeclarations(body, type);
1765
+ assert.equal(result.includes(`xmlns:user="${base}/userdata"`), true);
1766
+ assert.equal(result.includes(`xmlns:ser="${base}/servicedata"`), true);
1767
+ });
1768
+ done();
1769
+ } catch (error) {
1770
+ log.error(`Test Failure: ${error}`);
1771
+ done(error);
1772
+ }
1773
+ }).timeout(attemptTimeout);
1774
+
1775
+ it('should NOT include user or ser when absent from body', (done) => {
1776
+ try {
1777
+ // Minimal body with only sh: elements
1778
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1779
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1780
+
1781
+ assert.equal(result.includes('xmlns:user='), false);
1782
+ assert.equal(result.includes('xmlns:ser='), false);
1783
+ done();
1784
+ } catch (error) {
1785
+ log.error(`Test Failure: ${error}`);
1786
+ done(error);
1787
+ }
1788
+ }).timeout(attemptTimeout);
1789
+
1790
+ it('should always include xm even when absent from body', (done) => {
1791
+ try {
1792
+ // Minimal body — xm: never appears as an element prefix
1793
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1794
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1795
+
1796
+ assert.equal(result.includes('xmlns:xm="http://www.w3.org/2005/05/xmlmime"'), true);
1797
+ done();
1798
+ } catch (error) {
1799
+ log.error(`Test Failure: ${error}`);
1800
+ done(error);
1801
+ }
1802
+ }).timeout(attemptTimeout);
1803
+
1804
+ it('should include xsi and xsd declarations when used in body', (done) => {
1805
+ try {
1806
+ const body = '<sh:ShPull xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><UserIdentity xsi:type="xsd:string">test</UserIdentity></sh:ShPull>';
1807
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1808
+
1809
+ assert.equal(result.includes('xmlns:xsi='), true);
1810
+ assert.equal(result.includes('xmlns:xsd='), true);
1811
+ done();
1812
+ } catch (error) {
1813
+ log.error(`Test Failure: ${error}`);
1814
+ done(error);
1815
+ }
1816
+ }).timeout(attemptTimeout);
1817
+
1818
+ it('should prefer inline body declarations over fallback table', (done) => {
1819
+ try {
1820
+ // Body declares xsi with a custom URI — that should win over the known fallback
1821
+ const customUri = 'http://custom.example.com/schema-instance';
1822
+ const body = `<sh:ShPull xmlns:xsi="${customUri}"><UserIdentity xsi:type="string">test</UserIdentity></sh:ShPull>`;
1823
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1824
+
1825
+ assert.equal(result.includes(`xmlns:xsi="${customUri}"`), true);
1826
+ done();
1827
+ } catch (error) {
1828
+ log.error(`Test Failure: ${error}`);
1829
+ done(error);
1830
+ }
1831
+ }).timeout(attemptTimeout);
1832
+
1833
+ it('should not add declarations for unrecognised prefixes', (done) => {
1834
+ try {
1835
+ // zz: is not in the known fallback table and not declared inline
1836
+ const body = '<sh:ShPull><zz:Unknown>test</zz:Unknown></sh:ShPull>';
1837
+ const result = a.collectNamespaceDeclarations(body, 'EAS');
1838
+
1839
+ assert.equal(result.includes('xmlns:zz='), false);
1840
+ done();
1841
+ } catch (error) {
1842
+ log.error(`Test Failure: ${error}`);
1843
+ done(error);
1844
+ }
1845
+ }).timeout(attemptTimeout);
1846
+ });
1847
+
1684
1848
  describe('#buildOriginHost', () => {
1685
1849
  it('should have a buildOriginHost function', (done) => {
1686
1850
  try {
@@ -1694,12 +1858,13 @@ describe('[unit] Metaswitch Adapter Test', () => {
1694
1858
 
1695
1859
  it('should create OriginHost with credentials from adapter properties', (done) => {
1696
1860
  try {
1697
- const result = a.buildOriginHost();
1861
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1862
+ const result = a.buildOriginHost(body);
1698
1863
 
1699
1864
  assert.equal(result.error, undefined);
1700
1865
  assert.notEqual(result.originHost, undefined);
1701
- assert.equal(result.originHost.includes('<OriginHost>'), true);
1702
- assert.equal(result.originHost.includes('</OriginHost>'), true);
1866
+ assert.equal(result.originHost.includes('<sh:OriginHost>'), true);
1867
+ assert.equal(result.originHost.includes('</sh:OriginHost>'), true);
1703
1868
  assert.equal(result.originHost.includes('adminName='), true);
1704
1869
  assert.equal(result.originHost.includes('password='), true);
1705
1870
  assert.equal(result.originHost.includes('clientVersion='), true);
@@ -1713,7 +1878,8 @@ describe('[unit] Metaswitch Adapter Test', () => {
1713
1878
 
1714
1879
  it('should URL-encode credentials with special characters', (done) => {
1715
1880
  try {
1716
- const result = a.buildOriginHost();
1881
+ const body = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1882
+ const result = a.buildOriginHost(body);
1717
1883
 
1718
1884
  // If password contains &, it should be URL-encoded as %26
1719
1885
  // This test validates URL encoding is applied
@@ -1727,6 +1893,30 @@ describe('[unit] Metaswitch Adapter Test', () => {
1727
1893
  done(error);
1728
1894
  }
1729
1895
  }).timeout(attemptTimeout);
1896
+
1897
+ it('should detect and use namespace prefix from body', (done) => {
1898
+ try {
1899
+ const bodyWithNs = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
1900
+ const bodyWithoutNs = '<ShUpdate><UserIdentity>test</UserIdentity></ShUpdate>';
1901
+
1902
+ const resultWithNs = a.buildOriginHost(bodyWithNs);
1903
+ const resultWithoutNs = a.buildOriginHost(bodyWithoutNs);
1904
+
1905
+ // With namespace prefix
1906
+ assert.equal(resultWithNs.originHost.includes('<sh:OriginHost>'), true);
1907
+ assert.equal(resultWithNs.originHost.includes('</sh:OriginHost>'), true);
1908
+
1909
+ // Without namespace prefix
1910
+ assert.equal(resultWithoutNs.originHost.includes('<OriginHost>'), true);
1911
+ assert.equal(resultWithoutNs.originHost.includes('</OriginHost>'), true);
1912
+ assert.equal(resultWithoutNs.originHost.includes('<sh:OriginHost>'), false);
1913
+
1914
+ done();
1915
+ } catch (error) {
1916
+ log.error(`Test Failure: ${error}`);
1917
+ done(error);
1918
+ }
1919
+ }).timeout(attemptTimeout);
1730
1920
  });
1731
1921
 
1732
1922
  describe('#injectOriginHost', () => {