mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(library): include saved settings in scoped library summaries
Return saved organization and automation settings in the user-scoped library list so non-superuser summaries reflect the saved configuration. Cover enabled and disabled settings, library isolation, and permission denials with API regression tests. Closes #1664
This commit is contained in:
@@ -74,7 +74,7 @@ describe('LibraryRepository', () => {
|
|||||||
expect(where).toHaveBeenCalledWith({ op: 'eq', left: libraries.type, right: 'books' });
|
expect(where).toHaveBeenCalledWith({ op: 'eq', left: libraries.type, right: 'books' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('findAllForUser includes file rename eligibility and counts only present books', async () => {
|
it('findAllForUser includes organization and automation settings and counts only present books', async () => {
|
||||||
const orderBy = vi.fn().mockResolvedValue([]);
|
const orderBy = vi.fn().mockResolvedValue([]);
|
||||||
const groupBy = vi.fn().mockReturnValue({ orderBy });
|
const groupBy = vi.fn().mockReturnValue({ orderBy });
|
||||||
const where = vi.fn().mockReturnValue({ groupBy });
|
const where = vi.fn().mockReturnValue({ groupBy });
|
||||||
@@ -87,6 +87,10 @@ describe('LibraryRepository', () => {
|
|||||||
|
|
||||||
expect(db.select).toHaveBeenCalledWith(
|
expect(db.select).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
organizationMode: libraries.organizationMode,
|
||||||
|
autoScanCronExpression: libraries.autoScanCronExpression,
|
||||||
|
watch: libraries.watch,
|
||||||
|
fileWriteEnabled: libraries.fileWriteEnabled,
|
||||||
fileRenameEnabled: libraries.fileRenameEnabled,
|
fileRenameEnabled: libraries.fileRenameEnabled,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ export class LibraryRepository {
|
|||||||
icon: libraries.icon,
|
icon: libraries.icon,
|
||||||
displayOrder: libraries.displayOrder,
|
displayOrder: libraries.displayOrder,
|
||||||
coverAspectRatio: libraries.coverAspectRatio,
|
coverAspectRatio: libraries.coverAspectRatio,
|
||||||
|
watch: libraries.watch,
|
||||||
|
autoScanCronExpression: libraries.autoScanCronExpression,
|
||||||
|
organizationMode: libraries.organizationMode,
|
||||||
scanMode: libraries.scanMode,
|
scanMode: libraries.scanMode,
|
||||||
|
fileWriteEnabled: libraries.fileWriteEnabled,
|
||||||
fileRenameEnabled: libraries.fileRenameEnabled,
|
fileRenameEnabled: libraries.fileRenameEnabled,
|
||||||
createdAt: libraries.createdAt,
|
createdAt: libraries.createdAt,
|
||||||
updatedAt: libraries.updatedAt,
|
updatedAt: libraries.updatedAt,
|
||||||
|
|||||||
@@ -210,6 +210,98 @@ describe('Library admin workflows (e2e)', { timeout: SCENARIO_TIMEOUT_MS }, () =
|
|||||||
await closeAuthorizationMatrixE2EContext(ctx);
|
await closeAuthorizationMatrixE2EContext(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ organizationMode: 'book_per_file', autoScanCronExpression: '0 2 * * *', watch: true, fileWriteEnabled: true },
|
||||||
|
{ organizationMode: 'book_per_folder', autoScanCronExpression: null, watch: false, fileWriteEnabled: false },
|
||||||
|
])('returns saved library summary settings to non-superusers: $organizationMode', async (savedSettings) => {
|
||||||
|
const library = await createLibraryWithFolder(ctx);
|
||||||
|
const hiddenLibrary = await createLibraryWithFolder(ctx);
|
||||||
|
await ctx.db.update(schema.libraries).set(savedSettings).where(eq(schema.libraries.id, library.libraryId));
|
||||||
|
await grantLibraryAccess(ctx, manager.userId, library.libraryId, 'viewer');
|
||||||
|
|
||||||
|
const detailResponse = await ctx.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/v1/libraries/${library.libraryId}`,
|
||||||
|
headers: authHeader(manager.accessToken),
|
||||||
|
});
|
||||||
|
expect(detailResponse.statusCode).toBe(200);
|
||||||
|
expect(detailResponse.json()).toMatchObject(savedSettings);
|
||||||
|
|
||||||
|
const superuserResponse = await ctx.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/libraries',
|
||||||
|
headers: authHeader(ctx.adminToken),
|
||||||
|
});
|
||||||
|
expect(superuserResponse.statusCode).toBe(200);
|
||||||
|
const superuserLibraries = superuserResponse.json() as Array<{ id: number }>;
|
||||||
|
expect(superuserLibraries.find(({ id }) => id === library.libraryId)).toMatchObject(savedSettings);
|
||||||
|
|
||||||
|
const scopedResponse = await ctx.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/libraries',
|
||||||
|
headers: authHeader(manager.accessToken),
|
||||||
|
});
|
||||||
|
expect(scopedResponse.statusCode).toBe(200);
|
||||||
|
const scopedLibraries = scopedResponse.json() as Array<{ id: number }>;
|
||||||
|
expect(scopedLibraries.map(({ id }) => id)).not.toContain(hiddenLibrary.libraryId);
|
||||||
|
expect(scopedLibraries.find(({ id }) => id === library.libraryId)).toMatchObject({
|
||||||
|
...savedSettings,
|
||||||
|
accessLevel: 'viewer',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not grant management or file-write actions to viewers who can read automation settings', async () => {
|
||||||
|
const library = await createLibraryWithFolder(ctx);
|
||||||
|
const savedSettings = { watch: true, autoScanCronExpression: '0 2 * * *', fileWriteEnabled: true };
|
||||||
|
await ctx.db.update(schema.libraries).set(savedSettings).where(eq(schema.libraries.id, library.libraryId));
|
||||||
|
await grantLibraryAccess(ctx, scopedUser.userId, library.libraryId, 'viewer');
|
||||||
|
|
||||||
|
const listResponse = await ctx.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/libraries',
|
||||||
|
headers: authHeader(scopedUser.accessToken),
|
||||||
|
});
|
||||||
|
expect(listResponse.statusCode).toBe(200);
|
||||||
|
const visibleLibraries = listResponse.json() as Array<{ id: number }>;
|
||||||
|
expect(visibleLibraries.find(({ id }) => id === library.libraryId)).toMatchObject(savedSettings);
|
||||||
|
|
||||||
|
for (const payload of [{ watch: false }, { autoScanCronExpression: null }, { fileWriteEnabled: false }]) {
|
||||||
|
const response = await ctx.app.inject({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: `/api/v1/libraries/${library.libraryId}`,
|
||||||
|
headers: authHeader(scopedUser.accessToken),
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
expectError(response, 403, 'Missing permission: manage_libraries');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const action of ['scan', 'refresh-covers']) {
|
||||||
|
const response = await ctx.app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/v1/scanner/libraries/${library.libraryId}/${action}`,
|
||||||
|
headers: authHeader(scopedUser.accessToken),
|
||||||
|
});
|
||||||
|
expectError(response, 403, 'Missing permission: manage_libraries');
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeUrl = `/api/v1/libraries/${library.libraryId}/write-metadata-to-files`;
|
||||||
|
expectError(
|
||||||
|
await ctx.app.inject({ method: 'POST', url: writeUrl, headers: authHeader(scopedUser.accessToken) }),
|
||||||
|
403,
|
||||||
|
'Missing permission: library_edit_metadata',
|
||||||
|
);
|
||||||
|
const metadataEditor = await createUserAndLogin(ctx, { permissions: [Permission.LibraryEditMetadata] });
|
||||||
|
await grantLibraryAccess(ctx, metadataEditor.userId, library.libraryId, 'viewer');
|
||||||
|
expectError(
|
||||||
|
await ctx.app.inject({ method: 'POST', url: writeUrl, headers: authHeader(metadataEditor.accessToken) }),
|
||||||
|
403,
|
||||||
|
'Insufficient library access level',
|
||||||
|
);
|
||||||
|
|
||||||
|
const [storedLibrary] = await ctx.db.select().from(schema.libraries).where(eq(schema.libraries.id, library.libraryId));
|
||||||
|
expect(storedLibrary).toMatchObject(savedSettings);
|
||||||
|
});
|
||||||
|
|
||||||
describe('date added recompute', () => {
|
describe('date added recompute', () => {
|
||||||
async function finishRecompute(libraryId: number): Promise<AddedAtRecomputeJob> {
|
async function finishRecompute(libraryId: number): Promise<AddedAtRecomputeJob> {
|
||||||
let result: AddedAtRecomputeJob;
|
let result: AddedAtRecomputeJob;
|
||||||
|
|||||||
Reference in New Issue
Block a user