번호 자동 부여 대응 및 API 공통 처리 보강
This commit is contained in:
@@ -93,5 +93,62 @@ void main() {
|
||||
|
||||
verify(() => mapper.map(dioError)).called(1);
|
||||
});
|
||||
|
||||
group('unwrapAsMap', () {
|
||||
test('data 키가 있으면 내부 맵을 반환한다', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
statusCode: 200,
|
||||
data: {
|
||||
'data': {'token': 'abc'},
|
||||
},
|
||||
);
|
||||
|
||||
expect(client.unwrapAsMap(response), {'token': 'abc'});
|
||||
});
|
||||
|
||||
test('data 키가 없으면 원본 맵을 반환한다', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: RequestOptions(path: '/menus'),
|
||||
statusCode: 200,
|
||||
data: {'items': []},
|
||||
);
|
||||
|
||||
expect(client.unwrapAsMap(response), {'items': []});
|
||||
});
|
||||
|
||||
test('본문이 null이면 빈 맵을 반환한다', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: RequestOptions(path: '/menus'),
|
||||
statusCode: 200,
|
||||
);
|
||||
|
||||
expect(client.unwrapAsMap(response), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('unwrap', () {
|
||||
test('data 키가 있는 리스트를 반환한다', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: RequestOptions(path: '/reports'),
|
||||
statusCode: 200,
|
||||
data: {
|
||||
'data': [1, 2, 3],
|
||||
},
|
||||
);
|
||||
|
||||
expect(client.unwrap(response), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('data 키가 없으면 원본을 그대로 반환한다', () {
|
||||
final response = Response<dynamic>(
|
||||
requestOptions: RequestOptions(path: '/health'),
|
||||
statusCode: 200,
|
||||
data: {'status': 'ok'},
|
||||
);
|
||||
|
||||
expect(client.unwrap(response), {'status': 'ok'});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,4 +80,60 @@ void main() {
|
||||
expect(asIterable(action), contains('approve'));
|
||||
expect(asIterable(resource), contains('stock-transactions'));
|
||||
});
|
||||
|
||||
group('401 응답 메시지 매핑', () {
|
||||
test('invalid credentials 메시지를 한글로 변환한다', () {
|
||||
final mapper = ApiErrorMapper();
|
||||
final requestOptions = RequestOptions(path: '/auth/login');
|
||||
final dioException = DioException(
|
||||
requestOptions: requestOptions,
|
||||
type: DioExceptionType.badResponse,
|
||||
response: Response<dynamic>(
|
||||
requestOptions: requestOptions,
|
||||
statusCode: 401,
|
||||
data: {
|
||||
'error': {
|
||||
'message': 'invalid credentials',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final exception = mapper.map(dioException);
|
||||
|
||||
expect(exception.code, ApiErrorCode.unauthorized);
|
||||
expect(exception.message, '아이디 또는 비밀번호가 올바르지 않습니다.');
|
||||
});
|
||||
|
||||
test('token expired 메시지를 세션 만료 문구로 변환하고 상세를 보존한다', () {
|
||||
final mapper = ApiErrorMapper();
|
||||
final requestOptions = RequestOptions(path: '/auth/refresh');
|
||||
final dioException = DioException(
|
||||
requestOptions: requestOptions,
|
||||
type: DioExceptionType.badResponse,
|
||||
response: Response<dynamic>(
|
||||
requestOptions: requestOptions,
|
||||
statusCode: 401,
|
||||
data: {
|
||||
'error': {
|
||||
'message': 'token expired',
|
||||
'details': [
|
||||
{'field': 'token', 'message': '만료되었습니다.'},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final exception = mapper.map(dioException);
|
||||
|
||||
expect(exception.message, '세션이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
final tokenDetails = exception.details?['token'];
|
||||
if (tokenDetails is Iterable) {
|
||||
expect(tokenDetails, contains('만료되었습니다.'));
|
||||
} else {
|
||||
expect(tokenDetails, '만료되었습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,11 +15,28 @@ void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
registerFallbackValue(
|
||||
Response<dynamic>(requestOptions: RequestOptions(path: '/')),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = ApprovalRepositoryRemote(apiClient: apiClient);
|
||||
when(() => apiClient.unwrapAsMap(any())).thenAnswer((invocation) {
|
||||
final response = invocation.positionalArguments.first;
|
||||
if (response is Response) {
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final nested = data['data'];
|
||||
if (nested is Map<String, dynamic>) {
|
||||
return nested;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return <String, dynamic>{};
|
||||
});
|
||||
});
|
||||
|
||||
test('list는 신규 필터 파라미터를 전달한다', () async {
|
||||
@@ -93,7 +110,6 @@ void main() {
|
||||
|
||||
final input = ApprovalCreateInput(
|
||||
transactionId: 9001,
|
||||
approvalNo: 'APP-2025-0001',
|
||||
approvalStatusId: 1,
|
||||
requestedById: 7,
|
||||
note: ' 신규 결재 ',
|
||||
@@ -113,7 +129,7 @@ void main() {
|
||||
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.containsKey('approval_no'), isFalse);
|
||||
expect(payload['approval_status_id'], 1);
|
||||
expect(payload['requested_by_id'], 7);
|
||||
expect(payload['note'], '신규 결재');
|
||||
|
||||
@@ -116,17 +116,18 @@ void main() {
|
||||
expect(transactionField, findsOneWidget);
|
||||
expect(approvalField, findsOneWidget);
|
||||
|
||||
final transactionInput = find.descendant(
|
||||
of: transactionField,
|
||||
matching: find.byType(EditableText),
|
||||
expect(
|
||||
find.descendant(
|
||||
of: transactionField,
|
||||
matching: find.byType(EditableText),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
final approvalInput = find.descendant(
|
||||
of: approvalField,
|
||||
matching: find.byType(EditableText),
|
||||
expect(
|
||||
find.descendant(of: approvalField, matching: find.byType(EditableText)),
|
||||
findsNothing,
|
||||
);
|
||||
await tester.enterText(transactionInput.first, 'IN-TEST-001');
|
||||
await tester.enterText(approvalInput.first, 'APP-TEST-001');
|
||||
await tester.pump();
|
||||
expect(find.text('저장 시 자동 생성'), findsAtLeastNWidgets(2));
|
||||
|
||||
final productFields = find.byType(InventoryProductAutocompleteField);
|
||||
expect(productFields, findsWidgets);
|
||||
@@ -161,7 +162,7 @@ void main() {
|
||||
expect(find.text('동일 제품이 중복되었습니다.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('입고 등록 모달은 거래번호와 결재번호를 필수로 요구한다', (tester) async {
|
||||
testWidgets('입고 등록 모달은 번호 입력 없이 저장 안내만 제공한다', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
@@ -190,10 +191,14 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(ShadButton, '입고 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('저장 시 자동 생성'), findsAtLeastNWidgets(2));
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '저장'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('거래번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('거래번호를 입력하세요.'), findsNothing);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsNothing);
|
||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||
expect(find.textContaining('창고를 선택'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ void main() {
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
testWidgets('출고 등록 모달은 거래번호와 결재번호를 필수로 요구한다', (tester) async {
|
||||
testWidgets('출고 등록 모달은 번호 입력 없이 자동 생성 안내를 제공한다', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
@@ -54,10 +54,14 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(ShadButton, '출고 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('저장 시 자동 생성'), findsAtLeastNWidgets(2));
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '저장'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('거래번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('거래번호를 입력하세요.'), findsNothing);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsNothing);
|
||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||
expect(find.text('창고를 선택하세요.'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ void main() {
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
testWidgets('대여 등록 모달은 거래번호와 결재번호를 필수로 요구한다', (tester) async {
|
||||
testWidgets('대여 등록 모달은 번호 입력 없이 자동 생성 안내를 제공한다', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
@@ -54,10 +54,14 @@ void main() {
|
||||
await tester.tap(find.widgetWithText(ShadButton, '대여 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('저장 시 자동 생성'), findsAtLeastNWidgets(2));
|
||||
|
||||
await tester.tap(find.widgetWithText(ShadButton, '저장'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('거래번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsOneWidget);
|
||||
expect(find.text('거래번호를 입력하세요.'), findsNothing);
|
||||
expect(find.text('결재번호를 입력하세요.'), findsNothing);
|
||||
expect(find.textContaining('자동완성에서 선택'), findsOneWidget);
|
||||
expect(find.text('최소 1개의 고객사를 선택하세요.'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,6 +164,11 @@ void main() {
|
||||
),
|
||||
],
|
||||
customers: [TransactionCustomerCreateInput(customerId: 7)],
|
||||
approval: StockTransactionApprovalInput(
|
||||
requestedById: 9,
|
||||
approvalStatusId: 5,
|
||||
note: '승인 요청',
|
||||
),
|
||||
);
|
||||
|
||||
await repository.create(input);
|
||||
@@ -185,6 +190,12 @@ void main() {
|
||||
expect(payload['created_by_id'], 9);
|
||||
expect(payload['lines'], isA<List>());
|
||||
expect(payload['customers'], isA<List>());
|
||||
expect(payload.containsKey('transaction_no'), isFalse);
|
||||
final approvalPayload = payload['approval'] as Map<String, dynamic>;
|
||||
expect(approvalPayload.containsKey('approval_no'), isFalse);
|
||||
expect(approvalPayload['requested_by_id'], 9);
|
||||
expect(approvalPayload['approval_status_id'], 5);
|
||||
expect(approvalPayload['note'], '승인 요청');
|
||||
});
|
||||
|
||||
test('submit은 /submit 엔드포인트를 호출한다', () async {
|
||||
|
||||
@@ -14,11 +14,28 @@ void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
registerFallbackValue(
|
||||
Response<dynamic>(requestOptions: RequestOptions(path: '/')),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
apiClient = _MockApiClient();
|
||||
repository = CustomerRepositoryRemote(apiClient: apiClient);
|
||||
when(() => apiClient.unwrapAsMap(any())).thenAnswer((invocation) {
|
||||
final response = invocation.positionalArguments.first;
|
||||
if (response is Response) {
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final nested = data['data'];
|
||||
if (nested is Map<String, dynamic>) {
|
||||
return nested;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return <String, dynamic>{};
|
||||
});
|
||||
});
|
||||
|
||||
test('list 호출 시 필터를 쿼리에 포함한다', () async {
|
||||
|
||||
Reference in New Issue
Block a user