diff --git a/package.json b/package.json index c6525348d..60a04132a 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,16 @@ "types": "./dist/governance/index.d.ts", "default": "./dist/governance/index.cjs" } + }, + "./notifications": { + "import": { + "types": "./dist/notifications/index.d.ts", + "default": "./dist/notifications/index.mjs" + }, + "require": { + "types": "./dist/notifications/index.d.ts", + "default": "./dist/notifications/index.cjs" + } } }, "files": [ diff --git a/rollup.config.js b/rollup.config.js index aa42cc39b..4904bb9d2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -228,6 +228,11 @@ const serviceEntries = [ name: 'governance', input: 'src/services/governance/index.ts', output: 'governance/index' + }, + { + name: 'notifications', + input: 'src/services/notification/index.ts', + output: 'notifications/index' } ]; diff --git a/src/models/notification/index.ts b/src/models/notification/index.ts new file mode 100644 index 000000000..037da9f1d --- /dev/null +++ b/src/models/notification/index.ts @@ -0,0 +1,6 @@ +/** + * Notification models barrel export. + */ + +export * from './notifications.types'; +export * from './notifications.models'; diff --git a/src/models/notification/notifications.constants.ts b/src/models/notification/notifications.constants.ts new file mode 100644 index 000000000..ebdd4b68e --- /dev/null +++ b/src/models/notification/notifications.constants.ts @@ -0,0 +1,9 @@ +/** + * Notification field mappings (API field name → SDK field name). + * + * Semantic renames only — case conversion is handled by `pascalToCamelCaseKeys()` + * (not needed here, the API already returns camelCase). + */ +export const NotificationMap: { [key: string]: string } = { + isRead: 'hasRead', +}; diff --git a/src/models/notification/notifications.internal-types.ts b/src/models/notification/notifications.internal-types.ts new file mode 100644 index 000000000..a77ccec24 --- /dev/null +++ b/src/models/notification/notifications.internal-types.ts @@ -0,0 +1,40 @@ +/** + * Internal-only types for the Notification service. + * + * NOT exported from the public barrel (`src/models/notification/index.ts`). + */ + +import type { NotificationGetResponse } from './notifications.types'; + +/** + * Raw notification entry shape as returned by `/odata/v1/NotificationEntry` — uses the + * API's `isRead` field (renamed to `hasRead` in the SDK response) and includes the + * internal/transport-layer fields the SDK drops before returning to consumers. + */ +export interface RawNotificationEntry extends Omit { + /** API read flag — renamed to `hasRead` in {@link NotificationGetResponse}. */ + isRead: boolean; + entityOrgName?: string | null; + entityTenantName?: string | null; + serviceRegistryName?: string | null; + messageTemplateKey?: string | null; + messageVersion?: number; + publicationId?: string; + correlationId?: string | null; + partitionKey?: string; +} + +/** + * Fields stripped from each {@link RawNotificationEntry} before it is returned to the + * SDK consumer as a {@link NotificationGetResponse}. + */ +export const INTERNAL_NOTIFICATION_FIELDS = [ + 'entityOrgName', + 'entityTenantName', + 'serviceRegistryName', + 'messageTemplateKey', + 'messageVersion', + 'publicationId', + 'correlationId', + 'partitionKey', +] as const satisfies ReadonlyArray; diff --git a/src/models/notification/notifications.models.ts b/src/models/notification/notifications.models.ts new file mode 100644 index 000000000..b2bf1bc42 --- /dev/null +++ b/src/models/notification/notifications.models.ts @@ -0,0 +1,69 @@ +/** + * Notification service model — public response shapes and the ServiceModel interface + * that drives generated API documentation. + */ + +import type { + HasPaginationOptions, + NonPaginatedResponse, + PaginatedResponse, +} from '../../utils/pagination/types'; + +import type { + NotificationGetAllOptions, + NotificationGetResponse, +} from './notifications.types'; + +/** + * Public surface of the Notifications service. JSDoc on this interface drives + * the generated API reference documentation. + * + * Every method takes the tenant GUID as the first argument — the notification + * API identifies the acting tenant via the `X-UIPATH-Internal-TenantId` header + * and the SDK forwards `tenantId` into that header on each call. + */ +export interface NotificationServiceModel { + /** + * Lists notifications from the current user's inbox. + * + * Returns the full list when no pagination params are provided, or a paginated cursor result + * when any of `pageSize`/`cursor`/`jumpToPage` is supplied. Supports OData `filter` and + * `orderby` query options. + * + * @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`) + * @returns Promise resolving to either a {@link NonPaginatedResponse}<{@link NotificationGetResponse}> or a {@link PaginatedResponse}<{@link NotificationGetResponse}> when pagination options are used. + * + * @example Basic usage + * ```typescript + * import { Notifications } from '@uipath/uipath-typescript/notifications'; + * + * const notifications = new Notifications(sdk); + * const all = await notifications.getAll(''); + * ``` + * + * @example Filter unread, most recent first + * ```typescript + * const unread = await notifications.getAll('', { + * filter: 'hasRead eq false', + * orderby: 'publishedOn desc', + * }); + * ``` + * + * @example First page with pagination + * ```typescript + * const page1 = await notifications.getAll('', { pageSize: 20 }); + * if (page1.hasNextPage) { + * const page2 = await notifications.getAll('', { cursor: page1.nextCursor }); + * } + * ``` + * @internal + */ + getAll( + tenantId: string, + options?: T + ): Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + >; +} diff --git a/src/models/notification/notifications.types.ts b/src/models/notification/notifications.types.ts new file mode 100644 index 000000000..495c1a900 --- /dev/null +++ b/src/models/notification/notifications.types.ts @@ -0,0 +1,86 @@ +/** + * Notification inbox types — raw API shapes and request/response options. + */ + +import type { RequestOptions } from '../common/types'; +import type { PaginationOptions } from '../../utils/pagination/types'; + +/** + * Priority level assigned to a notification by the publisher. + */ +export enum NotificationPriority { + Low = 'Low', + Medium = 'Medium', + High = 'High', + Critical = 'Critical', +} + +/** + * Severity classification of a notification topic. + */ +export enum NotificationCategory { + /** Informational — no action required. */ + Info = 'Info', + /** Successful operation completed. */ + Success = 'Success', + /** Warning — degraded behaviour or non-fatal issue. */ + Warn = 'Warn', + /** Error — operation failed but the system continues. */ + Error = 'Error', + /** Fatal — unrecoverable failure. */ + Fatal = 'Fatal', +} + +/** + * Notification entry as returned by `GET /odata/v1/NotificationEntry`. + * + * Field selection: many internal/transport-layer fields (`partitionKey`, `correlationId`, + * `publicationId`, `messageVersion`, `messageTemplateKey`, `serviceRegistryName`, + * `entityOrgName`, `entityTenantName`) are returned by the API but dropped from the SDK + * because they have no use for an application developer. + */ +export interface NotificationGetResponse { + /** Notification GUID. */ + id: string; + /** Resolved notification message text. */ + message: string | null; + /** Whether the user has read this notification. */ + hasRead: boolean; + /** Name of the publisher (e.g. `Orchestrator`, `Actions`). */ + publisherName: string; + /** Publisher GUID. */ + publisherId: string; + /** Human-readable topic name. */ + topicName: string; + /** Stable topic identifier (e.g. `Process.JobExecution.Faulted`). */ + topicKeyName: string; + /** Topic GUID. */ + topicId: string; + /** GUID of the user this notification belongs to (returned uppercase by the API). */ + userId: string; + /** Email of the user. Often `null`. */ + userEmail: string | null; + /** Tenant GUID this notification belongs to. Often `null` for org-scoped notifications. */ + tenantId: string | null; + /** Notification priority. */ + priority: NotificationPriority; + /** Notification severity category. */ + category: NotificationCategory; + /** JSON string of template parameters — parse with `JSON.parse()`. May be `null`. */ + messageParam: string | null; + /** URL to navigate to when the notification is clicked. */ + redirectionUrl: string | null; + /** Unix epoch **seconds** when the notification was published. */ + publishedOn: number; +} + +/** + * Options for `Notifications.getAll()`. + * + * Supports OData query options (`filter`, `orderby`) and SDK cursor pagination. + * + * Notes: + * - `$select` and `$expand` are not exposed because the API returns 500 on `$select` + * and there are no expandable relationships on this endpoint. + */ +export type NotificationGetAllOptions = Omit & PaginationOptions; diff --git a/src/services/notification/index.ts b/src/services/notification/index.ts new file mode 100644 index 000000000..25b830e26 --- /dev/null +++ b/src/services/notification/index.ts @@ -0,0 +1,29 @@ +/** + * Notification Service Module + * + * Provides access to the UiPath Notification platform from the perspective of an + * authenticated user (UserContext): + * - `Notifications` — list operations on the user's inbox (further operations land in + * follow-up PRs) + * + * Publishing (sending) notifications is a first-party service action and is NOT part of this module. + * + * @example + * ```typescript + * import { UiPath } from '@uipath/uipath-typescript/core'; + * import { Notifications } from '@uipath/uipath-typescript/notifications'; + * + * const sdk = new UiPath(config); + * await sdk.initialize(); + * + * const notifications = new Notifications(sdk); + * const unread = await notifications.getAll('', { filter: 'isRead eq false' }); + * ``` + * + * @module + */ + +export { NotificationService as Notifications } from './notifications'; + +// Models (types, enums, response shapes) +export * from '../../models/notification'; diff --git a/src/services/notification/notifications.ts b/src/services/notification/notifications.ts new file mode 100644 index 000000000..2e2518ad9 --- /dev/null +++ b/src/services/notification/notifications.ts @@ -0,0 +1,132 @@ +/** + * NotificationService — manages the current user's notification inbox. + */ + +import { track } from '../../core/telemetry'; +import { BaseService } from '../base'; + +import type { + NotificationGetAllOptions, + NotificationGetResponse, +} from '../../models/notification/notifications.types'; +import type { + NotificationServiceModel, +} from '../../models/notification/notifications.models'; +import { + INTERNAL_NOTIFICATION_FIELDS, + type RawNotificationEntry, +} from '../../models/notification/notifications.internal-types'; + +import { NotificationMap } from '../../models/notification/notifications.constants'; +import { ODATA_OFFSET_PARAMS, ODATA_PAGINATION } from '../../utils/constants/common'; +import { NOTIFICATION_ENDPOINTS } from '../../utils/constants/endpoints'; +import { TENANT_ID } from '../../utils/constants/headers'; +import { createHeaders } from '../../utils/http/headers'; +import { transformData, transformOptions } from '../../utils/transform'; +import { + HasPaginationOptions, + NonPaginatedResponse, + PaginatedResponse, +} from '../../utils/pagination/types'; +import { PaginationHelpers } from '../../utils/pagination/helpers'; +import { PaginationType } from '../../utils/pagination/internal-types'; + +/** + * Service for interacting with the UiPath Notification inbox. + * + * Provides list operations against the current user's notifications (the + * `/odata/v1/NotificationEntry` API). Further inbox operations (mark-read, + * delete) land in follow-up PRs. + * + * Every public method takes the acting tenant GUID as the first argument — the + * notification API identifies the tenant via the `X-UIPATH-Internal-TenantId` + * header and the SDK forwards `tenantId` into that header on each call. + */ +export class NotificationService extends BaseService implements NotificationServiceModel { + /** + * Lists notifications from the current user's inbox. + * + * Returns the full list when no pagination params are provided, or a paginated cursor result + * when any of `pageSize`/`cursor`/`jumpToPage` is supplied. Supports OData `filter` and + * `orderby` query options. + * + * @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`) + * @param options - Optional OData query and pagination options + * @returns Promise resolving to either a {@link NonPaginatedResponse}<{@link NotificationGetResponse}> or a {@link PaginatedResponse}<{@link NotificationGetResponse}> when pagination options are used. + * + * @example Basic usage + * ```typescript + * import { Notifications } from '@uipath/uipath-typescript/notifications'; + * + * const notifications = new Notifications(sdk); + * const all = await notifications.getAll(''); + * ``` + * + * @example Filter unread, most recent first + * ```typescript + * const unread = await notifications.getAll('', { + * filter: 'hasRead eq false', + * orderby: 'publishedOn desc', + * }); + * ``` + * + * @example First page with pagination + * ```typescript + * const page1 = await notifications.getAll('', { pageSize: 20 }); + * if (page1.hasNextPage) { + * const page2 = await notifications.getAll('', { cursor: page1.nextCursor }); + * } + * ``` + * @internal + */ + @track('Notifications.GetAll') + async getAll( + tenantId: string, + options?: T + ): Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + > { + // Rewrite renamed SDK field names → API names inside OData strings + // (e.g. `hasRead` → `isRead`) before delegating, so callers use the same + // field names for filtering/sorting that they see in the response. + const apiOptions = options ? transformOptions(options, NotificationMap) : options; + + return PaginationHelpers.getAll({ + serviceAccess: this.createPaginationServiceAccess(), + getEndpoint: () => NOTIFICATION_ENDPOINTS.GET_ALL, + headers: createHeaders({ [TENANT_ID]: tenantId }), + transformFn: stripInternalNotificationFields, + pagination: { + paginationType: PaginationType.OFFSET, + itemsField: ODATA_PAGINATION.ITEMS_FIELD, + totalCountField: ODATA_PAGINATION.TOTAL_COUNT_FIELD, + paginationParams: { + pageSizeParam: ODATA_OFFSET_PARAMS.PAGE_SIZE_PARAM, + offsetParam: ODATA_OFFSET_PARAMS.OFFSET_PARAM, + countParam: ODATA_OFFSET_PARAMS.COUNT_PARAM, + }, + }, + }, apiOptions) as Promise< + T extends HasPaginationOptions + ? PaginatedResponse + : NonPaginatedResponse + >; + } +} + +/** + * Drops internal/transport-layer fields from a raw notification entry and applies + * the SDK field renames (e.g. `isRead` → `hasRead`) before returning it to the + * consumer. Exported as module-level for testability. + * + * @internal + */ +export function stripInternalNotificationFields(item: RawNotificationEntry): NotificationGetResponse { + const stripped: RawNotificationEntry = { ...item }; + for (const field of INTERNAL_NOTIFICATION_FIELDS) { + delete stripped[field]; + } + return transformData(stripped, NotificationMap) as unknown as NotificationGetResponse; +} diff --git a/src/utils/constants/endpoints/base.ts b/src/utils/constants/endpoints/base.ts index 6bfa8f393..cef473f74 100644 --- a/src/utils/constants/endpoints/base.ts +++ b/src/utils/constants/endpoints/base.ts @@ -9,3 +9,14 @@ export const IDENTITY_BASE = 'identity_'; export const AUTOPILOT_BASE = 'autopilotforeveryone_'; export const LLMOPS_BASE = 'llmopstenant_'; export const INSIGHTS_RTM_BASE = 'insightsrtm_'; +/** + * Notification service base. The notification service is routed at the **organization** + * level — its URLs do not include a tenant segment (unlike most UiPath services). + * + * The `../` prefix relies on `URL` path normalization to collapse the tenant segment + * that {@link ApiClient} unconditionally inserts (`{orgName}/{tenantName}/{path}`). Concretely, + * `{orgName}/{tenantName}/../notificationservice_/...` resolves to `{orgName}/notificationservice_/...`. + * + * Do NOT remove the leading `../`. See real-API curl confirmation in the PR description. + */ +export const NOTIFICATION_BASE = '../notificationservice_'; diff --git a/src/utils/constants/endpoints/index.ts b/src/utils/constants/endpoints/index.ts index 1b9c6c596..aab9ae159 100644 --- a/src/utils/constants/endpoints/index.ts +++ b/src/utils/constants/endpoints/index.ts @@ -38,3 +38,6 @@ export * from './agents'; // Governance endpoints export * from './governance'; + +// Notification endpoints +export * from './notification'; diff --git a/src/utils/constants/endpoints/notification.ts b/src/utils/constants/endpoints/notification.ts new file mode 100644 index 000000000..4c97f529a --- /dev/null +++ b/src/utils/constants/endpoints/notification.ts @@ -0,0 +1,18 @@ +/** + * Notification Service Endpoints + * + * Inbox endpoints under the `notificationservice_/notificationserviceapi` prefix. + * + * URLs route at the **organization** level (no tenant segment); see {@link NOTIFICATION_BASE}. + */ + +import { NOTIFICATION_BASE } from './base'; + +const NOTIFICATION_API_BASE = `${NOTIFICATION_BASE}/notificationserviceapi`; + +/** + * Notification inbox endpoints + */ +export const NOTIFICATION_ENDPOINTS = { + GET_ALL: `${NOTIFICATION_API_BASE}/odata/v1/NotificationEntry`, +} as const; diff --git a/tests/unit/services/notification/notifications.test.ts b/tests/unit/services/notification/notifications.test.ts new file mode 100644 index 000000000..c6419d408 --- /dev/null +++ b/tests/unit/services/notification/notifications.test.ts @@ -0,0 +1,137 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { + NotificationService, + stripInternalNotificationFields, +} from '../../../../src/services/notification/notifications'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + createBasicNotificationEntry, + NOTIFICATION_TEST_CONSTANTS, + TEST_CONSTANTS, + createMockError, +} from '../../../utils/mocks'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { TENANT_ID } from '../../../../src/utils/constants/headers'; +import { NotificationCategory, NotificationPriority } from '../../../../src/models/notification'; +import type { RawNotificationEntry } from '../../../../src/models/notification/notifications.internal-types'; + +// ===== MOCKING ===== +vi.mock('../../../../src/core/http/api-client'); + +const mocks = vi.hoisted(() => import('../../../utils/mocks/core')); + +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// Shorthand for asserting the tenant header is forwarded on each call +const TENANT_HEADER = { [TENANT_ID]: NOTIFICATION_TEST_CONSTANTS.TENANT_ID }; + +// ===== TEST SUITE ===== +describe('NotificationService Unit Tests', () => { + let notificationService: NotificationService; + let mockApiClient: ReturnType; + + beforeEach(() => { + const { instance } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + vi.mocked(ApiClient).mockImplementation(() => mockApiClient as unknown as ApiClient); + vi.mocked(PaginationHelpers.getAll).mockReset(); + + notificationService = new NotificationService(instance); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return a list of notifications via PaginationHelpers with OData pagination params and tenant header', async () => { + const items = [createBasicNotificationEntry()]; + vi.mocked(PaginationHelpers.getAll).mockResolvedValue({ items, totalCount: 1 }); + + const result = await notificationService.getAll(NOTIFICATION_TEST_CONSTANTS.TENANT_ID); + + expect(result.items.length).toBe(1); + expect(result.totalCount).toBe(1); + + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.any(Function), + headers: TENANT_HEADER, + transformFn: stripInternalNotificationFields, + pagination: expect.objectContaining({ + itemsField: 'value', + totalCountField: '@odata.count', + paginationParams: expect.objectContaining({ + pageSizeParam: '$top', + offsetParam: '$skip', + countParam: '$count', + }), + }), + }), + undefined + ); + }); + + it('rewrites renamed SDK field names in OData options to API names before delegating', async () => { + vi.mocked(PaginationHelpers.getAll).mockResolvedValue({ items: [], totalCount: 0 }); + + // Caller uses the SDK field name `hasRead` in the filter... + await notificationService.getAll(NOTIFICATION_TEST_CONSTANTS.TENANT_ID, { + filter: 'hasRead eq false', + orderby: 'publishedOn desc', + pageSize: 20, + }); + + // ...and the service rewrites it to the API field name `isRead`. + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.anything(), + { filter: 'isRead eq false', orderby: 'publishedOn desc', pageSize: 20 } + ); + }); + + it('should propagate errors from PaginationHelpers', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error); + + await expect( + notificationService.getAll(NOTIFICATION_TEST_CONSTANTS.TENANT_ID) + ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); + + describe('stripInternalNotificationFields', () => { + it('removes all 8 internal fields without mutating the original', () => { + const raw: RawNotificationEntry = createBasicNotificationEntry(); + const before = JSON.stringify(raw); + + const stripped = stripInternalNotificationFields(raw); + + // original untouched (shallow-copy semantics) + expect(JSON.stringify(raw)).toBe(before); + + // every internal field stripped + expect(stripped).not.toHaveProperty('entityOrgName'); + expect(stripped).not.toHaveProperty('entityTenantName'); + expect(stripped).not.toHaveProperty('serviceRegistryName'); + expect(stripped).not.toHaveProperty('messageTemplateKey'); + expect(stripped).not.toHaveProperty('messageVersion'); + expect(stripped).not.toHaveProperty('publicationId'); + expect(stripped).not.toHaveProperty('correlationId'); + expect(stripped).not.toHaveProperty('partitionKey'); + + // public fields preserved with exact values + expect(stripped.id).toBe(NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID); + expect(stripped.priority).toBe(NotificationPriority.High); + expect(stripped.category).toBe(NotificationCategory.Error); + expect(stripped.publishedOn).toBe(NOTIFICATION_TEST_CONSTANTS.PUBLISHED_ON); + expect(stripped.message).toBe(NOTIFICATION_TEST_CONSTANTS.MESSAGE); + + // API `isRead` is renamed to `hasRead` in the SDK response + expect(stripped.hasRead).toBe(false); + expect(stripped).not.toHaveProperty('isRead'); + }); + }); +}); diff --git a/tests/utils/constants/index.ts b/tests/utils/constants/index.ts index 6f88aebe8..b0f038b53 100644 --- a/tests/utils/constants/index.ts +++ b/tests/utils/constants/index.ts @@ -19,3 +19,4 @@ export * from './memory'; export * from './traces'; export * from './agents'; export * from './governance'; +export * from './notification'; diff --git a/tests/utils/constants/notification.ts b/tests/utils/constants/notification.ts new file mode 100644 index 000000000..aee905759 --- /dev/null +++ b/tests/utils/constants/notification.ts @@ -0,0 +1,30 @@ +/** + * Notification test constants. + */ + +export const NOTIFICATION_TEST_CONSTANTS = { + // Tenant GUID — passed as the first arg to every Notifications method + // (sent to the API via the X-UIPATH-Internal-TenantId header) + TENANT_ID: '99999999-9999-4999-8999-999999999999', + + // Notification entry identifiers + NOTIFICATION_ID: '11111111-1111-4111-8111-111111111111', + + // Publisher / topic IDs used inside notification-entry fixtures + PUBLISHER_ID: '44444444-4444-4444-4444-444444444444', + PUBLISHER_NAME: 'Orchestrator', + TOPIC_ID: '55555555-5555-4555-8555-555555555555', + TOPIC_NAME: 'Process.JobExecution.Faulted', + TOPIC_KEY_NAME: 'Process.JobExecution.Faulted', + + // User identifier + USER_ID: '66666666-6666-4666-8666-666666666666', + + // Notification content (mirrors real-API field shapes captured during onboarding) + MESSAGE: 'Job XYZ failed in folder ABC', + MESSAGE_PARAM: '{"jobId":"","folderName":""}', + REDIRECTION_URL: 'https://alpha.uipath.com/orchestrator_/jobs/', + + // Unix epoch seconds (API returns seconds, not ms) + PUBLISHED_ON: 1780981200, +} as const; diff --git a/tests/utils/mocks/index.ts b/tests/utils/mocks/index.ts index 49e54fd4d..9cdfa44fb 100644 --- a/tests/utils/mocks/index.ts +++ b/tests/utils/mocks/index.ts @@ -22,6 +22,7 @@ export * from './attachments'; export * from './memory'; export * from './traces'; export * from './governance'; +export * from './notification'; // Re-export constants for convenience export * from '../constants'; \ No newline at end of file diff --git a/tests/utils/mocks/notification.ts b/tests/utils/mocks/notification.ts new file mode 100644 index 000000000..b1c6f4425 --- /dev/null +++ b/tests/utils/mocks/notification.ts @@ -0,0 +1,47 @@ +/** + * Notification mock factories. + * + * Shapes mirror the real API responses captured live during onboarding (NOT the + * Swagger spec, which omits some nullable behaviour). + */ + +import { + NotificationCategory, + NotificationPriority, +} from '../../../src/models/notification'; +import type { RawNotificationEntry } from '../../../src/models/notification/notifications.internal-types'; +import { NOTIFICATION_TEST_CONSTANTS } from '../constants/notification'; + +/** + * Builds a raw notification entry mirroring a live API response. + */ +export const createBasicNotificationEntry = ( + overrides?: Partial +): RawNotificationEntry => ({ + id: NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID, + message: NOTIFICATION_TEST_CONSTANTS.MESSAGE, + isRead: false, + publisherName: NOTIFICATION_TEST_CONSTANTS.PUBLISHER_NAME, + publisherId: NOTIFICATION_TEST_CONSTANTS.PUBLISHER_ID, + topicName: NOTIFICATION_TEST_CONSTANTS.TOPIC_NAME, + topicKeyName: NOTIFICATION_TEST_CONSTANTS.TOPIC_KEY_NAME, + topicId: NOTIFICATION_TEST_CONSTANTS.TOPIC_ID, + userId: NOTIFICATION_TEST_CONSTANTS.USER_ID, + userEmail: null, + tenantId: null, + priority: NotificationPriority.High, + category: NotificationCategory.Error, + messageParam: NOTIFICATION_TEST_CONSTANTS.MESSAGE_PARAM, + redirectionUrl: NOTIFICATION_TEST_CONSTANTS.REDIRECTION_URL, + publishedOn: NOTIFICATION_TEST_CONSTANTS.PUBLISHED_ON, + // Internal fields the API includes but the SDK drops: + entityOrgName: null, + entityTenantName: null, + serviceRegistryName: null, + messageTemplateKey: null, + messageVersion: 1, + publicationId: '00000000-0000-0000-0000-000000000000', + correlationId: null, + partitionKey: 'testorg|testtenant|partition-key-value', + ...overrides, +});