diff --git a/bump-version.ts b/bump-version.ts new file mode 100644 index 0000000..9b970c5 --- /dev/null +++ b/bump-version.ts @@ -0,0 +1,56 @@ +// deno run --allow-read --allow-write bump-version.ts [major|minor|patch] + +let [bumpType] = Deno.args; +if (!bumpType) { + const answer = prompt('No argument provided. Default to patch? [Y/n]')?.trim().toLowerCase(); + if (answer === '' || answer === 'y' || answer === 'yes') { + bumpType = 'patch'; + console.log('Defaulting to patch.'); + } else { + console.log('Aborted.'); + Deno.exit(0); + } +} +if (!['major', 'minor', 'patch'].includes(bumpType)) { + console.error('Usage: deno task bump [major|minor|patch]'); + Deno.exit(1); +} + +const denoJsonPath = './deno.json'; +const denoJsonRaw = await Deno.readTextFile(denoJsonPath); +const denoJson = JSON.parse(denoJsonRaw); + +if (!denoJson.version) { + console.error('No version key found in deno.json'); + Deno.exit(1); +} + +const versionParts = denoJson.version.split('.').map(Number); +if (versionParts.length !== 3 || versionParts.some(isNaN)) { + console.error('Invalid version format in deno.json. Expected format: x.y.z'); + Deno.exit(1); +} + +const oldVersion = denoJson.version; +let [major, minor, patch] = versionParts; + +switch (bumpType) { + case 'major': + major++; + minor = 0; + patch = 0; + break; + case 'minor': + minor++; + patch = 0; + break; + case 'patch': + patch++; + break; +} + +const newVersion = `${major}.${minor}.${patch}`; +denoJson.version = newVersion; + +await Deno.writeTextFile(denoJsonPath, JSON.stringify(denoJson, null, 2) + '\n'); +console.log(`Bumped version from ${oldVersion} to ${newVersion}`); diff --git a/deno.json b/deno.json index 3fe3be9..4cb3fb5 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@johanfive/xmas", - "version": "0.0.2", + "version": "0.0.3", "exports": "./src/index.ts", "license": "MIT", "imports": { @@ -11,7 +11,8 @@ "tasks": { "cache": "DENO_TLS_CA_STORE=system deno cache --reload src/**/*.ts", "sandbox": "DENO_TLS_CA_STORE=system deno run --allow-read --allow-net --env-file=sandbox/.env --allow-env sandbox/index.ts", - "sandbox:validate-docs": "DENO_TLS_CA_STORE=system deno run --allow-read --allow-net --env-file=sandbox/.env --allow-env sandbox/validate-docs.ts" + "sandbox:validate-docs": "DENO_TLS_CA_STORE=system deno run --allow-read --allow-net --env-file=sandbox/.env --allow-env sandbox/validate-docs.ts", + "bump": "deno run --allow-read --allow-write bump-version.ts" }, "fmt": { "singleQuote": true, diff --git a/sandbox/index.ts b/sandbox/index.ts index 655a0ed..21be7f5 100644 --- a/sandbox/index.ts +++ b/sandbox/index.ts @@ -90,11 +90,50 @@ async function testPreExistingOAuthTokens() { } } +async function testGroupsSupervisors() { + console.log('\n=== Scenario 5: Test groups.getSupervisors ==='); + const { hostname, username, password } = config.basicAuth; + if (!hostname || !username || !password) { + console.warn( + '[WARNING] groups.getSupervisors: Skipped (missing hostname, username, or password)', + ); + return; + } + try { + const xm = new XmApi(config.basicAuth); + // First, get a list of groups to find one we can test with + const groupsResponse = await xm.groups.get({ query: { limit: 1 } }); + console.log( + '[INFO] groups.getSupervisors: Found groups:', + groupsResponse.status, + groupsResponse.body, + ); + + if (groupsResponse.body.data && groupsResponse.body.data.length > 0) { + const firstGroup = groupsResponse.body.data[0]; + const groupId = firstGroup.id || firstGroup.targetName; + console.log(`[INFO] groups.getSupervisors: Testing with group: ${groupId}`); + + const supervisorsResponse = await xm.groups.getSupervisors(groupId, { query: { limit: 5 } }); + console.log( + '[SUCCESS] groups.getSupervisors:', + supervisorsResponse.status, + supervisorsResponse.body, + ); + } else { + console.log('[INFO] groups.getSupervisors: No groups found to test with'); + } + } catch (err) { + printError('[ERROR] groups.getSupervisors:', err); + } +} + // Run all scenarios sequentially await testBasicAuthOnly(); await testOauthViaBasicAuthWithExplicitClientId(); await testPasswordGrantWithDiscovery(); await testPreExistingOAuthTokens(); +await testGroupsSupervisors(); function printError(context: string, err: unknown) { if (err instanceof Error) { diff --git a/src/endpoints/groups/index.test.ts b/src/endpoints/groups/index.test.ts index 9145cc3..f8cb6a1 100644 --- a/src/endpoints/groups/index.test.ts +++ b/src/endpoints/groups/index.test.ts @@ -23,6 +23,16 @@ const mockSingleGroupBody = { created: '2025-01-01T00:00:00.000Z', }; +const mockSinglePersonBody = { + id: 'test-person-id', + targetName: 'jdoe', + firstName: 'John', + lastName: 'Doe', + recipientType: 'PERSON', + status: 'ACTIVE', + created: '2025-01-01T00:00:00.000Z', +}; + const mockPaginatedGroupsBody = { count: 1, total: 1, @@ -32,6 +42,15 @@ const mockPaginatedGroupsBody = { }, }; +const mockPaginatedPeopleBody = { + count: 1, + total: 1, + data: [mockSinglePersonBody], + links: { + self: '/api/xm/1/groups/test-group-id/supervisors?limit=100&offset=0', + }, +}; + Deno.test('GroupsEndpoint', async (t) => { await t.step('get() - List Groups', async (t) => { await t.step('makes GET request without parameters', async () => { @@ -354,6 +373,85 @@ Deno.test('GroupsEndpoint', async (t) => { }); }); + await t.step('getSupervisors() - Get Group Supervisors', async (t) => { + await t.step('makes GET request with group ID', async () => { + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'GET', + url: 'https://test.xmatters.com/api/xm/1/groups/test-group-id/supervisors', + headers: TestConstants.BASIC_AUTH_HEADERS, + }, + mockedResponse: { + status: 200, + headers: { 'content-type': 'application/json' }, + body: mockPaginatedPeopleBody, + }, + }]); + await groups.getSupervisors('test-group-id'); + }); + + await t.step('makes GET request with group targetName', async () => { + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'GET', + url: 'https://test.xmatters.com/api/xm/1/groups/Oracle Administrators/supervisors', + headers: TestConstants.BASIC_AUTH_HEADERS, + }, + mockedResponse: { + status: 200, + headers: { 'content-type': 'application/json' }, + body: mockPaginatedPeopleBody, + }, + }]); + await groups.getSupervisors('Oracle Administrators'); + }); + + await t.step('makes GET request with custom headers', async () => { + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'GET', + url: 'https://test.xmatters.com/api/xm/1/groups/test-group-id/supervisors', + headers: { + ...TestConstants.BASIC_AUTH_HEADERS, + 'X-Custom-Header': 'custom-value', + }, + }, + mockedResponse: { + status: 200, + headers: { 'content-type': 'application/json' }, + body: mockPaginatedPeopleBody, + }, + }]); + await groups.getSupervisors('test-group-id', { + headers: { + 'X-Custom-Header': 'custom-value', + }, + }); + }); + + await t.step('makes GET request with query parameters', async () => { + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'GET', + url: + 'https://test.xmatters.com/api/xm/1/groups/test-group-id/supervisors?limit=5&offset=10', + headers: TestConstants.BASIC_AUTH_HEADERS, + }, + mockedResponse: { + status: 200, + headers: { 'content-type': 'application/json' }, + body: mockPaginatedPeopleBody, + }, + }]); + await groups.getSupervisors('test-group-id', { + query: { + limit: 5, + offset: 10, + }, + }); + }); + }); + await t.step('delete() - Delete Group', async (t) => { await t.step('makes DELETE request with group ID', async () => { mockHttpClient.setReqRes([{ diff --git a/src/endpoints/integrations/index.test.ts b/src/endpoints/integrations/index.test.ts new file mode 100644 index 0000000..08e8518 --- /dev/null +++ b/src/endpoints/integrations/index.test.ts @@ -0,0 +1,175 @@ +import { expect } from 'std/expect/mod.ts'; +import { IntegrationsEndpoint } from './index.ts'; +import { MockHttpClient, MockLogger, TestConstants } from 'core/test-utils.ts'; +import { RequestHandler } from 'core/request-handler.ts'; + +// Shared test infrastructure - MockHttpClient auto-resets between tests +const mockHttpClient = new MockHttpClient(); +const mockLogger = new MockLogger(); + +const requestHandler = new RequestHandler({ + httpClient: mockHttpClient, + logger: mockLogger, + ...TestConstants.BASIC_CONFIG, +}); + +const integrations = new IntegrationsEndpoint(requestHandler); + +const mockTriggerResponseBody = { + requestId: 'test-request-id-12345', +}; + +// Integration headers (no auth since skipAuth: true) +const INTEGRATION_HEADERS = { + 'Content-Type': TestConstants.BASIC_AUTH_HEADERS['Content-Type'], + 'Accept': TestConstants.BASIC_AUTH_HEADERS['Accept'], + 'User-Agent': TestConstants.BASIC_AUTH_HEADERS['User-Agent'], +} as const; + +Deno.test('IntegrationsEndpoint', async (t) => { + await t.step('trigger() - Trigger Integration', async (t) => { + await t.step('makes POST request with payload', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers'; + const payload = { + properties: { + subject: 'Test Alert', + body: 'This is a test integration trigger', + priority: 'HIGH', + }, + recipients: ['test-user@example.com'], + }; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: INTEGRATION_HEADERS, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + const response = await integrations.trigger(integrationUrl, payload); + expect(response.status).toBe(202); + expect(response.body.requestId).toBe('test-request-id-12345'); + }); + + await t.step('makes POST request with apiKey in URL', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers?apiKey=test-api-key'; + const payload = { + message: 'Simple notification', + timestamp: '2025-01-01T12:00:00Z', + }; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: INTEGRATION_HEADERS, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + await integrations.trigger(integrationUrl, payload); + }); + + await t.step('makes POST request with custom headers', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers'; + const payload = { data: 'test' }; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: { + ...INTEGRATION_HEADERS, + 'X-Custom-Header': 'custom-value', + 'X-Source-System': 'monitoring-tool', + }, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + await integrations.trigger(integrationUrl, payload, { + headers: { + 'X-Custom-Header': 'custom-value', + 'X-Source-System': 'monitoring-tool', + }, + }); + }); + + await t.step('makes POST request with empty payload', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers'; + const payload = {}; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: INTEGRATION_HEADERS, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + await integrations.trigger(integrationUrl, payload); + }); + + await t.step('makes POST request with string payload', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers'; + const payload = 'simple string payload'; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: INTEGRATION_HEADERS, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + await integrations.trigger(integrationUrl, payload); + }); + + await t.step('makes POST request with array payload', async () => { + const integrationUrl = + 'https://test.xmatters.com/api/integration/1/functions/test-function-id/triggers'; + const payload = [ + { id: 1, message: 'First item' }, + { id: 2, message: 'Second item' }, + ]; + mockHttpClient.setReqRes([{ + expectedRequest: { + method: 'POST', + url: integrationUrl, + headers: INTEGRATION_HEADERS, + body: payload, + }, + mockedResponse: { + status: 202, + headers: { 'content-type': 'application/json' }, + body: mockTriggerResponseBody, + }, + }]); + await integrations.trigger(integrationUrl, payload); + }); + }); +});