9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
296 lines
8.0 KiB
Dart
296 lines
8.0 KiB
Dart
import 'package:buzz/shared/deeplink/deep_link.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
_inviteTests();
|
|
_buildMessageLinkTests();
|
|
|
|
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('normalizes trailing slash in buzz join handoff', () {
|
|
final link = parseInviteDeepLink(
|
|
Uri.parse(
|
|
'buzz://join?relay=wss%3A%2F%2Frelay.example.com%2F&code=abc123',
|
|
),
|
|
);
|
|
expect(link?.relayUrl, 'wss://relay.example.com');
|
|
});
|
|
|
|
test('rejects plaintext public buzz join handoff', () {
|
|
final relay = Uri.encodeQueryComponent('ws://relay.example.com');
|
|
expect(
|
|
parseInviteDeepLink(Uri.parse('buzz://join?relay=$relay&code=abc')),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
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 non-public invite relay destinations', () {
|
|
for (final url in [
|
|
'https://127.0.0.1/invite/abc',
|
|
'https://169.254.169.254/invite/abc',
|
|
'https://192.168.1.1/invite/abc',
|
|
'https://[::1]/invite/abc',
|
|
'https://[::ffff:127.0.0.1]/invite/abc',
|
|
]) {
|
|
expect(parseInviteDeepLink(Uri.parse(url)), isNull, reason: url);
|
|
}
|
|
});
|
|
|
|
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',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
void _buildMessageLinkTests() {
|
|
group('buildMessageLink', () {
|
|
test('builds channel + id link', () {
|
|
expect(
|
|
buildMessageLink(channelId: 'd14cd131', messageId: 'abc123'),
|
|
'buzz://message?channel=d14cd131&id=abc123',
|
|
);
|
|
});
|
|
|
|
test('includes thread root when present', () {
|
|
expect(
|
|
buildMessageLink(
|
|
channelId: 'd14cd131',
|
|
messageId: 'abc123',
|
|
threadRootId: 'root99',
|
|
),
|
|
'buzz://message?channel=d14cd131&id=abc123&thread=root99',
|
|
);
|
|
});
|
|
|
|
test('treats empty thread root as absent', () {
|
|
expect(
|
|
buildMessageLink(
|
|
channelId: 'd14cd131',
|
|
messageId: 'abc123',
|
|
threadRootId: '',
|
|
),
|
|
'buzz://message?channel=d14cd131&id=abc123',
|
|
);
|
|
});
|
|
|
|
test('round-trips through parseMessageDeepLink', () {
|
|
final url = buildMessageLink(
|
|
channelId: 'chan-1',
|
|
messageId: 'msg-1',
|
|
threadRootId: 'root-1',
|
|
);
|
|
final parsed = parseMessageDeepLink(Uri.parse(url));
|
|
expect(
|
|
parsed,
|
|
const MessageDeepLink(
|
|
channelId: 'chan-1',
|
|
messageId: 'msg-1',
|
|
threadRootId: 'root-1',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('throws on empty channel or id', () {
|
|
expect(
|
|
() => buildMessageLink(channelId: '', messageId: 'abc'),
|
|
throwsArgumentError,
|
|
);
|
|
expect(
|
|
() => buildMessageLink(channelId: 'chan', messageId: ''),
|
|
throwsArgumentError,
|
|
);
|
|
});
|
|
});
|
|
}
|