Main coves client
1import 'package:coves_flutter/models/coves_session.dart';
2import 'package:coves_flutter/services/api_exceptions.dart';
3import 'package:coves_flutter/services/comment_service.dart';
4import 'package:dio/dio.dart';
5import 'package:flutter_test/flutter_test.dart';
6import 'package:mockito/annotations.dart';
7import 'package:mockito/mockito.dart';
8
9import 'comment_service_test.mocks.dart';
10
11@GenerateMocks([Dio])
12void main() {
13 group('CommentService', () {
14 group('CreateCommentResponse', () {
15 test('should create response with uri and cid', () {
16 const response = CreateCommentResponse(
17 uri: 'at://did:plc:test/social.coves.community.comment/123',
18 cid: 'bafy123',
19 );
20
21 expect(
22 response.uri,
23 'at://did:plc:test/social.coves.community.comment/123',
24 );
25 expect(response.cid, 'bafy123');
26 });
27 });
28
29 group('createComment', () {
30 late MockDio mockDio;
31 late CommentService commentService;
32 late CovesSession testSession;
33
34 setUp(() {
35 mockDio = MockDio();
36 testSession = CovesSession(
37 token: 'test-token',
38 did: 'did:plc:test',
39 sessionId: 'test-session-id',
40 handle: 'test.user',
41 );
42
43 // Setup default interceptors behavior
44 when(mockDio.interceptors).thenReturn(Interceptors());
45
46 commentService = CommentService(
47 sessionGetter: () async => testSession,
48 tokenRefresher: () async => true,
49 signOutHandler: () async {},
50 dio: mockDio,
51 );
52 });
53
54 test('should create comment successfully', () async {
55 when(
56 mockDio.post<Map<String, dynamic>>(
57 '/xrpc/social.coves.community.comment.create',
58 data: anyNamed('data'),
59 ),
60 ).thenAnswer(
61 (_) async => Response(
62 requestOptions: RequestOptions(path: ''),
63 statusCode: 200,
64 data: {
65 'uri': 'at://did:plc:test/social.coves.community.comment/abc123',
66 'cid': 'bafy123',
67 },
68 ),
69 );
70
71 final response = await commentService.createComment(
72 rootUri: 'at://did:plc:author/social.coves.post.record/post123',
73 rootCid: 'rootCid123',
74 parentUri: 'at://did:plc:author/social.coves.post.record/post123',
75 parentCid: 'parentCid123',
76 content: 'This is a test comment',
77 );
78
79 expect(
80 response.uri,
81 'at://did:plc:test/social.coves.community.comment/abc123',
82 );
83 expect(response.cid, 'bafy123');
84
85 verify(
86 mockDio.post<Map<String, dynamic>>(
87 '/xrpc/social.coves.community.comment.create',
88 data: {
89 'reply': {
90 'root': {
91 'uri': 'at://did:plc:author/social.coves.post.record/post123',
92 'cid': 'rootCid123',
93 },
94 'parent': {
95 'uri': 'at://did:plc:author/social.coves.post.record/post123',
96 'cid': 'parentCid123',
97 },
98 },
99 'content': 'This is a test comment',
100 },
101 ),
102 ).called(1);
103 });
104
105 test('should throw AuthenticationException when no session', () async {
106 final serviceWithoutSession = CommentService(
107 sessionGetter: () async => null,
108 tokenRefresher: () async => true,
109 signOutHandler: () async {},
110 dio: mockDio,
111 );
112
113 expect(
114 () => serviceWithoutSession.createComment(
115 rootUri: 'at://did:plc:author/post/123',
116 rootCid: 'rootCid',
117 parentUri: 'at://did:plc:author/post/123',
118 parentCid: 'parentCid',
119 content: 'Test comment',
120 ),
121 throwsA(isA<AuthenticationException>()),
122 );
123 });
124
125 test('should throw ApiException on network error', () async {
126 when(
127 mockDio.post<Map<String, dynamic>>(
128 '/xrpc/social.coves.community.comment.create',
129 data: anyNamed('data'),
130 ),
131 ).thenThrow(
132 DioException(
133 requestOptions: RequestOptions(path: ''),
134 type: DioExceptionType.connectionError,
135 message: 'Connection failed',
136 ),
137 );
138
139 expect(
140 () => commentService.createComment(
141 rootUri: 'at://did:plc:author/post/123',
142 rootCid: 'rootCid',
143 parentUri: 'at://did:plc:author/post/123',
144 parentCid: 'parentCid',
145 content: 'Test comment',
146 ),
147 throwsA(isA<ApiException>()),
148 );
149 });
150
151 test('should throw AuthenticationException on 401 response', () async {
152 when(
153 mockDio.post<Map<String, dynamic>>(
154 '/xrpc/social.coves.community.comment.create',
155 data: anyNamed('data'),
156 ),
157 ).thenThrow(
158 DioException(
159 requestOptions: RequestOptions(path: ''),
160 type: DioExceptionType.badResponse,
161 response: Response(
162 requestOptions: RequestOptions(path: ''),
163 statusCode: 401,
164 data: {'error': 'Unauthorized'},
165 ),
166 ),
167 );
168
169 expect(
170 () => commentService.createComment(
171 rootUri: 'at://did:plc:author/post/123',
172 rootCid: 'rootCid',
173 parentUri: 'at://did:plc:author/post/123',
174 parentCid: 'parentCid',
175 content: 'Test comment',
176 ),
177 throwsA(isA<AuthenticationException>()),
178 );
179 });
180
181 test(
182 'should throw ApiException on invalid response (null data)',
183 () async {
184 when(
185 mockDio.post<Map<String, dynamic>>(
186 '/xrpc/social.coves.community.comment.create',
187 data: anyNamed('data'),
188 ),
189 ).thenAnswer(
190 (_) async => Response(
191 requestOptions: RequestOptions(path: ''),
192 statusCode: 200,
193 data: null,
194 ),
195 );
196
197 expect(
198 () => commentService.createComment(
199 rootUri: 'at://did:plc:author/post/123',
200 rootCid: 'rootCid',
201 parentUri: 'at://did:plc:author/post/123',
202 parentCid: 'parentCid',
203 content: 'Test comment',
204 ),
205 throwsA(
206 isA<ApiException>().having(
207 (e) => e.message,
208 'message',
209 contains('no data'),
210 ),
211 ),
212 );
213 },
214 );
215
216 test(
217 'should throw ApiException on invalid response (missing uri)',
218 () async {
219 when(
220 mockDio.post<Map<String, dynamic>>(
221 '/xrpc/social.coves.community.comment.create',
222 data: anyNamed('data'),
223 ),
224 ).thenAnswer(
225 (_) async => Response(
226 requestOptions: RequestOptions(path: ''),
227 statusCode: 200,
228 data: {'cid': 'bafy123'},
229 ),
230 );
231
232 expect(
233 () => commentService.createComment(
234 rootUri: 'at://did:plc:author/post/123',
235 rootCid: 'rootCid',
236 parentUri: 'at://did:plc:author/post/123',
237 parentCid: 'parentCid',
238 content: 'Test comment',
239 ),
240 throwsA(
241 isA<ApiException>().having(
242 (e) => e.message,
243 'message',
244 contains('missing uri'),
245 ),
246 ),
247 );
248 },
249 );
250
251 test(
252 'should throw ApiException on invalid response (empty uri)',
253 () async {
254 when(
255 mockDio.post<Map<String, dynamic>>(
256 '/xrpc/social.coves.community.comment.create',
257 data: anyNamed('data'),
258 ),
259 ).thenAnswer(
260 (_) async => Response(
261 requestOptions: RequestOptions(path: ''),
262 statusCode: 200,
263 data: {'uri': '', 'cid': 'bafy123'},
264 ),
265 );
266
267 expect(
268 () => commentService.createComment(
269 rootUri: 'at://did:plc:author/post/123',
270 rootCid: 'rootCid',
271 parentUri: 'at://did:plc:author/post/123',
272 parentCid: 'parentCid',
273 content: 'Test comment',
274 ),
275 throwsA(
276 isA<ApiException>().having(
277 (e) => e.message,
278 'message',
279 contains('missing uri'),
280 ),
281 ),
282 );
283 },
284 );
285
286 test('should throw ApiException on server error', () async {
287 when(
288 mockDio.post<Map<String, dynamic>>(
289 '/xrpc/social.coves.community.comment.create',
290 data: anyNamed('data'),
291 ),
292 ).thenThrow(
293 DioException(
294 requestOptions: RequestOptions(path: ''),
295 type: DioExceptionType.badResponse,
296 response: Response(
297 requestOptions: RequestOptions(path: ''),
298 statusCode: 500,
299 data: {'error': 'Internal server error'},
300 ),
301 message: 'Internal server error',
302 ),
303 );
304
305 expect(
306 () => commentService.createComment(
307 rootUri: 'at://did:plc:author/post/123',
308 rootCid: 'rootCid',
309 parentUri: 'at://did:plc:author/post/123',
310 parentCid: 'parentCid',
311 content: 'Test comment',
312 ),
313 throwsA(isA<ApiException>()),
314 );
315 });
316
317 test('should send correct parent for nested reply', () async {
318 when(
319 mockDio.post<Map<String, dynamic>>(
320 '/xrpc/social.coves.community.comment.create',
321 data: anyNamed('data'),
322 ),
323 ).thenAnswer(
324 (_) async => Response(
325 requestOptions: RequestOptions(path: ''),
326 statusCode: 200,
327 data: {
328 'uri': 'at://did:plc:test/social.coves.community.comment/reply1',
329 'cid': 'bafyReply',
330 },
331 ),
332 );
333
334 await commentService.createComment(
335 rootUri: 'at://did:plc:author/social.coves.post.record/post123',
336 rootCid: 'postCid',
337 parentUri:
338 'at://did:plc:commenter/social.coves.community.comment/comment1',
339 parentCid: 'commentCid',
340 content: 'This is a nested reply',
341 );
342
343 verify(
344 mockDio.post<Map<String, dynamic>>(
345 '/xrpc/social.coves.community.comment.create',
346 data: {
347 'reply': {
348 'root': {
349 'uri': 'at://did:plc:author/social.coves.post.record/post123',
350 'cid': 'postCid',
351 },
352 'parent': {
353 'uri':
354 'at://did:plc:commenter/social.coves.community.comment/'
355 'comment1',
356 'cid': 'commentCid',
357 },
358 },
359 'content': 'This is a nested reply',
360 },
361 ),
362 ).called(1);
363 });
364 });
365 });
366}