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('should throw ApiException on invalid response (null data)', () async {
182 when(
183 mockDio.post<Map<String, dynamic>>(
184 '/xrpc/social.coves.community.comment.create',
185 data: anyNamed('data'),
186 ),
187 ).thenAnswer(
188 (_) async => Response(
189 requestOptions: RequestOptions(path: ''),
190 statusCode: 200,
191 data: null,
192 ),
193 );
194
195 expect(
196 () => commentService.createComment(
197 rootUri: 'at://did:plc:author/post/123',
198 rootCid: 'rootCid',
199 parentUri: 'at://did:plc:author/post/123',
200 parentCid: 'parentCid',
201 content: 'Test comment',
202 ),
203 throwsA(
204 isA<ApiException>().having(
205 (e) => e.message,
206 'message',
207 contains('no data'),
208 ),
209 ),
210 );
211 });
212
213 test('should throw ApiException on invalid response (missing uri)', () async {
214 when(
215 mockDio.post<Map<String, dynamic>>(
216 '/xrpc/social.coves.community.comment.create',
217 data: anyNamed('data'),
218 ),
219 ).thenAnswer(
220 (_) async => Response(
221 requestOptions: RequestOptions(path: ''),
222 statusCode: 200,
223 data: {'cid': 'bafy123'},
224 ),
225 );
226
227 expect(
228 () => commentService.createComment(
229 rootUri: 'at://did:plc:author/post/123',
230 rootCid: 'rootCid',
231 parentUri: 'at://did:plc:author/post/123',
232 parentCid: 'parentCid',
233 content: 'Test comment',
234 ),
235 throwsA(
236 isA<ApiException>().having(
237 (e) => e.message,
238 'message',
239 contains('missing uri'),
240 ),
241 ),
242 );
243 });
244
245 test('should throw ApiException on invalid response (empty uri)', () async {
246 when(
247 mockDio.post<Map<String, dynamic>>(
248 '/xrpc/social.coves.community.comment.create',
249 data: anyNamed('data'),
250 ),
251 ).thenAnswer(
252 (_) async => Response(
253 requestOptions: RequestOptions(path: ''),
254 statusCode: 200,
255 data: {'uri': '', 'cid': 'bafy123'},
256 ),
257 );
258
259 expect(
260 () => commentService.createComment(
261 rootUri: 'at://did:plc:author/post/123',
262 rootCid: 'rootCid',
263 parentUri: 'at://did:plc:author/post/123',
264 parentCid: 'parentCid',
265 content: 'Test comment',
266 ),
267 throwsA(
268 isA<ApiException>().having(
269 (e) => e.message,
270 'message',
271 contains('missing uri'),
272 ),
273 ),
274 );
275 });
276
277 test('should throw ApiException on server error', () async {
278 when(
279 mockDio.post<Map<String, dynamic>>(
280 '/xrpc/social.coves.community.comment.create',
281 data: anyNamed('data'),
282 ),
283 ).thenThrow(
284 DioException(
285 requestOptions: RequestOptions(path: ''),
286 type: DioExceptionType.badResponse,
287 response: Response(
288 requestOptions: RequestOptions(path: ''),
289 statusCode: 500,
290 data: {'error': 'Internal server error'},
291 ),
292 message: 'Internal server error',
293 ),
294 );
295
296 expect(
297 () => commentService.createComment(
298 rootUri: 'at://did:plc:author/post/123',
299 rootCid: 'rootCid',
300 parentUri: 'at://did:plc:author/post/123',
301 parentCid: 'parentCid',
302 content: 'Test comment',
303 ),
304 throwsA(isA<ApiException>()),
305 );
306 });
307
308 test('should send correct parent for nested reply', () async {
309 when(
310 mockDio.post<Map<String, dynamic>>(
311 '/xrpc/social.coves.community.comment.create',
312 data: anyNamed('data'),
313 ),
314 ).thenAnswer(
315 (_) async => Response(
316 requestOptions: RequestOptions(path: ''),
317 statusCode: 200,
318 data: {
319 'uri': 'at://did:plc:test/social.coves.community.comment/reply1',
320 'cid': 'bafyReply',
321 },
322 ),
323 );
324
325 await commentService.createComment(
326 rootUri: 'at://did:plc:author/social.coves.post.record/post123',
327 rootCid: 'postCid',
328 parentUri:
329 'at://did:plc:commenter/social.coves.community.comment/comment1',
330 parentCid: 'commentCid',
331 content: 'This is a nested reply',
332 );
333
334 verify(
335 mockDio.post<Map<String, dynamic>>(
336 '/xrpc/social.coves.community.comment.create',
337 data: {
338 'reply': {
339 'root': {
340 'uri': 'at://did:plc:author/social.coves.post.record/post123',
341 'cid': 'postCid',
342 },
343 'parent': {
344 'uri':
345 'at://did:plc:commenter/social.coves.community.comment/'
346 'comment1',
347 'cid': 'commentCid',
348 },
349 },
350 'content': 'This is a nested reply',
351 },
352 ),
353 ).called(1);
354 });
355 });
356 });
357}