import { test } from 'node:test'; import assert from 'node:assert/strict'; import { io } from 'socket.io-client'; import { randomUUID } from 'node:crypto'; import { fixture } from './helpers.js'; import { metersBetween, NearbyLocations } from '../src/nearby.js'; const point = { latitude: 37.5665, longitude: 126.978, accuracy: 20 }; test('거리 계산과 동의한 양쪽 좌표, 반경·만료·여러 탭·변경 알림을 검사한다', () => { const changes = [], locations = new NearbyLocations(100, (ids) => changes.push(ids)); try { assert.equal(metersBetween(point, point), 0); assert( metersBetween({ latitude: 0, longitude: 179.9999 }, { latitude: 0, longitude: -179.9999 }) < 30, ); locations.set('a', 'a1', point); assert.equal(changes.length, 0, '혼자서는 불필요한 목록 갱신을 하지 않음'); locations.set('b', 'b1', { ...point, latitude: point.latitude + 0.0004 }); assert.equal(locations.nearby('a', 'b'), true); const before = changes.length; locations.set('b', 'b1', { ...point, latitude: point.latitude + 0.0004 }); assert.equal(changes.length, before, '같은 위치를 갱신해도 목록 재전송 없음'); locations.set('a', 'a2', point); locations.clear('a1'); assert.equal(locations.nearby('a', 'b'), true); locations.clear('a2'); assert.equal(locations.nearby('a', 'b'), false); locations.set('a', 'a1', point); locations.points.get('a1').until = Date.now() - 1; assert.equal(locations.nearby('a', 'b'), false); locations.clear('a1'); assert(changes.at(-1).includes('b')); locations.set('a', 'a1', { ...point, latitude: point.latitude + 0.01 }); assert.equal(locations.nearby('a', 'b'), false); } finally { locations.close(); } }); async function positioned(f, ip) { const client = await f.create(ip, false); client.socket = io(f.url, { transports: ['websocket'], reconnection: false, auth: { csrfToken: client.csrf }, extraHeaders: { Origin: f.cfg.appUrl, Cookie: client.cookie, 'X-Forwarded-For': ip }, }); await new Promise((resolve, reject) => { client.socket.once('connect', resolve); client.socket.once('connect_error', reject); }); return client; } const publish = (client, position) => client.socket.timeout(3000).emitWithAck('location:update', position); test('다른 네트워크도 양쪽 위치 허용 시 발견·전송하고 좌표는 목록과 DB에 공개하지 않는다', async () => { const f = await fixture(false), clients = []; try { const a = await positioned(f, '8.8.8.8'), b = await positioned(f, '1.1.1.1'), c = await positioned(f, '9.9.9.9'); clients.push(a, b, c); const list = async (client) => (await f.call(client, '/discovery/devices')).json().devices; assert.equal((await list(a)).length, 0); assert.equal((await publish(a, point)).ok, true); assert.equal((await list(a)).length, 0); assert.equal((await publish(b, { ...point, latitude: point.latitude + 0.0004 })).ok, true); assert.equal((await publish(c, { ...point, latitude: point.latitude + 0.01 })).ok, true); assert.deepEqual( (await list(a)).map((device) => device.id), [b.deviceId], ); assert.deepEqual((await list(a))[0].sources, ['nearby']); const snapshot = await f.call(a, '/device/snapshot'); assert(!snapshot.body.includes('latitude') && !snapshot.body.includes('longitude')); assert( !f.sqlite .prepare("SELECT sql FROM sqlite_master WHERE type='table'") .all() .some((row) => /latitude|longitude/i.test(row.sql)), ); const request = await f.request(a, b, 'https://example.com/nearby'); assert.equal(request.statusCode, 200, request.body); assert.equal( ( await f.call( b, `/transfer-requests/${request.json().requestId}/accept`, 'POST', {}, randomUUID(), ) ).statusCode, 200, ); assert.equal( ( await f.call( a, `/transfers/${request.json().requestId}/complete`, 'POST', { urls: ['https://example.com/nearby'] }, randomUUID(), ) ).statusCode, 200, ); b.socket.emit('location:clear'); await new Promise((resolve) => setTimeout(resolve, 30)); assert.equal((await list(a)).length, 0); assert.equal((await f.request(a, b, 'https://example.com/outside')).statusCode, 403); } finally { clients.forEach((client) => client.socket.disconnect()); await f.close(); } }); test('기기 숨김·방 참여·세션 종료·부정확한 위치와 잘못된 위치를 검사한다', async () => { const f = await fixture(false), clients = []; try { const a = await positioned(f, '8.8.8.8'), b = await positioned(f, '1.1.1.1'); clients.push(a, b); assert.equal((await publish(a, { ...point, latitude: 91 })).ok, false); assert.equal((await publish(a, { ...point, accuracy: 10000 })).code, 'LOCATION_INACCURATE'); await publish(a, point); await publish(b, point); await f.call(b, '/device/me', 'PATCH', { discoverable: false }); assert.equal(f.service.nearby.guests.has(f.service.guest(b.cookie.split('=')[1]).id), false); assert.equal((await publish(b, point)).code, 'LOCATION_UNAVAILABLE'); await f.call(b, '/device/me', 'PATCH', { discoverable: true }); await f.space(a, b); assert.equal((await publish(a, point)).code, 'LOCATION_UNAVAILABLE'); const d = await positioned(f, '9.9.9.9'); clients.push(d); await publish(d, point); assert.equal( (await f.call(d, '/discovery/devices')).json().devices.length, 0, '방 안 기기는 외부 위치 목록에 나오지 않음', ); const guestId = f.service.guest(d.cookie.split('=')[1]).id; d.socket.disconnect(); await new Promise((resolve) => setTimeout(resolve, 30)); assert.equal(f.service.nearby.guests.has(guestId), false); } finally { clients.forEach((client) => client.socket.disconnect()); await f.close(); } }); test('수락 클릭으로 새 탭이 열려 위치가 제거돼도 해당 요청만 완료할 수 있다', async () => { const f = await fixture(false), clients = []; try { const a = await positioned(f, '8.8.8.8'), b = await positioned(f, '1.1.1.1'); clients.push(a, b); await publish(a, point); await publish(b, point); const response = await f.request(a, b, 'https://example.com/accepted'), id = response.json().requestId; b.socket.emit('location:accept', { requestId: id }); b.socket.emit('location:clear'); await new Promise((resolve) => setTimeout(resolve, 30)); assert.equal( f.service.nearby.nearby( f.service.guest(a.cookie.split('=')[1]).id, f.service.guest(b.cookie.split('=')[1]).id, ), false, ); assert.equal( (await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID())).statusCode, 200, ); assert.equal(f.service.locationAcceptances.has(id), false); } finally { clients.forEach((client) => client.socket.disconnect()); await f.close(); } });