1import 'package:coves_flutter/services/streamable_service.dart'; 2import 'package:dio/dio.dart'; 3import 'package:flutter_test/flutter_test.dart'; 4import 'package:http_mock_adapter/http_mock_adapter.dart'; 5 6void main() { 7 group('StreamableService', () { 8 late Dio dio; 9 late DioAdapter dioAdapter; 10 late StreamableService service; 11 12 setUp(() { 13 dio = Dio(); 14 dioAdapter = DioAdapter(dio: dio); 15 service = StreamableService(dio: dio); 16 }); 17 18 group('extractShortcode', () { 19 test('extracts shortcode from standard URL', () { 20 expect( 21 StreamableService.extractShortcode('https://streamable.com/abc123'), 22 'abc123', 23 ); 24 }); 25 26 test('extracts shortcode from /e/ URL', () { 27 expect( 28 StreamableService.extractShortcode('https://streamable.com/e/abc123'), 29 'abc123', 30 ); 31 }); 32 33 test('extracts shortcode from URL without scheme', () { 34 expect( 35 StreamableService.extractShortcode('streamable.com/xyz789'), 36 'xyz789', 37 ); 38 }); 39 40 test('extracts shortcode from /e/ URL without scheme', () { 41 expect( 42 StreamableService.extractShortcode('streamable.com/e/xyz789'), 43 'xyz789', 44 ); 45 }); 46 47 test('returns null for empty path', () { 48 expect( 49 StreamableService.extractShortcode('https://streamable.com/'), 50 null, 51 ); 52 }); 53 54 test('returns null for invalid URL', () { 55 expect(StreamableService.extractShortcode('not a url'), null); 56 }); 57 58 test('handles URL with query parameters', () { 59 expect( 60 StreamableService.extractShortcode( 61 'https://streamable.com/abc123?autoplay=1', 62 ), 63 'abc123', 64 ); 65 }); 66 67 test('handles /e/ URL with query parameters', () { 68 expect( 69 StreamableService.extractShortcode( 70 'https://streamable.com/e/abc123?autoplay=1', 71 ), 72 'abc123', 73 ); 74 }); 75 }); 76 77 group('getVideoUrl', () { 78 test('fetches and returns MP4 URL successfully', () async { 79 const shortcode = 'abc123'; 80 const videoUrl = '//cdn.streamable.com/video/mp4/abc123.mp4'; 81 82 dioAdapter.onGet( 83 'https://api.streamable.com/videos/$shortcode', 84 (server) => server.reply(200, { 85 'files': { 86 'mp4': {'url': videoUrl}, 87 }, 88 }), 89 ); 90 91 final result = await service.getVideoUrl( 92 'https://streamable.com/$shortcode', 93 ); 94 95 expect(result, 'https:$videoUrl'); 96 }); 97 98 test('handles /e/ URL format', () async { 99 const shortcode = 'xyz789'; 100 const videoUrl = '//cdn.streamable.com/video/mp4/xyz789.mp4'; 101 102 dioAdapter.onGet( 103 'https://api.streamable.com/videos/$shortcode', 104 (server) => server.reply(200, { 105 'files': { 106 'mp4': {'url': videoUrl}, 107 }, 108 }), 109 ); 110 111 final result = await service.getVideoUrl( 112 'https://streamable.com/e/$shortcode', 113 ); 114 115 expect(result, 'https:$videoUrl'); 116 }); 117 118 test('caches video URLs', () async { 119 const shortcode = 'cached123'; 120 const videoUrl = '//cdn.streamable.com/video/mp4/cached123.mp4'; 121 122 dioAdapter.onGet( 123 'https://api.streamable.com/videos/$shortcode', 124 (server) => server.reply(200, { 125 'files': { 126 'mp4': {'url': videoUrl}, 127 }, 128 }), 129 ); 130 131 // First call - should hit the API 132 final result1 = await service.getVideoUrl( 133 'https://streamable.com/$shortcode', 134 ); 135 expect(result1, 'https:$videoUrl'); 136 137 // Second call - should use cache (no additional network request) 138 final result2 = await service.getVideoUrl( 139 'https://streamable.com/$shortcode', 140 ); 141 expect(result2, 'https:$videoUrl'); 142 }); 143 144 test('returns null for invalid shortcode', () async { 145 const shortcode = 'invalid'; 146 147 dioAdapter.onGet( 148 'https://api.streamable.com/videos/$shortcode', 149 (server) => server.reply(404, {'error': 'Not found'}), 150 ); 151 152 final result = await service.getVideoUrl( 153 'https://streamable.com/$shortcode', 154 ); 155 156 expect(result, null); 157 }); 158 159 test('returns null when files field is missing', () async { 160 const shortcode = 'nofiles123'; 161 162 dioAdapter.onGet( 163 'https://api.streamable.com/videos/$shortcode', 164 (server) => server.reply(200, {'status': 'ok'}), 165 ); 166 167 final result = await service.getVideoUrl( 168 'https://streamable.com/$shortcode', 169 ); 170 171 expect(result, null); 172 }); 173 174 test('returns null when mp4 field is missing', () async { 175 const shortcode = 'nomp4123'; 176 177 dioAdapter.onGet( 178 'https://api.streamable.com/videos/$shortcode', 179 (server) => server.reply(200, { 180 'files': {'webm': {}}, 181 }), 182 ); 183 184 final result = await service.getVideoUrl( 185 'https://streamable.com/$shortcode', 186 ); 187 188 expect(result, null); 189 }); 190 191 test('returns null when URL field is missing', () async { 192 const shortcode = 'nourl123'; 193 194 dioAdapter.onGet( 195 'https://api.streamable.com/videos/$shortcode', 196 (server) => server.reply(200, { 197 'files': { 198 'mp4': {'status': 'processing'}, 199 }, 200 }), 201 ); 202 203 final result = await service.getVideoUrl( 204 'https://streamable.com/$shortcode', 205 ); 206 207 expect(result, null); 208 }); 209 210 test('returns null on network error', () async { 211 const shortcode = 'error500'; 212 213 dioAdapter.onGet( 214 'https://api.streamable.com/videos/$shortcode', 215 (server) => server.throws( 216 500, 217 DioException( 218 requestOptions: RequestOptions( 219 path: 'https://api.streamable.com/videos/$shortcode', 220 ), 221 ), 222 ), 223 ); 224 225 final result = await service.getVideoUrl( 226 'https://streamable.com/$shortcode', 227 ); 228 229 expect(result, null); 230 }); 231 232 test('returns null when shortcode extraction fails', () async { 233 final result = await service.getVideoUrl('invalid-url'); 234 expect(result, null); 235 }); 236 237 test('prepends https to protocol-relative URLs', () async { 238 const shortcode = 'protocol123'; 239 const videoUrl = '//cdn.streamable.com/video/mp4/protocol123.mp4'; 240 241 dioAdapter.onGet( 242 'https://api.streamable.com/videos/$shortcode', 243 (server) => server.reply(200, { 244 'files': { 245 'mp4': {'url': videoUrl}, 246 }, 247 }), 248 ); 249 250 final result = await service.getVideoUrl( 251 'https://streamable.com/$shortcode', 252 ); 253 254 expect(result, startsWith('https://')); 255 expect(result, 'https:$videoUrl'); 256 }); 257 258 test('does not modify URLs that already have protocol', () async { 259 const shortcode = 'hasprotocol123'; 260 const videoUrl = 261 'https://cdn.streamable.com/video/mp4/hasprotocol123.mp4'; 262 263 dioAdapter.onGet( 264 'https://api.streamable.com/videos/$shortcode', 265 (server) => server.reply(200, { 266 'files': { 267 'mp4': {'url': videoUrl}, 268 }, 269 }), 270 ); 271 272 final result = await service.getVideoUrl( 273 'https://streamable.com/$shortcode', 274 ); 275 276 expect(result, videoUrl); 277 }); 278 }); 279 }); 280}