Main coves client
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}