결재 API 계약 보완 및 테스트 정리
This commit is contained in:
@@ -245,7 +245,7 @@ class _StubApprovalRepository implements ApprovalRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> create(ApprovalInput input) {
|
||||
Future<Approval> create(ApprovalCreateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ class _StubApprovalRepository implements ApprovalRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Approval> update(int id, ApprovalInput input) {
|
||||
Future<Approval> update(ApprovalUpdateInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,104 @@ void main() {
|
||||
expect(query['include'], 'steps,histories');
|
||||
});
|
||||
|
||||
test('create는 필수 필드를 전달한다', () async {
|
||||
const path = '/api/v1/approvals';
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 5001,
|
||||
'approval_no': 'APP-2025-0001',
|
||||
'transaction_id': 9001,
|
||||
},
|
||||
},
|
||||
statusCode: 201,
|
||||
requestOptions: RequestOptions(path: path),
|
||||
),
|
||||
);
|
||||
|
||||
final input = ApprovalCreateInput(
|
||||
transactionId: 9001,
|
||||
approvalNo: 'APP-2025-0001',
|
||||
approvalStatusId: 1,
|
||||
requestedById: 7,
|
||||
note: ' 신규 결재 ',
|
||||
);
|
||||
|
||||
await repository.create(input);
|
||||
|
||||
final captured = verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured;
|
||||
|
||||
expect(captured.first, equals(path));
|
||||
final payload = captured[1] as Map<String, dynamic>;
|
||||
expect(payload['transaction_id'], 9001);
|
||||
expect(payload['approval_no'], 'APP-2025-0001');
|
||||
expect(payload['approval_status_id'], 1);
|
||||
expect(payload['requested_by_id'], 7);
|
||||
expect(payload['note'], '신규 결재');
|
||||
});
|
||||
|
||||
test('update는 id를 포함해 패치를 수행한다', () async {
|
||||
const path = '/api/v1/approvals/5001';
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
path,
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 5001,
|
||||
'approval_no': 'APP-2025-0001',
|
||||
'approval_status': {'id': 2, 'status_name': '진행중'},
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: path),
|
||||
),
|
||||
);
|
||||
|
||||
final input = ApprovalUpdateInput(
|
||||
id: 5001,
|
||||
approvalStatusId: 2,
|
||||
note: '보류',
|
||||
);
|
||||
|
||||
await repository.update(input);
|
||||
|
||||
final captured = verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
captureAny(),
|
||||
data: captureAny(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).captured;
|
||||
|
||||
expect(captured.first, equals(path));
|
||||
final payload = captured[1] as Map<String, dynamic>;
|
||||
expect(payload['id'], 5001);
|
||||
expect(payload['approval_status_id'], 2);
|
||||
expect(payload['note'], '보류');
|
||||
});
|
||||
|
||||
Map<String, dynamic> buildStep({
|
||||
required int id,
|
||||
required int order,
|
||||
|
||||
@@ -15,7 +15,10 @@ import 'package:superport_v2/features/inventory/lookups/domain/repositories/inve
|
||||
class _MockApprovalRepository extends Mock implements ApprovalRepository {}
|
||||
|
||||
/// Approval 생성 요청을 대체하기 위한 가짜 입력.
|
||||
class _FakeApprovalInput extends Fake implements ApprovalInput {}
|
||||
class _FakeApprovalCreateInput extends Fake implements ApprovalCreateInput {}
|
||||
|
||||
/// Approval 수정 요청을 대체하기 위한 가짜 입력.
|
||||
class _FakeApprovalUpdateInput extends Fake implements ApprovalUpdateInput {}
|
||||
|
||||
/// 단계 행위 요청을 대체하기 위한 가짜 입력.
|
||||
class _FakeStepActionInput extends Fake implements ApprovalStepActionInput {}
|
||||
@@ -68,7 +71,8 @@ void main() {
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeApprovalInput());
|
||||
registerFallbackValue(_FakeApprovalCreateInput());
|
||||
registerFallbackValue(_FakeApprovalUpdateInput());
|
||||
registerFallbackValue(_FakeStepActionInput());
|
||||
registerFallbackValue(_FakeStepAssignmentInput());
|
||||
});
|
||||
|
||||
@@ -18,7 +18,9 @@ import 'package:superport_v2/features/inventory/lookups/domain/repositories/inve
|
||||
|
||||
class _MockApprovalRepository extends Mock implements ApprovalRepository {}
|
||||
|
||||
class _FakeApprovalInput extends Fake implements ApprovalInput {}
|
||||
class _FakeApprovalCreateInput extends Fake implements ApprovalCreateInput {}
|
||||
|
||||
class _FakeApprovalUpdateInput extends Fake implements ApprovalUpdateInput {}
|
||||
|
||||
class _MockApprovalTemplateRepository extends Mock
|
||||
implements ApprovalTemplateRepository {}
|
||||
@@ -45,7 +47,8 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(_FakeApprovalInput());
|
||||
registerFallbackValue(_FakeApprovalCreateInput());
|
||||
registerFallbackValue(_FakeApprovalUpdateInput());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
||||
@@ -6,10 +6,16 @@ import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/services/token_storage.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/auth/application/auth_service.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/auth_session.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/login_request.dart';
|
||||
import 'package:superport_v2/features/auth/domain/repositories/auth_repository.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';
|
||||
@@ -21,9 +27,69 @@ class _MockGroupRepository extends Mock implements GroupRepository {}
|
||||
class _MockGroupPermissionRepository extends Mock
|
||||
implements GroupPermissionRepository {}
|
||||
|
||||
class _MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
class _FakeTokenStorage implements TokenStorage {
|
||||
String? _accessToken;
|
||||
String? _refreshToken;
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
_accessToken = null;
|
||||
_refreshToken = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> readAccessToken() async => _accessToken;
|
||||
|
||||
@override
|
||||
Future<String?> readRefreshToken() async => _refreshToken;
|
||||
|
||||
@override
|
||||
Future<void> writeAccessToken(String? token) async {
|
||||
_accessToken = token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeRefreshToken(String? token) async {
|
||||
_refreshToken = token;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
LoginRequest(identifier: '', password: '', rememberMe: false),
|
||||
);
|
||||
});
|
||||
|
||||
late _MockAuthRepository authRepository;
|
||||
late _FakeTokenStorage tokenStorage;
|
||||
late AuthService authService;
|
||||
|
||||
setUp(() {
|
||||
authRepository = _MockAuthRepository();
|
||||
tokenStorage = _FakeTokenStorage();
|
||||
authService = AuthService(
|
||||
repository: authRepository,
|
||||
tokenStorage: tokenStorage,
|
||||
);
|
||||
final sampleSession = AuthSession(
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
user: const AuthenticatedUser(id: 1, name: '테스터'),
|
||||
permissions: const [],
|
||||
);
|
||||
|
||||
GetIt.I.registerSingleton<AuthService>(authService);
|
||||
|
||||
when(() => authRepository.login(any())).thenAnswer((_) async => sampleSession);
|
||||
when(() => authRepository.refresh(any())).thenThrow(UnimplementedError());
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
@@ -98,8 +98,8 @@ void main() {
|
||||
|
||||
expect(captured.first, equals(path));
|
||||
final query = captured[1] as Map<String, dynamic>;
|
||||
expect(query['date_from'], '2024-01-01');
|
||||
expect(query['date_to'], '2024-01-31');
|
||||
expect(query['from'], '2024-01-01');
|
||||
expect(query['to'], '2024-01-31');
|
||||
expect(query['format'], 'xlsx');
|
||||
expect(query['transaction_status_id'], 3);
|
||||
expect(query['approval_status_id'], 7);
|
||||
@@ -143,10 +143,69 @@ void main() {
|
||||
|
||||
final result = await repository.exportApprovals(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'], DateTime(2024, 2, 1).toIso8601String());
|
||||
expect(query['to'], DateTime(2024, 2, 15).toIso8601String());
|
||||
expect(query['format'], 'pdf');
|
||||
expect(query.containsKey('transaction_status_id'), isFalse);
|
||||
expect(result.hasBytes, isTrue);
|
||||
expect(result.bytes, isNotNull);
|
||||
expect(result.filename, 'approval.pdf');
|
||||
expect(result.mimeType, 'application/pdf');
|
||||
expect(result.hasDownloadUrl, isFalse);
|
||||
});
|
||||
|
||||
test('exportApprovals는 transaction_status_id 파라미터를 전달한다', () 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: [4, 5, 6],
|
||||
filename: 'approval.xlsx',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
),
|
||||
);
|
||||
|
||||
final request = ReportExportRequest(
|
||||
from: DateTime(2024, 4, 1),
|
||||
to: DateTime(2024, 4, 30),
|
||||
format: ReportExportFormat.xlsx,
|
||||
transactionStatusId: 2,
|
||||
approvalStatusId: 4,
|
||||
);
|
||||
|
||||
await repository.exportApprovals(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['transaction_status_id'], 2);
|
||||
expect(query['approval_status_id'], 4);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ 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/config/environment.dart';
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||
import 'package:superport_v2/core/theme/superport_shad_theme.dart';
|
||||
import 'package:superport_v2/core/services/token_storage.dart';
|
||||
import 'package:superport_v2/features/auth/application/auth_service.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/auth_session.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/login_request.dart';
|
||||
import 'package:superport_v2/features/auth/domain/repositories/auth_repository.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';
|
||||
@@ -222,9 +229,74 @@ class _StubGroupPermissionRepository implements GroupPermissionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
class _MockAuthRepository extends Mock implements AuthRepository {}
|
||||
|
||||
class _FakeTokenStorage implements TokenStorage {
|
||||
String? _accessToken;
|
||||
String? _refreshToken;
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
_accessToken = null;
|
||||
_refreshToken = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> readAccessToken() async => _accessToken;
|
||||
|
||||
@override
|
||||
Future<String?> readRefreshToken() async => _refreshToken;
|
||||
|
||||
@override
|
||||
Future<void> writeAccessToken(String? token) async {
|
||||
_accessToken = token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeRefreshToken(String? token) async {
|
||||
_refreshToken = token;
|
||||
}
|
||||
}
|
||||
|
||||
AuthSession _buildSampleSession() {
|
||||
return const AuthSession(
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresAt: null,
|
||||
user: AuthenticatedUser(id: 1, name: '테스터'),
|
||||
permissions: [],
|
||||
);
|
||||
}
|
||||
|
||||
void _registerAuthService(
|
||||
_MockAuthRepository repository,
|
||||
_FakeTokenStorage storage,
|
||||
) {
|
||||
final service = AuthService(repository: repository, tokenStorage: storage);
|
||||
when(() => repository.login(any())).thenAnswer((_) async => _buildSampleSession());
|
||||
when(() => repository.refresh(any())).thenThrow(UnimplementedError());
|
||||
GetIt.I.registerSingleton<AuthService>(service);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
LoginRequest(identifier: '', password: '', rememberMe: false),
|
||||
);
|
||||
});
|
||||
|
||||
late _MockAuthRepository authRepository;
|
||||
late _FakeTokenStorage tokenStorage;
|
||||
|
||||
setUp(() async {
|
||||
await GetIt.I.reset();
|
||||
authRepository = _MockAuthRepository();
|
||||
tokenStorage = _FakeTokenStorage();
|
||||
_registerAuthService(authRepository, tokenStorage);
|
||||
});
|
||||
|
||||
setUpAll(() async {
|
||||
await Environment.initialize();
|
||||
});
|
||||
@@ -240,12 +312,10 @@ void main() {
|
||||
view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
await GetIt.I.reset();
|
||||
GetIt.I.registerSingleton<GroupRepository>(_StubGroupRepository());
|
||||
GetIt.I.registerSingleton<GroupPermissionRepository>(
|
||||
_StubGroupPermissionRepository(),
|
||||
);
|
||||
addTearDown(() async => GetIt.I.reset());
|
||||
|
||||
final router = _createTestRouter();
|
||||
await tester.pumpWidget(_TestApp(router: router));
|
||||
|
||||
Reference in New Issue
Block a user