Slave/lifecycle #11

Merged
nobswebdev merged 9 commits from slave/lifecycle into master 2026-10-27 21:12:04 +00:00
2 changed files with 140 additions and 9 deletions
Showing only changes of commit c147a010b5 - Show all commits
@@ -320,6 +320,56 @@ describe('UploadService', () => {
expect(storageService.read).not.toHaveBeenCalled();
expect(uploadRepository.increment).not.toHaveBeenCalled();
});
it('throws NotFoundException when maxViews is exhausted', async () => {
uploadRepository.findOne.mockResolvedValue({
storagePath: fixedId,
iv: Buffer.alloc(12, 0x11),
maxViews: 1,
viewCount: 1,
zk: {
passwordProtected: false,
wrappedDek: null,
wrapIv: null,
salt: null
}
});
await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException);
expect(storageService.read).not.toHaveBeenCalled();
expect(uploadRepository.increment).not.toHaveBeenCalled();
});
it('purges upload after the last allowed view', async () => {
const iv = Buffer.alloc(12, 0x11);
const ciphertext = Buffer.alloc(64, 0xcd);
const upload = {
id: fixedId,
storagePath: fixedId,
iv,
maxViews: 1,
viewCount: 0,
zk: {
passwordProtected: false,
wrappedDek: null,
wrapIv: null,
salt: null
}
};
uploadRepository.findOne.mockResolvedValue(upload);
storageService.read.mockResolvedValue(ciphertext);
await expect(service.getZkUploadCiphertext(fixedId)).resolves.toEqual({
iv: iv.toString('base64'),
ciphertext: ciphertext.toString('base64'),
passwordProtected: false
});
expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1);
expect(storageService.unlink).toHaveBeenCalledWith(fixedId);
expect(uploadRepository.remove).toHaveBeenCalledWith(upload);
});
});
describe('getHotlinkImage', () => {
@@ -395,6 +445,63 @@ describe('UploadService', () => {
expect(storageService.read).not.toHaveBeenCalled();
expect(uploadRepository.increment).not.toHaveBeenCalled();
});
it('throws NotFoundException when maxViews is exhausted', async () => {
uploadRepository.findOne.mockResolvedValue({
storagePath: fixedId,
iv,
maxViews: 2,
viewCount: 2,
hotlink: { mimeType: 'image/png' }
});
await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException);
expect(storageService.read).not.toHaveBeenCalled();
expect(uploadRepository.increment).not.toHaveBeenCalled();
});
it('purges upload after the last allowed view', async () => {
const upload = {
id: fixedId,
storagePath: fixedId,
iv,
maxViews: 1,
viewCount: 0,
hotlink: { mimeType: 'image/png' }
};
uploadRepository.findOne.mockResolvedValue(upload);
storageService.read.mockResolvedValue(ciphertext);
await expect(service.getHotlinkImage(fixedId, secret)).resolves.toEqual({
body: plaintext,
mimeType: 'image/png'
});
expect(uploadRepository.increment).toHaveBeenCalledWith({ id: fixedId }, 'viewCount', 1);
expect(storageService.unlink).toHaveBeenCalledWith(fixedId);
expect(uploadRepository.remove).toHaveBeenCalledWith(upload);
});
it('does not purge when views remain below maxViews', async () => {
uploadRepository.findOne.mockResolvedValue({
storagePath: fixedId,
iv,
maxViews: 3,
viewCount: 0,
hotlink: { mimeType: 'image/png' }
});
storageService.read.mockResolvedValue(ciphertext);
await expect(service.getHotlinkImage(fixedId, secret)).resolves.toEqual({
body: plaintext,
mimeType: 'image/png'
});
expect(uploadRepository.increment).toHaveBeenCalled();
expect(storageService.unlink).not.toHaveBeenCalled();
expect(uploadRepository.remove).not.toHaveBeenCalled();
});
});
describe('deleteUpload', () => {
@@ -132,6 +132,10 @@ export class UploadService {
throw new NotFoundException();
}
if (typeof upload.maxViews === 'number' && upload.viewCount >= upload.maxViews) {
throw new NotFoundException();
}
let ciphertextBuffer: Buffer;
try {
@@ -162,6 +166,12 @@ export class UploadService {
await this.uploadRepository.increment({ id }, 'viewCount', 1);
const nextViewCount = upload.viewCount + 1;
if (typeof upload.maxViews === 'number' && nextViewCount >= upload.maxViews) {
await this.purgeUpload(upload, this.uploadRepository);
}
return result;
}
@@ -179,6 +189,10 @@ export class UploadService {
throw new NotFoundException();
}
if (typeof upload.maxViews === 'number' && upload.viewCount >= upload.maxViews) {
throw new NotFoundException();
}
let ciphertextBuffer: Buffer;
try {
@@ -203,6 +217,12 @@ export class UploadService {
await this.uploadRepository.increment({ id }, 'viewCount', 1);
const nextViewCount = upload.viewCount + 1;
if (typeof upload.maxViews === 'number' && nextViewCount >= upload.maxViews) {
await this.purgeUpload(upload, this.uploadRepository);
}
return {
body,
mimeType: upload.hotlink.mimeType
@@ -221,6 +241,11 @@ export class UploadService {
throw new NotFoundException();
}
await this.purgeUpload(upload, uploadRepository);
});
}
private async purgeUpload(upload: Upload, uploadRepository: Repository<Upload>): Promise<void> {
try {
await this.storageService.unlink(upload.storagePath);
} catch (error: unknown) {
@@ -230,7 +255,6 @@ export class UploadService {
}
await uploadRepository.remove(upload);
});
}
private hashDeleteToken(token: string): string {