98 lines
4.7 KiB
JavaScript
98 lines
4.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
import ts from 'typescript';
|
|
import * as vue from 'vue';
|
|
import { compileScript, parse } from 'vue/compiler-sfc';
|
|
|
|
function loadTS(source, imports = {}, globals = {}) {
|
|
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
|
|
const exports = {};
|
|
new Function('exports', 'require', ...Object.keys(globals), js)(exports, name => {
|
|
assert.ok(imports[name], `unexpected module ${name}`);
|
|
return imports[name];
|
|
}, ...Object.values(globals));
|
|
return exports;
|
|
}
|
|
const helper = loadTS(readFileSync(new URL('../src/utils/avatar-image.ts', import.meta.url), 'utf8'));
|
|
|
|
test('versioned uploads use thumbnails, while external, local and signed URLs remain intact', () => {
|
|
for (const prefix of ['https://cdn.example.com/media/2026/08/', '/uploads/']) {
|
|
const src = `${prefix}42-1788189400000-av1.jpg`;
|
|
assert.equal(helper.avatarImageSource(src), `${prefix}42-1788189400000-av1-thumb.jpg`);
|
|
assert.equal(helper.avatarImageSource(src, 'display'), src);
|
|
}
|
|
for (const src of ['', undefined, 'https://cdn.example.com/42-1.png', 'https://oauth.example.com/photo.jpg',
|
|
'https://cdn.example.com/42-1-av1.jpg?signature=secret', 'https://cdn.example.com/42-1-av1.jpg#hash',
|
|
'https://cdn.example.com/42-1-av1-thumb.jpg', 'https://cdn.example.com/42-1-av1-original.png',
|
|
'/tmp/42-1-av1.jpg', 'file:///tmp/42-1-av1.jpg', 'blob:https://app.example.com/42-1-av1.jpg']) {
|
|
assert.equal(helper.avatarImageSource(src), src || '');
|
|
}
|
|
});
|
|
|
|
test('avatar component falls back once, then resets when reused for another user', async () => {
|
|
const source = readFileSync(new URL('../src/components/AvatarImage.vue', import.meta.url), 'utf8');
|
|
const { descriptor } = parse(source);
|
|
const script = compileScript(descriptor, { id: 'avatar-regression' });
|
|
const component = loadTS(script.content, { vue, '../utils/avatar-image': helper }).default;
|
|
const props = vue.reactive({ src: 'https://cdn.example.com/42-1-av1.jpg', variant: 'thumbnail', lazy: true });
|
|
const scope = vue.effectScope();
|
|
const state = scope.run(() => component.setup(props, { expose() {} }));
|
|
try {
|
|
assert.match(state.imageSource.value, /-thumb\.jpg$/);
|
|
state.onError();
|
|
assert.equal(state.imageSource.value, props.src);
|
|
state.onError();
|
|
assert.equal(state.imageSource.value, props.src);
|
|
props.src = 'https://cdn.example.com/43-2-av1.jpg';
|
|
await vue.nextTick();
|
|
assert.equal(state.imageSource.value, 'https://cdn.example.com/43-2-av1-thumb.jpg');
|
|
props.variant = 'display';
|
|
await vue.nextTick();
|
|
assert.equal(state.imageSource.value, props.src);
|
|
} finally { scope.stop(); }
|
|
});
|
|
|
|
function uploadClient(retry = false) {
|
|
const uploads = [];
|
|
const storage = { accessToken: 'old-token', refreshToken: 'refresh-token' };
|
|
const uni = {
|
|
getStorageSync: key => storage[key],
|
|
setStorageSync: (key, value) => { storage[key] = value; },
|
|
uploadFile(options) {
|
|
uploads.push(options);
|
|
queueMicrotask(() => options.success(retry && uploads.length === 1
|
|
? { statusCode: 401, data: JSON.stringify({ code: 401, message: 'expired' }) }
|
|
: { statusCode: 200, data: JSON.stringify({ code: 0, data: { url: 'https://cdn.example.com/42-1-av1.jpg' } }) }));
|
|
},
|
|
request(options) {
|
|
assert.match(options.url, /\/auth\/token\/refresh$/);
|
|
queueMicrotask(() => options.success({ statusCode: 200, data: { code: 0, data: { accessToken: 'new-token' } } }));
|
|
},
|
|
};
|
|
const source = readFileSync(new URL('../src/utils/request.ts', import.meta.url), 'utf8')
|
|
.replaceAll('import.meta.env.VITE_API_ORIGIN', '"https://api.example.com"');
|
|
const client = loadTS(source, { './device': { getDeviceHeaders: () => ({ 'X-Device-Id': 'test-device' }) },
|
|
'./network-errors': { isLoopbackServer: () => false } }, { uni });
|
|
return { client, uploads };
|
|
}
|
|
|
|
test('avatar upload purpose and refreshed authorization survive a token retry', async () => {
|
|
const { client, uploads } = uploadClient(true);
|
|
assert.equal(await client.api.uploadAvatar('/tmp/camera.jpg'), 'https://cdn.example.com/42-1-av1.jpg');
|
|
assert.equal(uploads.length, 2);
|
|
for (const upload of uploads) {
|
|
assert.deepEqual(upload.formData, { purpose: 'avatar' });
|
|
assert.equal(upload.filePath, '/tmp/camera.jpg');
|
|
assert.equal(upload.header['X-Device-Id'], 'test-device');
|
|
}
|
|
assert.equal(uploads[1].header.Authorization, 'Bearer new-token');
|
|
});
|
|
|
|
test('ordinary media uploads do not request avatar processing', async () => {
|
|
const { client, uploads } = uploadClient();
|
|
await client.api.uploadMedia('/tmp/evidence.png');
|
|
assert.equal(uploads.length, 1);
|
|
assert.equal(uploads[0].formData, undefined);
|
|
});
|