tighten postgres compatibility tests and diagnostics

This commit is contained in:
Kevin Gatera 2026-03-03 15:12:38 -05:00
parent 06ad6a722f
commit 7d97d09191
No known key found for this signature in database
GPG key ID: F0D9F5932458CFB9
7 changed files with 366 additions and 3 deletions

View file

@ -62,7 +62,7 @@ describe('libraryItemsBookFilters postgres query safety', () => {
booksToExclude: [],
bookSeriesToInclude: []
})
const warnStub = sinon.stub(Logger, 'warn')
const debugStub = sinon.stub(Logger, 'debug')
await libraryItemsBookFilters.getFilteredLibraryItems('library-1', { canAccessExplicitContent: true }, 'authors', 'author-1', 'media.metadata.publishedYear', true, true, [], 20, 0)
@ -71,7 +71,7 @@ describe('libraryItemsBookFilters postgres query safety', () => {
expect(displayTitleExpression).to.include('COALESCE(NULL, libraryItem.title)')
expect(displayTitleExpression).to.not.include('IN ()')
expect(warnStub.calledOnce).to.equal(true)
expect(debugStub.calledWithMatch('collapse-series produced no include IDs')).to.equal(true)
})
it('should escape collapse-series ids safely for postgres subquery', async () => {

View file

@ -0,0 +1,75 @@
const { expect } = require('chai')
const sinon = require('sinon')
const Database = require('../../../../server/Database')
const libraryItemsPodcastFilters = require('../../../../server/utils/queries/libraryItemsPodcastFilters')
describe('libraryItemsPodcastFilters dialect behavior', () => {
let originalSequelize
let originalServerSettings
beforeEach(() => {
originalSequelize = Database.sequelize
originalServerSettings = global.ServerSettings
global.ServerSettings = { sortingIgnorePrefix: false }
})
afterEach(() => {
Database.sequelize = originalSequelize
global.ServerSettings = originalServerSettings
sinon.restore()
})
it('should build postgres json tag permission predicate', () => {
Database.sequelize = { getDialect: () => 'postgres' }
const { podcastWhere, replacements } = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery({
canAccessExplicitContent: true,
permissions: {
accessAllTags: false,
itemTagsSelected: ['fiction'],
selectedTagsNotAccessible: false
}
})
expect(replacements.userTagsSelected).to.deep.equal(['fiction'])
expect(podcastWhere[0].attribute.val).to.include('jsonb_array_elements_text')
})
it('should build sqlite no-case author sort expression', () => {
Database.sequelize = { getDialect: () => 'sqlite' }
const order = libraryItemsPodcastFilters.getOrder('media.metadata.author', false)
expect(order[0][0].val).to.include('podcast.author COLLATE NOCASE')
})
it('should build postgres lower author sort expression', () => {
Database.sequelize = { getDialect: () => 'postgres' }
const order = libraryItemsPodcastFilters.getOrder('media.metadata.author', false)
expect(order[0][0].val).to.include('LOWER(podcast.author)')
})
it('should use postgres json duration extraction in podcast stats query', async () => {
const queryStub = sinon.stub()
queryStub.onFirstCall().resolves([[{ totalSize: 1000 }]])
queryStub.onSecondCall().resolves([[{ totalDuration: '12.3', totalItems: '4', numAudioFiles: '9' }]])
Database.sequelize = {
getDialect: () => 'postgres',
query: queryStub
}
const result = await libraryItemsPodcastFilters.getPodcastLibraryStats('library-1')
expect(queryStub.secondCall.args[0]).to.include('NULLIF(pe.audioFile::jsonb #>>')
expect(result).to.deep.equal({
totalSize: 1000,
totalDuration: '12.3',
numAudioFiles: '9',
totalItems: '4'
})
})
})

View file

@ -0,0 +1,107 @@
const { expect } = require('chai')
const sinon = require('sinon')
const Database = require('../../../../server/Database')
const seriesFilters = require('../../../../server/utils/queries/seriesFilters')
const libraryItemsBookFilters = require('../../../../server/utils/queries/libraryItemsBookFilters')
function encodedFilter(group, value) {
return `${group}.${encodeURIComponent(Buffer.from(value).toString('base64'))}`
}
describe('seriesFilters dialect behavior', () => {
let originalSequelize
let originalServerSettings
let modelStubs
function setDialect(dialect) {
Database.sequelize = {
getDialect: () => dialect,
random: sinon.stub(),
models: modelStubs
}
}
beforeEach(() => {
originalSequelize = Database.sequelize
originalServerSettings = global.ServerSettings
modelStubs = {
series: {
findAndCountAll: sinon.stub().resolves({ rows: [], count: 0 })
},
bookSeries: {},
book: {},
libraryItem: {},
author: {},
feed: {}
}
global.ServerSettings = { sortingIgnorePrefix: false }
sinon.stub(libraryItemsBookFilters, 'getUserPermissionBookWhereQuery').returns({
bookWhere: [],
replacements: {}
})
})
afterEach(() => {
Database.sequelize = originalSequelize
global.ServerSettings = originalServerSettings
sinon.restore()
})
it('should build postgres progress filter with TRUE and FALSE literals', async () => {
setDialect('postgres')
await seriesFilters.getFilteredSeries(
{ id: 'library-1', settings: { hideSingleBookSeries: false } },
{ id: 'user-1', canAccessExplicitContent: true, permissions: { accessAllTags: true } },
encodedFilter('progress', 'not-started'),
'name',
false,
[],
10,
0
)
const findOptions = modelStubs.series.findAndCountAll.firstCall.args[0]
const progressWhere = findOptions.where[1]
expect(progressWhere.attribute.val).to.include('mp.isFinished = TRUE')
})
it('should build sqlite no-case name sort expression', async () => {
setDialect('sqlite')
await seriesFilters.getFilteredSeries(
{ id: 'library-1', settings: { hideSingleBookSeries: false } },
{ id: 'user-1', canAccessExplicitContent: true, permissions: { accessAllTags: true } },
null,
'name',
false,
[],
10,
0
)
const findOptions = modelStubs.series.findAndCountAll.firstCall.args[0]
expect(findOptions.order[0][0].val).to.equal('series.name COLLATE NOCASE')
})
it('should build postgres lower-case name sort expression', async () => {
setDialect('postgres')
await seriesFilters.getFilteredSeries(
{ id: 'library-1', settings: { hideSingleBookSeries: false } },
{ id: 'user-1', canAccessExplicitContent: true, permissions: { accessAllTags: true } },
null,
'name',
false,
[],
10,
0
)
const findOptions = modelStubs.series.findAndCountAll.firstCall.args[0]
expect(findOptions.order[0][0].val).to.equal('LOWER(series.name)')
})
})