151 lines
5.8 KiB
JavaScript
151 lines
5.8 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { isAllowedR2MediaUrl, isOwnedTiktokMediaUrl, prepareTiktokPhotoUrls, verifyPublicPhotoUrl } = require('../lib/tiktok-assets');
|
|
|
|
const verifyPhotoUrl = async () => {};
|
|
|
|
const publicBaseUrl = 'https://greenlenspro.com/storage';
|
|
|
|
test('accepts only HTTPS media URLs under the configured GreenLens storage path', () => {
|
|
assert.equal(isOwnedTiktokMediaUrl('https://greenlenspro.com/storage/plant-images/slide.jpg', publicBaseUrl), true);
|
|
assert.equal(isOwnedTiktokMediaUrl('https://greenlenspro.com.evil.test/storage/slide.jpg', publicBaseUrl), false);
|
|
assert.equal(isOwnedTiktokMediaUrl('http://greenlenspro.com/storage/slide.jpg', publicBaseUrl), false);
|
|
assert.equal(isOwnedTiktokMediaUrl('https://greenlenspro.com/other/slide.jpg', publicBaseUrl), false);
|
|
});
|
|
|
|
test('keeps existing GreenLens storage URLs without uploading them again', async () => {
|
|
let uploads = 0;
|
|
const url = 'https://greenlenspro.com/storage/plant-images/slide.jpg';
|
|
const result = await prepareTiktokPhotoUrls([url], {
|
|
publicBaseUrl,
|
|
verifyPhotoUrl,
|
|
uploadImage: async () => { uploads += 1; },
|
|
});
|
|
|
|
assert.deepEqual(result, [url]);
|
|
assert.equal(uploads, 0);
|
|
});
|
|
|
|
test('hosts base64 photos on the verified GreenLens domain', async () => {
|
|
const uploaded = [];
|
|
const result = await prepareTiktokPhotoUrls([{ imageBase64: Buffer.from('source').toString('base64') }], {
|
|
publicBaseUrl,
|
|
verifyPhotoUrl,
|
|
normalizePhoto: async (buffer) => {
|
|
assert.equal(buffer.toString(), 'source');
|
|
return Buffer.from('jpeg');
|
|
},
|
|
uploadImage: async (base64, contentType) => {
|
|
uploaded.push({ base64, contentType });
|
|
return { url: 'https://greenlenspro.com/storage/plant-images/normalized.jpg' };
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(result, ['https://greenlenspro.com/storage/plant-images/normalized.jpg']);
|
|
assert.deepEqual(uploaded, [{ base64: Buffer.from('jpeg').toString('base64'), contentType: 'image/jpeg' }]);
|
|
});
|
|
|
|
test('allows only HTTPS R2 source hosts without credentials or custom ports', () => {
|
|
assert.equal(isAllowedR2MediaUrl('https://pub-example.r2.dev/slide.jpg'), true);
|
|
assert.equal(isAllowedR2MediaUrl('http://pub-example.r2.dev/slide.jpg'), false);
|
|
assert.equal(isAllowedR2MediaUrl('https://user:pass@pub-example.r2.dev/slide.jpg'), false);
|
|
assert.equal(isAllowedR2MediaUrl('https://pub-example.r2.dev:8443/slide.jpg'), false);
|
|
assert.equal(isAllowedR2MediaUrl('https://r2.dev.evil.test/slide.jpg'), false);
|
|
});
|
|
|
|
test('rehosts an external R2 photo instead of leaking its URL into the TikTok payload', async () => {
|
|
const sourceUrl = 'https://pub-example.r2.dev/slide.png';
|
|
const result = await prepareTiktokPhotoUrls([sourceUrl], {
|
|
publicBaseUrl,
|
|
verifyPhotoUrl,
|
|
downloadPhoto: async (url) => {
|
|
assert.equal(url, sourceUrl);
|
|
return Buffer.from('png');
|
|
},
|
|
normalizePhoto: async () => Buffer.from('jpeg'),
|
|
uploadImage: async () => ({ url: 'https://greenlenspro.com/storage/plant-images/slide.jpg' }),
|
|
});
|
|
|
|
assert.deepEqual(result, ['https://greenlenspro.com/storage/plant-images/slide.jpg']);
|
|
assert.equal(result.includes(sourceUrl), false);
|
|
});
|
|
|
|
test('prefers supplied base64 over an external legacy URL', async () => {
|
|
let downloaded = false;
|
|
const result = await prepareTiktokPhotoUrls([{
|
|
url: 'https://pub-example.r2.dev/slide.png',
|
|
imageBase64: Buffer.from('local').toString('base64'),
|
|
}], {
|
|
publicBaseUrl,
|
|
verifyPhotoUrl,
|
|
downloadPhoto: async () => { downloaded = true; },
|
|
normalizePhoto: async (buffer) => {
|
|
assert.equal(buffer.toString(), 'local');
|
|
return Buffer.from('jpeg');
|
|
},
|
|
uploadImage: async () => ({ url: 'https://greenlenspro.com/storage/plant-images/slide.jpg' }),
|
|
});
|
|
|
|
assert.deepEqual(result, ['https://greenlenspro.com/storage/plant-images/slide.jpg']);
|
|
assert.equal(downloaded, false);
|
|
});
|
|
|
|
test('rejects a storage result outside the verified GreenLens domain', async () => {
|
|
await assert.rejects(
|
|
prepareTiktokPhotoUrls([{ imageBase64: Buffer.from('source').toString('base64') }], {
|
|
publicBaseUrl,
|
|
verifyPhotoUrl,
|
|
normalizePhoto: async () => Buffer.from('jpeg'),
|
|
uploadImage: async () => ({ url: 'https://pub-example.r2.dev/slide.jpg' }),
|
|
}),
|
|
/unverified public URL/,
|
|
);
|
|
});
|
|
|
|
const okHeaders = (extra = {}) => ({
|
|
get: (name) => ({ 'content-type': 'image/jpeg', 'content-length': '1024', ...extra }[name.toLowerCase()] ?? null),
|
|
});
|
|
|
|
test('verifyPublicPhotoUrl accepts a reachable JPEG under 20 MB', async () => {
|
|
await verifyPublicPhotoUrl('https://greenlenspro.com/storage/a.jpg', {
|
|
fetchImpl: async () => ({ status: 200, headers: okHeaders() }),
|
|
});
|
|
});
|
|
|
|
test('verifyPublicPhotoUrl retries transient 404s before failing', async () => {
|
|
let calls = 0;
|
|
await verifyPublicPhotoUrl('https://greenlenspro.com/storage/a.jpg', {
|
|
fetchImpl: async () => {
|
|
calls += 1;
|
|
return calls < 3
|
|
? { status: 404, headers: okHeaders() }
|
|
: { status: 200, headers: okHeaders() };
|
|
},
|
|
});
|
|
assert.equal(calls, 3);
|
|
});
|
|
|
|
test('verifyPublicPhotoUrl rejects unsupported content types without retrying', async () => {
|
|
let calls = 0;
|
|
await assert.rejects(
|
|
verifyPublicPhotoUrl('https://greenlenspro.com/storage/a.gif', {
|
|
fetchImpl: async () => {
|
|
calls += 1;
|
|
return { status: 200, headers: { get: (n) => (n.toLowerCase() === 'content-type' ? 'image/gif' : null) } };
|
|
},
|
|
}),
|
|
/unsupported content type/,
|
|
);
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test('verifyPublicPhotoUrl rejects photos larger than 20 MB', async () => {
|
|
await assert.rejects(
|
|
verifyPublicPhotoUrl('https://greenlenspro.com/storage/a.jpg', {
|
|
fetchImpl: async () => ({ status: 200, headers: okHeaders({ 'content-length': String(21 * 1024 * 1024) }) }),
|
|
}),
|
|
/larger than 20 MB/,
|
|
);
|
|
});
|