From 96de890bddafcbbbb9c749e71a55fbdfc3e82bf0 Mon Sep 17 00:00:00 2001 From: Jens Bickel <235654990+JensBickel@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:19:19 +0100 Subject: [PATCH] test(placeholders): add server coverage for placeholder creation, serialization, and query visibility --- .../SeriesController.placeholders.test.js | 78 +++++++++++++++++ .../models/LibraryItem.placeholder.test.js | 47 +++++++++++ ...braryItemsBookFilters.placeholders.test.js | 84 +++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 test/server/controllers/SeriesController.placeholders.test.js create mode 100644 test/server/models/LibraryItem.placeholder.test.js create mode 100644 test/server/utils/queries/libraryItemsBookFilters.placeholders.test.js diff --git a/test/server/controllers/SeriesController.placeholders.test.js b/test/server/controllers/SeriesController.placeholders.test.js new file mode 100644 index 000000000..079e006fb --- /dev/null +++ b/test/server/controllers/SeriesController.placeholders.test.js @@ -0,0 +1,78 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') +const sinon = require('sinon') + +const Database = require('../../../server/Database') +const ApiRouter = require('../../../server/routers/ApiRouter') +const SeriesController = require('../../../server/controllers/SeriesController') +const ApiCacheManager = require('../../../server/managers/ApiCacheManager') +const Auth = require('../../../server/Auth') +const Logger = require('../../../server/Logger') +const User = require('../../../server/models/User') + +describe('SeriesController placeholders', () => { + /** @type {ApiRouter} */ + let apiRouter + let library + let libraryFolder + let series + let user + + 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, 'warn') + + library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + series = await Database.seriesModel.create({ name: 'Test Series', libraryId: library.id }) + + user = await Database.userModel.create({ + username: 'Admin', + type: 'admin', + isActive: true, + permissions: User.getDefaultPermissionsForUserType('admin'), + bookmarks: [], + extraData: { seriesHideFromContinueListening: [] } + }) + }) + + afterEach(async () => { + sinon.restore() + await Database.sequelize.sync({ force: true }) + }) + + it('creates placeholder items in a series', async () => { + expect(SeriesController.createPlaceholder).to.be.a('function') + + const fakeReq = { + params: { + id: library.id, + seriesId: series.id + }, + user, + body: {} + } + + const fakeRes = { + status: sinon.stub().returnsThis(), + json: sinon.spy(), + sendStatus: sinon.spy() + } + + await SeriesController.createPlaceholder.bind(apiRouter)(fakeReq, fakeRes) + + expect(fakeRes.json.calledOnce).to.be.true + const payload = fakeRes.json.firstCall.args[0] + expect(payload).to.include({ mediaType: 'book', isPlaceholder: true }) + expect(payload.media.metadata.series.some((entry) => entry.id === series.id)).to.be.true + }) +}) diff --git a/test/server/models/LibraryItem.placeholder.test.js b/test/server/models/LibraryItem.placeholder.test.js new file mode 100644 index 000000000..20a6377a5 --- /dev/null +++ b/test/server/models/LibraryItem.placeholder.test.js @@ -0,0 +1,47 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') + +const Database = require('../../../server/Database') + +describe('LibraryItem placeholder serialization', () => { + beforeEach(async () => { + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + }) + + afterEach(async () => { + await Database.sequelize.sync({ force: true }) + }) + + it('includes isPlaceholder in minified and expanded JSON', async () => { + const library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + + const book = await Database.bookModel.create({ + title: 'Placeholder Book', + audioFiles: [], + tags: [], + narrators: [], + genres: [], + chapters: [] + }) + + const libraryItem = await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id, + isPlaceholder: true + }) + + const expanded = await Database.libraryItemModel.getExpandedById(libraryItem.id) + + const minifiedJson = expanded.toOldJSONMinified() + const expandedJson = expanded.toOldJSONExpanded() + + expect(minifiedJson).to.have.property('isPlaceholder', true) + expect(expandedJson).to.have.property('isPlaceholder', true) + }) +}) diff --git a/test/server/utils/queries/libraryItemsBookFilters.placeholders.test.js b/test/server/utils/queries/libraryItemsBookFilters.placeholders.test.js new file mode 100644 index 000000000..7d20f463f --- /dev/null +++ b/test/server/utils/queries/libraryItemsBookFilters.placeholders.test.js @@ -0,0 +1,84 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') + +const Database = require('../../../../server/Database') +const libraryItemsBookFilters = require('../../../../server/utils/queries/libraryItemsBookFilters') +const User = require('../../../../server/models/User') + +describe('libraryItemsBookFilters placeholders', () => { + let library + let libraryFolder + let user + + beforeEach(async () => { + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + + library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + user = await Database.userModel.create({ + username: 'Admin', + type: 'admin', + isActive: true, + permissions: User.getDefaultPermissionsForUserType('admin'), + bookmarks: [], + extraData: { seriesHideFromContinueListening: [] } + }) + }) + + afterEach(async () => { + await Database.sequelize.sync({ force: true }) + }) + + const createBookWithItem = async ({ title, isPlaceholder }) => { + const book = await Database.bookModel.create({ + title, + audioFiles: [], + tags: [], + narrators: [], + genres: [], + chapters: [] + }) + + return Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id, + isPlaceholder + }) + } + + it('excludes placeholders from general library list queries', async () => { + await createBookWithItem({ title: 'Real Book', isPlaceholder: false }) + await createBookWithItem({ title: 'Placeholder Book', isPlaceholder: true }) + + const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems( + library.id, + user, + null, + null, + 'addedAt', + true, + false, + [], + 20, + 0 + ) + + expect(count).to.equal(1) + expect(libraryItems).to.have.length(1) + expect(libraryItems[0].media.title).to.equal('Real Book') + }) + + it('excludes placeholders from search results', async () => { + await createBookWithItem({ title: 'Real Book', isPlaceholder: false }) + await createBookWithItem({ title: 'Placeholder Book', isPlaceholder: true }) + + const results = await libraryItemsBookFilters.search(user, library, 'Placeholder', 20, 0) + + expect(results.book).to.have.length(0) + }) +})