chore: 통합 테스트 환경과 보고서 리모트 구성

This commit is contained in:
JiWoong Sul
2025-10-14 18:11:57 +09:00
parent 8067416c09
commit 7e0f7b1c55
25 changed files with 1608 additions and 1 deletions

View File

@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/core/common/models/paginated_result.dart';
import 'package:superport_v2/core/constants/app_sections.dart';
import 'package:superport_v2/core/permissions/permission_manager.dart';
import 'package:superport_v2/core/permissions/permission_resources.dart';
import 'package:superport_v2/features/login/presentation/pages/login_page.dart';
import 'package:superport_v2/features/masters/group/domain/entities/group.dart';
import 'package:superport_v2/features/masters/group/domain/repositories/group_repository.dart';
import 'package:superport_v2/features/masters/group_permission/domain/entities/group_permission.dart';
import 'package:superport_v2/features/masters/group_permission/domain/repositories/group_permission_repository.dart';
class _MockGroupRepository extends Mock implements GroupRepository {}
class _MockGroupPermissionRepository extends Mock
implements GroupPermissionRepository {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
tearDown(() async {
await GetIt.I.reset();
});
testWidgets('로그인 성공 시 권한 동기화 후 대시보드로 이동한다', (tester) async {
final groupRepository = _MockGroupRepository();
final permissionRepository = _MockGroupPermissionRepository();
GetIt.I.registerSingleton<GroupRepository>(groupRepository);
GetIt.I.registerSingleton<GroupPermissionRepository>(permissionRepository);
when(
() => groupRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isDefault: any(named: 'isDefault'),
isActive: any(named: 'isActive'),
includePermissions: any(named: 'includePermissions'),
includeEmployees: any(named: 'includeEmployees'),
),
).thenAnswer((invocation) async {
final items = [Group(id: 1, groupName: '관리자')];
return PaginatedResult<Group>(
items: items,
page: 1,
pageSize: 1,
total: 1,
);
});
when(
() => permissionRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
groupId: any(named: 'groupId'),
menuId: any(named: 'menuId'),
isActive: any(named: 'isActive'),
includeDeleted: any(named: 'includeDeleted'),
),
).thenAnswer(
(_) async => PaginatedResult<GroupPermission>(
items: [
GroupPermission(
id: 1,
group: GroupPermissionGroup(id: 1, groupName: '관리자'),
menu: GroupPermissionMenu(
id: 10,
menuCode: 'INBOUND',
menuName: '입고',
path: '/inventory/inbound',
),
canCreate: true,
canRead: true,
),
],
page: 1,
pageSize: 200,
total: 1,
),
);
final manager = PermissionManager();
final router = GoRouter(
initialLocation: loginRoutePath,
routes: [
GoRoute(
path: loginRoutePath,
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: dashboardRoutePath,
builder: (context, state) =>
const Scaffold(body: Center(child: Text('대시보드'))),
),
],
);
await tester.pumpWidget(
PermissionScope(
manager: manager,
child: ShadApp.router(routerConfig: router),
),
);
await tester.enterText(find.byType(EditableText).at(0), 'user@superport');
await tester.enterText(find.byType(EditableText).at(1), 'password');
await tester.tap(find.text('로그인'));
await tester.pumpAndSettle();
expect(
router.routerDelegate.currentConfiguration.last.matchedLocation,
dashboardRoutePath,
);
expect(
manager.can(PermissionResources.stockTransactions, PermissionAction.view),
isTrue,
);
expect(
manager.can(
PermissionResources.stockTransactions,
PermissionAction.create,
),
isTrue,
);
});
testWidgets('권한 동기화 실패 시 오류 메시지를 표시한다', (tester) async {
final groupRepository = _MockGroupRepository();
final permissionRepository = _MockGroupPermissionRepository();
GetIt.I.registerSingleton<GroupRepository>(groupRepository);
GetIt.I.registerSingleton<GroupPermissionRepository>(permissionRepository);
when(
() => groupRepository.list(
page: any(named: 'page'),
pageSize: any(named: 'pageSize'),
query: any(named: 'query'),
isDefault: any(named: 'isDefault'),
isActive: any(named: 'isActive'),
includePermissions: any(named: 'includePermissions'),
includeEmployees: any(named: 'includeEmployees'),
),
).thenThrow(Exception('network error'));
final manager = PermissionManager();
final router = GoRouter(
initialLocation: loginRoutePath,
routes: [
GoRoute(
path: loginRoutePath,
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: dashboardRoutePath,
builder: (context, state) =>
const Scaffold(body: Center(child: Text('대시보드'))),
),
],
);
await tester.pumpWidget(
PermissionScope(
manager: manager,
child: ShadApp.router(routerConfig: router),
),
);
await tester.enterText(find.byType(EditableText).at(0), 'user@superport');
await tester.enterText(find.byType(EditableText).at(1), 'password');
await tester.tap(find.text('로그인'));
await tester.pumpAndSettle();
expect(find.text('권한 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요.'), findsOneWidget);
expect(
router.routerDelegate.currentConfiguration.last.matchedLocation,
loginRoutePath,
);
expect(
manager.can(PermissionResources.stockTransactions, PermissionAction.view),
isTrue,
);
});
}

View File

@@ -0,0 +1,152 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:superport_v2/core/network/api_client.dart';
import 'package:superport_v2/features/reporting/data/repositories/reporting_repository_remote.dart';
import 'package:superport_v2/features/reporting/domain/entities/report_export_format.dart';
import 'package:superport_v2/features/reporting/domain/entities/report_export_request.dart';
class _MockApiClient extends Mock implements ApiClient {}
void main() {
late ApiClient apiClient;
late ReportingRepositoryRemote repository;
setUpAll(() {
registerFallbackValue(Options());
registerFallbackValue(CancelToken());
});
setUp(() {
apiClient = _MockApiClient();
repository = ReportingRepositoryRemote(apiClient: apiClient);
});
Response<Uint8List> jsonResponse(String path, Map<String, dynamic> body) {
final bytes = Uint8List.fromList(jsonEncode(body).codeUnits);
return Response<Uint8List>(
data: bytes,
headers: Headers.fromMap({
'content-type': ['application/json'],
}),
requestOptions: RequestOptions(path: path),
statusCode: 200,
);
}
Response<Uint8List> binaryResponse(
String path, {
required List<int> bytes,
required String filename,
required String mimeType,
}) {
return Response<Uint8List>(
data: Uint8List.fromList(bytes),
headers: Headers.fromMap({
'content-type': [mimeType],
'content-disposition': ['attachment; filename="$filename"'],
}),
requestOptions: RequestOptions(path: path),
statusCode: 200,
);
}
test('exportTransactions는 download_url을 파싱한다', () async {
const path = '/api/v1/reports/transactions/export';
when(
() => apiClient.get<Uint8List>(
path,
query: any(named: 'query'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer(
(_) async => jsonResponse(path, {
'data': {
'download_url': 'https://example.com/report.xlsx',
'filename': 'report.xlsx',
'mime_type':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'expires_at': '2025-01-01T00:00:00Z',
},
}),
);
final request = ReportExportRequest(
from: DateTime(2024, 1, 1),
to: DateTime(2024, 1, 31),
format: ReportExportFormat.xlsx,
transactionTypeId: 3,
statusId: 1,
warehouseId: 9,
);
final result = await repository.exportTransactions(request);
final captured = verify(
() => apiClient.get<Uint8List>(
captureAny(),
query: captureAny(named: 'query'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).captured;
expect(captured.first, equals(path));
final query = captured[1] as Map<String, dynamic>;
expect(query['from'], request.from.toIso8601String());
expect(query['to'], request.to.toIso8601String());
expect(query['format'], 'xlsx');
expect(query['type_id'], 3);
expect(query['status_id'], 1);
expect(query['warehouse_id'], 9);
expect(result.downloadUrl.toString(), 'https://example.com/report.xlsx');
expect(result.filename, 'report.xlsx');
expect(
result.mimeType,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
expect(result.expiresAt, DateTime.parse('2025-01-01T00:00:00Z'));
expect(result.hasDownloadUrl, isTrue);
expect(result.hasBytes, isFalse);
});
test('exportApprovals는 바이너리 응답을 처리한다', () async {
const path = '/api/v1/reports/approvals/export';
when(
() => apiClient.get<Uint8List>(
path,
query: any(named: 'query'),
options: any(named: 'options'),
cancelToken: any(named: 'cancelToken'),
),
).thenAnswer(
(_) async => binaryResponse(
path,
bytes: [1, 2, 3],
filename: 'approval.pdf',
mimeType: 'application/pdf',
),
);
final request = ReportExportRequest(
from: DateTime(2024, 2, 1),
to: DateTime(2024, 2, 15),
format: ReportExportFormat.pdf,
statusId: 5,
);
final result = await repository.exportApprovals(request);
expect(result.hasBytes, isTrue);
expect(result.bytes, isNotNull);
expect(result.filename, 'approval.pdf');
expect(result.mimeType, 'application/pdf');
expect(result.hasDownloadUrl, isFalse);
});
}