mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add extensible notification providers + UI/API
Introduce a provider-based notification system and wire it through the API and admin UI. Added INotificationProvider + notification service implementation and providers (apprise, discord, ntfy, pushover), plus a GET /api/admin/notifications/providers endpoint to expose provider metadata. Refactored code to use provider type strings (removed enum coupling), updated masking/encryption calls, and simplified the test notification endpoint to accept backendId or type+config and call sendToBackend directly. UI: NotificationsTab now fetches provider metadata and renders provider cards and dynamic config forms (fields driven by provider metadata). Added config field rendering, improved backend cards, and edit/delete actions. APIs: New providers route, updated admin notification CRUD routes to validate provider types dynamically, updated test route schema. Added download-client categories POST API to fetch categories from clients and wired postImportCategory handling in download-client routes. Other notable changes: BookDate now fetches Claude models dynamically from Anthropic's Models API; added paginated model fetch helper. Added ALLOW_WEAK_PASSWORD flag exposure to auth providers and password change logic. Doc updates and various tests added/updated. File-organization doc clarifies EPERM fix using stream-based copy.
This commit is contained in:
@@ -100,8 +100,8 @@ describe('substituteTemplate', () => {
|
||||
expect(result).toBe('Author/Title/Narrator');
|
||||
});
|
||||
|
||||
it('should handle mixed forward and backward slashes', () => {
|
||||
const template = '{author}\\{title}/{narrator}';
|
||||
it('should resolve escaped braces to literal brace characters', () => {
|
||||
const template = '{author}/\\{{narrator}\\}/{title}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title',
|
||||
@@ -109,7 +109,7 @@ describe('substituteTemplate', () => {
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/Title/Narrator');
|
||||
expect(result).toBe('Author/{Narrator}/Title');
|
||||
});
|
||||
|
||||
it('should trim dots from path components', () => {
|
||||
@@ -145,6 +145,74 @@ describe('substituteTemplate', () => {
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Audiobooks/Author/Books/Title');
|
||||
});
|
||||
|
||||
it('should resolve escaped left brace only', () => {
|
||||
const template = '{author}/\\{prefix {title}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title'
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/{prefix Title');
|
||||
});
|
||||
|
||||
it('should resolve escaped right brace only', () => {
|
||||
const template = '{author}/{title} suffix\\}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title'
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/Title suffix}');
|
||||
});
|
||||
|
||||
it('should resolve multiple escaped brace pairs', () => {
|
||||
const template = '\\{{author}\\}/\\{{title}\\}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title'
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('{Author}/{Title}');
|
||||
});
|
||||
|
||||
it('should handle escaped braces with missing optional variable', () => {
|
||||
const template = '{author}/\\{{narrator}\\}/{title}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title'
|
||||
// narrator is missing
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/{}/Title');
|
||||
});
|
||||
|
||||
it('should handle escaped braces adjacent to path separators', () => {
|
||||
const template = '{author}/\\{{narrator}\\}/{title}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title',
|
||||
narrator: 'Michael Kramer'
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/{Michael Kramer}/Title');
|
||||
});
|
||||
|
||||
it('should handle escaped braces around static text', () => {
|
||||
const template = '{author}/\\{narrated\\}/{title}';
|
||||
const variables: TemplateVariables = {
|
||||
author: 'Author',
|
||||
title: 'Title'
|
||||
};
|
||||
|
||||
const result = substituteTemplate(template, variables);
|
||||
expect(result).toBe('Author/{narrated}/Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTemplate', () => {
|
||||
@@ -205,8 +273,8 @@ describe('validateTemplate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject backslashes in template', () => {
|
||||
const result = validateTemplate('{author}\\{title}');
|
||||
it('should reject backslashes that are not brace escapes', () => {
|
||||
const result = validateTemplate('{author}\\n{title}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('forward slashes');
|
||||
});
|
||||
@@ -230,6 +298,42 @@ describe('validateTemplate', () => {
|
||||
expect(result.error).toContain('{narrator}');
|
||||
expect(result.error).toContain('{asin}');
|
||||
});
|
||||
|
||||
it('should accept escaped braces around a variable', () => {
|
||||
const result = validateTemplate('{author}/\\{{narrator}\\}/{title}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept escaped braces around static text', () => {
|
||||
const result = validateTemplate('{author}/\\{custom\\}/{title}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept escaped left brace only', () => {
|
||||
const result = validateTemplate('{author}/\\{prefix {title}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept escaped right brace only', () => {
|
||||
const result = validateTemplate('{author}/{title} suffix\\}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept multiple escaped brace pairs', () => {
|
||||
const result = validateTemplate('\\{{author}\\}/\\{{title}\\}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept backslash before brace but reject backslash before other characters', () => {
|
||||
const result = validateTemplate('{author}\\n/\\{{title}\\}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('forward slashes');
|
||||
});
|
||||
|
||||
it('should accept a template that is only escaped braces', () => {
|
||||
const result = validateTemplate('\\{\\}');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateMockPreviews', () => {
|
||||
@@ -305,6 +409,17 @@ describe('generateMockPreviews', () => {
|
||||
expect(preview).toContain(' - B');
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve escaped braces in previews', () => {
|
||||
const template = '{author}/\\{{narrator}\\}/{title}';
|
||||
const previews = generateMockPreviews(template);
|
||||
|
||||
// First two mock entries have narrators
|
||||
expect(previews[0]).toContain('{Michael Kramer}');
|
||||
expect(previews[1]).toContain('{Stephen Fry}');
|
||||
// Third mock entry has no narrator - escaped braces remain empty
|
||||
expect(previews[2]).toContain('{}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidVariables', () => {
|
||||
|
||||
Reference in New Issue
Block a user