1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-11-27 22:38:10 +02:00
Files
pigallery2/test/backend/unit/model/sql/AlbumManager.spec.ts
2025-09-12 22:32:02 +02:00

366 lines
14 KiB
TypeScript

import {DBTestHelper} from '../../../DBTestHelper';
import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
import {AlbumManager} from '../../../../../src/backend/model/database/AlbumManager';
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../src/common/entities/SearchQueryDTO';
import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection';
import {AlbumBaseEntity} from '../../../../../src/backend/model/database/enitites/album/AlbumBaseEntity';
import {Utils} from '../../../../../src/common/Utils';
import {MediaDTO} from '../../../../../src/common/entities/MediaDTO';
import {SavedSearchDTO} from '../../../../../src/common/entities/album/SavedSearchDTO';
import {ProjectedAlbumCacheEntity} from '../../../../../src/backend/model/database/enitites/album/ProjectedAlbumCacheEntity';
import {SessionContext} from '../../../../../src/backend/model/SessionContext';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const chai = require('chai');
chai.use(deepEqualInAnyOrder);
const {expect} = chai;
// to help WebStorm to handle the test cases
declare let describe: any;
declare const after: any;
declare const before: any;
declare const beforeEach: any;
declare const afterEach: any;
declare const it: any;
const tmpDescribe = describe;
describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
describe('AlbumManager', (sqlHelper: DBTestHelper) => {
describe = tmpDescribe;
const setUpSqlDB = async () => {
await sqlHelper.initDB();
await sqlHelper.setUpTestGallery();
await ObjectManagers.getInstance().init();
};
const createProjectionSession = async (query: any): Promise<SessionContext> => {
await ObjectManagers.getInstance().init();
return await ObjectManagers.getInstance().SessionManager.buildContext({
allowQuery: query,
overrideAllowBlockList: true
} as any);
};
const toAlbumCover = (m: MediaDTO): MediaDTO => {
// generated dirs for test contain everything, not like return values from the server.
const tmpDir: ParentDirectoryDTO = m.directory as ParentDirectoryDTO;
const tmpM = tmpDir.media;
const tmpD = tmpDir.directories;
const tmpP = tmpDir.cache.cover;
const tmpMT = tmpDir.metaFile;
delete tmpDir.directories;
delete tmpDir.media;
delete tmpDir.cache.cover;
delete tmpDir.metaFile;
const ret = Utils.clone(m);
delete ret.id;
ret.directory = {path: ret.directory.path, name: ret.directory.name};
delete ret.metadata;
tmpDir.directories = tmpD;
tmpDir.media = tmpM;
tmpDir.cache.cover = tmpP;
tmpDir.metaFile = tmpMT;
return ret;
};
before(setUpSqlDB);
after(sqlHelper.clearDB);
describe('Saved search', () => {
beforeEach(setUpSqlDB);
afterEach(sqlHelper.clearDB);
it('should add album', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
await am.addSavedSearch('Test Album', Utils.clone(query));
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
id: 1,
name: 'Test Album',
locked: false,
searchQuery: query
} as SavedSearchDTO]);
});
it('should delete album', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Test Album', Utils.clone(query));
await am.addSavedSearch('Test Album2', Utils.clone(query), true);
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([
{
id: 1,
name: 'Test Album',
locked: false,
searchQuery: query
} as SavedSearchDTO,
{
id: 2,
name: 'Test Album2',
locked: true,
searchQuery: query
} as SavedSearchDTO]);
await am.deleteAlbum(1);
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
id: 2,
name: 'Test Album2',
locked: true,
searchQuery: query
} as SavedSearchDTO]);
try {
await am.deleteAlbum(2);
expect(false).to.be.equal(true); // should not reach
} catch (e) {
expect(e.message).to.equal('Could not delete album, id:2');
}
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
id: 2,
name: 'Test Album2',
locked: true,
searchQuery: query
} as SavedSearchDTO]);
});
});
it('should list album', async () => {
const am = new AlbumManager();
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Test Album', Utils.clone(query));
expect(await am.getAll(DBTestHelper.defaultSession)).to.deep.equalInAnyOrder([{
id: 1,
name: 'Test Album',
searchQuery: query,
locked: false,
cache: {
cover: toAlbumCover(sqlHelper.testGalleyEntities.p),
id: 1,
itemCount: 1,
oldestMedia: sqlHelper.testGalleyEntities.p.metadata.creationDate,
valid: true,
youngestMedia: sqlHelper.testGalleyEntities.p.metadata.creationDate,
},
} as SavedSearchDTO]);
});
describe('Projection Tests', () => {
beforeEach(setUpSqlDB);
afterEach(sqlHelper.clearDB);
it('should get albums with projection session filtering by filename', async () => {
const am = new AlbumManager();
// Create albums with different search queries
const query1: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
const query2: TextSearch = {text: 'sw1', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Album Photo1', Utils.clone(query1));
await am.addSavedSearch('Album SW1', Utils.clone(query2));
// Create projection session that filters by filename
const projectionSession = await createProjectionSession({
type: SearchQueryTypes.file_name,
text: 'sw1',
matchType: TextSearchQueryMatchTypes.like
});
const albumsWithProjection = await am.getAll(projectionSession);
const albumsWithDefault = await am.getAll(DBTestHelper.defaultSession);
expect(albumsWithProjection).to.not.be.undefined;
expect(albumsWithDefault).to.not.be.undefined;
// Albums should exist but may have different cache content based on projection
expect(albumsWithProjection.length).to.equal(2);
expect(albumsWithDefault.length).to.equal(2);
// Verify projection keys are different
expect(projectionSession.user.projectionKey).to.not.equal(DBTestHelper.defaultSession.user.projectionKey);
// Albums with projection should have different cache results
const projectedAlbum1 = albumsWithProjection.find(a => a.name === 'Album Photo1');
const defaultAlbum1 = albumsWithDefault.find(a => a.name === 'Album Photo1');
expect(projectedAlbum1).to.not.be.undefined;
expect(defaultAlbum1).to.not.be.undefined;
// The album searching for 'photo1' should have no items under the 'sw1' filename projection
if (projectedAlbum1.cache) {
expect(projectedAlbum1.cache.itemCount).to.equal(0);
}
if (defaultAlbum1.cache) {
expect(defaultAlbum1.cache.itemCount).to.equal(1);
}
});
it('should handle cache correctly with different projections', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
// Create album that will match the projection
const query: TextSearch = {text: 'photo', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Test Album Cache', Utils.clone(query));
// Get albums with default session first
const albumsDefault = await am.getAll(DBTestHelper.defaultSession);
expect(albumsDefault.length).to.be.equal(1);
// Create projection session that will also match content
const projectionSession = await createProjectionSession({
type: SearchQueryTypes.file_name,
text: 'photo',
matchType: TextSearchQueryMatchTypes.like
});
// Trigger cache filling for projection
const albumsProjected = await am.getAll(projectionSession);
expect(albumsProjected.length).to.be.equal(1);
// Verify that cache entries exist for both projection keys
const defaultCacheEntries = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {projectionKey: DBTestHelper.defaultSession.user.projectionKey}});
const projectionCacheEntries = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {projectionKey: projectionSession.user.projectionKey}});
expect(defaultCacheEntries).to.be.equal(1);
expect(projectionCacheEntries).to.be.equal(1);
});
it('should return albums with empty cache when projection has no matches', async () => {
const am = new AlbumManager();
// Create album that searches for content that won't match projection
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
await am.addSavedSearch('No Match Album', Utils.clone(query));
// Create projection session that filters to non-existent content
const projectionSession = await createProjectionSession({
type: SearchQueryTypes.file_name,
text: 'nonexistent',
matchType: TextSearchQueryMatchTypes.like
});
const albumsWithProjection = await am.getAll(projectionSession);
expect(albumsWithProjection).to.not.be.undefined;
expect(albumsWithProjection.length).to.equal(1);
const album = albumsWithProjection[0];
expect(album.name).to.equal('No Match Album');
// Album should exist but cache should indicate no matches
if (album.cache) {
expect(album.cache.itemCount).to.equal(0);
expect(album.cache.cover).to.be.null;
}
});
it('should handle multiple albums with different projection results', async () => {
const am = new AlbumManager();
// Create multiple albums with different search criteria
const queries = [
{name: 'Album All Photos', query: {text: 'photo', type: SearchQueryTypes.any_text} as TextSearch},
{name: 'Album SW1', query: {text: 'sw1', type: SearchQueryTypes.any_text} as TextSearch},
{name: 'Album Boba', query: {text: 'Boba', type: SearchQueryTypes.any_text} as TextSearch}
];
for (const {name, query} of queries) {
await am.addSavedSearch(name, Utils.clone(query));
}
// Test with different projections
const projectionSW1 = await createProjectionSession({
type: SearchQueryTypes.file_name,
text: 'sw1',
matchType: TextSearchQueryMatchTypes.like
});
const albumsDefault = await am.getAll(DBTestHelper.defaultSession);
const albumsSW1 = await am.getAll(projectionSW1);
expect(albumsDefault.length).to.equal(3);
expect(albumsSW1.length).to.equal(3);
// Check that different albums have different results under projections
const defaultAlbumSW1 = albumsDefault.find(a => a.name === 'Album SW1');
const projectedAlbumSW1 = albumsSW1.find(a => a.name === 'Album SW1');
if (defaultAlbumSW1?.cache && projectedAlbumSW1?.cache) {
// SW1 album should have same or fewer results under SW1 filename projection
expect(projectedAlbumSW1.cache.itemCount).to.be.lessThanOrEqual(defaultAlbumSW1.cache.itemCount);
}
});
it('should invalidate cache properly for projections', async () => {
const am = new AlbumManager();
const connection = await SQLConnection.getConnection();
// Create album
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
await am.addSavedSearch('Cache Test Album', Utils.clone(query));
// Build cache for default session
await am.getAll(DBTestHelper.defaultSession);
// Verify cache exists and is valid
let validCacheCount = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {valid: true, projectionKey: DBTestHelper.defaultSession.user.projectionKey}});
expect(validCacheCount).to.be.greaterThan(0);
// Reset covers (invalidate cache)
await am.invalidateCache();
// Cache should be marked as invalid but still exist
const totalCacheCount = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {projectionKey: DBTestHelper.defaultSession.user.projectionKey}});
validCacheCount = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {valid: true, projectionKey: DBTestHelper.defaultSession.user.projectionKey}});
expect(totalCacheCount).to.be.greaterThan(0);
expect(validCacheCount).to.equal(0);
// Accessing albums again should rebuild cache
await am.getAll(DBTestHelper.defaultSession);
validCacheCount = await connection.getRepository(ProjectedAlbumCacheEntity)
.count({where: {valid: true, projectionKey: DBTestHelper.defaultSession.user.projectionKey}});
expect(validCacheCount).to.be.greaterThan(0);
});
});
});