diff --git a/packages/stream_feeds/dart_test.yaml b/packages/stream_feeds/dart_test.yaml index e66bfe9d..5455f622 100644 --- a/packages/stream_feeds/dart_test.yaml +++ b/packages/stream_feeds/dart_test.yaml @@ -12,6 +12,7 @@ tags: feed-list: follow-list: member-list: + moderation-client: moderation-config-list: poll-list: poll-vote-list: \ No newline at end of file diff --git a/packages/stream_feeds/lib/src/client/feeds_client_impl.dart b/packages/stream_feeds/lib/src/client/feeds_client_impl.dart index bee8acd5..75b6b9ed 100644 --- a/packages/stream_feeds/lib/src/client/feeds_client_impl.dart +++ b/packages/stream_feeds/lib/src/client/feeds_client_impl.dart @@ -56,7 +56,6 @@ import '../state/query/poll_votes_query.dart'; import '../state/query/polls_query.dart'; import '../ws/feeds_ws_event.dart'; import 'endpoint_config.dart'; -import 'moderation_client.dart'; class StreamFeedsClientImpl implements StreamFeedsClient { StreamFeedsClientImpl({ diff --git a/packages/stream_feeds/lib/src/client/moderation_client.dart b/packages/stream_feeds/lib/src/client/moderation_client.dart index 0d1c3df4..f14adb26 100644 --- a/packages/stream_feeds/lib/src/client/moderation_client.dart +++ b/packages/stream_feeds/lib/src/client/moderation_client.dart @@ -1,8 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../stream_feeds.dart' show ModerationConfigsQuery; import '../generated/api/api.dart' as api; +import '../models.dart' show ModerationConfigData, PaginationResult; import '../repository/moderation_repository.dart'; +import '../state.dart' show ModerationConfigsQuery; /// Controller exposing moderation functionalities. /// @@ -160,8 +161,9 @@ class ModerationClient { /// /// Retrieves moderation configurations using the specified [queryModerationConfigsRequest] filters and pagination. /// - /// Returns a [Result] containing a [api.QueryModerationConfigsResponse] or an error. - Future queryModerationConfigs({ + /// Returns a [Result] containing a [PaginationResult] of [ModerationConfigData] or an error. + Future>> + queryModerationConfigs({ required ModerationConfigsQuery queryModerationConfigsRequest, }) { return _moderationRepository diff --git a/packages/stream_feeds/lib/src/feeds_client.dart b/packages/stream_feeds/lib/src/feeds_client.dart index 814e28a0..c8fe475c 100644 --- a/packages/stream_feeds/lib/src/feeds_client.dart +++ b/packages/stream_feeds/lib/src/feeds_client.dart @@ -42,6 +42,8 @@ import 'state/query/moderation_configs_query.dart'; import 'state/query/poll_votes_query.dart'; import 'state/query/polls_query.dart'; +export 'client/moderation_client.dart'; + /// {@template stream_feeds_client} /// Stream Feeds client for building scalable newsfeeds and activity streams. /// diff --git a/packages/stream_feeds/test/client/moderation_client_test.dart b/packages/stream_feeds/test/client/moderation_client_test.dart new file mode 100644 index 00000000..e7a9936b --- /dev/null +++ b/packages/stream_feeds/test/client/moderation_client_test.dart @@ -0,0 +1,587 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:stream_feeds_test/stream_feeds_test.dart'; + +void main() { + // ============================================================ + // FEATURE: Ban Operations + // ============================================================ + + group('ban', () { + setUpAll(() { + registerFallbackValue(const BanRequest(targetUserId: 'fallback')); + }); + + moderationClientTest( + 'should ban user successfully', + body: (tester) async { + const request = BanRequest(targetUserId: 'user-123'); + + tester.mockApi( + (api) => api.ban(banRequest: request), + result: createDefaultBanResponse(), + ); + + final result = await tester.moderation.ban(banRequest: request); + + expect(result.isSuccess, isTrue); + tester.verifyApi((api) => api.ban(banRequest: request)); + }, + ); + + moderationClientTest( + 'should handle ban failure', + body: (tester) async { + const request = BanRequest(targetUserId: 'user-123'); + + tester.mockApiFailure( + (api) => api.ban(banRequest: request), + error: Exception('Failed to ban user'), + ); + + final result = await tester.moderation.ban(banRequest: request); + + expect(result.isFailure, isTrue); + tester.verifyApi((api) => api.ban(banRequest: request)); + }, + ); + }); + + // ============================================================ + // FEATURE: Mute Operations + // ============================================================ + + group('mute', () { + setUpAll(() { + registerFallbackValue(const MuteRequest(targetIds: [])); + }); + + moderationClientTest( + 'should mute users successfully', + body: (tester) async { + const request = MuteRequest(targetIds: ['user-123']); + + tester.mockApi( + (api) => api.mute(muteRequest: request), + result: createDefaultMuteResponse(), + ); + + final result = await tester.moderation.mute(muteRequest: request); + + expect(result.isSuccess, isTrue); + tester.verifyApi((api) => api.mute(muteRequest: request)); + }, + ); + + moderationClientTest( + 'should handle mute failure', + body: (tester) async { + const request = MuteRequest(targetIds: ['user-123']); + + tester.mockApiFailure( + (api) => api.mute(muteRequest: request), + error: Exception('Failed to mute user'), + ); + + final result = await tester.moderation.mute(muteRequest: request); + + expect(result.isFailure, isTrue); + tester.verifyApi((api) => api.mute(muteRequest: request)); + }, + ); + }); + + // ============================================================ + // FEATURE: Block Operations + // ============================================================ + + group('blockUsers', () { + setUpAll(() { + registerFallbackValue(const BlockUsersRequest(blockedUserId: 'fallback')); + }); + + moderationClientTest( + 'should block user successfully', + body: (tester) async { + const request = BlockUsersRequest(blockedUserId: 'user-123'); + + tester.mockApi( + (api) => api.blockUsers(blockUsersRequest: request), + result: createDefaultBlockUsersResponse(blockedUserId: 'user-123'), + ); + + final result = await tester.moderation.blockUsers( + blockUsersRequest: request, + ); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.blockUsers(blockUsersRequest: request), + ); + }, + ); + + moderationClientTest( + 'should handle block failure', + body: (tester) async { + const request = BlockUsersRequest(blockedUserId: 'user-123'); + + tester.mockApiFailure( + (api) => api.blockUsers(blockUsersRequest: request), + error: Exception('Failed to block user'), + ); + + final result = await tester.moderation.blockUsers( + blockUsersRequest: request, + ); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.blockUsers(blockUsersRequest: request), + ); + }, + ); + }); + + group('unblockUsers', () { + setUpAll(() { + registerFallbackValue( + const UnblockUsersRequest(blockedUserId: 'fallback'), + ); + }); + + moderationClientTest( + 'should unblock user successfully', + body: (tester) async { + const request = UnblockUsersRequest(blockedUserId: 'user-123'); + + tester.mockApi( + (api) => api.unblockUsers(unblockUsersRequest: request), + result: createDefaultUnblockUsersResponse(), + ); + + final result = await tester.moderation.unblockUsers( + unblockUsersRequest: request, + ); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.unblockUsers(unblockUsersRequest: request), + ); + }, + ); + + moderationClientTest( + 'should handle unblock failure', + body: (tester) async { + const request = UnblockUsersRequest(blockedUserId: 'user-123'); + + tester.mockApiFailure( + (api) => api.unblockUsers(unblockUsersRequest: request), + error: Exception('Failed to unblock user'), + ); + + final result = await tester.moderation.unblockUsers( + unblockUsersRequest: request, + ); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.unblockUsers(unblockUsersRequest: request), + ); + }, + ); + }); + + group('getBlockedUsers', () { + moderationClientTest( + 'should get blocked users successfully', + body: (tester) async { + tester.mockApi( + (api) => api.getBlockedUsers(), + result: createDefaultGetBlockedUsersResponse( + blocks: [ + createDefaultBlockedUserResponse(blockedUserId: 'user-123'), + ], + ), + ); + + final result = await tester.moderation.getBlockedUsers(); + + expect(result.isSuccess, isTrue); + tester.verifyApi((api) => api.getBlockedUsers()); + }, + ); + + moderationClientTest( + 'should handle get blocked users failure', + body: (tester) async { + tester.mockApiFailure( + (api) => api.getBlockedUsers(), + error: Exception('Failed to get blocked users'), + ); + + final result = await tester.moderation.getBlockedUsers(); + + expect(result.isFailure, isTrue); + tester.verifyApi((api) => api.getBlockedUsers()); + }, + ); + }); + + // ============================================================ + // FEATURE: Flag Operations + // ============================================================ + + group('flag', () { + setUpAll(() { + registerFallbackValue( + const FlagRequest(entityId: 'fallback', entityType: 'fallback'), + ); + }); + + moderationClientTest( + 'should flag content successfully', + body: (tester) async { + const request = FlagRequest( + entityId: 'activity-123', + entityType: 'activity', + ); + + tester.mockApi( + (api) => api.flag(flagRequest: request), + result: createDefaultFlagResponse(itemId: 'activity-123'), + ); + + final result = await tester.moderation.flag(flagRequest: request); + + expect(result.isSuccess, isTrue); + tester.verifyApi((api) => api.flag(flagRequest: request)); + }, + ); + + moderationClientTest( + 'should handle flag failure', + body: (tester) async { + const request = FlagRequest( + entityId: 'activity-123', + entityType: 'activity', + ); + + tester.mockApiFailure( + (api) => api.flag(flagRequest: request), + error: Exception('Failed to flag content'), + ); + + final result = await tester.moderation.flag(flagRequest: request); + + expect(result.isFailure, isTrue); + tester.verifyApi((api) => api.flag(flagRequest: request)); + }, + ); + }); + + // ============================================================ + // FEATURE: Moderation Actions + // ============================================================ + + group('submitAction', () { + setUpAll(() { + registerFallbackValue( + const SubmitActionRequest( + actionType: SubmitActionRequestActionType.markReviewed, + itemId: 'fallback', + ), + ); + }); + + moderationClientTest( + 'should submit moderation action successfully', + body: (tester) async { + const request = SubmitActionRequest( + actionType: SubmitActionRequestActionType.markReviewed, + itemId: 'item-123', + ); + + tester.mockApi( + (api) => api.submitAction(submitActionRequest: request), + result: createDefaultSubmitActionResponse(), + ); + + final result = await tester.moderation.submitAction( + submitActionRequest: request, + ); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.submitAction(submitActionRequest: request), + ); + }, + ); + + moderationClientTest( + 'should handle submit action failure', + body: (tester) async { + const request = SubmitActionRequest( + actionType: SubmitActionRequestActionType.ban, + itemId: 'item-456', + ); + + tester.mockApiFailure( + (api) => api.submitAction(submitActionRequest: request), + error: Exception('Failed to submit action'), + ); + + final result = await tester.moderation.submitAction( + submitActionRequest: request, + ); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.submitAction(submitActionRequest: request), + ); + }, + ); + }); + + // ============================================================ + // FEATURE: Review Queue Operations + // ============================================================ + + group('queryReviewQueue', () { + setUpAll(() { + registerFallbackValue(const QueryReviewQueueRequest()); + }); + + moderationClientTest( + 'should query review queue successfully', + body: (tester) async { + const request = QueryReviewQueueRequest(limit: 20); + + tester.mockApi( + (api) => api.queryReviewQueue(queryReviewQueueRequest: request), + result: createDefaultQueryReviewQueueResponse(next: 'next-cursor'), + ); + + final result = await tester.moderation.queryReviewQueue( + queryReviewQueueRequest: request, + ); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.queryReviewQueue(queryReviewQueueRequest: request), + ); + }, + ); + + moderationClientTest( + 'should handle query review queue failure', + body: (tester) async { + const request = QueryReviewQueueRequest(); + + tester.mockApiFailure( + (api) => api.queryReviewQueue(queryReviewQueueRequest: request), + error: Exception('Failed to query review queue'), + ); + + final result = await tester.moderation.queryReviewQueue( + queryReviewQueueRequest: request, + ); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.queryReviewQueue(queryReviewQueueRequest: request), + ); + }, + ); + }); + + // ============================================================ + // FEATURE: Config Operations + // ============================================================ + + group('upsertConfig', () { + setUpAll(() { + registerFallbackValue(const UpsertConfigRequest(key: 'fallback')); + }); + + moderationClientTest( + 'should upsert config successfully', + body: (tester) async { + const request = UpsertConfigRequest(key: 'config-1'); + + tester.mockApi( + (api) => api.upsertConfig(upsertConfigRequest: request), + result: createDefaultUpsertConfigResponse(key: 'config-1'), + ); + + final result = await tester.moderation.upsertConfig(request); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.upsertConfig(upsertConfigRequest: request), + ); + }, + ); + + moderationClientTest( + 'should handle upsert config failure', + body: (tester) async { + const request = UpsertConfigRequest(key: 'config-1'); + + tester.mockApiFailure( + (api) => api.upsertConfig(upsertConfigRequest: request), + error: Exception('Failed to upsert config'), + ); + + final result = await tester.moderation.upsertConfig(request); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.upsertConfig(upsertConfigRequest: request), + ); + }, + ); + }); + + group('deleteConfig', () { + moderationClientTest( + 'should delete config successfully', + body: (tester) async { + const key = 'config-1'; + + tester.mockApi( + (api) => api.deleteConfig(key: key, team: null), + result: createDefaultDeleteModerationConfigResponse(), + ); + + final result = await tester.moderation.deleteConfig(key: key); + + expect(result.isSuccess, isTrue); + tester.verifyApi( + (api) => api.deleteConfig(key: key, team: null), + ); + }, + ); + + moderationClientTest( + 'should handle delete config failure', + body: (tester) async { + const key = 'config-1'; + + tester.mockApiFailure( + (api) => api.deleteConfig(key: key, team: null), + error: Exception('Failed to delete config'), + ); + + final result = await tester.moderation.deleteConfig(key: key); + + expect(result.isFailure, isTrue); + tester.verifyApi( + (api) => api.deleteConfig(key: key, team: null), + ); + }, + ); + }); + + group('getConfig', () { + moderationClientTest( + 'should get config successfully', + body: (tester) async { + const key = 'config-1'; + + tester.mockApi( + (api) => api.getConfig(key: key, team: null), + result: createDefaultGetConfigResponse(key: key), + ); + + final result = await tester.moderation.getConfig(key: key); + + expect(result.isSuccess, isTrue); + tester.verifyApi((api) => api.getConfig(key: key, team: null)); + }, + ); + + moderationClientTest( + 'should handle get config failure', + body: (tester) async { + const key = 'config-1'; + + tester.mockApiFailure( + (api) => api.getConfig(key: key, team: null), + error: Exception('Config not found'), + ); + + final result = await tester.moderation.getConfig(key: key); + + expect(result.isFailure, isTrue); + tester.verifyApi((api) => api.getConfig(key: key, team: null)); + }, + ); + }); + + group('queryModerationConfigs', () { + setUpAll(() { + registerFallbackValue(const QueryModerationConfigsRequest()); + }); + + moderationClientTest( + 'should query moderation configs successfully', + body: (tester) async { + const query = ModerationConfigsQuery(limit: 10); + final request = query.toRequest(); + + tester.mockApi( + (api) => api.queryModerationConfigs( + queryModerationConfigsRequest: request, + ), + result: createDefaultQueryModerationConfigsResponse( + configs: [createDefaultConfigResponse()], + ), + ); + + final result = await tester.moderation.queryModerationConfigs( + queryModerationConfigsRequest: query, + ); + + expect(result.isSuccess, isTrue); + final paginationResult = result.getOrThrow(); + expect(paginationResult.items, isNotEmpty); + + tester.verifyApi( + (api) => api.queryModerationConfigs( + queryModerationConfigsRequest: request, + ), + ); + }, + ); + + moderationClientTest( + 'should handle query moderation configs failure', + body: (tester) async { + const query = ModerationConfigsQuery(limit: 10); + final request = query.toRequest(); + + tester.mockApiFailure( + (api) => api.queryModerationConfigs( + queryModerationConfigsRequest: request, + ), + error: Exception('Failed to query configs'), + ); + + final result = await tester.moderation.queryModerationConfigs( + queryModerationConfigsRequest: query, + ); + + expect(result.isFailure, isTrue); + + tester.verifyApi( + (api) => api.queryModerationConfigs( + queryModerationConfigsRequest: request, + ), + ); + }, + ); + }); +} diff --git a/packages/stream_feeds_test/lib/src/helpers/api_mocker_mixin.dart b/packages/stream_feeds_test/lib/src/helpers/api_mocker_mixin.dart index f14b9208..e4fde66b 100644 --- a/packages/stream_feeds_test/lib/src/helpers/api_mocker_mixin.dart +++ b/packages/stream_feeds_test/lib/src/helpers/api_mocker_mixin.dart @@ -36,6 +36,26 @@ mixin ApiMockerMixin { return mockApiResult(apiCall, result: Result.success(result)); } + /// Sets up a mock API failure response for the given API call. + /// + /// Use this when you need to return an error: + /// ```dart + /// tester.mockApiFailure( + /// (api) => api.addActivity(...), + /// error: Exception('Failed to add activity'), + /// ); + /// ``` + void mockApiFailure( + Future> Function(MockDefaultApi api) apiCall, { + required Object error, + StackTrace? stackTrace, + }) { + return mockApiResult( + apiCall, + result: Result.failure(error, stackTrace ?? StackTrace.current), + ); + } + /// Sets up a mock API response with a custom Result. /// /// Use this when you need to return a failure: diff --git a/packages/stream_feeds_test/lib/src/helpers/test_data.dart b/packages/stream_feeds_test/lib/src/helpers/test_data.dart index d47c55af..502b2428 100644 --- a/packages/stream_feeds_test/lib/src/helpers/test_data.dart +++ b/packages/stream_feeds_test/lib/src/helpers/test_data.dart @@ -884,3 +884,127 @@ QueryModerationConfigsResponse createDefaultQueryModerationConfigsResponse({ duration: '10ms', ); } + +ConfigResponse createDefaultConfigResponse({ + String key = 'default-config', + String team = 'default-team', +}) { + return ConfigResponse( + key: key, + team: team, + async: true, + createdAt: DateTime(2021, 1, 1), + updatedAt: DateTime(2021, 2, 1), + supportedVideoCallHarmTypes: const [], + ); +} + +BanResponse createDefaultBanResponse() { + return const BanResponse(duration: '10ms'); +} + +MuteResponse createDefaultMuteResponse({ + List? mutes, +}) { + return MuteResponse( + duration: '10ms', + mutes: mutes, + ); +} + +BlockUsersResponse createDefaultBlockUsersResponse({ + String blockedUserId = 'user-123', + String blockedByUserId = 'user-1', +}) { + return BlockUsersResponse( + blockedUserId: blockedUserId, + blockedByUserId: blockedByUserId, + createdAt: DateTime(2021, 1, 1), + duration: '10ms', + ); +} + +UnblockUsersResponse createDefaultUnblockUsersResponse() { + return const UnblockUsersResponse(duration: '10ms'); +} + +GetBlockedUsersResponse createDefaultGetBlockedUsersResponse({ + List blocks = const [], +}) { + return GetBlockedUsersResponse( + blocks: blocks, + duration: '10ms', + ); +} + +BlockedUserResponse createDefaultBlockedUserResponse({ + String blockedUserId = 'user-123', + String userId = 'user-1', +}) { + return BlockedUserResponse( + blockedUserId: blockedUserId, + blockedUser: createDefaultUserResponse(id: blockedUserId), + userId: userId, + user: createDefaultUserResponse(id: userId), + createdAt: DateTime(2021, 1, 1), + ); +} + +FlagResponse createDefaultFlagResponse({ + String itemId = 'activity-123', +}) { + return FlagResponse( + duration: '10ms', + itemId: itemId, + ); +} + +SubmitActionResponse createDefaultSubmitActionResponse({ + ReviewQueueItemResponse? item, +}) { + return SubmitActionResponse( + duration: '10ms', + item: item, + ); +} + +QueryReviewQueueResponse createDefaultQueryReviewQueueResponse({ + String? next, + String? prev, + List items = const [], +}) { + return QueryReviewQueueResponse( + actionConfig: const {}, + items: items, + next: next, + prev: prev, + stats: const {}, + duration: '10ms', + ); +} + +UpsertConfigResponse createDefaultUpsertConfigResponse({ + String key = 'config-1', +}) { + return UpsertConfigResponse( + config: createDefaultModerationConfigResponse(key: key), + duration: '10ms', + ); +} + +DeleteModerationConfigResponse createDefaultDeleteModerationConfigResponse() { + return const DeleteModerationConfigResponse(duration: '10ms'); +} + +GetConfigResponse createDefaultGetConfigResponse({ + String key = 'config-1', + String? team, +}) { + return GetConfigResponse( + config: createDefaultModerationConfigResponse( + key: key, + team: team ?? 'team-id', + ), + duration: '10ms', + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/moderation_client_tester.dart b/packages/stream_feeds_test/lib/src/testers/moderation_client_tester.dart new file mode 100644 index 00000000..0aaff600 --- /dev/null +++ b/packages/stream_feeds_test/lib/src/testers/moderation_client_tester.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:test/test.dart' as test; + +import '../helpers/mocks.dart'; +import 'base_tester.dart'; + +/// Test helper for moderation client operations. +/// +/// Automatically sets up client and test infrastructure. +/// Tests are tagged with 'moderation-client' by default for filtering. +/// +/// [user] is optional, the authenticated user for the test client (defaults to luke_skywalker). +/// [setUp] is optional and runs before [body] for setting up mocks and test state. +/// [body] is the test callback that receives a [ModerationClientTester] for interactions. +/// [verify] is optional and runs after [body] for verifying API calls and interactions. +/// [tearDown] is optional and runs after [verify] for custom cleanup. +/// [skip] is optional, skip this test. +/// [tags] is optional, tags for test filtering. Defaults to ['moderation-client']. +/// [timeout] is optional, custom timeout for this test. +/// +/// Example: +/// ```dart +/// moderationClientTest( +/// 'should ban user via API', +/// setUp: (tester) { +/// tester.mockApi( +/// (api) => api.ban(banRequest: any(named: 'banRequest')), +/// result: createDefaultBanResponse(), +/// ); +/// }, +/// body: (tester) async { +/// const request = BanRequest(targetUserId: 'user-123'); +/// final result = await tester.moderation.ban(banRequest: request); +/// expect(result.isSuccess, isTrue); +/// }, +/// verify: (tester) => tester.verifyApi( +/// (api) => api.ban(banRequest: any(named: 'banRequest')), +/// ), +/// ); +/// ``` +@isTest +void moderationClientTest( + String description, { + User user = const User(id: 'luke_skywalker'), + FutureOr Function(ModerationClientTester tester)? setUp, + required FutureOr Function(ModerationClientTester tester) body, + FutureOr Function(ModerationClientTester tester)? verify, + FutureOr Function(ModerationClientTester tester)? tearDown, + bool skip = false, + Iterable tags = const ['moderation-client'], + test.Timeout? timeout, +}) { + return testWithTester( + description, + user: user, + build: (client) => client.moderation, + createTesterFn: _createModerationClientTester, + setUp: setUp, + body: body, + verify: verify, + tearDown: tearDown, + skip: skip, + tags: tags, + timeout: timeout, + ); +} + +/// A test utility for moderation client operations. +/// +/// Provides helper methods for mocking and verifying moderation API calls. +/// +/// Resources are automatically cleaned up after the test completes. +final class ModerationClientTester extends BaseTester { + const ModerationClientTester._({ + required super.subject, + required super.wsStreamController, + required super.feedsApi, + }); + + /// The Moderation client being tested. + ModerationClient get moderation => subject; +} + +// Creates a ModerationClientTester for testing moderation client operations. +// +// Automatically sets up WebSocket connection and registers cleanup handlers. +// This function is for internal use by moderationClientTest only. +Future _createModerationClientTester({ + required ModerationClient subject, + required StreamFeedsClient client, + required MockDefaultApi feedsApi, + required MockWebSocketChannel webSocketChannel, +}) { + return createTester( + client: client, + webSocketChannel: webSocketChannel, + create: (wsStreamController) => ModerationClientTester._( + subject: subject, + wsStreamController: wsStreamController, + feedsApi: feedsApi, + ), + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/activity_comment_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/activity_comment_list_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/activity_comment_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/activity_comment_list_tester.dart index 2b1d57b4..b1cf216a 100644 --- a/packages/stream_feeds_test/lib/src/testers/activity_comment_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/activity_comment_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for activity comment list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/activity_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/activity_list_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/activity_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/activity_list_tester.dart index 1fd69202..24e4d7d9 100644 --- a/packages/stream_feeds_test/lib/src/testers/activity_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/activity_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for activity list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/state/activity_reaction_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/activity_reaction_list_tester.dart new file mode 100644 index 00000000..93f8f9e6 --- /dev/null +++ b/packages/stream_feeds_test/lib/src/testers/state/activity_reaction_list_tester.dart @@ -0,0 +1,162 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:test/test.dart' as test; + +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; + +/// Test helper for activity reaction list operations. +/// +/// Automatically sets up WebSocket connection, client, and test infrastructure. +/// Tests are tagged with 'activity-reaction-list' by default for filtering. +/// +/// [user] is optional, the authenticated user for the test client (defaults to luke_skywalker). + +/// [build] constructs the [ActivityReactionList] under test using the provided [StreamFeedsClient]. +/// [setUp] is optional and runs before [body] for setting up mocks and test state. +/// [body] is the test callback that receives an [ActivityReactionListTester] for interactions. +/// [verify] is optional and runs after [body] for verifying API calls and interactions. +/// [tearDown] is optional and runs after [verify] for cleanup operations. +/// [skip] is optional, skip this test. +/// [tags] is optional, tags for test filtering. Defaults to ['activity-reaction-list']. +/// [timeout] is optional, custom timeout for this test. +/// +/// Example: +/// ```dart +/// activityReactionListTest( +/// 'queries initial reactions', +/// build: (client) => client.activityReactionList( +/// ActivityReactionsQuery( +/// activityId: 'activity-1', +/// ), +/// ), +/// setUp: (tester) => tester.get(), +/// body: (tester) async { +/// expect(tester.activityReactionListState.reactions, hasLength(3)); +/// }, +/// ); +/// ``` +@isTest +void activityReactionListTest( + String description, { + User user = const User(id: 'luke_skywalker'), + required ActivityReactionList Function(StreamFeedsClient client) build, + FutureOr Function(ActivityReactionListTester tester)? setUp, + required FutureOr Function(ActivityReactionListTester tester) body, + FutureOr Function(ActivityReactionListTester tester)? verify, + FutureOr Function(ActivityReactionListTester tester)? tearDown, + bool skip = false, + Iterable tags = const ['activity-reaction-list'], + test.Timeout? timeout, +}) { + return testWithTester( + description, + user: user, + build: build, + createTesterFn: _createActivityReactionListTester, + setUp: setUp, + body: body, + verify: verify, + tearDown: tearDown, + skip: skip, + tags: tags, + timeout: timeout, + ); +} + +/// A test utility for activity reaction list operations with WebSocket support. +/// +/// Provides helper methods for emitting events and verifying activity reaction list state. +/// +/// Resources are automatically cleaned up after the test completes. +final class ActivityReactionListTester + extends BaseTester { + const ActivityReactionListTester._({ + required ActivityReactionList activityReactionList, + required super.wsStreamController, + required super.feedsApi, + }) : super(subject: activityReactionList); + + /// The activity reaction list being tested. + ActivityReactionList get activityReactionList => subject; + + /// Current state of the activity reaction list. + ActivityReactionListState get activityReactionListState { + return activityReactionList.state; + } + + /// Stream of activity reaction list state updates. + Stream get activityReactionListStateStream { + return activityReactionList.stream; + } + + /// Gets the activity reaction list by fetching it from the API. + /// + /// Call this in event tests to set up initial state before emitting events. + /// Skip this in API tests that only verify method calls. + /// + /// Parameters: + /// - [modifyResponse]: Optional function to customize the activity reaction list response + Future>> get({ + QueryActivityReactionsResponse Function( + QueryActivityReactionsResponse, + )? modifyResponse, + }) { + final query = activityReactionList.query; + + final defaultReactionListResponse = + createDefaultQueryActivityReactionsResponse( + reactions: [ + createDefaultReactionResponse(activityId: query.activityId), + createDefaultReactionResponse( + activityId: query.activityId, + userId: 'user-2', + ), + createDefaultReactionResponse( + activityId: query.activityId, + userId: 'user-3', + ), + ], + ); + + mockApi( + (api) => api.queryActivityReactions( + activityId: query.activityId, + queryActivityReactionsRequest: query.toRequest(), + ), + result: switch (modifyResponse) { + final modifier? => modifier(defaultReactionListResponse), + _ => defaultReactionListResponse, + }, + ); + + return activityReactionList.get(); + } +} + +// Creates an ActivityReactionListTester for testing activity reaction list operations. +// +// Automatically sets up WebSocket connection and registers cleanup handlers. +// This function is for internal use by activityReactionListTest only. +Future _createActivityReactionListTester({ + required StreamFeedsClient client, + required ActivityReactionList subject, + required MockDefaultApi feedsApi, + required MockWebSocketChannel webSocketChannel, +}) { + // Dispose activity reaction list after test + test.addTearDown(subject.dispose); + + return createTester( + client: client, + webSocketChannel: webSocketChannel, + create: (wsStreamController) => ActivityReactionListTester._( + activityReactionList: subject, + wsStreamController: wsStreamController, + feedsApi: feedsApi, + ), + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/activity_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/activity_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/activity_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/activity_tester.dart index 99d2dd88..b43161ac 100644 --- a/packages/stream_feeds_test/lib/src/testers/activity_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/activity_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for activity operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/bookmark_folder_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/bookmark_folder_list_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/bookmark_folder_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/bookmark_folder_list_tester.dart index 1e44c546..0d50b179 100644 --- a/packages/stream_feeds_test/lib/src/testers/bookmark_folder_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/bookmark_folder_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for bookmark folder list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/bookmark_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/bookmark_list_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/bookmark_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/bookmark_list_tester.dart index 9a2aa6a2..ee016348 100644 --- a/packages/stream_feeds_test/lib/src/testers/bookmark_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/bookmark_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for bookmark list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/comment_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/comment_list_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/comment_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/comment_list_tester.dart index f55c6ace..d9dd5123 100644 --- a/packages/stream_feeds_test/lib/src/testers/comment_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/comment_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for comment list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/state/comment_reaction_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/comment_reaction_list_tester.dart new file mode 100644 index 00000000..7783cc70 --- /dev/null +++ b/packages/stream_feeds_test/lib/src/testers/state/comment_reaction_list_tester.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:test/test.dart' as test; + +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; + +/// Test helper for comment reaction list operations. +/// +/// Automatically sets up WebSocket connection, client, and test infrastructure. +/// Tests are tagged with 'comment-reaction-list' by default for filtering. +/// +/// [user] is optional, the authenticated user for the test client (defaults to luke_skywalker). + +/// [build] constructs the [CommentReactionList] under test using the provided [StreamFeedsClient]. +/// [setUp] is optional and runs before [body] for setting up mocks and test state. +/// [body] is the test callback that receives a [CommentReactionListTester] for interactions. +/// [verify] is optional and runs after [body] for verifying API calls and interactions. +/// [tearDown] is optional and runs after [verify] for cleanup operations. +/// [skip] is optional, skip this test. +/// [tags] is optional, tags for test filtering. Defaults to ['comment-reaction-list']. +/// [timeout] is optional, custom timeout for this test. +/// +/// Example: +/// ```dart +/// commentReactionListTest( +/// 'queries initial reactions', +/// build: (client) => client.commentReactionList( +/// CommentReactionsQuery( +/// commentId: 'comment-1', +/// ), +/// ), +/// setUp: (tester) => tester.get(), +/// body: (tester) async { +/// expect(tester.commentReactionListState.reactions, hasLength(3)); +/// }, +/// ); +/// ``` +@isTest +void commentReactionListTest( + String description, { + User user = const User(id: 'luke_skywalker'), + required CommentReactionList Function(StreamFeedsClient client) build, + FutureOr Function(CommentReactionListTester tester)? setUp, + required FutureOr Function(CommentReactionListTester tester) body, + FutureOr Function(CommentReactionListTester tester)? verify, + FutureOr Function(CommentReactionListTester tester)? tearDown, + bool skip = false, + Iterable tags = const ['comment-reaction-list'], + test.Timeout? timeout, +}) { + return testWithTester( + description, + user: user, + build: build, + createTesterFn: _createCommentReactionListTester, + setUp: setUp, + body: body, + verify: verify, + tearDown: tearDown, + skip: skip, + tags: tags, + timeout: timeout, + ); +} + +/// A test utility for comment reaction list operations with WebSocket support. +/// +/// Provides helper methods for emitting events and verifying comment reaction list state. +/// +/// Resources are automatically cleaned up after the test completes. +final class CommentReactionListTester extends BaseTester { + const CommentReactionListTester._({ + required CommentReactionList commentReactionList, + required super.wsStreamController, + required super.feedsApi, + }) : super(subject: commentReactionList); + + /// The comment reaction list being tested. + CommentReactionList get commentReactionList => subject; + + /// Current state of the comment reaction list. + CommentReactionListState get commentReactionListState { + return commentReactionList.state; + } + + /// Stream of comment reaction list state updates. + Stream get commentReactionListStateStream { + return commentReactionList.stream; + } + + /// Gets the comment reaction list by fetching it from the API. + /// + /// Call this in event tests to set up initial state before emitting events. + /// Skip this in API tests that only verify method calls. + /// + /// Parameters: + /// - [modifyResponse]: Optional function to customize the comment reaction list response + Future>> get({ + QueryCommentReactionsResponse Function( + QueryCommentReactionsResponse, + )? modifyResponse, + }) { + final query = commentReactionList.query; + + final defaultReactionListResponse = + createDefaultQueryCommentReactionsResponse( + reactions: [ + createDefaultReactionResponse(commentId: query.commentId), + createDefaultReactionResponse( + commentId: query.commentId, + userId: 'user-2', + ), + createDefaultReactionResponse( + commentId: query.commentId, + userId: 'user-3', + ), + ], + ); + + mockApi( + (api) => api.queryCommentReactions( + id: query.commentId, + queryCommentReactionsRequest: query.toRequest(), + ), + result: switch (modifyResponse) { + final modifier? => modifier(defaultReactionListResponse), + _ => defaultReactionListResponse, + }, + ); + + return commentReactionList.get(); + } +} + +// Creates a CommentReactionListTester for testing comment reaction list operations. +// +// Automatically sets up WebSocket connection and registers cleanup handlers. +// This function is for internal use by commentReactionListTest only. +Future _createCommentReactionListTester({ + required StreamFeedsClient client, + required CommentReactionList subject, + required MockDefaultApi feedsApi, + required MockWebSocketChannel webSocketChannel, +}) { + // Dispose comment reaction list after test + test.addTearDown(subject.dispose); + + return createTester( + client: client, + webSocketChannel: webSocketChannel, + create: (wsStreamController) => CommentReactionListTester._( + commentReactionList: subject, + wsStreamController: wsStreamController, + feedsApi: feedsApi, + ), + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/comment_reply_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/comment_reply_list_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/comment_reply_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/comment_reply_list_tester.dart index 8987e80b..712d4dd7 100644 --- a/packages/stream_feeds_test/lib/src/testers/comment_reply_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/comment_reply_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for comment reply list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/feed_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/feed_list_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/feed_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/feed_list_tester.dart index 820b8aab..3dbfee3b 100644 --- a/packages/stream_feeds_test/lib/src/testers/feed_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/feed_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for feed list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/feed_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/feed_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/feed_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/feed_tester.dart index fa3988e5..8dc783fc 100644 --- a/packages/stream_feeds_test/lib/src/testers/feed_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/feed_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for feed operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/follow_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/follow_list_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/follow_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/follow_list_tester.dart index f628364c..85d497a1 100644 --- a/packages/stream_feeds_test/lib/src/testers/follow_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/follow_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for follow list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/state/member_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/member_list_tester.dart new file mode 100644 index 00000000..575c21d2 --- /dev/null +++ b/packages/stream_feeds_test/lib/src/testers/state/member_list_tester.dart @@ -0,0 +1,147 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:test/test.dart' as test; + +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; + +/// Test helper for member list operations. +/// +/// Automatically sets up WebSocket connection, client, and test infrastructure. +/// Tests are tagged with 'member-list' by default for filtering. +/// +/// [user] is optional, the authenticated user for the test client (defaults to luke_skywalker). + +/// [build] constructs the [MemberList] under test using the provided [StreamFeedsClient]. +/// [setUp] is optional and runs before [body] for setting up mocks and test state. +/// [body] is the test callback that receives a [MemberListTester] for interactions. +/// [verify] is optional and runs after [body] for verifying API calls and interactions. +/// [tearDown] is optional and runs after [verify] for cleanup operations. +/// [skip] is optional, skip this test. +/// [tags] is optional, tags for test filtering. Defaults to ['member-list']. +/// [timeout] is optional, custom timeout for this test. +/// +/// Example: +/// ```dart +/// memberListTest( +/// 'queries initial members', +/// build: (client) => client.memberList( +/// MembersQuery(fid: FeedId(group: 'user', id: 'john')), +/// ), +/// setUp: (tester) => tester.get(), +/// body: (tester) async { +/// expect(tester.memberListState.members, hasLength(3)); +/// }, +/// ); +/// ``` +@isTest +void memberListTest( + String description, { + User user = const User(id: 'luke_skywalker'), + required MemberList Function(StreamFeedsClient client) build, + FutureOr Function(MemberListTester tester)? setUp, + required FutureOr Function(MemberListTester tester) body, + FutureOr Function(MemberListTester tester)? verify, + FutureOr Function(MemberListTester tester)? tearDown, + bool skip = false, + Iterable tags = const ['member-list'], + test.Timeout? timeout, +}) { + return testWithTester( + description, + user: user, + build: build, + createTesterFn: _createMemberListTester, + setUp: setUp, + body: body, + verify: verify, + tearDown: tearDown, + skip: skip, + tags: tags, + timeout: timeout, + ); +} + +/// A test utility for member list operations with WebSocket support. +/// +/// Provides helper methods for emitting events and verifying member list state. +/// +/// Resources are automatically cleaned up after the test completes. +final class MemberListTester extends BaseTester { + const MemberListTester._({ + required MemberList memberList, + required super.wsStreamController, + required super.feedsApi, + }) : super(subject: memberList); + + /// The member list being tested. + MemberList get memberList => subject; + + /// Current state of the member list. + MemberListState get memberListState => memberList.state; + + /// Stream of member list state updates. + Stream get memberListStateStream => memberList.stream; + + /// Gets the member list by fetching it from the API. + /// + /// Call this in event tests to set up initial state before emitting events. + /// Skip this in API tests that only verify method calls. + /// + /// Parameters: + /// - [modifyResponse]: Optional function to customize the member list response + Future>> get({ + QueryFeedMembersResponse Function(QueryFeedMembersResponse)? modifyResponse, + }) { + final query = memberList.query; + + final defaultMemberListResponse = createDefaultQueryFeedMembersResponse( + members: [ + createDefaultFeedMemberResponse(id: 'member-1'), + createDefaultFeedMemberResponse(id: 'member-2'), + createDefaultFeedMemberResponse(id: 'member-3'), + ], + ); + + mockApi( + (api) => api.queryFeedMembers( + feedId: query.fid.id, + feedGroupId: query.fid.group, + queryFeedMembersRequest: query.toRequest(), + ), + result: switch (modifyResponse) { + final modifier? => modifier(defaultMemberListResponse), + _ => defaultMemberListResponse, + }, + ); + + return memberList.get(); + } +} + +// Creates a MemberListTester for testing member list operations. +// +// Automatically sets up WebSocket connection and registers cleanup handlers. +// This function is for internal use by memberListTest only. +Future _createMemberListTester({ + required StreamFeedsClient client, + required MemberList subject, + required MockDefaultApi feedsApi, + required MockWebSocketChannel webSocketChannel, +}) { + // Dispose member list after test + test.addTearDown(subject.dispose); + + return createTester( + client: client, + webSocketChannel: webSocketChannel, + create: (wsStreamController) => MemberListTester._( + memberList: subject, + wsStreamController: wsStreamController, + feedsApi: feedsApi, + ), + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/state/moderation_config_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/moderation_config_list_tester.dart new file mode 100644 index 00000000..9022755e --- /dev/null +++ b/packages/stream_feeds_test/lib/src/testers/state/moderation_config_list_tester.dart @@ -0,0 +1,150 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:stream_feeds/stream_feeds.dart'; +import 'package:test/test.dart' as test; + +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; + +/// Test helper for moderation config list operations. +/// +/// Automatically sets up WebSocket connection, client, and test infrastructure. +/// Tests are tagged with 'moderation-config-list' by default for filtering. +/// +/// [user] is optional, the authenticated user for the test client (defaults to luke_skywalker). +/// [build] constructs the [ModerationConfigList] under test using the provided [StreamFeedsClient]. +/// [setUp] is optional and runs before [body] for setting up mocks and test state. +/// [body] is the test callback that receives a [ModerationConfigListTester] for interactions. +/// [verify] is optional and runs after [body] for verifying API calls and interactions. +/// [tearDown] is optional and runs after [verify] for cleanup operations. +/// [skip] is optional, skip this test. +/// [tags] is optional, tags for test filtering. Defaults to ['moderation-config-list']. +/// [timeout] is optional, custom timeout for this test. +/// +/// Example: +/// ```dart +/// moderationConfigListTest( +/// 'should query initial configs', +/// build: (client) => client.moderationConfigList(ModerationConfigsQuery()), +/// setUp: (tester) => tester.get(), +/// body: (tester) async { +/// expect(tester.moderationConfigListState.configs, hasLength(3)); +/// }, +/// ); +/// ``` +@isTest +void moderationConfigListTest( + String description, { + User user = const User(id: 'luke_skywalker'), + required ModerationConfigList Function(StreamFeedsClient client) build, + FutureOr Function(ModerationConfigListTester tester)? setUp, + required FutureOr Function(ModerationConfigListTester tester) body, + FutureOr Function(ModerationConfigListTester tester)? verify, + FutureOr Function(ModerationConfigListTester tester)? tearDown, + bool skip = false, + Iterable tags = const ['moderation-config-list'], + test.Timeout? timeout, +}) { + return testWithTester( + description, + user: user, + build: build, + createTesterFn: _createModerationConfigListTester, + setUp: setUp, + body: body, + verify: verify, + tearDown: tearDown, + skip: skip, + tags: tags, + timeout: timeout, + ); +} + +/// A test utility for moderation config list operations with WebSocket support. +/// +/// Provides helper methods for emitting events and verifying moderation config list state. +/// +/// Resources are automatically cleaned up after the test completes. +final class ModerationConfigListTester + extends BaseTester { + const ModerationConfigListTester._({ + required ModerationConfigList moderationConfigList, + required super.wsStreamController, + required super.feedsApi, + }) : super(subject: moderationConfigList); + + /// The moderation config list being tested. + ModerationConfigList get moderationConfigList => subject; + + /// Current state of the moderation config list. + ModerationConfigListState get moderationConfigListState { + return moderationConfigList.state; + } + + /// Stream of moderation config list state updates. + Stream get moderationConfigListStateStream { + return moderationConfigList.stream; + } + + /// Gets the moderation config list by fetching it from the API. + /// + /// Call this in event tests to set up initial state before emitting events. + /// Skip this in API tests that only verify method calls. + /// + /// Parameters: + /// - [modifyResponse]: Optional function to customize the moderation config list response + Future>> get({ + QueryModerationConfigsResponse Function( + QueryModerationConfigsResponse, + )? modifyResponse, + }) { + final query = moderationConfigList.query; + + final defaultConfigListResponse = + createDefaultQueryModerationConfigsResponse( + configs: [ + createDefaultModerationConfigResponse(key: 'config-1'), + createDefaultModerationConfigResponse(key: 'config-2'), + createDefaultModerationConfigResponse(key: 'config-3'), + ], + ); + + mockApi( + (api) => api.queryModerationConfigs( + queryModerationConfigsRequest: query.toRequest(), + ), + result: switch (modifyResponse) { + final modifier? => modifier(defaultConfigListResponse), + _ => defaultConfigListResponse, + }, + ); + + return moderationConfigList.get(); + } +} + +// Creates a ModerationConfigListTester for testing moderation config list operations. +// +// Automatically sets up WebSocket connection and registers cleanup handlers. +// This function is for internal use by moderationConfigListTest only. +Future _createModerationConfigListTester({ + required ModerationConfigList subject, + required StreamFeedsClient client, + required MockDefaultApi feedsApi, + required MockWebSocketChannel webSocketChannel, +}) { + // Dispose moderation config list after test + test.addTearDown(subject.dispose); + + return createTester( + client: client, + webSocketChannel: webSocketChannel, + create: (wsStreamController) => ModerationConfigListTester._( + moderationConfigList: subject, + wsStreamController: wsStreamController, + feedsApi: feedsApi, + ), + ); +} diff --git a/packages/stream_feeds_test/lib/src/testers/poll_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/poll_list_tester.dart similarity index 97% rename from packages/stream_feeds_test/lib/src/testers/poll_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/poll_list_tester.dart index 8d7d63ec..571e88fd 100644 --- a/packages/stream_feeds_test/lib/src/testers/poll_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/poll_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for poll list operations. /// diff --git a/packages/stream_feeds_test/lib/src/testers/poll_vote_list_tester.dart b/packages/stream_feeds_test/lib/src/testers/state/poll_vote_list_tester.dart similarity index 98% rename from packages/stream_feeds_test/lib/src/testers/poll_vote_list_tester.dart rename to packages/stream_feeds_test/lib/src/testers/state/poll_vote_list_tester.dart index b5938455..9a523d88 100644 --- a/packages/stream_feeds_test/lib/src/testers/poll_vote_list_tester.dart +++ b/packages/stream_feeds_test/lib/src/testers/state/poll_vote_list_tester.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart' as test; -import '../helpers/mocks.dart'; -import '../helpers/test_data.dart'; -import 'base_tester.dart'; +import '../../helpers/mocks.dart'; +import '../../helpers/test_data.dart'; +import '../base_tester.dart'; /// Test helper for poll vote list operations. /// diff --git a/packages/stream_feeds_test/lib/stream_feeds_test.dart b/packages/stream_feeds_test/lib/stream_feeds_test.dart index 924fd877..f4fac960 100644 --- a/packages/stream_feeds_test/lib/stream_feeds_test.dart +++ b/packages/stream_feeds_test/lib/stream_feeds_test.dart @@ -22,20 +22,21 @@ export 'src/helpers/test_data.dart'; export 'src/helpers/web_socket_mocks.dart'; // Testers -export 'src/testers/activity_comment_list_tester.dart'; -export 'src/testers/activity_list_tester.dart'; -export 'src/testers/activity_reaction_list_tester.dart'; -export 'src/testers/activity_tester.dart'; export 'src/testers/base_tester.dart'; -export 'src/testers/bookmark_folder_list_tester.dart'; -export 'src/testers/bookmark_list_tester.dart'; -export 'src/testers/comment_list_tester.dart'; -export 'src/testers/comment_reaction_list_tester.dart'; -export 'src/testers/comment_reply_list_tester.dart'; -export 'src/testers/feed_list_tester.dart'; -export 'src/testers/feed_tester.dart'; -export 'src/testers/follow_list_tester.dart'; -export 'src/testers/member_list_tester.dart'; -export 'src/testers/moderation_config_list_tester.dart'; -export 'src/testers/poll_list_tester.dart'; -export 'src/testers/poll_vote_list_tester.dart'; +export 'src/testers/moderation_client_tester.dart'; +export 'src/testers/state/activity_comment_list_tester.dart'; +export 'src/testers/state/activity_list_tester.dart'; +export 'src/testers/state/activity_reaction_list_tester.dart'; +export 'src/testers/state/activity_tester.dart'; +export 'src/testers/state/bookmark_folder_list_tester.dart'; +export 'src/testers/state/bookmark_list_tester.dart'; +export 'src/testers/state/comment_list_tester.dart'; +export 'src/testers/state/comment_reaction_list_tester.dart'; +export 'src/testers/state/comment_reply_list_tester.dart'; +export 'src/testers/state/feed_list_tester.dart'; +export 'src/testers/state/feed_tester.dart'; +export 'src/testers/state/follow_list_tester.dart'; +export 'src/testers/state/member_list_tester.dart'; +export 'src/testers/state/moderation_config_list_tester.dart'; +export 'src/testers/state/poll_list_tester.dart'; +export 'src/testers/state/poll_vote_list_tester.dart';