Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,23 @@ export class WebSocketManager extends EventEmitter {

private async register(connectionConfig: SubscribeRequest) {
try {
// X-ORGANIZATION-ID header is only required for INT environments
const isIntEnv = this.webex.internal?.services?.isIntegrationEnvironment() || false;
const orgId = this.webex.credentials.getOrgId();

if (isIntEnv && orgId) {
LoggerProxy.log(`[WebSocketManager] Adding X-ORGANIZATION-ID header for INT environment`, {
module: WEB_SOCKET_MANAGER_FILE,
method: METHODS.REGISTER,
});
}

const subscribeResponse: SubscribeResponse = await this.webex.request({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: HTTP_METHODS.POST,
body: connectionConfig,
headers: isIntEnv && orgId ? {'X-ORGANIZATION-ID': orgId} : undefined,
});
this.url = subscribeResponse.body.webSocketUrl;
} catch (e) {
Expand Down
2 changes: 2 additions & 0 deletions packages/@webex/contact-center/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ interface IWebexInternal {
get: (service: string) => string;
/** Wait for service catalog to be loaded */
waitForCatalog: (service: string) => Promise<void>;
/** Check if current environment is INT (integration) */
isIntegrationEnvironment: () => boolean;
/** Host catalog for service discovery */
_hostCatalog: Record<string, ServiceHost[]>;
/** Service URLs cache */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ describe('WebSocketManager', () => {

mockWebex = {
request: jest.fn(),
credentials: {
getOrgId: jest.fn().mockReturnValue('test-org-id'),
},
internal: {
services: {
isIntegrationEnvironment: jest.fn().mockReturnValue(true), // INT environment by default
},
},
} as unknown as WebexSDK;

mockWorker = {
Expand Down Expand Up @@ -107,22 +115,84 @@ describe('WebSocketManager', () => {
expect(webSocketManager).toBeDefined();
});

it('should register and connect to WebSocket', async () => {
it('should register and connect to WebSocket with X-ORGANIZATION-ID header for INT environment', async () => {
const subscribeResponse = {
body: {
webSocketUrl: 'wss://fake-url',
},
};

// Mock INT environment (services.isIntegrationEnvironment returns true)
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(true);
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);

await webSocketManager.initWebSocket({ body: fakeSubscribeRequest });

expect(mockWebex.request).toHaveBeenCalledWith({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: 'POST',
body: fakeSubscribeRequest,
headers: {'X-ORGANIZATION-ID': 'test-org-id'},
});
});

it('should register and connect to WebSocket without X-ORGANIZATION-ID header for production environment', async () => {
const subscribeResponse = {
body: {
webSocketUrl: 'wss://fake-url',
},
};

// Mock production environment (services.isIntegrationEnvironment returns false)
(mockWebex.internal.services.isIntegrationEnvironment as jest.Mock).mockReturnValue(false);
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);

// Create new WebSocketManager instance with production mock
webSocketManager = new WebSocketManager({ webex: mockWebex });

setTimeout(() => {
MockWebSocket.inst.onopen();
MockWebSocket.inst.onmessage({ data: JSON.stringify({ type: "Welcome" }) });
}, 1);

await webSocketManager.initWebSocket({ body: fakeSubscribeRequest });

expect(mockWebex.request).toHaveBeenCalledWith({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: 'POST',
body: fakeSubscribeRequest,
headers: undefined,
});
});

it('should not send X-ORGANIZATION-ID header when services.isIntegrationEnvironment is not available', async () => {
const subscribeResponse = {
body: {
webSocketUrl: 'wss://fake-url',
},
};

// Mock services not available (defaults to production behavior)
(mockWebex as any).internal = undefined;
(mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse);

webSocketManager = new WebSocketManager({ webex: mockWebex });

setTimeout(() => {
MockWebSocket.inst.onopen();
MockWebSocket.inst.onmessage({ data: JSON.stringify({ type: "Welcome" }) });
}, 1);

await webSocketManager.initWebSocket({ body: fakeSubscribeRequest });

expect(mockWebex.request).toHaveBeenCalledWith({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: 'POST',
body: fakeSubscribeRequest,
headers: undefined,
});
});

Expand Down
21 changes: 21 additions & 0 deletions packages/@webex/webex-core/src/lib/services-v2/services-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,27 @@ const Services = WebexPlugin.extend({
});
});
},

/**
* Checks if the current environment is an integration (INT) environment
* by examining the u2c discovery URL from webex config.
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
* @returns {boolean} True if INT environment, false otherwise
*/
isIntegrationEnvironment(): boolean {
try {
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
const isInt = u2cUrl.includes('intb');

this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);

return isInt;
} catch (error) {
this.logger.error('services: failed to determine integration environment', error);

return false;
}
},
/**
* saves all the services from the pre and post catalog service
* @param {ActiveServices} activeServices
Expand Down
22 changes: 22 additions & 0 deletions packages/@webex/webex-core/src/lib/services/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,28 @@ const Services = WebexPlugin.extend({

return !!hostCatalog[host]?.length;
},

/**
* Checks if the current environment is an integration (INT) environment
* by examining the u2c discovery URL from webex config.
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
* @returns {boolean} True if INT environment, false otherwise
*/
isIntegrationEnvironment() {
try {
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
const isInt = u2cUrl.includes('intb');

this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);

return isInt;
} catch (error) {
this.logger.error('services: failed to determine integration environment', error);

return false;
}
},

/**
* Merge provided active cluster mappings into current state.
* @param {Record<string,string>} activeServices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,57 @@ describe('webex-core', () => {
});
});

describe('#isIntegrationEnvironment', () => {
it('returns true when u2c URL contains "intb"', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
},
},
};
assert.isTrue(services.isIntegrationEnvironment());
});

it('returns false when u2c URL does not contain "intb" (production)', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c.wbx2.com/u2c/api/v1',
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when u2c URL is for FedRAMP', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when u2c URL is undefined', () => {
services.webex.config = {
services: {
discovery: {
u2c: undefined,
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when config is not available', () => {
services.webex.config = undefined;
assert.isFalse(services.isIntegrationEnvironment());
});
});

describe('U2C catalog cache behavior (v2)', () => {
const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
let windowBackup;
Expand Down
51 changes: 51 additions & 0 deletions packages/@webex/webex-core/test/unit/spec/services/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,57 @@ describe('webex-core', () => {
});
});

describe('#isIntegrationEnvironment', () => {
it('returns true when u2c URL contains "intb"', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
},
},
};
assert.isTrue(services.isIntegrationEnvironment());
});

it('returns false when u2c URL does not contain "intb" (production)', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c.wbx2.com/u2c/api/v1',
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when u2c URL is for FedRAMP', () => {
services.webex.config = {
services: {
discovery: {
u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when u2c URL is undefined', () => {
services.webex.config = {
services: {
discovery: {
u2c: undefined,
},
},
};
assert.isFalse(services.isIntegrationEnvironment());
});

it('returns false when config is not available', () => {
services.webex.config = undefined;
assert.isFalse(services.isIntegrationEnvironment());
});
});

describe('U2C catalog cache behavior', () => {
let webex;
let services;
Expand Down
Loading