audiobookshelf/test/server/controllers/LibraryItemController_merge.test.js
2026-02-12 19:57:04 +02:00

148 lines
5.4 KiB
JavaScript

const { expect } = require('chai')
const { Sequelize } = require('sequelize')
const sinon = require('sinon')
const fs = require('../../../server/libs/fsExtra')
const Path = require('path')
const Database = require('../../../server/Database')
const ApiRouter = require('../../../server/routers/ApiRouter')
const LibraryItemController = require('../../../server/controllers/LibraryItemController')
const ApiCacheManager = require('../../../server/managers/ApiCacheManager')
const Auth = require('../../../server/Auth')
const Logger = require('../../../server/Logger')
const LibraryItemScanner = require('../../../server/scanner/LibraryItemScanner')
describe('LibraryItemController Merge', () => {
/** @type {ApiRouter} */
let apiRouter
beforeEach(async () => {
global.ServerSettings = {}
Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '')
await Database.buildModels()
apiRouter = new ApiRouter({
auth: new Auth(),
apiCacheManager: new ApiCacheManager()
})
sinon.stub(Logger, 'info')
sinon.stub(Logger, 'error')
sinon.stub(Logger, 'warn')
// Mock fs-extra methods
sinon.stub(fs, 'ensureDir').resolves()
sinon.stub(fs, 'move').resolves()
sinon.stub(fs, 'pathExists').resolves(false)
sinon.stub(fs, 'readdir').resolves([])
sinon.stub(fs, 'remove').resolves()
sinon.stub(fs, 'rmdir').resolves()
// Mock Scanner
sinon.stub(LibraryItemScanner, 'scanLibraryItem').resolves()
})
afterEach(async () => {
sinon.restore()
// Clear all tables
await Database.sequelize.sync({ force: true })
})
describe('batchMerge', () => {
it('should merge two file-based items into a new folder', async () => {
// Setup Library and Folder
const library = await Database.libraryModel.create({ name: 'Book Lib', mediaType: 'book' })
const libraryFolder = await Database.libraryFolderModel.create({ path: '/books', libraryId: library.id })
// Item 1: File in root
const book1 = await Database.bookModel.create({ title: 'Book 1', audioFiles: [] })
const item1 = await Database.libraryItemModel.create({
mediaId: book1.id,
mediaType: 'book',
libraryId: library.id,
libraryFolderId: libraryFolder.id,
path: '/books/book1.mp3',
relPath: 'book1.mp3',
isFile: true
})
// Create Author
const author = await Database.authorModel.create({ name: 'Author 1', libraryId: library.id })
await Database.bookAuthorModel.create({ bookId: book1.id, authorId: author.id })
await item1.save()
// Item 2: File in root
const book2 = await Database.bookModel.create({ title: 'Book 1 Part 2', audioFiles: [] })
const item2 = await Database.libraryItemModel.create({
mediaId: book2.id,
mediaType: 'book',
libraryId: library.id,
libraryFolderId: libraryFolder.id,
path: '/books/book2.mp3',
relPath: 'book2.mp3',
isFile: true
})
item2.media = book2
// Mock user
const user = { canDelete: true, username: 'admin' }
const req = {
user,
body: {
libraryItemIds: [item1.id, item2.id]
}
}
const res = {
json: sinon.spy(),
sendStatus: sinon.spy(),
status: sinon.stub().returnsThis(),
send: sinon.spy()
}
await LibraryItemController.batchMerge.bind(apiRouter)(req, res)
// Verify response
if (res.status.called) {
console.log('Error status:', res.status.args[0])
console.log('Error send:', res.send.args[0])
}
expect(res.json.calledOnce).to.be.true
const result = res.json.args[0][0]
expect(result.success).to.be.true
expect(result.successIds).to.have.lengthOf(1) // Only item2 is "processed" (deleted), item1 is updated
expect(result.successIds).to.include(item2.id)
// Verify fs calls
// Should create folder "Author 1 - Book 1"
const expectedFolderPath = Path.join('/books', 'Author 1 - Book 1')
expect(fs.ensureDir.calledWith(expectedFolderPath)).to.be.true
// Should move item1
expect(fs.move.calledWith('/books/book1.mp3', Path.join(expectedFolderPath, 'book1.mp3'))).to.be.true
// Should move item2
expect(fs.move.calledWith('/books/book2.mp3', Path.join(expectedFolderPath, 'book2.mp3'))).to.be.true
// item1 should be updated (path change) - NOT checked here because scanLibraryItem is mocked and it handles the update
// const updatedItem1 = await Database.libraryItemModel.findByPk(item1.id)
// expect(updatedItem1.path).to.equal(Path.join(expectedFolderPath, 'book1.mp3'))
// item2 should be deleted
const updatedItem2 = await Database.libraryItemModel.findByPk(item2.id)
expect(updatedItem2).to.be.null
// Verify Scanner called
expect(LibraryItemScanner.scanLibraryItem.called).to.be.true
const scanArgs = LibraryItemScanner.scanLibraryItem.firstCall.args
expect(scanArgs[0]).to.equal(item1.id)
expect(scanArgs[1]).to.deep.equal({
path: expectedFolderPath,
relPath: 'Author 1 - Book 1',
isFile: false
})
})
})
})