fix: better updating of links from photo_album

This commit is contained in:
2026-02-14 22:23:41 +01:00
parent 7a1d15d256
commit 51f58d608d
8 changed files with 344 additions and 37 deletions

View File

@@ -280,6 +280,172 @@ describe('PostMediaEngine', () => {
});
});
describe('linkManyToPost', () => {
it('should link multiple media files to a post in a single batch', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2', 'media-3'];
// No existing links
selectMockData = [];
// Setup mock media for each
mockGetMedia.mockImplementation((id: string) =>
Promise.resolve(createMockMedia({ id, linkedPostIds: [] }))
);
const result = await engine.linkManyToPost(postId, mediaIds);
expect(result.linked).toHaveLength(3);
expect(result.skipped).toHaveLength(0);
expect(insertedValues).toHaveLength(3);
});
it('should skip already linked media and include them in skipped array', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2', 'media-3'];
// media-1 is already linked
selectMockData = [
{ id: 'link-1', projectId: 'test-project', postId, mediaId: 'media-1', sortOrder: 0, createdAt: new Date() },
];
mockGetMedia.mockImplementation((id: string) =>
Promise.resolve(createMockMedia({ id, linkedPostIds: id === 'media-1' ? [postId] : [] }))
);
const result = await engine.linkManyToPost(postId, mediaIds);
expect(result.linked).toHaveLength(2);
expect(result.linked).toContain('media-2');
expect(result.linked).toContain('media-3');
expect(result.skipped).toHaveLength(1);
expect(result.skipped).toContain('media-1');
});
it('should emit mediaBatchLinked event once at the end', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2'];
selectMockData = [];
mockGetMedia.mockResolvedValue(createMockMedia({ id: 'any', linkedPostIds: [] }));
const handler = vi.fn();
engine.on('mediaBatchLinked', handler);
// Should NOT emit individual mediaLinked events
const individualHandler = vi.fn();
engine.on('mediaLinked', individualHandler);
await engine.linkManyToPost(postId, mediaIds);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ postId, mediaIds: expect.arrayContaining(['media-1', 'media-2']) });
expect(individualHandler).not.toHaveBeenCalled();
});
it('should not emit event if no media was linked', async () => {
const postId = 'post-1';
const mediaIds = ['media-1'];
// media-1 is already linked
selectMockData = [
{ id: 'link-1', projectId: 'test-project', postId, mediaId: 'media-1', sortOrder: 0, createdAt: new Date() },
];
mockGetMedia.mockResolvedValue(createMockMedia({ id: 'media-1', linkedPostIds: [postId] }));
const handler = vi.fn();
engine.on('mediaBatchLinked', handler);
await engine.linkManyToPost(postId, mediaIds);
expect(handler).not.toHaveBeenCalled();
});
it('should update sortOrder incrementally for batch-linked media', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2', 'media-3'];
selectMockData = [];
mockGetMedia.mockResolvedValue(createMockMedia({ linkedPostIds: [] }));
await engine.linkManyToPost(postId, mediaIds);
// Check that sort orders are sequential
const sortOrders = insertedValues.map(v => v.sortOrder);
expect(sortOrders).toEqual([0, 1, 2]);
});
});
describe('unlinkManyFromPost', () => {
it('should unlink multiple media files from a post in a single batch', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2', 'media-3'];
mockGetMedia.mockImplementation((id: string) =>
Promise.resolve(createMockMedia({ id, linkedPostIds: [postId] }))
);
const result = await engine.unlinkManyFromPost(postId, mediaIds);
expect(result.unlinked).toHaveLength(3);
// deleteCalled flag is set to true when any delete is called
expect(deleteCalled).toBe(true);
});
it('should emit mediaBatchUnlinked event once at the end', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2'];
mockGetMedia.mockResolvedValue(createMockMedia({ linkedPostIds: [postId] }));
const handler = vi.fn();
engine.on('mediaBatchUnlinked', handler);
// Should NOT emit individual mediaUnlinked events
const individualHandler = vi.fn();
engine.on('mediaUnlinked', individualHandler);
await engine.unlinkManyFromPost(postId, mediaIds);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ postId, mediaIds: expect.arrayContaining(['media-1', 'media-2']) });
expect(individualHandler).not.toHaveBeenCalled();
});
it('should not emit event if no media was unlinked', async () => {
const postId = 'post-1';
const mediaIds: string[] = [];
const handler = vi.fn();
engine.on('mediaBatchUnlinked', handler);
await engine.unlinkManyFromPost(postId, mediaIds);
expect(handler).not.toHaveBeenCalled();
});
it('should update media sidecars to remove postId', async () => {
const postId = 'post-1';
const mediaIds = ['media-1', 'media-2'];
mockGetMedia.mockImplementation((id: string) =>
Promise.resolve(createMockMedia({ id, linkedPostIds: [postId, 'other-post'] }))
);
await engine.unlinkManyFromPost(postId, mediaIds);
// Both media should have their sidecars updated
expect(mockUpdateMedia).toHaveBeenCalledTimes(2);
expect(mockUpdateMedia).toHaveBeenCalledWith(
'media-1',
expect.objectContaining({ linkedPostIds: ['other-post'] })
);
expect(mockUpdateMedia).toHaveBeenCalledWith(
'media-2',
expect.objectContaining({ linkedPostIds: ['other-post'] })
);
});
});
describe('getLinkedPostsForMedia', () => {
it('should return all posts linked to a media file', async () => {
const mediaId = 'media-1';

View File

@@ -130,6 +130,8 @@ const mockPostMediaEngine = {
setProjectContext: vi.fn(),
linkMediaToPost: vi.fn(),
unlinkMediaFromPost: vi.fn(),
linkManyToPost: vi.fn(),
unlinkManyFromPost: vi.fn(),
getLinkedMediaForPost: vi.fn(),
getLinkedPostsForMedia: vi.fn(),
reorderMediaForPost: vi.fn(),
@@ -878,6 +880,32 @@ describe('IPC Handlers', () => {
});
});
describe('postMedia:linkMany', () => {
it('should batch link multiple media to post', async () => {
const batchResult = { linked: ['media-1', 'media-2'], skipped: [] };
mockPostMediaEngine.linkManyToPost.mockResolvedValue(batchResult);
const mediaIds = ['media-1', 'media-2'];
const result = await invokeHandler('postMedia:linkMany', 'post-1', mediaIds);
expect(mockPostMediaEngine.linkManyToPost).toHaveBeenCalledWith('post-1', mediaIds);
expect(result).toEqual(batchResult);
});
});
describe('postMedia:unlinkMany', () => {
it('should batch unlink multiple media from post', async () => {
const batchResult = { unlinked: ['media-1', 'media-2'] };
mockPostMediaEngine.unlinkManyFromPost.mockResolvedValue(batchResult);
const mediaIds = ['media-1', 'media-2'];
const result = await invokeHandler('postMedia:unlinkMany', 'post-1', mediaIds);
expect(mockPostMediaEngine.unlinkManyFromPost).toHaveBeenCalledWith('post-1', mediaIds);
expect(result).toEqual(batchResult);
});
});
describe('postMedia:getForPost', () => {
it('should return media linked to a post', async () => {
const linkedMedia = [createMockMedia(), createMockMedia()];