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'; 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(); } });