Skip to content
Open
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
50 changes: 50 additions & 0 deletions src/models/notification/notifications.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ export type NotificationMarkAllReadResponse = OperationResponse<{
read: true;
}>;

/**
* Response from `deleteNotifications()`.
*
* `notificationIds` echoes the IDs that were deleted.
*/
export type NotificationDeleteResponse = OperationResponse<{
notificationIds: string[];
}>;

/**
* Response from `deleteAll()`.
*/
export type NotificationDeleteAllResponse = OperationResponse<{
all: true;
}>;

/**
* Public surface of the Notifications service. JSDoc on this interface drives
* the generated API reference documentation.
Expand Down Expand Up @@ -132,4 +148,38 @@ export interface NotificationServiceModel {
* ```
*/
markAllRead(tenantId: string): Promise<NotificationMarkAllReadResponse>;

/**
* Deletes the given notifications.
*
* @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`)
* @param notificationIds - GUIDs of notifications to delete. Must be non-empty —
* the server rejects an empty array with HTTP 400.
* @returns Operation result echoing the deleted IDs
* {@link NotificationDeleteResponse}
*
* @example
* ```typescript
* await notifications.deleteNotifications('<tenantId>', ['<notificationId-1>', '<notificationId-2>']);
* ```
* @internal
*/
deleteNotifications(tenantId: string, notificationIds: string[]): Promise<NotificationDeleteResponse>;

/**
* Deletes all notifications from the current user's inbox.
*
* Uses the server-side `deleteAll` flag — no per-notification IDs are sent.
*
* @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`)
* @returns Operation result confirming the bulk delete
* {@link NotificationDeleteAllResponse}
*
* @example
* ```typescript
* await notifications.deleteAll('<tenantId>');
* ```
* @internal
*/
deleteAll(tenantId: string): Promise<NotificationDeleteAllResponse>;
}
52 changes: 52 additions & 0 deletions src/services/notification/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {
NotificationGetResponse,
} from '../../models/notification/notifications.types';
import type {
NotificationDeleteAllResponse,
NotificationDeleteResponse,
NotificationMarkAllReadResponse,
NotificationServiceModel,
NotificationUpdateReadResponse,
Expand Down Expand Up @@ -169,6 +171,56 @@ export class NotificationService extends BaseService implements NotificationServ
return { success: true, data: { all: true, read: true } };
}

/**
* Deletes the given notifications.
*
* @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`)
* @param notificationIds - GUIDs of notifications to delete. Must be non-empty —
* the server rejects an empty array with HTTP 400.
* @returns Operation result echoing the deleted IDs
* {@link NotificationDeleteResponse}
*
* @example
* ```typescript
* await notifications.deleteNotifications('<tenantId>', ['<notificationId-1>', '<notificationId-2>']);
* ```
* @internal
*/
@track('Notifications.DeleteNotifications')
async deleteNotifications(tenantId: string, notificationIds: string[]): Promise<NotificationDeleteResponse> {
await this.post(NOTIFICATION_ENDPOINTS.DELETE_BULK, {
// API spec misspells the key as `notifcationIds` — preserve it.
notifcationIds: notificationIds,
deleteAll: false,
}, { headers: createHeaders({ [TENANT_ID]: tenantId }) });
return { success: true, data: { notificationIds } };
}

/**
* Deletes all notifications from the current user's inbox.
*
* Uses the server-side `deleteAll` flag — no per-notification IDs are sent.
*
* @param tenantId - Tenant GUID (sent via `X-UIPATH-Internal-TenantId`)
* @returns Operation result confirming the bulk delete
* {@link NotificationDeleteAllResponse}
*
* @example
* ```typescript
* await notifications.deleteAll('<tenantId>');
* ```
* @internal
*/
@track('Notifications.DeleteAll')
async deleteAll(tenantId: string): Promise<NotificationDeleteAllResponse> {
await this.post(NOTIFICATION_ENDPOINTS.DELETE_BULK, {
// API spec misspells the key as `notifcationIds` — preserve it.
notifcationIds: [],
deleteAll: true,
}, { headers: createHeaders({ [TENANT_ID]: tenantId }) });
return { success: true, data: { all: true } };
}

private async updateRead(
tenantId: string,
notificationIds: string[],
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants/endpoints/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ const NOTIFICATION_API_BASE = `${NOTIFICATION_BASE}/notificationserviceapi`;
export const NOTIFICATION_ENDPOINTS = {
GET_ALL: `${NOTIFICATION_API_BASE}/odata/v1/NotificationEntry`,
UPDATE_READ: `${NOTIFICATION_API_BASE}/odata/v1/NotificationEntry/UiPath.NotificationService.Api.UpdateRead`,
DELETE_BULK: `${NOTIFICATION_API_BASE}/odata/v1/NotificationEntry/UiPath.NotificationService.Api.DeleteBulk`,
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,7 @@ describe.each(modes)('Notifications - Integration Tests [%s]', (mode) => {
expect(result.data).toEqual({ all: true, read: true });
});
});

// Note: no deleteNotifications / deleteAll integration tests — these destructively
// mutate the inbox with no SDK-level undo. Run manually if needed.
});
50 changes: 50 additions & 0 deletions tests/unit/services/notification/notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,56 @@ describe('NotificationService Unit Tests', () => {
});
});

describe('deleteNotifications', () => {
it('should POST notifcationIds (preserving the API typo), deleteAll=false, and tenant header', async () => {
mockApiClient.post.mockResolvedValue(undefined);
const ids = [
NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID,
NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID_2,
];

const result = await notificationService.deleteNotifications(NOTIFICATION_TEST_CONSTANTS.TENANT_ID, ids);

expect(mockApiClient.post).toHaveBeenCalledWith(
NOTIFICATION_ENDPOINTS.DELETE_BULK,
{ notifcationIds: ids, deleteAll: false },
{ headers: TENANT_HEADER }
);
expect(result).toEqual({ success: true, data: { notificationIds: ids } });
});

it('should propagate errors', async () => {
mockApiClient.post.mockRejectedValue(createMockError(TEST_CONSTANTS.ERROR_MESSAGE));

await expect(
notificationService.deleteNotifications(NOTIFICATION_TEST_CONSTANTS.TENANT_ID, [NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID])
).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
});
Comment on lines +207 to +213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the testing conventions, entity-specific methods (those that operate on specific IDs) should use domain-specific error constants, not the generic TEST_CONSTANTS.ERROR_MESSAGE. Compare with the markRead error test (line 129) which correctly uses NOTIFICATION_TEST_CONSTANTS.ERROR_NOTIFICATION_NOT_FOUND.

Suggested change
it('should propagate errors', async () => {
mockApiClient.post.mockRejectedValue(createMockError(TEST_CONSTANTS.ERROR_MESSAGE));
await expect(
notificationService.deleteNotifications(NOTIFICATION_TEST_CONSTANTS.TENANT_ID, [NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID])
).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE);
});
it('should propagate errors', async () => {
mockApiClient.post.mockRejectedValue(createMockError(NOTIFICATION_TEST_CONSTANTS.ERROR_NOTIFICATION_NOT_FOUND));
await expect(
notificationService.deleteNotifications(NOTIFICATION_TEST_CONSTANTS.TENANT_ID, [NOTIFICATION_TEST_CONSTANTS.NOTIFICATION_ID])
).rejects.toThrow(NOTIFICATION_TEST_CONSTANTS.ERROR_NOTIFICATION_NOT_FOUND);
});

});

describe('deleteAll', () => {
it('should POST deleteAll=true with empty notifcationIds array and tenant header', async () => {
mockApiClient.post.mockResolvedValue(undefined);

const result = await notificationService.deleteAll(NOTIFICATION_TEST_CONSTANTS.TENANT_ID);

expect(mockApiClient.post).toHaveBeenCalledWith(
NOTIFICATION_ENDPOINTS.DELETE_BULK,
{ notifcationIds: [], deleteAll: true },
{ headers: TENANT_HEADER }
);
expect(result).toEqual({ success: true, data: { all: true } });
});

it('should propagate errors', async () => {
mockApiClient.post.mockRejectedValue(createMockError(TEST_CONSTANTS.ERROR_MESSAGE));

await expect(
notificationService.deleteAll(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();
Expand Down
Loading