From 9489e2286b4df966fbf8978720eff9adb52a8f48 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 11:05:58 +0100 Subject: [PATCH 1/7] chore: add tests for current behavior --- test/storage/object/put.test.js | 157 +++++++++++++++++++ test/storage/version/put.test.js | 256 +++++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+) diff --git a/test/storage/object/put.test.js b/test/storage/object/put.test.js index 353655ab..b88428f5 100644 --- a/test/storage/object/put.test.js +++ b/test/storage/object/put.test.js @@ -45,4 +45,161 @@ describe('Object storage', () => { assert.strictEqual(resp.status, 201); }); }); + + describe('Binary file versioning', () => { + it('Creates version for JPEG image upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-jpeg-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock JPEG file (using a simple byte array to simulate binary data) + const jpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46]); + const jpegFile = new File([jpegData], 'test-image.jpg', { type: 'image/jpeg' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'images/test-image.jpg', + pathname: '/images/test-image', + propsKey: 'images/test-image.jpg.props', + ext: 'jpg' + }; + + const obj = { data: jpegFile, guid: 'jpeg-guid-123' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-jpeg-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'image/jpeg'); + assert.strictEqual(versionCalls[0].guid, 'jpeg-guid-123'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for PNG image upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-png-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock PNG file (PNG signature) + const pngData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const pngFile = new File([pngData], 'test-image.png', { type: 'image/png' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'images/test-image.png', + pathname: '/images/test-image', + propsKey: 'images/test-image.png.props', + ext: 'png' + }; + + const obj = { data: pngFile }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-png-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'image/png'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for MP4 video upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-video-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock MP4 file (ftyp box signature) + const mp4Data = new Uint8Array([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70]); + const mp4File = new File([mp4Data], 'test-video.mp4', { type: 'video/mp4' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'videos/test-video.mp4', + pathname: '/videos/test-video', + propsKey: 'videos/test-video.mp4.props', + ext: 'mp4' + }; + + const obj = { data: mp4File, guid: 'video-guid-456' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-video-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'video/mp4'); + assert.strictEqual(versionCalls[0].guid, 'video-guid-456'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for SVG image upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-svg-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create an SVG file (text-based but still an image) + const svgContent = ''; + const svgFile = new File([svgContent], 'test-image.svg', { type: 'image/svg+xml' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'images/test-image.svg', + pathname: '/images/test-image', + propsKey: 'images/test-image.svg.props', + ext: 'svg' + }; + + const obj = { data: svgFile, guid: 'svg-guid-789' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-svg-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'image/svg+xml'); + assert.strictEqual(versionCalls[0].guid, 'svg-guid-789'); + assert(versionCalls[0].update.body instanceof File); + }); + }); }); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index b6fafcdc..b8992b0d 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -866,4 +866,260 @@ describe('Version Put', () => { assert.strictEqual(putCommand.input.Body, 'test body'); assert.strictEqual(putCommand.input.ContentLength, 9); }); + + it('Test putVersion with JPEG binary content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate JPEG binary data + const jpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46]); + const jpegFile = new File([jpegData], 'image.jpg', { type: 'image/jpeg' }); + + const testParams = { + Bucket: 'media-bucket', + Org: 'testorg', + Body: jpegFile, + ID: 'jpeg-id-123', + Version: 'jpeg-version-1', + Ext: 'jpg', + Metadata: { Users: '["user@example.com"]', Path: 'images/photo.jpg' }, + ContentType: 'image/jpeg' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/jpeg-id-123/jpeg-version-1.jpg'); + assert.strictEqual(putCommand.input.Body, jpegFile); + assert.strictEqual(putCommand.input.ContentType, 'image/jpeg'); + assert.strictEqual(putCommand.input.Metadata.Users, '["user@example.com"]'); + assert.strictEqual(putCommand.input.Metadata.Path, 'images/photo.jpg'); + }); + + it('Test putVersion with PNG binary content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate PNG binary data + const pngData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const pngFile = new File([pngData], 'image.png', { type: 'image/png' }); + + const testParams = { + Bucket: 'media-bucket', + Org: 'testorg', + Body: pngFile, + ID: 'png-id-456', + Version: 'png-version-1', + Ext: 'png', + Metadata: { Users: '["user@example.com"]', Path: 'images/graphic.png' }, + ContentType: 'image/png' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/png-id-456/png-version-1.png'); + assert.strictEqual(putCommand.input.Body, pngFile); + assert.strictEqual(putCommand.input.ContentType, 'image/png'); + }); + + it('Test putVersion with MP4 video content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate MP4 binary data + const mp4Data = new Uint8Array([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70]); + const mp4File = new File([mp4Data], 'video.mp4', { type: 'video/mp4' }); + + const testParams = { + Bucket: 'media-bucket', + Org: 'testorg', + Body: mp4File, + ID: 'video-id-789', + Version: 'video-version-1', + Ext: 'mp4', + Metadata: { Users: '["user@example.com"]', Path: 'videos/demo.mp4' }, + ContentType: 'video/mp4' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/video-id-789/video-version-1.mp4'); + assert.strictEqual(putCommand.input.Body, mp4File); + assert.strictEqual(putCommand.input.ContentType, 'video/mp4'); + }); + + it('Test putVersion with SVG image content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // SVG is text-based but still an image + const svgContent = ''; + const svgFile = new File([svgContent], 'graphic.svg', { type: 'image/svg+xml' }); + + const testParams = { + Bucket: 'media-bucket', + Org: 'testorg', + Body: svgFile, + ID: 'svg-id-abc', + Version: 'svg-version-1', + Ext: 'svg', + Metadata: { Users: '["user@example.com"]', Path: 'images/icon.svg' }, + ContentType: 'image/svg+xml' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/svg-id-abc/svg-version-1.svg'); + assert.strictEqual(putCommand.input.Body, svgFile); + assert.strictEqual(putCommand.input.ContentType, 'image/svg+xml'); + }); + + it('Test putObjectWithVersion with JPEG preserves binary content on update', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + // Simulate existing JPEG file + const existingJpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46]); + const mockGetObject = async () => ({ + status: 200, + body: existingJpegData, + contentLength: existingJpegData.length, + contentType: 'image/jpeg', + etag: 'test-etag', + metadata: { + id: 'jpeg-id-existing', + version: 'jpeg-version-old', + timestamp: '1234567890', + users: '["olduser@example.com"]', + path: 'images/photo.jpg' + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'testorg', + ext: 'jpg', + users: [{ email: 'newuser@example.com' }] + }; + + // New JPEG data to upload + const newJpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE1, 0x00, 0x10, 0x4A, 0x46]); + const newJpegFile = new File([newJpegData], 'photo.jpg', { type: 'image/jpeg' }); + + const update = { + bucket: 'media-bucket', + org: 'testorg', + key: 'images/photo.jpg', + body: newJpegFile, + type: 'image/jpeg' + }; + + const result = await putObjectWithVersion(env, daCtx, update, true); + + assert.strictEqual(result.status, 200); + assert.strictEqual(result.metadata.id, 'jpeg-id-existing'); + + // Should have 2 commands: one for version, one for main object + assert.strictEqual(sentCommands.length, 2); + + // First command should store the old version + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Bucket, 'media-bucket'); + assert(versionCommand.input.Key.includes('.da-versions/jpeg-id-existing/')); + assert(versionCommand.input.Key.endsWith('.jpg')); + assert.strictEqual(versionCommand.input.Body, existingJpegData); + assert.strictEqual(versionCommand.input.ContentType, 'image/jpeg'); + assert.strictEqual(versionCommand.input.ContentLength, existingJpegData.length); + + // Second command should store the new content + const mainCommand = sentCommands[1]; + assert.strictEqual(mainCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(mainCommand.input.Key, 'testorg/images/photo.jpg'); + assert.strictEqual(mainCommand.input.Body, newJpegFile); + assert.strictEqual(mainCommand.input.ContentType, 'image/jpeg'); + }); }); From 7cae0f1a33f41ea05589de60cdde6f0bb2a718b5 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 11:34:41 +0100 Subject: [PATCH 2/7] chore: more tests --- test/storage/object/put.test.js | 255 ++++++++++++++++++++++ test/storage/version/put.test.js | 348 +++++++++++++++++++++++++++++++ 2 files changed, 603 insertions(+) diff --git a/test/storage/object/put.test.js b/test/storage/object/put.test.js index b88428f5..9a61ca77 100644 --- a/test/storage/object/put.test.js +++ b/test/storage/object/put.test.js @@ -201,5 +201,260 @@ describe('Object storage', () => { assert.strictEqual(versionCalls[0].guid, 'svg-guid-789'); assert(versionCalls[0].update.body instanceof File); }); + + it('Creates version for PDF document upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-pdf-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock PDF file (PDF signature: %PDF) + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34]); + const pdfFile = new File([pdfData], 'document.pdf', { type: 'application/pdf' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'documents/report.pdf', + pathname: '/documents/report', + propsKey: 'documents/report.pdf.props', + ext: 'pdf' + }; + + const obj = { data: pdfFile, guid: 'pdf-guid-abc' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-pdf-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'application/pdf'); + assert.strictEqual(versionCalls[0].guid, 'pdf-guid-abc'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for ZIP archive upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-zip-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock ZIP file (ZIP signature: PK) + const zipData = new Uint8Array([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00]); + const zipFile = new File([zipData], 'archive.zip', { type: 'application/zip' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'downloads/archive.zip', + pathname: '/downloads/archive', + propsKey: 'downloads/archive.zip.props', + ext: 'zip' + }; + + const obj = { data: zipFile, guid: 'zip-guid-def' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-zip-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'application/zip'); + assert.strictEqual(versionCalls[0].guid, 'zip-guid-def'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for generic binary file upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-binary-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a generic binary file with application/octet-stream (fallback content type) + const binaryData = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]); + const binaryFile = new File([binaryData], 'data.bin', { type: 'application/octet-stream' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'files/data.bin', + pathname: '/files/data', + propsKey: 'files/data.bin.props', + ext: 'bin' + }; + + const obj = { data: binaryFile, guid: 'binary-guid-ghi' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-binary-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'application/octet-stream'); + assert.strictEqual(versionCalls[0].guid, 'binary-guid-ghi'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for audio file upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-audio-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a mock MP3 file (ID3v2 signature) + const mp3Data = new Uint8Array([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00]); + const mp3File = new File([mp3Data], 'audio.mp3', { type: 'audio/mpeg' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'media/audio.mp3', + pathname: '/media/audio', + propsKey: 'media/audio.mp3.props', + ext: 'mp3' + }; + + const obj = { data: mp3File, guid: 'audio-guid-jkl' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-audio-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'audio/mpeg'); + assert.strictEqual(versionCalls[0].guid, 'audio-guid-jkl'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for HTML file upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-html-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create an HTML file with typical content + const htmlContent = ` + + + + Test Page + + +

Welcome to DA

+

This is a test HTML document for versioning.

+ +`; + const htmlFile = new File([htmlContent], 'index.html', { type: 'text/html' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'pages/index.html', + pathname: '/pages/index', + propsKey: 'pages/index.html.props', + ext: 'html' + }; + + const obj = { data: htmlFile, guid: 'html-guid-mno' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-html-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'text/html'); + assert.strictEqual(versionCalls[0].guid, 'html-guid-mno'); + assert(versionCalls[0].update.body instanceof File); + }); + + it('Creates version for JSON file upload', async () => { + const versionCalls = []; + const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { + versionCalls.push({ e, daCtx, update, body, guid }); + return { status: 201, metadata: { id: 'test-json-id' } }; + }; + + const putObjectWithVersioning = await esmock('../../../src/storage/object/put.js', { + '../../../src/storage/version/put.js': { + putObjectWithVersion: mockPutObjectWithVersion, + postObjectVersion, + } + }); + + // Create a JSON file with typical structured data + const jsonContent = JSON.stringify({ + title: 'Test Configuration', + version: '1.0.0', + settings: { + enabled: true, + options: ['option1', 'option2', 'option3'] + }, + metadata: { + author: 'test@example.com', + created: '2024-01-01T00:00:00Z' + } + }, null, 2); + const jsonFile = new File([jsonContent], 'config.json', { type: 'application/json' }); + + const daCtx = { + org: 'testorg', + site: 'testsite', + isFile: true, + key: 'config/settings.json', + pathname: '/config/settings', + propsKey: 'config/settings.json.props', + ext: 'json' + }; + + const obj = { data: jsonFile, guid: 'json-guid-pqr' }; + const resp = await putObjectWithVersioning(env, daCtx, obj); + + assert.strictEqual(resp.status, 201); + assert.strictEqual(resp.metadata.id, 'test-json-id'); + assert.strictEqual(versionCalls.length, 1); + assert.strictEqual(versionCalls[0].update.type, 'application/json'); + assert.strictEqual(versionCalls[0].guid, 'json-guid-pqr'); + assert(versionCalls[0].update.body instanceof File); + }); }); }); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index b8992b0d..40217eb7 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -1122,4 +1122,352 @@ describe('Version Put', () => { assert.strictEqual(mainCommand.input.Body, newJpegFile); assert.strictEqual(mainCommand.input.ContentType, 'image/jpeg'); }); + + it('Test putVersion with PDF document content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate PDF binary data + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34]); + const pdfFile = new File([pdfData], 'document.pdf', { type: 'application/pdf' }); + + const testParams = { + Bucket: 'docs-bucket', + Org: 'testorg', + Body: pdfFile, + ID: 'pdf-id-123', + Version: 'pdf-version-1', + Ext: 'pdf', + Metadata: { Users: '["user@example.com"]', Path: 'documents/report.pdf' }, + ContentType: 'application/pdf' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'docs-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/pdf-id-123/pdf-version-1.pdf'); + assert.strictEqual(putCommand.input.Body, pdfFile); + assert.strictEqual(putCommand.input.ContentType, 'application/pdf'); + }); + + it('Test putVersion with ZIP archive content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate ZIP binary data + const zipData = new Uint8Array([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00]); + const zipFile = new File([zipData], 'archive.zip', { type: 'application/zip' }); + + const testParams = { + Bucket: 'files-bucket', + Org: 'testorg', + Body: zipFile, + ID: 'zip-id-456', + Version: 'zip-version-1', + Ext: 'zip', + Metadata: { Users: '["user@example.com"]', Path: 'downloads/archive.zip' }, + ContentType: 'application/zip' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'files-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/zip-id-456/zip-version-1.zip'); + assert.strictEqual(putCommand.input.Body, zipFile); + assert.strictEqual(putCommand.input.ContentType, 'application/zip'); + }); + + it('Test putVersion with generic binary content (octet-stream)', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Generic binary data + const binaryData = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]); + const binaryFile = new File([binaryData], 'data.bin', { type: 'application/octet-stream' }); + + const testParams = { + Bucket: 'storage-bucket', + Org: 'testorg', + Body: binaryFile, + ID: 'binary-id-789', + Version: 'binary-version-1', + Ext: 'bin', + Metadata: { Users: '["user@example.com"]', Path: 'files/data.bin' }, + ContentType: 'application/octet-stream' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'storage-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/binary-id-789/binary-version-1.bin'); + assert.strictEqual(putCommand.input.Body, binaryFile); + assert.strictEqual(putCommand.input.ContentType, 'application/octet-stream'); + }); + + it('Test putVersion with audio file content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Simulate MP3 audio data + const mp3Data = new Uint8Array([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00]); + const mp3File = new File([mp3Data], 'audio.mp3', { type: 'audio/mpeg' }); + + const testParams = { + Bucket: 'media-bucket', + Org: 'testorg', + Body: mp3File, + ID: 'audio-id-abc', + Version: 'audio-version-1', + Ext: 'mp3', + Metadata: { Users: '["user@example.com"]', Path: 'media/song.mp3' }, + ContentType: 'audio/mpeg' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'media-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/audio-id-abc/audio-version-1.mp3'); + assert.strictEqual(putCommand.input.Body, mp3File); + assert.strictEqual(putCommand.input.ContentType, 'audio/mpeg'); + }); + + it('Test putVersion with HTML file content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Create HTML content + const htmlContent = ` + +Test +

Hello World

+`; + const htmlFile = new File([htmlContent], 'page.html', { type: 'text/html' }); + + const testParams = { + Bucket: 'content-bucket', + Org: 'testorg', + Body: htmlFile, + ID: 'html-id-def', + Version: 'html-version-1', + Ext: 'html', + Metadata: { Users: '["user@example.com"]', Path: 'pages/index.html' }, + ContentType: 'text/html' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'content-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/html-id-def/html-version-1.html'); + assert.strictEqual(putCommand.input.Body, htmlFile); + assert.strictEqual(putCommand.input.ContentType, 'text/html'); + }); + + it('Test putVersion with JSON file content', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + const { putVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + }, + }); + + // Create JSON content + const jsonContent = JSON.stringify({ + name: 'Test Config', + version: '2.0', + features: ['feature1', 'feature2'] + }, null, 2); + const jsonFile = new File([jsonContent], 'config.json', { type: 'application/json' }); + + const testParams = { + Bucket: 'data-bucket', + Org: 'testorg', + Body: jsonFile, + ID: 'json-id-ghi', + Version: 'json-version-1', + Ext: 'json', + Metadata: { Users: '["user@example.com"]', Path: 'config/settings.json' }, + ContentType: 'application/json' + }; + + const result = await putVersion({}, testParams); + + assert.strictEqual(result.status, 200); + assert.strictEqual(sentCommands.length, 1); + const putCommand = sentCommands[0]; + assert.strictEqual(putCommand.input.Bucket, 'data-bucket'); + assert.strictEqual(putCommand.input.Key, 'testorg/.da-versions/json-id-ghi/json-version-1.json'); + assert.strictEqual(putCommand.input.Body, jsonFile); + assert.strictEqual(putCommand.input.ContentType, 'application/json'); + }); + + it('Test putObjectWithVersion with HTML preserves content on update', async () => { + const sentCommands = []; + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { + $metadata: { httpStatusCode: 200 } + }; + } + }; + + // Simulate existing HTML file + const existingHtmlContent = '

Old Version

'; + const mockGetObject = async () => ({ + status: 200, + body: existingHtmlContent, + contentLength: existingHtmlContent.length, + contentType: 'text/html', + etag: 'test-etag-html', + metadata: { + id: 'html-id-existing', + version: 'html-version-old', + timestamp: '1234567890', + users: '["olduser@example.com"]', + path: 'pages/index.html' + } + }); + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const env = {}; + const daCtx = { + org: 'testorg', + ext: 'html', + users: [{ email: 'newuser@example.com' }] + }; + + // New HTML content to upload + const newHtmlContent = '

New Version

Updated content

'; + const newHtmlFile = new File([newHtmlContent], 'index.html', { type: 'text/html' }); + + const update = { + bucket: 'content-bucket', + org: 'testorg', + key: 'pages/index.html', + body: newHtmlFile, + type: 'text/html' + }; + + const result = await putObjectWithVersion(env, daCtx, update, true); + + assert.strictEqual(result.status, 200); + assert.strictEqual(result.metadata.id, 'html-id-existing'); + + // Should have 2 commands: one for version, one for main object + assert.strictEqual(sentCommands.length, 2); + + // First command should store the old version + const versionCommand = sentCommands[0]; + assert.strictEqual(versionCommand.input.Bucket, 'content-bucket'); + assert(versionCommand.input.Key.includes('.da-versions/html-id-existing/')); + assert(versionCommand.input.Key.endsWith('.html')); + assert.strictEqual(versionCommand.input.Body, existingHtmlContent); + assert.strictEqual(versionCommand.input.ContentType, 'text/html'); + assert.strictEqual(versionCommand.input.ContentLength, existingHtmlContent.length); + + // Second command should store the new content + const mainCommand = sentCommands[1]; + assert.strictEqual(mainCommand.input.Bucket, 'content-bucket'); + assert.strictEqual(mainCommand.input.Key, 'testorg/pages/index.html'); + assert.strictEqual(mainCommand.input.Body, newHtmlFile); + assert.strictEqual(mainCommand.input.ContentType, 'text/html'); + }); }); From c8965b2370ea0514dba805ad95ee9aa89f4a95b2 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 11:49:38 +0100 Subject: [PATCH 3/7] chore: more tests --- test/routes/source.test.js | 116 ++++++- test/storage/version/put.test.js | 575 +++++++++++++++++++++++++++++++ 2 files changed, 690 insertions(+), 1 deletion(-) diff --git a/test/routes/source.test.js b/test/routes/source.test.js index 52538b2e..73acf694 100644 --- a/test/routes/source.test.js +++ b/test/routes/source.test.js @@ -347,4 +347,118 @@ describe('Source Route', () => { assert.strictEqual('some data', putCalled[0].o.data); assert.strictEqual(202, resp.status); }); -}); \ No newline at end of file + + it('Test postSource with binary image file via FormData', async () => { + const putCalled = []; + const putCall = (e, c, o) => { + putCalled.push({e, c, o}); + return { status: 201, metadata: { id: 'image-guid-123' }}; + }; + + const ctx = { key: 'images/photo.jpg', ext: 'jpg', isFile: true }; + const hasPermission = () => true; + + const { postSource } = await esmock( + '../../src/routes/source.js', { + '../../src/storage/object/put.js': { + default: putCall + }, + '../../src/utils/auth.js': { + hasPermission + }, + } + ); + + // Create a binary image file (JPEG signature) + const jpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46]); + const imageFile = new File([jpegData], 'photo.jpg', { type: 'image/jpeg' }); + + const body = new FormData(); + body.append('data', imageFile); + body.append('guid', 'image-guid-123'); + + const opts = { body, method: 'POST' }; + const req = new Request('https://da.live/source/org/repo/images/photo.jpg', opts); + + const resp = await postSource({ req, env: {}, daCtx: ctx }); + assert.strictEqual(1, putCalled.length); + assert.strictEqual('image-guid-123', putCalled[0].o.guid); + assert(putCalled[0].o.data instanceof File); + assert.strictEqual('image/jpeg', putCalled[0].o.data.type); + assert.strictEqual(201, resp.status); + }); + + it('Test postSource with PNG image via FormData', async () => { + const putCalled = []; + const putCall = (e, c, o) => { + putCalled.push({e, c, o}); + return { status: 201, metadata: { id: 'png-guid-456' }}; + }; + + const ctx = { key: 'images/graphic.png', ext: 'png', isFile: true }; + const hasPermission = () => true; + + const { postSource } = await esmock( + '../../src/routes/source.js', { + '../../src/storage/object/put.js': { + default: putCall + }, + '../../src/utils/auth.js': { + hasPermission + }, + } + ); + + const pngData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const pngFile = new File([pngData], 'graphic.png', { type: 'image/png' }); + + const body = new FormData(); + body.append('data', pngFile); + + const opts = { body, method: 'POST' }; + const req = new Request('https://da.live/source/org/repo/images/graphic.png', opts); + + const resp = await postSource({ req, env: {}, daCtx: ctx }); + assert.strictEqual(1, putCalled.length); + assert(putCalled[0].o.data instanceof File); + assert.strictEqual('image/png', putCalled[0].o.data.type); + assert.strictEqual(201, resp.status); + }); + + it('Test postSource with PDF via FormData', async () => { + const putCalled = []; + const putCall = (e, c, o) => { + putCalled.push({e, c, o}); + return { status: 201, metadata: { id: 'pdf-guid-abc' }}; + }; + + const ctx = { key: 'docs/report.pdf', ext: 'pdf', isFile: true }; + const hasPermission = () => true; + + const { postSource } = await esmock( + '../../src/routes/source.js', { + '../../src/storage/object/put.js': { + default: putCall + }, + '../../src/utils/auth.js': { + hasPermission + }, + } + ); + + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34]); + const pdfFile = new File([pdfData], 'report.pdf', { type: 'application/pdf' }); + + const body = new FormData(); + body.append('data', pdfFile); + + const opts = { body, method: 'POST' }; + const req = new Request('https://da.live/source/org/repo/docs/report.pdf', opts); + + const resp = await postSource({ req, env: {}, daCtx: ctx }); + assert.strictEqual(1, putCalled.length); + assert(putCalled[0].o.data instanceof File); + assert.strictEqual('application/pdf', putCalled[0].o.data.type); + assert.strictEqual(201, resp.status); + }); +}); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index 40217eb7..eb9b8553 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -1470,4 +1470,579 @@ describe('Version Put', () => { assert.strictEqual(mainCommand.input.Body, newHtmlFile); assert.strictEqual(mainCommand.input.ContentType, 'text/html'); }); + + describe('Versioning behavior: CREATE vs UPDATE', () => { + it('JPEG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + // First call: file doesn't exist + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + // Second call: file exists + return { + status: 200, + body: new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0]), + contentLength: 4, + contentType: 'image/jpeg', + etag: 'old-etag', + metadata: { + id: 'jpeg-id-123', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'images/photo.jpg' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const jpegData = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE1, 0x00, 0x10]); + const jpegFile = new File([jpegData], 'photo.jpg', { type: 'image/jpeg' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'images/photo.jpg', + users: [{ email: 'user@example.com' }] + }; + + // FIRST CALL - file doesn't exist (404) + const update1 = { + bucket: 'test-bucket', + org: 'testorg', + key: 'images/photo.jpg', + body: jpegFile, + contentLength: jpegData.length, + type: 'image/jpeg' + }; + + sentCommands.length = 0; + const result1 = await putObjectWithVersion(env, daCtx, update1); + // File created for first time - could be 200 or 201 + assert(result1.status === 200 || result1.status === 201); + // Only 1 command: PutObject for main file, NO version created + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].input.Key, 'testorg/images/photo.jpg'); + + // SECOND CALL - file exists (200) + sentCommands.length = 0; + const result2 = await putObjectWithVersion(env, daCtx, update1); + assert(result2.status === 200 || result2.status === 201); + // 2 commands: PutObject for version + PutObject for main file + assert.strictEqual(sentCommands.length, 2); + assert(sentCommands[0].input.Key.includes('/.da-versions/')); // Version + assert.strictEqual(sentCommands[1].input.Key, 'testorg/images/photo.jpg'); // Main + }); + + it('PNG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: new Uint8Array([0x89, 0x50, 0x4E, 0x47]), + contentLength: 4, + contentType: 'image/png', + etag: 'old-etag', + metadata: { + id: 'png-id-456', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'images/graphic.png' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const pngData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + const pngFile = new File([pngData], 'graphic.png', { type: 'image/png' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'images/graphic.png', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: pngFile, + contentLength: pngData.length, + type: 'image/png' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('HTML: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: '

Old

', + contentLength: 40, + contentType: 'text/html', + etag: 'old-etag', + metadata: { + id: 'html-id-789', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'pages/index.html' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const htmlContent = '

New

'; + const htmlFile = new File([htmlContent], 'index.html', { type: 'text/html' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'pages/index.html', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: htmlFile, + contentLength: htmlContent.length, + type: 'text/html' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('JSON: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: JSON.stringify({ version: '1.0' }), + contentLength: 20, + contentType: 'application/json', + etag: 'old-etag', + metadata: { + id: 'json-id-abc', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'config/settings.json' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const jsonContent = JSON.stringify({ version: '2.0', features: ['new'] }); + const jsonFile = new File([jsonContent], 'settings.json', { type: 'application/json' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'config/settings.json', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: jsonFile, + contentLength: jsonContent.length, + type: 'application/json' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('PDF: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: new Uint8Array([0x25, 0x50, 0x44, 0x46]), + contentLength: 4, + contentType: 'application/pdf', + etag: 'old-etag', + metadata: { + id: 'pdf-id-def', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'docs/report.pdf' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34]); + const pdfFile = new File([pdfData], 'report.pdf', { type: 'application/pdf' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'docs/report.pdf', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: pdfFile, + contentLength: pdfData.length, + type: 'application/pdf' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('MP4: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: new Uint8Array([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70]), + contentLength: 8, + contentType: 'video/mp4', + etag: 'old-etag', + metadata: { + id: 'mp4-id-ghi', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'videos/demo.mp4' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const mp4Data = new Uint8Array([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D]); + const mp4File = new File([mp4Data], 'demo.mp4', { type: 'video/mp4' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'videos/demo.mp4', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: mp4File, + contentLength: mp4Data.length, + type: 'video/mp4' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('SVG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: '', + contentLength: 80, + contentType: 'image/svg+xml', + etag: 'old-etag', + metadata: { + id: 'svg-id-jkl', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'images/icon.svg' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const svgContent = ''; + const svgFile = new File([svgContent], 'icon.svg', { type: 'image/svg+xml' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'images/icon.svg', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: svgFile, + contentLength: svgContent.length, + type: 'image/svg+xml' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + + it('ZIP: New file (404) creates object WITHOUT version, existing file creates version', async () => { + const sentCommands = []; + let callCount = 0; + + const mockGetObject = async () => { + callCount++; + if (callCount === 1) { + return { status: 404, metadata: {}, body: '', contentLength: 0 }; + } + return { + status: 200, + body: new Uint8Array([0x50, 0x4B, 0x03, 0x04]), + contentLength: 4, + contentType: 'application/zip', + etag: 'old-etag', + metadata: { + id: 'zip-id-mno', + version: 'old-version', + timestamp: '1234567890', + users: '["user@example.com"]', + path: 'archives/data.zip' + } + }; + }; + + const mockS3Client = { + async send(cmd) { + sentCommands.push(cmd); + return { $metadata: { httpStatusCode: 200 } }; + } + }; + + const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', { + '../../../src/storage/object/get.js': { + default: mockGetObject + }, + '../../../src/storage/utils/version.js': { + ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, + }, + }); + + const zipData = new Uint8Array([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00]); + const zipFile = new File([zipData], 'data.zip', { type: 'application/zip' }); + + const env = {}; + const daCtx = { + org: 'testorg', + bucket: 'test-bucket', + key: 'archives/data.zip', + users: [{ email: 'user@example.com' }] + }; + + const update = { + body: zipFile, + contentLength: zipData.length, + type: 'application/zip' + }; + + // FIRST CALL - no version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 1); + + // SECOND CALL - creates version + sentCommands.length = 0; + await putObjectWithVersion(env, daCtx, update); + assert.strictEqual(sentCommands.length, 2); + }); + }); }); From dd98406b1b8d272d137c91494cfa1bb610592068 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 12:02:37 +0100 Subject: [PATCH 4/7] feat: no version for binaries --- src/storage/version/put.js | 78 ++++++++++++---------- test/storage/version/put.test.js | 110 +++++++++++++++++-------------- 2 files changed, 106 insertions(+), 82 deletions(-) diff --git a/src/storage/version/put.js b/src/storage/version/put.js index eee2b312..1fb9011f 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -56,6 +56,13 @@ export async function putVersion(config, { } } +function shouldCreateVersion(contentType) { + // Only create versions for HTML and JSON files + if (!contentType) return false; + const type = contentType.toLowerCase(); + return type.includes('text/html') || type.includes('application/json'); +} + function buildInput({ bucket, org, key, body, type, contentLength, }) { @@ -112,45 +119,50 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { } } - const pps = current.metadata?.preparsingstore || '0'; + // Only create versions for HTML and JSON files + const contentType = update.type || current.contentType; + const createVersion = shouldCreateVersion(contentType); - // Store the body if preparsingstore is not defined, so a once-off store + const pps = current.metadata?.preparsingstore || '0'; let storeBody = !body && pps === '0'; - const Preparsingstore = storeBody ? Timestamp : pps; + let Preparsingstore = storeBody ? Timestamp : pps; let Label = storeBody ? 'Collab Parse' : update.label; - if (daCtx.method === 'PUT' - && daCtx.ext === 'html' - && current.contentLength > EMPTY_DOC_SIZE - && (!update.body || update.body.size <= EMPTY_DOC_SIZE)) { - // we are about to empty the document body - // this should almost never happen but it does in some unexpectedcases - // we want then to store a version of the full document as a Restore Point - // eslint-disable-next-line no-console - console.warn(`Empty body, creating a restore point (${current.contentLength} / ${update.body?.size})`); - storeBody = true; - Label = 'Restore Point'; - } + if (createVersion) { + if (daCtx.method === 'PUT' + && daCtx.ext === 'html' + && current.contentLength > EMPTY_DOC_SIZE + && (!update.body || update.body.size <= EMPTY_DOC_SIZE)) { + // we are about to empty the document body + // this should almost never happen but it does in some unexpectedcases + // we want then to store a version of the full document as a Restore Point + // eslint-disable-next-line no-console + console.warn(`Empty body, creating a restore point (${current.contentLength} / ${update.body?.size})`); + storeBody = true; + Label = 'Restore Point'; + Preparsingstore = Timestamp; + } - const versionResp = await putVersion(config, { - Bucket: input.Bucket, - Org: daCtx.org, - Body: (body || storeBody ? current.body : ''), - ContentLength: (body || storeBody ? current.contentLength : undefined), - ContentType: current.contentType, - ID, - Version, - Ext: daCtx.ext, - Metadata: { - Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), - Timestamp: current.metadata?.timestamp || Timestamp, - Path: current.metadata?.path || Path, - Label, - }, - }); + const versionResp = await putVersion(config, { + Bucket: input.Bucket, + Org: daCtx.org, + Body: (body || storeBody ? current.body : ''), + ContentLength: (body || storeBody ? current.contentLength : undefined), + ContentType: current.contentType, + ID, + Version, + Ext: daCtx.ext, + Metadata: { + Users: current.metadata?.users || JSON.stringify([{ email: 'anonymous' }]), + Timestamp: current.metadata?.timestamp || Timestamp, + Path: current.metadata?.path || Path, + Label, + }, + }); - if (versionResp.status !== 200 && versionResp.status !== 412) { - return { status: versionResp.status, metadata: { id: ID } }; + if (versionResp.status !== 200 && versionResp.status !== 412) { + return { status: versionResp.status, metadata: { id: ID } }; + } } const client = ifMatch(config, `${current.etag}`); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index eb9b8553..517f055e 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -264,7 +264,7 @@ describe('Version Put', () => { const env = {}; const daCtx= { bucket: 'bkt', org: 'myorg', ext: 'html' }; - const update = { bucket: 'bkt', body: 'new-body', org: 'myorg', key: 'a/x.html' }; + const update = { bucket: 'bkt', body: 'new-body', org: 'myorg', key: 'a/x.html', type: 'text/html' }; const resp = await putObjectWithVersion(env, daCtx, update, true); assert.equal(200, resp.status); assert.equal('x123', resp.metadata.id); @@ -331,7 +331,7 @@ describe('Version Put', () => { const env = {}; const daCtx= { bucket: 'bbb', org: 'myorg', ext: 'html', users: [{"email": "foo@acme.org"}, {"email": "bar@acme.org"}] }; - const update = { bucket: 'bbb', body: 'new-body', org: 'myorg', key: 'a/x.html' }; + const update = { bucket: 'bbb', body: 'new-body', org: 'myorg', key: 'a/x.html', type: 'text/html' }; const resp = await putObjectWithVersion(env, daCtx, update, false); assert.equal(202, resp.status); assert.equal('q123-456', resp.metadata.id); @@ -570,7 +570,7 @@ describe('Version Put', () => { }, }); - const resp = await putObjectWithVersion({}, { method: 'HEAD' }, {}); + const resp = await putObjectWithVersion({}, { method: 'HEAD' }, { type: 'text/html' }); assert.equal(1, sentToS3.length); const input = sentToS3[0].input; assert.equal('', input.Body, 'Empty body for HEAD'); @@ -631,7 +631,7 @@ describe('Version Put', () => { org: 'o1', body: 'foobar', key: 'mypath', - type: 'test/plain', + type: 'text/html', } const ctx = { org: 'o1', @@ -650,7 +650,7 @@ describe('Version Put', () => { const input2 = sentToS3_2[0].input; assert.equal('foobar', input2.Body); assert.equal(6, input2.ContentLength); - assert.equal('test/plain', input2.ContentType); + assert.equal('text/html', input2.ContentType); assert.equal('o1/mypath', input2.Key); assert.equal('mypath', input2.Metadata.Path); assert.equal('[{"email":"hi@acme.com"}]', input2.Metadata.Users); @@ -708,7 +708,7 @@ describe('Version Put', () => { org: 'o1', body: '', key: 'mypath', - type: 'test/plain', + type: 'text/html', } const ctx = { org: 'o1', @@ -733,7 +733,7 @@ describe('Version Put', () => { const input2 = sentToS3_2[0].input; assert.equal('', input2.Body); assert.equal(0, input2.ContentLength); - assert.equal('test/plain', input2.ContentType); + assert.equal('text/html', input2.ContentType); assert.equal('o1/mypath', input2.Key); assert.equal('mypath', input2.Metadata.Path); assert.equal('[{"email":"hi@acme.com"}]', input2.Metadata.Users); @@ -836,7 +836,7 @@ describe('Version Put', () => { status: 200, body: 'test body', contentLength: 9, - contentType: 'text/plain', + contentType: 'text/html', metadata: { existing: 'metadata' } }); @@ -846,6 +846,7 @@ describe('Version Put', () => { }, '../../../src/storage/utils/version.js': { ifNoneMatch: () => mockS3Client, + ifMatch: () => mockS3Client, generateId: () => 'generated-id', generateVersion: () => 'generated-version' }, @@ -854,15 +855,15 @@ describe('Version Put', () => { const env = {}; const daCtx = { org: 'test-org', - ext: 'txt', + ext: 'html', users: [{ email: 'test@example.com' }] }; - await putObjectWithVersion(env, daCtx, { key: 'test-file.txt' }, 'test body', 'test-guid'); + await putObjectWithVersion(env, daCtx, { key: 'test-file.html', type: 'text/html' }, 'test body', 'test-guid'); - assert.strictEqual(sentCommands.length, 1); - const putCommand = sentCommands[0]; - assert.strictEqual(putCommand.input.ContentType, 'text/plain'); + assert.strictEqual(sentCommands.length, 2); // Version + main file + const putCommand = sentCommands[0]; // First command is the version + assert.strictEqual(putCommand.input.ContentType, 'text/html'); assert.strictEqual(putCommand.input.Body, 'test body'); assert.strictEqual(putCommand.input.ContentLength, 9); }); @@ -1103,20 +1104,11 @@ describe('Version Put', () => { assert.strictEqual(result.status, 200); assert.strictEqual(result.metadata.id, 'jpeg-id-existing'); - // Should have 2 commands: one for version, one for main object - assert.strictEqual(sentCommands.length, 2); - - // First command should store the old version - const versionCommand = sentCommands[0]; - assert.strictEqual(versionCommand.input.Bucket, 'media-bucket'); - assert(versionCommand.input.Key.includes('.da-versions/jpeg-id-existing/')); - assert(versionCommand.input.Key.endsWith('.jpg')); - assert.strictEqual(versionCommand.input.Body, existingJpegData); - assert.strictEqual(versionCommand.input.ContentType, 'image/jpeg'); - assert.strictEqual(versionCommand.input.ContentLength, existingJpegData.length); + // Binary files (like JPEG) do NOT create versions, only 1 command for main object + assert.strictEqual(sentCommands.length, 1); - // Second command should store the new content - const mainCommand = sentCommands[1]; + // Only command should store the new main object (no version for binaries) + const mainCommand = sentCommands[0]; assert.strictEqual(mainCommand.input.Bucket, 'media-bucket'); assert.strictEqual(mainCommand.input.Key, 'testorg/images/photo.jpg'); assert.strictEqual(mainCommand.input.Body, newJpegFile); @@ -1472,7 +1464,7 @@ describe('Version Put', () => { }); describe('Versioning behavior: CREATE vs UPDATE', () => { - it('JPEG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('JPEG: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -1545,17 +1537,16 @@ describe('Version Put', () => { assert.strictEqual(sentCommands.length, 1); assert.strictEqual(sentCommands[0].input.Key, 'testorg/images/photo.jpg'); - // SECOND CALL - file exists (200) + // SECOND CALL - file exists (200) - STILL NO VERSION for binaries sentCommands.length = 0; const result2 = await putObjectWithVersion(env, daCtx, update1); assert(result2.status === 200 || result2.status === 201); - // 2 commands: PutObject for version + PutObject for main file - assert.strictEqual(sentCommands.length, 2); - assert(sentCommands[0].input.Key.includes('/.da-versions/')); // Version - assert.strictEqual(sentCommands[1].input.Key, 'testorg/images/photo.jpg'); // Main + // Only 1 command: PutObject for main file, NO version for binaries + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].input.Key, 'testorg/images/photo.jpg'); }); - it('PNG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('PNG: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -1609,6 +1600,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'images/graphic.png', body: pngFile, contentLength: pngData.length, type: 'image/png' @@ -1619,10 +1613,10 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - still no version for binaries sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); - assert.strictEqual(sentCommands.length, 2); + assert.strictEqual(sentCommands.length, 1); }); it('HTML: New file (404) creates object WITHOUT version, existing file creates version', async () => { @@ -1679,6 +1673,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'pages/index.html', body: htmlFile, contentLength: htmlContent.length, type: 'text/html' @@ -1689,7 +1686,7 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - creates version for HTML sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 2); @@ -1749,6 +1746,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'config/settings.json', body: jsonFile, contentLength: jsonContent.length, type: 'application/json' @@ -1759,13 +1759,13 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - creates version for JSON sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 2); }); - it('PDF: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('PDF: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -1819,6 +1819,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'docs/report.pdf', body: pdfFile, contentLength: pdfData.length, type: 'application/pdf' @@ -1829,13 +1832,13 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - still no version for binaries sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); - assert.strictEqual(sentCommands.length, 2); + assert.strictEqual(sentCommands.length, 1); }); - it('MP4: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('MP4: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -1889,6 +1892,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'videos/demo.mp4', body: mp4File, contentLength: mp4Data.length, type: 'video/mp4' @@ -1899,13 +1905,13 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - still no version for binaries sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); - assert.strictEqual(sentCommands.length, 2); + assert.strictEqual(sentCommands.length, 1); }); - it('SVG: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('SVG: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -1959,6 +1965,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'images/icon.svg', body: svgFile, contentLength: svgContent.length, type: 'image/svg+xml' @@ -1969,13 +1978,13 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - still no version for binaries sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); - assert.strictEqual(sentCommands.length, 2); + assert.strictEqual(sentCommands.length, 1); }); - it('ZIP: New file (404) creates object WITHOUT version, existing file creates version', async () => { + it('ZIP: Binary files NEVER create versions (first or second POST)', async () => { const sentCommands = []; let callCount = 0; @@ -2029,6 +2038,9 @@ describe('Version Put', () => { }; const update = { + bucket: 'test-bucket', + org: 'testorg', + key: 'archives/data.zip', body: zipFile, contentLength: zipData.length, type: 'application/zip' @@ -2039,10 +2051,10 @@ describe('Version Put', () => { await putObjectWithVersion(env, daCtx, update); assert.strictEqual(sentCommands.length, 1); - // SECOND CALL - creates version + // SECOND CALL - still no version for binaries sentCommands.length = 0; await putObjectWithVersion(env, daCtx, update); - assert.strictEqual(sentCommands.length, 2); + assert.strictEqual(sentCommands.length, 1); }); }); }); From cfe5e9bec96785251575822c322e649111211259 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 12:04:21 +0100 Subject: [PATCH 5/7] chore: no version metadata --- src/storage/version/put.js | 26 ++++++++++++++++---------- test/storage/version/put.test.js | 4 ++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/storage/version/put.js b/src/storage/version/put.js index 1fb9011f..ac74e082 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -88,6 +88,10 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { return { status: 409, metadata: { id: ID } }; } + // Only create versions for HTML and JSON files + const contentType = update.type || current.contentType; + const createVersion = shouldCreateVersion(contentType); + const Version = current.metadata?.version || crypto.randomUUID(); const Users = JSON.stringify(getUsersForMetadata(daCtx.users)); const input = buildInput(update); @@ -96,11 +100,14 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { if (current.status === 404) { const client = ifNoneMatch(config); + const metadata = { ID, Users, Timestamp, Path }; + // Only include Version metadata for files that support versioning + if (createVersion) { + metadata.Version = Version; + } const command = new PutObjectCommand({ ...input, - Metadata: { - ID, Version, Users, Timestamp, Path, - }, + Metadata: metadata, }); try { const resp = await client.send(command); @@ -119,10 +126,6 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { } } - // Only create versions for HTML and JSON files - const contentType = update.type || current.contentType; - const createVersion = shouldCreateVersion(contentType); - const pps = current.metadata?.preparsingstore || '0'; let storeBody = !body && pps === '0'; let Preparsingstore = storeBody ? Timestamp : pps; @@ -166,11 +169,14 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { } const client = ifMatch(config, `${current.etag}`); + const metadata = { ID, Users, Timestamp, Path, Preparsingstore }; + // Only include Version metadata for files that support versioning + if (createVersion) { + metadata.Version = crypto.randomUUID(); + } const command = new PutObjectCommand({ ...input, - Metadata: { - ID, Version: crypto.randomUUID(), Users, Timestamp, Path, Preparsingstore, - }, + Metadata: metadata, }); try { const resp = await client.send(command); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index 517f055e..e314a412 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -388,7 +388,7 @@ describe('Version Put', () => { const env = {}; const daCtx= { bucket: 'b-b', org: 'myorg' }; - const update = { bucket: 'b-b', org: 'myorg', key: 'a/b/c' }; + const update = { bucket: 'b-b', org: 'myorg', key: 'a/b/c.html', type: 'text/html' }; const resp = await putObjectWithVersion(env, daCtx, update, true); assert.equal(201, resp.status); assert(resp.metadata.id, 'The ID should be set'); @@ -396,7 +396,7 @@ describe('Version Put', () => { assert.equal(1, s3Sent.length); assert.equal('b-b', s3Sent[0].input.Bucket); assert(s3Sent[0].input.Metadata.ID); - assert.equal('a/b/c', s3Sent[0].input.Metadata.Path); + assert.equal('a/b/c.html', s3Sent[0].input.Metadata.Path); assert(s3Sent[0].input.Metadata.Timestamp > 0); assert(s3Sent[0].input.Metadata.Version); }); From df633c971ee2cbc7dabacbb69b6c429ce2df2526 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Fri, 14 Nov 2025 12:07:45 +0100 Subject: [PATCH 6/7] chore: linting --- src/storage/version/put.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/storage/version/put.js b/src/storage/version/put.js index ac74e082..eaee49ef 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -100,7 +100,9 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { if (current.status === 404) { const client = ifNoneMatch(config); - const metadata = { ID, Users, Timestamp, Path }; + const metadata = { + ID, Users, Timestamp, Path, + }; // Only include Version metadata for files that support versioning if (createVersion) { metadata.Version = Version; @@ -169,7 +171,9 @@ export async function putObjectWithVersion(env, daCtx, update, body, guid) { } const client = ifMatch(config, `${current.etag}`); - const metadata = { ID, Users, Timestamp, Path, Preparsingstore }; + const metadata = { + ID, Users, Timestamp, Path, Preparsingstore, + }; // Only include Version metadata for files that support versioning if (createVersion) { metadata.Version = crypto.randomUUID(); From 0e3422c77abc55a64ea879dd7718b90a3ed9389f Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 3 Dec 2025 09:55:23 +0100 Subject: [PATCH 7/7] chore: post merge linting --- test/storage/object/put.test.js | 86 ++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/test/storage/object/put.test.js b/test/storage/object/put.test.js index 58e3e3bc..433eadbc 100644 --- a/test/storage/object/put.test.js +++ b/test/storage/object/put.test.js @@ -66,7 +66,9 @@ describe('Object storage', () => { it('Creates version for JPEG image upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-jpeg-id' } }; }; @@ -74,7 +76,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock JPEG file (using a simple byte array to simulate binary data) @@ -88,7 +90,7 @@ describe('Object storage', () => { key: 'images/test-image.jpg', pathname: '/images/test-image', propsKey: 'images/test-image.jpg.props', - ext: 'jpg' + ext: 'jpg', }; const obj = { data: jpegFile, guid: 'jpeg-guid-123' }; @@ -105,7 +107,9 @@ describe('Object storage', () => { it('Creates version for PNG image upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-png-id' } }; }; @@ -113,7 +117,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock PNG file (PNG signature) @@ -127,7 +131,7 @@ describe('Object storage', () => { key: 'images/test-image.png', pathname: '/images/test-image', propsKey: 'images/test-image.png.props', - ext: 'png' + ext: 'png', }; const obj = { data: pngFile }; @@ -143,7 +147,9 @@ describe('Object storage', () => { it('Creates version for MP4 video upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-video-id' } }; }; @@ -151,7 +157,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock MP4 file (ftyp box signature) @@ -165,7 +171,7 @@ describe('Object storage', () => { key: 'videos/test-video.mp4', pathname: '/videos/test-video', propsKey: 'videos/test-video.mp4.props', - ext: 'mp4' + ext: 'mp4', }; const obj = { data: mp4File, guid: 'video-guid-456' }; @@ -182,7 +188,9 @@ describe('Object storage', () => { it('Creates version for SVG image upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-svg-id' } }; }; @@ -190,7 +198,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create an SVG file (text-based but still an image) @@ -204,7 +212,7 @@ describe('Object storage', () => { key: 'images/test-image.svg', pathname: '/images/test-image', propsKey: 'images/test-image.svg.props', - ext: 'svg' + ext: 'svg', }; const obj = { data: svgFile, guid: 'svg-guid-789' }; @@ -221,7 +229,9 @@ describe('Object storage', () => { it('Creates version for PDF document upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-pdf-id' } }; }; @@ -229,7 +239,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock PDF file (PDF signature: %PDF) @@ -243,7 +253,7 @@ describe('Object storage', () => { key: 'documents/report.pdf', pathname: '/documents/report', propsKey: 'documents/report.pdf.props', - ext: 'pdf' + ext: 'pdf', }; const obj = { data: pdfFile, guid: 'pdf-guid-abc' }; @@ -260,7 +270,9 @@ describe('Object storage', () => { it('Creates version for ZIP archive upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-zip-id' } }; }; @@ -268,7 +280,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock ZIP file (ZIP signature: PK) @@ -282,7 +294,7 @@ describe('Object storage', () => { key: 'downloads/archive.zip', pathname: '/downloads/archive', propsKey: 'downloads/archive.zip.props', - ext: 'zip' + ext: 'zip', }; const obj = { data: zipFile, guid: 'zip-guid-def' }; @@ -299,7 +311,9 @@ describe('Object storage', () => { it('Creates version for generic binary file upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-binary-id' } }; }; @@ -307,7 +321,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a generic binary file with application/octet-stream (fallback content type) @@ -321,7 +335,7 @@ describe('Object storage', () => { key: 'files/data.bin', pathname: '/files/data', propsKey: 'files/data.bin.props', - ext: 'bin' + ext: 'bin', }; const obj = { data: binaryFile, guid: 'binary-guid-ghi' }; @@ -338,7 +352,9 @@ describe('Object storage', () => { it('Creates version for audio file upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-audio-id' } }; }; @@ -346,7 +362,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a mock MP3 file (ID3v2 signature) @@ -360,7 +376,7 @@ describe('Object storage', () => { key: 'media/audio.mp3', pathname: '/media/audio', propsKey: 'media/audio.mp3.props', - ext: 'mp3' + ext: 'mp3', }; const obj = { data: mp3File, guid: 'audio-guid-jkl' }; @@ -377,7 +393,9 @@ describe('Object storage', () => { it('Creates version for HTML file upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-html-id' } }; }; @@ -385,7 +403,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create an HTML file with typical content @@ -409,7 +427,7 @@ describe('Object storage', () => { key: 'pages/index.html', pathname: '/pages/index', propsKey: 'pages/index.html.props', - ext: 'html' + ext: 'html', }; const obj = { data: htmlFile, guid: 'html-guid-mno' }; @@ -426,7 +444,9 @@ describe('Object storage', () => { it('Creates version for JSON file upload', async () => { const versionCalls = []; const mockPutObjectWithVersion = async (e, daCtx, update, body, guid) => { - versionCalls.push({ e, daCtx, update, body, guid }); + versionCalls.push({ + e, daCtx, update, body, guid, + }); return { status: 201, metadata: { id: 'test-json-id' } }; }; @@ -434,7 +454,7 @@ describe('Object storage', () => { '../../../src/storage/version/put.js': { putObjectWithVersion: mockPutObjectWithVersion, postObjectVersion, - } + }, }); // Create a JSON file with typical structured data @@ -443,12 +463,12 @@ describe('Object storage', () => { version: '1.0.0', settings: { enabled: true, - options: ['option1', 'option2', 'option3'] + options: ['option1', 'option2', 'option3'], }, metadata: { author: 'test@example.com', - created: '2024-01-01T00:00:00Z' - } + created: '2024-01-01T00:00:00Z', + }, }, null, 2); const jsonFile = new File([jsonContent], 'config.json', { type: 'application/json' }); @@ -459,7 +479,7 @@ describe('Object storage', () => { key: 'config/settings.json', pathname: '/config/settings', propsKey: 'config/settings.json.props', - ext: 'json' + ext: 'json', }; const obj = { data: jsonFile, guid: 'json-guid-pqr' };