Main coves client
1import 'package:coves_flutter/utils/url_launcher.dart';
2import 'package:flutter/material.dart';
3import 'package:flutter_test/flutter_test.dart';
4import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
5
6import '../test_helpers/mock_url_launcher_platform.dart';
7
8void main() {
9 TestWidgetsFlutterBinding.ensureInitialized();
10
11 late MockUrlLauncherPlatform mockPlatform;
12
13 setUp(() {
14 mockPlatform = MockUrlLauncherPlatform();
15 UrlLauncherPlatform.instance = mockPlatform;
16 });
17
18 group('UrlLauncher', () {
19 group('Security Validation', () {
20 test('blocks javascript: scheme', () async {
21 final result = await UrlLauncher.launchExternalUrl(
22 'javascript:alert("xss")',
23 );
24 expect(result, false);
25 expect(mockPlatform.launchedUrls, isEmpty);
26 });
27
28 test('blocks file: scheme', () async {
29 final result = await UrlLauncher.launchExternalUrl(
30 'file:///etc/passwd',
31 );
32 expect(result, false);
33 expect(mockPlatform.launchedUrls, isEmpty);
34 });
35
36 test('blocks data: scheme', () async {
37 final result = await UrlLauncher.launchExternalUrl(
38 'data:text/html,<h1>XSS</h1>',
39 );
40 expect(result, false);
41 expect(mockPlatform.launchedUrls, isEmpty);
42 });
43
44 test('allows http scheme', () async {
45 final result = await UrlLauncher.launchExternalUrl(
46 'http://example.com',
47 );
48 expect(result, true);
49 expect(mockPlatform.launchedUrls, contains('http://example.com'));
50 });
51
52 test('allows https scheme', () async {
53 final result = await UrlLauncher.launchExternalUrl(
54 'https://example.com',
55 );
56 expect(result, true);
57 expect(mockPlatform.launchedUrls, contains('https://example.com'));
58 });
59
60 test('scheme check is case insensitive', () async {
61 final result = await UrlLauncher.launchExternalUrl(
62 'HTTPS://example.com',
63 );
64 expect(result, true);
65 // URL gets normalized to lowercase by url_launcher
66 expect(mockPlatform.launchedUrls, contains('https://example.com'));
67 });
68 });
69
70 group('Invalid URL Handling', () {
71 test('returns false for malformed URLs', () async {
72 final result = await UrlLauncher.launchExternalUrl('not a url');
73 expect(result, false);
74 });
75
76 test('returns false for empty string', () async {
77 final result = await UrlLauncher.launchExternalUrl('');
78 expect(result, false);
79 });
80
81 test('handles URLs with special characters', () async {
82 final result = await UrlLauncher.launchExternalUrl(
83 'https://example.com/path?query=value&other=123',
84 );
85 expect(result, true);
86 });
87 });
88
89 group('Error Snackbar Display', () {
90 testWidgets('shows snackbar when context provided and URL blocked', (
91 tester,
92 ) async {
93 await tester.pumpWidget(
94 MaterialApp(
95 home: Scaffold(
96 body: Builder(
97 builder: (context) {
98 return ElevatedButton(
99 onPressed: () async {
100 await UrlLauncher.launchExternalUrl(
101 'javascript:alert("xss")',
102 context: context,
103 );
104 },
105 child: const Text('Test'),
106 );
107 },
108 ),
109 ),
110 ),
111 );
112
113 // Tap button to trigger URL launch
114 await tester.tap(find.byType(ElevatedButton));
115 await tester.pump();
116
117 // Wait for snackbar animation
118 await tester.pumpAndSettle();
119
120 // Verify snackbar is displayed
121 expect(find.text('Invalid link format'), findsOneWidget);
122 });
123
124 testWidgets('shows snackbar when context provided and URL fails', (
125 tester,
126 ) async {
127 // Configure platform to fail
128 mockPlatform.canLaunchResponse = false;
129
130 await tester.pumpWidget(
131 MaterialApp(
132 home: Scaffold(
133 body: Builder(
134 builder: (context) {
135 return ElevatedButton(
136 onPressed: () async {
137 await UrlLauncher.launchExternalUrl(
138 'https://example.com',
139 context: context,
140 );
141 },
142 child: const Text('Test'),
143 );
144 },
145 ),
146 ),
147 ),
148 );
149
150 // Tap button to trigger URL launch
151 await tester.tap(find.byType(ElevatedButton));
152 await tester.pump();
153
154 // Wait for snackbar animation
155 await tester.pumpAndSettle();
156
157 // Verify snackbar is displayed
158 expect(find.text('Could not open link'), findsOneWidget);
159 });
160
161 test('does not crash when context is null', () async {
162 // Should not throw exception
163 expect(
164 () async => UrlLauncher.launchExternalUrl('javascript:alert("xss")'),
165 returnsNormally,
166 );
167 });
168 });
169
170 group('Successful Launches', () {
171 test('successfully launches valid https URL', () async {
172 final result = await UrlLauncher.launchExternalUrl(
173 'https://www.example.com/path',
174 );
175 expect(result, true);
176 expect(
177 mockPlatform.launchedUrls,
178 contains('https://www.example.com/path'),
179 );
180 });
181
182 test('uses external application mode', () async {
183 await UrlLauncher.launchExternalUrl('https://example.com');
184 expect(
185 mockPlatform.lastLaunchMode,
186 PreferredLaunchMode.externalApplication,
187 );
188 });
189 });
190 });
191}