mirror of
https://github.com/unraid/api.git
synced 2026-01-01 22:20:05 -06:00
test: filtering notifications
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { describe, beforeEach, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
Importance,
|
||||
type NotificationData,
|
||||
NotificationType,
|
||||
type Notification,
|
||||
type NotificationOverview,
|
||||
type NotificationCounts,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
|
||||
describe('NotificationsService', () => {
|
||||
// we run sequentially here because this module's state depends on external, shared systems
|
||||
// rn, it's complicated to make the tests atomic & isolated
|
||||
describe.sequential('NotificationsService', () => {
|
||||
const notificationImportance = Object.values(Importance);
|
||||
let service: NotificationsService;
|
||||
const basePath = '/tmp/test/notifications';
|
||||
const testPaths = {
|
||||
@@ -31,18 +35,12 @@ describe('NotificationsService', () => {
|
||||
service = module.get<NotificationsService>(NotificationsService); // this might need to be a module.resolve instead of get
|
||||
vi.spyOn(service, 'paths').mockImplementation(() => testPaths);
|
||||
|
||||
// clear the notifications directory
|
||||
await mkdir(testPaths.basePath, { recursive: true });
|
||||
await rm(testPaths.basePath, { force: true, recursive: true });
|
||||
|
||||
// init notification directories
|
||||
await mkdir(testPaths.UNREAD, { recursive: true });
|
||||
await mkdir(testPaths.ARCHIVE, { recursive: true });
|
||||
await service.deleteAllNotifications();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// clear notifications directory
|
||||
await rm(testPaths.basePath, { force: true, recursive: true });
|
||||
// make sure each test is isolated (as much as possible)
|
||||
afterEach(async () => {
|
||||
await service.deleteAllNotifications();
|
||||
});
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
@@ -65,17 +63,19 @@ describe('NotificationsService', () => {
|
||||
);
|
||||
}
|
||||
|
||||
async function doesExist(
|
||||
{ id }: Pick<Notification, 'id'>,
|
||||
type: NotificationType = NotificationType.UNREAD
|
||||
) {
|
||||
const storedNotification = await findById(id, type);
|
||||
expect(storedNotification).toBeDefined();
|
||||
return !!storedNotification;
|
||||
function doesExist(expectImplementation: typeof expect) {
|
||||
return async (
|
||||
{ id }: Pick<Notification, 'id'>,
|
||||
type: NotificationType = NotificationType.UNREAD
|
||||
) => {
|
||||
const storedNotification = await findById(id, type);
|
||||
expectImplementation(storedNotification).toBeDefined();
|
||||
return !!storedNotification;
|
||||
};
|
||||
}
|
||||
|
||||
async function forEachImportance(action: (importance: Importance) => Promise<void>) {
|
||||
for (const importance of Object.values(Importance)) {
|
||||
for (const importance of notificationImportance) {
|
||||
await action(importance);
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,8 @@ describe('NotificationsService', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// currently unused b/c of difficulty implementing NotificationOverview tests
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function forAllTypesAndImportances(
|
||||
action: (type: NotificationType, importance: Importance) => Promise<void>
|
||||
) {
|
||||
@@ -96,17 +98,65 @@ describe('NotificationsService', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function diffCounts(current: NotificationCounts, previous: NotificationCounts) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(current).map(([key]) => {
|
||||
return [key, current[key] - previous[key]] as const;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// currently unused b/c of difficulty implementing NotificationOverview tests
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function diffOverview(current: NotificationOverview, previous: NotificationOverview) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(current).map(([key]) => {
|
||||
return [key, diffCounts(current[key], previous[key])];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Sanity Tests
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
it('NotificationsService test setup should be defined', () => {
|
||||
it('NotificationsService test setup should be defined', ({ expect }) => {
|
||||
expect(service).toBeDefined();
|
||||
expect(service.paths()).toEqual(testPaths);
|
||||
const snapshot = service.getOverview();
|
||||
Object.values(testPaths).forEach((path) => expect(existsSync(path)).toBeTruthy());
|
||||
|
||||
const endSnapshot = service.getOverview();
|
||||
expect(snapshot).toEqual(endSnapshot);
|
||||
|
||||
// check that all counts are 0
|
||||
Object.values(snapshot.archive).forEach((count) => {
|
||||
expect(count).toEqual(0);
|
||||
});
|
||||
Object.values(snapshot.unread).forEach((count) => {
|
||||
expect(count).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly create, load, and delete a notification', async () => {
|
||||
it('generates unique ids', async () => {
|
||||
const notifications = await Promise.all([...new Array(100)].map(() => createNotification()));
|
||||
const notificationIds = new Set(notifications.map((notification) => notification.id));
|
||||
expect(notificationIds.size).toEqual(notifications.length);
|
||||
});
|
||||
|
||||
it('returns ISO timestamps', async () => {
|
||||
const isISODate = (date: string) => new Date(date).toISOString() === date;
|
||||
const created = await createNotification();
|
||||
const loaded = await findById(created.id);
|
||||
expect(isISODate(created.timestamp ?? '')).toBeTruthy();
|
||||
expect(isISODate(loaded?.timestamp ?? '')).toBeTruthy();
|
||||
});
|
||||
|
||||
/**========================================================================
|
||||
* CRUD Smoke Tests
|
||||
*========================================================================**/
|
||||
|
||||
it('can correctly create, load, and delete a notification', async ({ expect }) => {
|
||||
const notificationData: NotificationData = {
|
||||
title: 'Test Notification',
|
||||
subject: 'Test Subject',
|
||||
@@ -115,7 +165,7 @@ describe('NotificationsService', () => {
|
||||
};
|
||||
const notification = await createNotification(notificationData);
|
||||
|
||||
// data in returned notification matches?
|
||||
// data in returned notification (from createNotification) matches?
|
||||
Object.entries(notificationData).forEach(([key, value]) => {
|
||||
expect(notification[key]).toEqual(value);
|
||||
});
|
||||
@@ -123,10 +173,7 @@ describe('NotificationsService', () => {
|
||||
// data in stored notification matches?
|
||||
const storedNotification = await findById(notification.id);
|
||||
expect(storedNotification).toBeDefined();
|
||||
if (!storedNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!storedNotification) return; // stop the test if there's no stored notification
|
||||
expect(storedNotification.id).toEqual(notification.id);
|
||||
expect(storedNotification.timestamp).toEqual(notification.timestamp);
|
||||
Object.entries(notificationData).forEach(([key, value]) => {
|
||||
@@ -139,20 +186,24 @@ describe('NotificationsService', () => {
|
||||
expect(deleted).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can correctly archive and unarchive a notification', async () => {
|
||||
/**--------------------------------------------
|
||||
* CRUD: Update Tests
|
||||
*---------------------------------------------**/
|
||||
|
||||
it('can correctly archive and unarchive a notification', async ({ expect }) => {
|
||||
await forEachImportance(async (importance) => {
|
||||
const notification = await createNotification({ importance });
|
||||
|
||||
await service.archiveNotification(notification);
|
||||
let exists = await doesExist(notification, NotificationType.ARCHIVE);
|
||||
let exists = await doesExist(expect)(notification, NotificationType.ARCHIVE);
|
||||
if (!exists) return;
|
||||
|
||||
await service.markAsUnread(notification);
|
||||
exists = await doesExist(notification, NotificationType.UNREAD);
|
||||
exists = await doesExist(expect)(notification, NotificationType.UNREAD);
|
||||
});
|
||||
});
|
||||
|
||||
it('can archive & unarchive all', async () => {
|
||||
it('can archive & unarchive all', async ({ expect }) => {
|
||||
await forEachImportance(async (importance) => {
|
||||
const notifications = await Promise.all([
|
||||
createNotification({ importance }),
|
||||
@@ -169,4 +220,103 @@ describe('NotificationsService', () => {
|
||||
await service.unarchiveAll(importance);
|
||||
});
|
||||
});
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Filtering Tests
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
it.each(notificationImportance)('loadNotifications respects %s filter', async (importance) => {
|
||||
const notifications = await Promise.all([
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
]);
|
||||
expect(notifications.length).toEqual(9);
|
||||
|
||||
const loaded = await service.getNotifications({
|
||||
type: NotificationType.UNREAD,
|
||||
importance,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
expect(loaded.length).toEqual(3);
|
||||
});
|
||||
|
||||
it.each(notificationImportance)('archiveAll respects %s filter', async (importance) => {
|
||||
const notifications = await Promise.all([
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
]);
|
||||
expect(notifications.length).toEqual(9);
|
||||
|
||||
await service.archiveAll(importance);
|
||||
const unreads = await service.getNotifications({
|
||||
type: NotificationType.UNREAD,
|
||||
importance,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
const archives = await service.getNotifications({
|
||||
type: NotificationType.ARCHIVE,
|
||||
importance,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
expect(unreads.length).toEqual(0);
|
||||
expect(archives.length).toEqual(3);
|
||||
});
|
||||
|
||||
it.each(notificationImportance)('unarchiveAll respects %s filter', async (importance) => {
|
||||
const notifications = await Promise.all([
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.ALERT }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.INFO }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
createNotification({ importance: Importance.WARNING }),
|
||||
]);
|
||||
expect(notifications.length).toEqual(9);
|
||||
|
||||
// test unarchive
|
||||
await service.archiveAll();
|
||||
await service.unarchiveAll(importance);
|
||||
const unreads = await service.getNotifications({
|
||||
type: NotificationType.UNREAD,
|
||||
importance,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
const archives = await service.getNotifications({
|
||||
type: NotificationType.ARCHIVE,
|
||||
importance,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
expect(unreads.length).toEqual(3);
|
||||
expect(archives.length).toEqual(0);
|
||||
});
|
||||
|
||||
/**========================================================================
|
||||
* Overview (Notification Stats) State
|
||||
*========================================================================**/
|
||||
|
||||
it.skip('calculates stats correctly', async () => {
|
||||
// todo implement
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import { getters } from '@app/store';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readdir, rename, unlink, writeFile } from 'fs/promises';
|
||||
import { mkdir, readdir, rename, rm, unlink, writeFile } from 'fs/promises';
|
||||
import { basename, join } from 'path';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { isFulfilled, isRejected, unraidTimestamp } from '@app/utils';
|
||||
@@ -227,6 +227,37 @@ export class NotificationsService {
|
||||
return { notification, overview: NotificationsService.overview };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all notifications from disk, but preserves
|
||||
* notification directories.
|
||||
*
|
||||
* Resets the notification overview to all zeroes.
|
||||
*/
|
||||
public async deleteAllNotifications() {
|
||||
const { basePath, UNREAD, ARCHIVE } = this.paths();
|
||||
// ensure the directory exists before deleting
|
||||
await mkdir(basePath, { recursive: true });
|
||||
await rm(basePath, { force: true, recursive: true });
|
||||
// recreate each notification directory
|
||||
await mkdir(UNREAD, { recursive: true });
|
||||
await mkdir(ARCHIVE, { recursive: true });
|
||||
NotificationsService.overview = {
|
||||
unread: {
|
||||
alert: 0,
|
||||
info: 0,
|
||||
warning: 0,
|
||||
total: 0,
|
||||
},
|
||||
archive: {
|
||||
alert: 0,
|
||||
info: 0,
|
||||
warning: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
return this.getOverview();
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* CRUD: Updating Notifications
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
Reference in New Issue
Block a user