208 lines
7.4 KiB
JavaScript
208 lines
7.4 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { eq } from 'drizzle-orm';
|
|
import { fixture } from './helpers.js';
|
|
import { classifyClipboard, clipboardCharacterCount } from '@hop/shared';
|
|
import * as s from '../src/db/schema.js';
|
|
|
|
const raw =
|
|
' 첫 줄 😀\r\n日本語の文章\n\t URL은 https://example.com 안에 있어요. \n<script>window.__xss = true</script>';
|
|
function limits(f) {
|
|
f.cfg.sessionRate = 100;
|
|
f.cfg.ipRate = 200;
|
|
f.cfg.pairRate = 100;
|
|
}
|
|
async function textRequest(f, a, b, value, bytes = Buffer.byteLength(value)) {
|
|
return f.call(
|
|
a,
|
|
'/transfer-requests',
|
|
'POST',
|
|
{ targetDeviceId: b.deviceId, kind: 'text', count: 1, byteSize: bytes },
|
|
randomUUID(),
|
|
);
|
|
}
|
|
async function signed(f, client, email) {
|
|
const id = randomUUID();
|
|
f.service.db
|
|
.insert(s.users)
|
|
.values({
|
|
id,
|
|
email,
|
|
displayName: email,
|
|
passwordHash: await f.service.auth.password('12345678'),
|
|
status: 'active',
|
|
verifiedAt: Date.now(),
|
|
createdAt: Date.now(),
|
|
})
|
|
.run();
|
|
const login = await f.call(client, '/auth/login', 'POST', { email, password: '12345678' });
|
|
assert.equal(login.statusCode, 200);
|
|
client.cookie += '; ' + String(login.headers['set-cookie']).split(';')[0];
|
|
return id;
|
|
}
|
|
|
|
test('클립보드는 전체 단일 URL만 링크로 판별하고 나머지 원문과 이모지를 보존한다', () => {
|
|
for (const [input, url] of [
|
|
['https://example.com/article', 'https://example.com/article'],
|
|
['http://127.0.0.1/path', 'http://127.0.0.1/path'],
|
|
['www.example.com', 'https://www.example.com/'],
|
|
['example.com/path', 'https://example.com/path'],
|
|
])
|
|
assert.deepEqual(classifyClipboard(input), { kind: 'url', url });
|
|
for (const value of [
|
|
raw,
|
|
'',
|
|
' ',
|
|
'여기 https://example.com 보세요',
|
|
'https://one.test/\nhttps://two.test/',
|
|
'javascript:alert(1)',
|
|
'https://user:pass@example.com',
|
|
'https://example.com/\n',
|
|
])
|
|
assert.deepEqual(classifyClipboard(value), { kind: 'text', text: value });
|
|
assert.equal(clipboardCharacterCount('한글日本語😀'), 6);
|
|
});
|
|
|
|
test('텍스트는 수락 전 비공개이며 수락 후 원문·UTF-8·접근권·읽음·멱등성을 유지한다', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
limits(f);
|
|
const a = await f.create(),
|
|
b = await f.create(),
|
|
c = await f.create();
|
|
await f.space(a, b);
|
|
const response = await textRequest(f, a, b, raw);
|
|
assert.equal(response.statusCode, 200, response.body);
|
|
const id = response.json().requestId;
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${id}/complete`, 'POST', { text: raw }, randomUUID()))
|
|
.statusCode,
|
|
409,
|
|
);
|
|
assert.equal((await f.call(b, '/device/items')).json().items.length, 0);
|
|
assert.equal((await f.call(b, '/device/snapshot')).json().requests[0].text, undefined);
|
|
assert.notEqual(
|
|
(await f.call(c, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode,
|
|
200,
|
|
);
|
|
await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID());
|
|
assert.notEqual(
|
|
(await f.call(c, `/transfers/${id}/complete`, 'POST', { text: raw }, randomUUID()))
|
|
.statusCode,
|
|
200,
|
|
);
|
|
const key = randomUUID();
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${id}/complete`, 'POST', { text: raw }, key)).statusCode,
|
|
200,
|
|
);
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${id}/complete`, 'POST', { text: raw }, key)).statusCode,
|
|
200,
|
|
);
|
|
const item = (await f.call(b, '/device/items')).json().items[0];
|
|
assert.equal(item.kind, 'text');
|
|
assert.equal(item.text, raw);
|
|
assert.equal(item.url, null);
|
|
assert.equal((await f.call(c, '/device/items')).json().items.length, 0);
|
|
assert.equal(f.service.db.select().from(s.items).all().length, 1);
|
|
assert.equal(f.service.db.select().from(s.files).all().length, 0);
|
|
await f.call(b, `/access/${item.accessId}/read`, 'PATCH', { read: true });
|
|
assert.equal((await f.call(a, '/device/items')).json().items[0].recipientRead, true);
|
|
assert(item.expiresAt - item.createdAt <= 7 * 86400000);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('텍스트의 UTF-8 256KiB 한도와 본문 타입·빈 내용·승인 크기 변경을 검사한다', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
limits(f);
|
|
const a = await f.create(),
|
|
b = await f.create();
|
|
await f.space(a, b);
|
|
assert.equal(f.cfg.maxTextBytes, 256 * 1024);
|
|
assert.equal((await textRequest(f, a, b, '가'.repeat(87382))).statusCode, 413);
|
|
const boundary = 'x'.repeat(256 * 1024);
|
|
const id = (await textRequest(f, a, b, boundary)).json().requestId;
|
|
await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID());
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${id}/complete`, 'POST', { text: boundary }, randomUUID()))
|
|
.statusCode,
|
|
200,
|
|
);
|
|
const small = (await textRequest(f, a, b, 'x')).json().requestId;
|
|
await f.call(b, `/transfer-requests/${small}/accept`, 'POST', {}, randomUUID());
|
|
for (const [body, status] of [
|
|
[{ text: boundary + 'x' }, 413],
|
|
[{ text: ' ' }, 400],
|
|
[{ text: '変更 😀' }, 409],
|
|
[{ urls: ['https://example.com/'] }, 400],
|
|
[{ text: 'x', urls: [] }, 400],
|
|
])
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${small}/complete`, 'POST', body, randomUUID())).statusCode,
|
|
status,
|
|
);
|
|
assert.equal(
|
|
(await f.call(a, `/transfers/${small}/complete`, 'POST', { text: 'x' }, randomUUID()))
|
|
.statusCode,
|
|
200,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('로그인 중 받은 텍스트는 7일 기록하고 임시 저장·검색·보관함 재전송도 원문을 유지한다', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
limits(f);
|
|
const a = await f.create(),
|
|
b = await f.create();
|
|
await f.space(a, b);
|
|
await signed(f, b, 'text@hop.test');
|
|
const id = (await textRequest(f, a, b, raw)).json().requestId;
|
|
await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID());
|
|
await f.call(a, `/transfers/${id}/complete`, 'POST', { text: raw }, randomUUID());
|
|
const receipt = (await f.call(b, '/inbox')).json().items[0];
|
|
assert.equal(receipt.text, raw);
|
|
const saved = await f.call(
|
|
b,
|
|
'/vault',
|
|
'POST',
|
|
{ kind: 'text', text: raw, note: '保存 메모' },
|
|
randomUUID(),
|
|
);
|
|
assert.equal(saved.statusCode, 200, saved.body);
|
|
const vault = (await f.call(b, '/vault?search=' + encodeURIComponent('日本語'))).json().items;
|
|
assert.equal(vault.length, 1);
|
|
assert.equal(vault[0].text, raw);
|
|
const forwarded = await f.call(
|
|
b,
|
|
`/vault/${vault[0].accessId}/send`,
|
|
'POST',
|
|
{ targetDeviceId: a.deviceId },
|
|
randomUUID(),
|
|
);
|
|
assert.equal(forwarded.statusCode, 200, forwarded.body);
|
|
const sentId = forwarded.json().requestId;
|
|
await f.call(a, `/transfer-requests/${sentId}/accept`, 'POST', {}, randomUUID());
|
|
assert(
|
|
(await f.call(a, '/device/items'))
|
|
.json()
|
|
.items.some((item) => item.role === 'recipient' && item.text === raw),
|
|
);
|
|
f.service.db
|
|
.update(s.items)
|
|
.set({ expiresAt: Date.now() - 1 })
|
|
.where(eq(s.items.id, vault[0].id))
|
|
.run();
|
|
assert.equal((await f.call(b, '/vault')).json().items.length, 0);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|