Files
buzz/mobile/test/shared/deeplink/deep_link_test.dart

205 lines
5.5 KiB
Dart

import 'package:buzz/shared/deeplink/deep_link.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
_inviteTests();
group('parseMessageDeepLink', () {
test('parses channel and id', () {
final link = parseMessageDeepLink(
Uri.parse('buzz://message?channel=d14cd131&id=abc123'),
);
expect(
link,
const MessageDeepLink(channelId: 'd14cd131', messageId: 'abc123'),
);
});
test('parses optional thread param', () {
final link = parseMessageDeepLink(
Uri.parse('buzz://message?channel=d14cd131&id=abc123&thread=root99'),
);
expect(link?.threadRootId, 'root99');
});
test('treats empty thread as absent', () {
final link = parseMessageDeepLink(
Uri.parse('buzz://message?channel=d14cd131&id=abc123&thread='),
);
expect(link, isNotNull);
expect(link?.threadRootId, isNull);
});
test('rejects missing channel', () {
expect(parseMessageDeepLink(Uri.parse('buzz://message?id=abc')), isNull);
});
test('rejects empty channel', () {
expect(
parseMessageDeepLink(Uri.parse('buzz://message?channel=&id=abc')),
isNull,
);
});
test('rejects missing id', () {
expect(
parseMessageDeepLink(Uri.parse('buzz://message?channel=d14cd131')),
isNull,
);
});
test('rejects non-buzz scheme', () {
expect(
parseMessageDeepLink(Uri.parse('https://message?channel=a&id=b')),
isNull,
);
});
test('rejects non-message host (connect is desktop-only)', () {
expect(
parseMessageDeepLink(Uri.parse('buzz://connect?relay=wss://x')),
isNull,
);
});
});
}
void _inviteTests() {
group('parseInviteDeepLink', () {
test('parses canonical HTTPS invite URL', () {
final link = parseInviteDeepLink(
Uri.parse('https://relay.example.com/invite/abc123'),
);
expect(
link,
const InviteDeepLink(
relayUrl: 'wss://relay.example.com',
code: 'abc123',
),
);
});
test('parses HTTP invite URL for local/dev relays', () {
final link = parseInviteDeepLink(
Uri.parse('http://localhost:3000/invite/dev-code'),
);
expect(
link,
const InviteDeepLink(relayUrl: 'ws://localhost:3000', code: 'dev-code'),
);
});
test('parses buzz join handoff link', () {
final link = parseInviteDeepLink(
Uri.parse(
'buzz://join?relay=wss%3A%2F%2Frelay.example.com&code=abc123',
),
);
expect(
link,
const InviteDeepLink(
relayUrl: 'wss://relay.example.com',
code: 'abc123',
),
);
});
test('preserves policy receipt in buzz join handoff', () {
final link = parseInviteDeepLink(
Uri.parse(
'buzz://join?relay=wss%3A%2F%2Frelay.example.com&code=abc123&policy_receipt=receipt.value',
),
);
expect(
link,
const InviteDeepLink(
relayUrl: 'wss://relay.example.com',
code: 'abc123',
policyReceipt: 'receipt.value',
),
);
});
test('rejects non-invite HTTPS paths', () {
expect(
parseInviteDeepLink(Uri.parse('https://relay.example.com/api/invites')),
isNull,
);
expect(
parseInviteDeepLink(Uri.parse('https://relay.example.com/invite/')),
isNull,
);
expect(
parseInviteDeepLink(Uri.parse('https://relay.example.com/invite/a/b')),
isNull,
);
});
test('rejects credentials and fragments', () {
expect(
parseInviteDeepLink(
Uri.parse('https://user:pass@relay.example.com/invite/abc'),
),
isNull,
);
expect(
parseInviteDeepLink(
Uri.parse('https://relay.example.com/invite/abc#x'),
),
isNull,
);
expect(
parseInviteDeepLink(
Uri.parse(
'buzz://join?relay=wss%3A%2F%2Fuser%3Apass%40relay.example.com&code=abc',
),
),
isNull,
);
});
test('rejects buzz join without websocket relay or code', () {
expect(
parseInviteDeepLink(
Uri.parse('buzz://join?relay=https://relay.example.com&code=abc'),
),
isNull,
);
expect(
parseInviteDeepLink(
Uri.parse('buzz://join?relay=wss://relay.example.com'),
),
isNull,
);
expect(
parseInviteDeepLink(Uri.parse('buzz://connect?relay=wss://x')),
isNull,
);
});
test('rejects buzz join with dangerous relay schemes', () {
// The `relay=` param is an allowlist — only `ws` / `wss` are safe to
// hand to a Nostr relay session. Anything else must be dropped by the
// parser so a hostile QR / share link can't smuggle a browser scheme
// (`javascript:`, `data:`), a local resource (`file:`), or an
// unrelated transport (`ftp:`, `chrome:`) into the join flow.
for (final hostile in [
'javascript:alert(1)',
'data:text/html,evil',
'file:///etc/passwd',
'ftp://relay.example.com',
'chrome://settings',
'about:blank',
'ssh://relay.example.com',
]) {
final encoded = Uri.encodeQueryComponent(hostile);
expect(
parseInviteDeepLink(Uri.parse('buzz://join?relay=$encoded&code=abc')),
isNull,
reason: 'must reject relay scheme in $hostile',
);
}
});
});
}