From 073e6ac1f20959a8ab7b60c31f9d3c2c870ec5ee Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Tue, 27 Oct 2026 14:59:50 +0100 Subject: [PATCH] Increment view_count after successful ZK and hotlink GETs. Bump count only when ciphertext or decrypted image is ready; 404 paths do not increment. Cover both flows in UploadService tests. --- .../modules/upload/services/UploadService.spec.ts | 14 ++++++++++++-- .../src/modules/upload/services/UploadService.ts | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/upload/services/UploadService.spec.ts b/backend/src/modules/upload/services/UploadService.spec.ts index e1e8fbc..04bc0c9 100644 --- a/backend/src/modules/upload/services/UploadService.spec.ts +++ b/backend/src/modules/upload/services/UploadService.spec.ts @@ -51,14 +51,15 @@ describe('UploadService', () => { let service: UploadService; let dataSource: jest.Mocked>; let storageService: jest.Mocked>; - let uploadRepository: { create: jest.Mock; save: jest.Mock; findOne: jest.Mock }; + let uploadRepository: { create: jest.Mock; save: jest.Mock; findOne: jest.Mock; increment: jest.Mock }; let transaction: jest.Mock; beforeEach(() => { uploadRepository = { create: jest.fn((entityLike: unknown) => entityLike), save: jest.fn(async (entity: unknown) => entity), - findOne: jest.fn() + findOne: jest.fn(), + increment: jest.fn().mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }) }; transaction = jest.fn(async (runInTransaction: (manager: EntityManager) => Promise) => { @@ -224,6 +225,7 @@ describe('UploadService', () => { relations: ['zk'] }); expect(storageService.read).toHaveBeenCalledWith(fixedId); + expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1); }); it('includes wrap fields when password protected', async () => { @@ -253,6 +255,7 @@ describe('UploadService', () => { wrapIv: wrapIv.toString('base64'), salt: salt.toString('base64') }); + expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1); }); it('throws NotFoundException when upload is missing or not zk', async () => { @@ -260,6 +263,7 @@ describe('UploadService', () => { await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException); expect(storageService.read).not.toHaveBeenCalled(); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); it('throws NotFoundException when ciphertext file is missing', async () => { @@ -271,6 +275,7 @@ describe('UploadService', () => { storageService.read.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })); await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); it('throws NotFoundException when password protected but wrap data is incomplete', async () => { @@ -287,6 +292,7 @@ describe('UploadService', () => { storageService.read.mockResolvedValue(Buffer.alloc(8)); await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); }); @@ -314,6 +320,7 @@ describe('UploadService', () => { where: { id: fixedId, mode: UploadMode.Hotlink }, relations: ['hotlink'] }); + expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1); }); it('throws NotFoundException when upload is missing or not hotlink', async () => { @@ -321,6 +328,7 @@ describe('UploadService', () => { await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException); expect(storageService.read).not.toHaveBeenCalled(); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); it('throws NotFoundException when secret is wrong but well-formed', async () => { @@ -334,6 +342,7 @@ describe('UploadService', () => { const wrongSecret = Buffer.alloc(32, 0x99).toString('base64url'); await expect(service.getHotlinkImage(fixedId, wrongSecret)).rejects.toThrow(NotFoundException); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); it('throws NotFoundException when ciphertext file is missing', async () => { @@ -345,6 +354,7 @@ describe('UploadService', () => { storageService.read.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })); await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException); + expect(uploadRepository.increment).not.toHaveBeenCalled(); }); }); diff --git a/backend/src/modules/upload/services/UploadService.ts b/backend/src/modules/upload/services/UploadService.ts index e50efaf..489220e 100644 --- a/backend/src/modules/upload/services/UploadService.ts +++ b/backend/src/modules/upload/services/UploadService.ts @@ -155,6 +155,8 @@ export class UploadService { result.salt = toBase64(upload.zk.salt); } + await this.uploadRepository.increment({ id }, 'viewCount', 1); + return result; } @@ -190,6 +192,8 @@ export class UploadService { throw new NotFoundException(); } + await this.uploadRepository.increment({ id }, 'viewCount', 1); + return { body, mimeType: upload.hotlink.mimeType