Add automatic nearby discovery and simplify device presence UI
This commit is contained in:
37
apps/server/test/device-info.test.js
Normal file
37
apps/server/test/device-info.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readDeviceInfo } from '../../web/src/deviceInfo.js';
|
||||
|
||||
test('모델 힌트가 없거나 거절된 브라우저도 정상 연결할 수 있다', async () => {
|
||||
assert.deepEqual(await readDeviceInfo({}), { model: '', platform: '', manufacturer: '' });
|
||||
assert.deepEqual(
|
||||
await readDeviceInfo({
|
||||
userAgentData: {
|
||||
getHighEntropyValues: async () => {
|
||||
throw Error('withheld');
|
||||
},
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
test('Android 모델을 구분하고 브라우저 브랜드를 제조사로 사용하지 않는다', async () => {
|
||||
const nav = {
|
||||
userAgentData: {
|
||||
getHighEntropyValues: async (hints) => {
|
||||
assert.deepEqual(hints, ['model']);
|
||||
return { model: 'SM-S938B', platform: 'Android', brands: [{ brand: 'Chrome' }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
assert.deepEqual(await readDeviceInfo(nav), {
|
||||
model: 'SM-S938B',
|
||||
platform: 'Android',
|
||||
manufacturer: 'Samsung',
|
||||
});
|
||||
nav.userAgentData.getHighEntropyValues = async () => ({
|
||||
model: 'Unknown phone',
|
||||
platform: 'Android',
|
||||
});
|
||||
assert.equal((await readDeviceInfo(nav)).manufacturer, '');
|
||||
});
|
||||
184
apps/server/test/nearby.test.js
Normal file
184
apps/server/test/nearby.test.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user