mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-01 04:33:46 +02:00
rename
This commit is contained in:
30
src/services/__tests__/config.test.js
Normal file
30
src/services/__tests__/config.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Mock router to avoid transitive i18n.global error from columns.jsx
|
||||
vi.mock('../../plugins/router.js', () => ({
|
||||
router: { beforeEach: vi.fn(), push: vi.fn() },
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
|
||||
import { transformKey } from '../config.js';
|
||||
|
||||
describe('transformKey', () => {
|
||||
test('lowercases and prefixes with config:', () => {
|
||||
expect(transformKey('Foo')).toBe('config:foo');
|
||||
});
|
||||
|
||||
test('handles already lowercase key', () => {
|
||||
expect(transformKey('bar')).toBe('config:bar');
|
||||
});
|
||||
|
||||
test('handles key with mixed case and numbers', () => {
|
||||
expect(transformKey('MyKey123')).toBe('config:mykey123');
|
||||
});
|
||||
|
||||
test('handles empty string', () => {
|
||||
expect(transformKey('')).toBe('config:');
|
||||
});
|
||||
|
||||
test('converts non-string values via String()', () => {
|
||||
expect(transformKey(42)).toBe('config:42');
|
||||
expect(transformKey(null)).toBe('config:null');
|
||||
});
|
||||
});
|
||||
53
src/services/__tests__/confusables.test.js
Normal file
53
src/services/__tests__/confusables.test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import removeConfusables, { removeWhitespace } from '../confusables.js';
|
||||
|
||||
describe('removeConfusables', () => {
|
||||
test('returns ASCII strings unchanged (fast path)', () => {
|
||||
expect(removeConfusables('Hello World')).toBe('HelloWorld');
|
||||
});
|
||||
|
||||
test('converts circled letters to ASCII', () => {
|
||||
expect(removeConfusables('Ⓗⓔⓛⓛⓞ')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('converts fullwidth letters to ASCII', () => {
|
||||
expect(removeConfusables('Hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('converts Cyrillic confusables', () => {
|
||||
// Cyrillic А, В, С look like Latin A, B, C
|
||||
expect(removeConfusables('АВС')).toBe('ABC');
|
||||
});
|
||||
|
||||
test('handles mixed confusables and normal chars', () => {
|
||||
expect(removeConfusables('Ⓣest')).toBe('Test');
|
||||
});
|
||||
|
||||
test('strips combining marks', () => {
|
||||
// 'e' + combining acute accent → normalized then combining mark stripped → 'e'
|
||||
const input = 'e\u0301';
|
||||
const result = removeConfusables(input);
|
||||
expect(result).toBe('e');
|
||||
});
|
||||
|
||||
test('returns empty string for empty input', () => {
|
||||
expect(removeConfusables('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWhitespace', () => {
|
||||
test('removes regular spaces', () => {
|
||||
expect(removeWhitespace('a b c')).toBe('abc');
|
||||
});
|
||||
|
||||
test('removes tabs and newlines', () => {
|
||||
expect(removeWhitespace('a\tb\nc')).toBe('abc');
|
||||
});
|
||||
|
||||
test('returns string without whitespace unchanged', () => {
|
||||
expect(removeWhitespace('abc')).toBe('abc');
|
||||
});
|
||||
|
||||
test('returns empty string for empty input', () => {
|
||||
expect(removeWhitespace('')).toBe('');
|
||||
});
|
||||
});
|
||||
220
src/services/__tests__/gameLog.test.js
Normal file
220
src/services/__tests__/gameLog.test.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { LogWatcherService } from '../gameLog.js';
|
||||
|
||||
const svc = new LogWatcherService();
|
||||
|
||||
describe('parseRawGameLog', () => {
|
||||
test('parses location type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'location', [
|
||||
'wrld_123:456',
|
||||
'Test World'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'location',
|
||||
location: 'wrld_123:456',
|
||||
worldName: 'Test World'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses location-destination type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'location-destination', [
|
||||
'wrld_abc:789'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'location-destination',
|
||||
location: 'wrld_abc:789'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses player-joined type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'player-joined', [
|
||||
'TestUser',
|
||||
'usr_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'player-joined',
|
||||
displayName: 'TestUser',
|
||||
userId: 'usr_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses player-left type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'player-left', [
|
||||
'TestUser',
|
||||
'usr_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'player-left',
|
||||
displayName: 'TestUser',
|
||||
userId: 'usr_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses notification type', () => {
|
||||
const json = '{"type":"invite"}';
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'notification', [json]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'notification',
|
||||
json
|
||||
});
|
||||
});
|
||||
|
||||
test('parses event type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'event', ['some-event']);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'event',
|
||||
event: 'some-event'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses video-play type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'video-play', [
|
||||
'https://example.com/video.mp4',
|
||||
'Player1'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'video-play',
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
displayName: 'Player1'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses resource-load-string type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'resource-load-string', [
|
||||
'https://example.com/res'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'resource-load-string',
|
||||
resourceUrl: 'https://example.com/res'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses resource-load-image type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'resource-load-image', [
|
||||
'https://example.com/img.png'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'resource-load-image',
|
||||
resourceUrl: 'https://example.com/img.png'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses avatar-change type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'avatar-change', [
|
||||
'User1',
|
||||
'CoolAvatar'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'avatar-change',
|
||||
displayName: 'User1',
|
||||
avatarName: 'CoolAvatar'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses photon-id type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'photon-id', [
|
||||
'User1',
|
||||
'42'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'photon-id',
|
||||
displayName: 'User1',
|
||||
photonId: '42'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses screenshot type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'screenshot', [
|
||||
'/path/to/screenshot.png'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'screenshot',
|
||||
screenshotPath: '/path/to/screenshot.png'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses sticker-spawn type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'sticker-spawn', [
|
||||
'usr_abc',
|
||||
'StickerUser',
|
||||
'inv_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'sticker-spawn',
|
||||
userId: 'usr_abc',
|
||||
displayName: 'StickerUser',
|
||||
inventoryId: 'inv_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses video-sync type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'video-sync', [
|
||||
'123.456'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'video-sync',
|
||||
timestamp: '123.456'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses vrcx type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'vrcx', ['some-data']);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'vrcx',
|
||||
data: 'some-data'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses api-request type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'api-request', [
|
||||
'https://api.vrchat.cloud/api/1/users'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'api-request',
|
||||
url: 'https://api.vrchat.cloud/api/1/users'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses udon-exception type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'udon-exception', [
|
||||
'NullRef'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'udon-exception',
|
||||
data: 'NullRef'
|
||||
});
|
||||
});
|
||||
|
||||
test('handles types with no extra fields', () => {
|
||||
for (const type of [
|
||||
'portal-spawn',
|
||||
'vrc-quit',
|
||||
'openvr-init',
|
||||
'desktop-mode'
|
||||
]) {
|
||||
const log = svc.parseRawGameLog('2024-01-01', type, []);
|
||||
expect(log).toEqual({ dt: '2024-01-01', type });
|
||||
}
|
||||
});
|
||||
|
||||
test('handles unknown type gracefully', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'unknown-type', ['foo']);
|
||||
expect(log).toEqual({ dt: '2024-01-01', type: 'unknown-type' });
|
||||
});
|
||||
});
|
||||
305
src/services/__tests__/request.test.js
Normal file
305
src/services/__tests__/request.test.js
Normal file
@@ -0,0 +1,305 @@
|
||||
// Mock router to avoid transitive i18n.global error from columns.jsx
|
||||
vi.mock('../../plugins/router.js', () => ({
|
||||
router: { beforeEach: vi.fn(), push: vi.fn() },
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
|
||||
import {
|
||||
buildRequestInit,
|
||||
parseResponse,
|
||||
processBulk,
|
||||
shouldIgnoreError
|
||||
} from '../request.js';
|
||||
|
||||
describe('buildRequestInit', () => {
|
||||
test('builds GET request with default method', () => {
|
||||
const init = buildRequestInit('users/usr_123');
|
||||
expect(init.method).toBe('GET');
|
||||
expect(init.url).toContain('users/usr_123');
|
||||
});
|
||||
|
||||
test('serializes GET params into URL search params', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
params: { n: 50, offset: 0 }
|
||||
});
|
||||
expect(init.url).toContain('n=50');
|
||||
expect(init.url).toContain('offset=0');
|
||||
});
|
||||
|
||||
test('does not set body for GET requests', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
params: { n: 50 }
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('sets JSON content-type and body for POST', () => {
|
||||
const init = buildRequestInit('auth/login', {
|
||||
method: 'POST',
|
||||
params: { username: 'test' }
|
||||
});
|
||||
expect(init.headers['Content-Type']).toBe(
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
expect(init.body).toBe(JSON.stringify({ username: 'test' }));
|
||||
});
|
||||
|
||||
test('sets empty body when POST has no params', () => {
|
||||
const init = buildRequestInit('auth/logout', { method: 'POST' });
|
||||
expect(init.body).toBe('{}');
|
||||
});
|
||||
|
||||
test('preserves custom headers in POST', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
method: 'PUT',
|
||||
headers: { 'X-Custom': 'value' },
|
||||
params: { a: 1 }
|
||||
});
|
||||
expect(init.headers['Content-Type']).toBe(
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
expect(init.headers['X-Custom']).toBe('value');
|
||||
});
|
||||
|
||||
test('skips body/headers for upload requests', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'PUT',
|
||||
uploadImage: true,
|
||||
params: { something: 1 }
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
expect(init.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips body/headers for uploadFilePUT', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'PUT',
|
||||
uploadFilePUT: true
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips body/headers for uploadImageLegacy', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'POST',
|
||||
uploadImageLegacy: true
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passes through extra options', () => {
|
||||
const init = buildRequestInit('test', {
|
||||
method: 'DELETE',
|
||||
inviteId: 'inv_123'
|
||||
});
|
||||
expect(init.method).toBe('DELETE');
|
||||
expect(init.inviteId).toBe('inv_123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponse', () => {
|
||||
test('returns response unchanged when no data', () => {
|
||||
const response = { status: 200 };
|
||||
expect(parseResponse(response)).toEqual({ status: 200 });
|
||||
});
|
||||
|
||||
test('parses valid JSON data', () => {
|
||||
const response = {
|
||||
status: 200,
|
||||
data: JSON.stringify({ name: 'test' })
|
||||
};
|
||||
const result = parseResponse(response);
|
||||
expect(result.data).toEqual({ name: 'test' });
|
||||
expect(result.hasApiError).toBeUndefined();
|
||||
expect(result.parseError).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detects API error in response data', () => {
|
||||
const response = {
|
||||
status: 404,
|
||||
data: JSON.stringify({
|
||||
error: { status_code: 404, message: 'Not found' }
|
||||
})
|
||||
};
|
||||
const result = parseResponse(response);
|
||||
expect(result.hasApiError).toBe(true);
|
||||
expect(result.data.error.message).toBe('Not found');
|
||||
});
|
||||
|
||||
test('flags parse error for invalid JSON', () => {
|
||||
const response = { status: 200, data: 'not valid json{{{' };
|
||||
const result = parseResponse(response);
|
||||
expect(result.parseError).toBe(true);
|
||||
expect(result.status).toBe(200);
|
||||
});
|
||||
|
||||
test('handles empty string data', () => {
|
||||
const response = { status: 200, data: '' };
|
||||
const result = parseResponse(response);
|
||||
// empty string is falsy, so treated as no data
|
||||
expect(result).toEqual({ status: 200, data: '' });
|
||||
});
|
||||
|
||||
test('handles null data', () => {
|
||||
const response = { status: 200, data: null };
|
||||
const result = parseResponse(response);
|
||||
expect(result).toEqual({ status: 200, data: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldIgnoreError', () => {
|
||||
test.each([
|
||||
[404, 'users/usr_123'],
|
||||
[404, 'worlds/wrld_123'],
|
||||
[404, 'avatars/avtr_123'],
|
||||
[404, 'groups/grp_123'],
|
||||
[404, 'file/file_123'],
|
||||
[-1, 'users/usr_123']
|
||||
])('ignores %i for single-segment resource %s', (code, endpoint) => {
|
||||
expect(shouldIgnoreError(code, endpoint)).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT ignore nested resource paths', () => {
|
||||
expect(shouldIgnoreError(404, 'users/usr_123/friends')).toBe(false);
|
||||
});
|
||||
|
||||
test('does NOT ignore 403 for resource lookups', () => {
|
||||
expect(shouldIgnoreError(403, 'users/usr_123')).toBe(false);
|
||||
});
|
||||
|
||||
test.each([403, 404, -1])('ignores %i for instances/ endpoints', (code) => {
|
||||
expect(shouldIgnoreError(code, 'instances/wrld_123:456')).toBe(true);
|
||||
});
|
||||
|
||||
test('ignores any code for analysis/ endpoints', () => {
|
||||
expect(shouldIgnoreError(500, 'analysis/something')).toBe(true);
|
||||
expect(shouldIgnoreError(200, 'analysis/data')).toBe(true);
|
||||
});
|
||||
|
||||
test.each([403, -1])('ignores %i for /mutuals endpoints', (code) => {
|
||||
expect(shouldIgnoreError(code, 'users/usr_123/mutuals')).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT ignore 404 for /mutuals', () => {
|
||||
expect(shouldIgnoreError(404, 'users/usr_123/mutuals')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for unmatched patterns', () => {
|
||||
expect(shouldIgnoreError(500, 'auth/login')).toBe(false);
|
||||
expect(shouldIgnoreError(200, 'config')).toBe(false);
|
||||
});
|
||||
|
||||
test('handles undefined endpoint', () => {
|
||||
expect(shouldIgnoreError(404, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processBulk', () => {
|
||||
test('fetches all pages until empty batch', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4, 5] }, { json: [] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const handle = vi.fn();
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, handle, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
expect(handle).toHaveBeenCalledTimes(3);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when N > 0 limit is reached', async () => {
|
||||
const fn = vi.fn(() => Promise.resolve({ json: [1, 2, 3] }));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: 5, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when N = 0 and batch < pageSize', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: 0, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when hasNext is false', async () => {
|
||||
const fn = vi.fn(() =>
|
||||
Promise.resolve({ json: [1, 2, 3], hasNext: false })
|
||||
);
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: -1, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('supports result.results array format', async () => {
|
||||
const pages = [{ results: [1, 2] }, { results: [] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 5 }, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('calls done(false) when fn throws', async () => {
|
||||
const fn = vi.fn(() => Promise.reject(new Error('network error')));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 5 }, done });
|
||||
|
||||
expect(done).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('increments offset correctly', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4, 5] }, { json: [] }];
|
||||
let call = 0;
|
||||
const offsets = [];
|
||||
const fn = vi.fn((params) => {
|
||||
offsets.push(params.offset);
|
||||
const result = pages[call++];
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
await processBulk({ fn, params: { n: 3 } });
|
||||
|
||||
expect(offsets).toEqual([0, 3, 5]);
|
||||
});
|
||||
|
||||
test('returns early if fn is not a function', async () => {
|
||||
const done = vi.fn();
|
||||
await processBulk({ fn: null, done });
|
||||
expect(done).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('uses custom limitParam', async () => {
|
||||
const pages = [{ json: [1, 2] }, { json: [1] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({
|
||||
fn,
|
||||
params: { limit: 2 },
|
||||
limitParam: 'limit',
|
||||
N: 0,
|
||||
done
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
97
src/services/__tests__/security.test.js
Normal file
97
src/services/__tests__/security.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import security, {
|
||||
hexToUint8Array,
|
||||
stdAESKey,
|
||||
uint8ArrayToHex
|
||||
} from '../security.js';
|
||||
|
||||
describe('hexToUint8Array', () => {
|
||||
test('converts hex string to Uint8Array', () => {
|
||||
const result = hexToUint8Array('0a1bff');
|
||||
expect(result).toEqual(new Uint8Array([0x0a, 0x1b, 0xff]));
|
||||
});
|
||||
|
||||
test('converts empty-ish input', () => {
|
||||
const result = hexToUint8Array('00');
|
||||
expect(result).toEqual(new Uint8Array([0]));
|
||||
});
|
||||
|
||||
test('returns null for empty string', () => {
|
||||
expect(hexToUint8Array('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uint8ArrayToHex', () => {
|
||||
test('converts Uint8Array to hex string', () => {
|
||||
const result = uint8ArrayToHex(new Uint8Array([0x0a, 0x1b, 0xff]));
|
||||
expect(result).toBe('0a1bff');
|
||||
});
|
||||
|
||||
test('pads single-digit hex values', () => {
|
||||
const result = uint8ArrayToHex(new Uint8Array([0, 1, 2]));
|
||||
expect(result).toBe('000102');
|
||||
});
|
||||
|
||||
test('converts empty array', () => {
|
||||
expect(uint8ArrayToHex(new Uint8Array([]))).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hex round-trip', () => {
|
||||
test('uint8Array → hex → uint8Array preserves data', () => {
|
||||
const original = new Uint8Array([0, 127, 255, 42, 1]);
|
||||
const hex = uint8ArrayToHex(original);
|
||||
const restored = hexToUint8Array(hex);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stdAESKey', () => {
|
||||
test('pads short key to 32 bytes', () => {
|
||||
const result = stdAESKey('abc');
|
||||
expect(result.length).toBe(32);
|
||||
// First 3 bytes should be 'abc'
|
||||
expect(result[0]).toBe('a'.charCodeAt(0));
|
||||
expect(result[1]).toBe('b'.charCodeAt(0));
|
||||
expect(result[2]).toBe('c'.charCodeAt(0));
|
||||
});
|
||||
|
||||
test('truncates long key to 32 bytes', () => {
|
||||
const longKey = 'a'.repeat(64);
|
||||
const result = stdAESKey(longKey);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
test('32-byte key stays unchanged', () => {
|
||||
const key = 'abcdefghijklmnopqrstuvwxyz012345'; // exactly 32 chars
|
||||
const result = stdAESKey(key);
|
||||
expect(result.length).toBe(32);
|
||||
const expected = new TextEncoder().encode(key);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt / decrypt round-trip', () => {
|
||||
test('encrypts and decrypts plaintext correctly', async () => {
|
||||
const plaintext = 'Hello, VRCX!';
|
||||
const key = 'my-secret-key';
|
||||
|
||||
const ciphertext = await security.encrypt(plaintext, key);
|
||||
expect(typeof ciphertext).toBe('string');
|
||||
expect(ciphertext.length).toBeGreaterThan(0);
|
||||
|
||||
const decrypted = await security.decrypt(ciphertext, key);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
test('different keys produce different ciphertext', async () => {
|
||||
const plaintext = 'secret data';
|
||||
const ct1 = await security.encrypt(plaintext, 'key1');
|
||||
const ct2 = await security.encrypt(plaintext, 'key2');
|
||||
expect(ct1).not.toBe(ct2);
|
||||
});
|
||||
|
||||
test('decrypt returns empty string for empty input', async () => {
|
||||
const result = await security.decrypt('', 'key');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user