mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(library): expose rename setting in scoped library list
Return fileRenameEnabled for non-superuser library responses so the bulk rename selector includes configured libraries. Closes #918
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Library } from '@bookorbit/types'
|
||||
|
||||
const libraries = ref<Library[]>([])
|
||||
const fetchLibraries = vi.fn<() => Promise<void>>()
|
||||
const selectLibrary = vi.fn<(libraryId: number) => void>()
|
||||
const loadPreview = vi.fn<() => Promise<void>>()
|
||||
|
||||
const bulk = {
|
||||
selectedLibraryId: ref<number | null>(null),
|
||||
page: ref(1),
|
||||
pageSize: ref(50),
|
||||
statusFilter: ref(undefined),
|
||||
totalPages: ref(1),
|
||||
previewItems: ref([]),
|
||||
previewTotal: ref(0),
|
||||
totalByStatus: ref({ will_rename: 0, unchanged: 0, collision: 0, no_pattern: 0, error: 0 }),
|
||||
loading: ref(false),
|
||||
previewError: ref<string | null>(null),
|
||||
executing: ref(false),
|
||||
executionStats: ref(null),
|
||||
executionError: ref<string | null>(null),
|
||||
selectLibrary,
|
||||
loadPreview,
|
||||
execute: vi.fn<() => Promise<void>>(),
|
||||
cancelExecution: vi.fn<() => void>(),
|
||||
setPage: vi.fn<(page: number) => void>(),
|
||||
setStatusFilter: vi.fn<() => void>(),
|
||||
}
|
||||
|
||||
vi.mock('@/features/library/composables/useLibraries', () => ({
|
||||
useLibraries: () => ({ libraries, fetchLibraries }),
|
||||
}))
|
||||
|
||||
vi.mock('../../composables/useBulkRename', () => ({
|
||||
useBulkRename: () => bulk,
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useElementSize: () => ({ width: ref(1024), height: ref(768) }),
|
||||
}))
|
||||
|
||||
import BulkRenameView from './BulkRenameView.vue'
|
||||
|
||||
function makeLibrary(overrides: Partial<Library> = {}): Library {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Enabled Library',
|
||||
icon: null,
|
||||
displayOrder: 0,
|
||||
coverAspectRatio: '2/3',
|
||||
watch: false,
|
||||
autoScanCronExpression: null,
|
||||
metadataPrecedence: [],
|
||||
formatPriority: [],
|
||||
allowedFormats: [],
|
||||
organizationMode: 'book_per_file',
|
||||
excludePatterns: [],
|
||||
readingThreshold: 10,
|
||||
markAsFinishedPercentComplete: 95,
|
||||
fileNamingPattern: null,
|
||||
fileWriteEnabled: false,
|
||||
fileWriteWriteCover: false,
|
||||
fileWriteEpubEnabled: false,
|
||||
fileWriteEpubMaxFileSizeMb: 100,
|
||||
fileWriteFb2Enabled: false,
|
||||
fileWriteFb2MaxFileSizeMb: 100,
|
||||
fileWritePdfEnabled: false,
|
||||
fileWritePdfMaxFileSizeMb: 100,
|
||||
fileWriteCbxEnabled: false,
|
||||
fileWriteCbxMaxFileSizeMb: 500,
|
||||
fileWriteKindleEnabled: false,
|
||||
fileWriteKindleMaxFileSizeMb: 100,
|
||||
fileWriteAudioEnabled: false,
|
||||
fileWriteAudioMaxFileSizeMb: 500,
|
||||
fileRenameEnabled: true,
|
||||
folders: [],
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return shallowMount(BulkRenameView, {
|
||||
global: {
|
||||
stubs: {
|
||||
BulkRenameConfirmDialog: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('BulkRenameView library eligibility', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
fetchLibraries.mockResolvedValue(undefined)
|
||||
loadPreview.mockResolvedValue(undefined)
|
||||
libraries.value = []
|
||||
bulk.selectedLibraryId.value = null
|
||||
})
|
||||
|
||||
it('lists every library with file rename enabled and excludes disabled libraries', async () => {
|
||||
libraries.value = [
|
||||
makeLibrary({ id: 1, name: 'First Enabled Library' }),
|
||||
makeLibrary({ id: 2, name: 'Rename Disabled', fileRenameEnabled: false }),
|
||||
makeLibrary({ id: 3, name: 'Second Enabled Library' }),
|
||||
]
|
||||
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
const options = wrapper.findAll('select option').slice(1)
|
||||
expect(options.map((option) => option.text())).toEqual(['First Enabled Library', 'Second Enabled Library'])
|
||||
expect(wrapper.text()).not.toContain('No libraries have file rename enabled')
|
||||
expect(fetchLibraries).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows the empty state when every library has file rename disabled', async () => {
|
||||
libraries.value = [makeLibrary({ id: 1, fileRenameEnabled: false }), makeLibrary({ id: 2, fileRenameEnabled: false })]
|
||||
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.findAll('select option')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('No libraries have file rename enabled')
|
||||
})
|
||||
|
||||
it('selects an eligible library and loads its preview', async () => {
|
||||
libraries.value = [makeLibrary({ id: 17 })]
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('select').setValue('17')
|
||||
|
||||
expect(selectLibrary).toHaveBeenCalledWith(17)
|
||||
expect(loadPreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,7 @@ vi.mock('drizzle-orm', () => ({
|
||||
sql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ op: 'sql', text: strings.join(''), values })),
|
||||
}));
|
||||
|
||||
import { libraries } from '../../db/schema';
|
||||
import { LibraryRepository } from './library.repository';
|
||||
|
||||
describe('LibraryRepository', () => {
|
||||
@@ -51,6 +52,24 @@ describe('LibraryRepository', () => {
|
||||
expect(txUpdateSet).toHaveBeenNthCalledWith(2, { displayOrder: 6 });
|
||||
});
|
||||
|
||||
it('findAllForUser includes file rename eligibility in the scoped library projection', async () => {
|
||||
const orderBy = vi.fn().mockResolvedValue([]);
|
||||
const groupBy = vi.fn().mockReturnValue({ orderBy });
|
||||
const leftJoin = vi.fn().mockReturnValue({ groupBy });
|
||||
const innerJoin = vi.fn().mockReturnValue({ leftJoin });
|
||||
const from = vi.fn().mockReturnValue({ innerJoin });
|
||||
db.select.mockReturnValue({ from });
|
||||
|
||||
await repo.findAllForUser(42);
|
||||
|
||||
expect(db.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fileRenameEnabled: libraries.fileRenameEnabled,
|
||||
}),
|
||||
);
|
||||
expect(orderBy).toHaveBeenCalledWith(libraries.displayOrder, libraries.name);
|
||||
});
|
||||
|
||||
it('getStats aggregates counts, sizes, and format map', async () => {
|
||||
const countChain = {
|
||||
from: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -39,6 +39,7 @@ export class LibraryRepository {
|
||||
displayOrder: libraries.displayOrder,
|
||||
coverAspectRatio: libraries.coverAspectRatio,
|
||||
scanMode: libraries.scanMode,
|
||||
fileRenameEnabled: libraries.fileRenameEnabled,
|
||||
createdAt: libraries.createdAt,
|
||||
updatedAt: libraries.updatedAt,
|
||||
bookCount: sql<number>`count(${books.id})::int`,
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('LibraryService', () => {
|
||||
});
|
||||
|
||||
it('findAll uses scoped folder query for non-superusers', async () => {
|
||||
libraryRepo.findAllForUser.mockResolvedValue([{ id: 10, name: 'A', coverAspectRatio: '1/1' }]);
|
||||
libraryRepo.findAllForUser.mockResolvedValue([{ id: 10, name: 'A', coverAspectRatio: '1/1', fileRenameEnabled: true }]);
|
||||
libraryRepo.findFoldersByLibraryIds.mockResolvedValue([{ id: 1, libraryId: 10, path: '/a', createdAt: new Date() }]);
|
||||
|
||||
const result = await service.findAll({ id: 7, isSuperuser: false, contentFilters: EMPTY_CONTENT_FILTER_RULES } as any);
|
||||
@@ -105,6 +105,7 @@ describe('LibraryService', () => {
|
||||
expect(libraryRepo.findAllFolders).not.toHaveBeenCalled();
|
||||
expect(result[0].folders).toEqual([{ id: 1, path: '/a', createdAt: expect.any(Date) }]);
|
||||
expect(result[0].coverAspectRatio).toBe('1/1');
|
||||
expect(result[0].fileRenameEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('passes contentFilters to findAllForUser for non-superuser and skips for superuser', async () => {
|
||||
|
||||
Reference in New Issue
Block a user