fix: API 응답 파싱 오류 수정 및 에러 처리 개선
주요 변경사항: - 창고 관리 API 응답 구조와 DTO 불일치 수정 - WarehouseLocationDto에 code, manager_phone 필드 추가 - RemoteDataSource에서 API 응답을 DTO 구조에 맞게 변환 - 회사 관리 API 응답 파싱 오류 수정 - CompanyResponse의 필수 필드를 nullable로 변경 - PaginatedResponse 구조 매핑 로직 개선 - 에러 처리 및 로깅 개선 - Service Layer에 상세 에러 로깅 추가 - Controller에서 에러 타입별 처리 - 새로운 유틸리티 추가 - ResponseInterceptor: API 응답 정규화 - DebugLogger: 디버깅 도구 - HealthCheckService: 서버 상태 확인 - 문서화 - API 통합 테스트 가이드 - 에러 분석 보고서 - 리팩토링 계획서
This commit is contained in:
326
test/api/api_error_diagnosis_test.dart
Normal file
326
test/api/api_error_diagnosis_test.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
import 'package:superport/core/utils/login_diagnostics.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
/// API 에러 진단을 위한 테스트
|
||||
/// 실제 API 호출 시 발생하는 타입 에러와 응답 형식 문제를 파악합니다.
|
||||
void main() {
|
||||
group('API 응답 형식 및 타입 에러 진단', () {
|
||||
test('로그인 응답 JSON 파싱 - snake_case 필드명', () {
|
||||
// API가 snake_case로 응답하는 경우
|
||||
final snakeCaseResponse = {
|
||||
'access_token': 'test_token_123',
|
||||
'refresh_token': 'refresh_token_456',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// 파싱 시도
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(snakeCaseResponse);
|
||||
print('[성공] snake_case 응답 파싱 성공');
|
||||
print('Access Token: ${loginResponse.accessToken}');
|
||||
print('User Email: ${loginResponse.user.email}');
|
||||
|
||||
// 검증
|
||||
expect(loginResponse.accessToken, 'test_token_123');
|
||||
expect(loginResponse.refreshToken, 'refresh_token_456');
|
||||
expect(loginResponse.user.email, 'test@example.com');
|
||||
} catch (e, stackTrace) {
|
||||
print('[실패] snake_case 응답 파싱 실패');
|
||||
print('에러: $e');
|
||||
print('스택 트레이스: $stackTrace');
|
||||
fail('snake_case 응답 파싱에 실패했습니다: $e');
|
||||
}
|
||||
});
|
||||
|
||||
test('로그인 응답 JSON 파싱 - camelCase 필드명', () {
|
||||
// API가 camelCase로 응답하는 경우
|
||||
final camelCaseResponse = {
|
||||
'accessToken': 'test_token_123',
|
||||
'refreshToken': 'refresh_token_456',
|
||||
'tokenType': 'Bearer',
|
||||
'expiresIn': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// 파싱 시도
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(camelCaseResponse);
|
||||
print('[성공] camelCase 응답 파싱 성공');
|
||||
print('Access Token: ${loginResponse.accessToken}');
|
||||
|
||||
// 이 테스트는 실패할 것으로 예상됨 (현재 모델이 snake_case 기준)
|
||||
fail('camelCase 응답이 성공하면 안됩니다 (모델이 snake_case 기준)');
|
||||
} catch (e) {
|
||||
print('[예상된 실패] camelCase 응답 파싱 실패 (정상)');
|
||||
print('에러: $e');
|
||||
// 이는 예상된 동작임
|
||||
expect(e, isNotNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('다양한 API 응답 형식 처리 테스트', () {
|
||||
// 테스트 케이스들
|
||||
final testCases = [
|
||||
{
|
||||
'name': '형식 1: success/data 래핑',
|
||||
'response': {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'token1',
|
||||
'refresh_token': 'refresh1',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'user1',
|
||||
'email': 'user1@test.com',
|
||||
'name': '사용자1',
|
||||
'role': 'USER',
|
||||
},
|
||||
},
|
||||
},
|
||||
'expectSuccess': false, // 직접 파싱은 실패해야 함
|
||||
},
|
||||
{
|
||||
'name': '형식 2: 직접 응답',
|
||||
'response': {
|
||||
'access_token': 'token2',
|
||||
'refresh_token': 'refresh2',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 2,
|
||||
'username': 'user2',
|
||||
'email': 'user2@test.com',
|
||||
'name': '사용자2',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
'expectSuccess': true,
|
||||
},
|
||||
{
|
||||
'name': '형식 3: 필수 필드 누락',
|
||||
'response': {
|
||||
'access_token': 'token3',
|
||||
// refresh_token 누락
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 3,
|
||||
'username': 'user3',
|
||||
'email': 'user3@test.com',
|
||||
'name': '사용자3',
|
||||
'role': 'USER',
|
||||
},
|
||||
},
|
||||
'expectSuccess': false,
|
||||
},
|
||||
];
|
||||
|
||||
for (final testCase in testCases) {
|
||||
print('\n테스트: ${testCase['name']}');
|
||||
final response = testCase['response'] as Map<String, dynamic>;
|
||||
final expectSuccess = testCase['expectSuccess'] as bool;
|
||||
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(response);
|
||||
if (expectSuccess) {
|
||||
print('✅ 파싱 성공 (예상대로)');
|
||||
expect(loginResponse.accessToken, isNotNull);
|
||||
} else {
|
||||
print('❌ 파싱 성공 (실패해야 하는데 성공함)');
|
||||
fail('${testCase['name']} - 파싱이 실패해야 하는데 성공했습니다');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!expectSuccess) {
|
||||
print('✅ 파싱 실패 (예상대로): $e');
|
||||
} else {
|
||||
print('❌ 파싱 실패 (성공해야 하는데 실패함): $e');
|
||||
fail('${testCase['name']} - 파싱이 성공해야 하는데 실패했습니다: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('AuthUser 모델 파싱 테스트', () {
|
||||
final testUser = {
|
||||
'id': 100,
|
||||
'username': 'johndoe',
|
||||
'email': 'john@example.com',
|
||||
'name': 'John Doe',
|
||||
'role': 'ADMIN',
|
||||
};
|
||||
|
||||
try {
|
||||
final user = AuthUser.fromJson(testUser);
|
||||
expect(user.id, 100);
|
||||
expect(user.username, 'johndoe');
|
||||
expect(user.email, 'john@example.com');
|
||||
expect(user.name, 'John Doe');
|
||||
expect(user.role, 'ADMIN');
|
||||
print('✅ AuthUser 파싱 성공');
|
||||
} catch (e) {
|
||||
fail('AuthUser 파싱 실패: $e');
|
||||
}
|
||||
});
|
||||
|
||||
test('실제 API 응답 시뮬레이션', () async {
|
||||
// 실제 API가 반환할 수 있는 다양한 응답들
|
||||
final possibleResponses = [
|
||||
// Spring Boot 기본 응답
|
||||
Response(
|
||||
data: {
|
||||
'timestamp': '2024-01-31T10:00:00',
|
||||
'status': 200,
|
||||
'data': {
|
||||
'access_token': 'jwt_token_here',
|
||||
'refresh_token': 'refresh_token_here',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
),
|
||||
// FastAPI 스타일 응답
|
||||
Response(
|
||||
data: {
|
||||
'access_token': 'jwt_token_here',
|
||||
'refresh_token': 'refresh_token_here',
|
||||
'token_type': 'bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
),
|
||||
];
|
||||
|
||||
for (var i = 0; i < possibleResponses.length; i++) {
|
||||
final response = possibleResponses[i];
|
||||
print('\n응답 형식 ${i + 1} 테스트:');
|
||||
print('응답 데이터: ${response.data}');
|
||||
|
||||
// ResponseInterceptor 시뮬레이션
|
||||
if (response.data is Map<String, dynamic>) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
// 이미 정규화된 형식인지 확인
|
||||
if (data.containsKey('success') && data.containsKey('data')) {
|
||||
print('이미 정규화된 형식');
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(data['data']);
|
||||
print('✅ 정규화된 형식 파싱 성공');
|
||||
} catch (e) {
|
||||
print('❌ 정규화된 형식 파싱 실패: $e');
|
||||
}
|
||||
} else if (data.containsKey('access_token') || data.containsKey('accessToken')) {
|
||||
print('직접 데이터 형식 - 정규화 필요');
|
||||
// 정규화
|
||||
final normalizedData = {
|
||||
'success': true,
|
||||
'data': data,
|
||||
};
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(normalizedData['data'] as Map<String, dynamic>);
|
||||
print('✅ 직접 데이터 형식 파싱 성공');
|
||||
} catch (e) {
|
||||
print('❌ 직접 데이터 형식 파싱 실패: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('로그인 진단 도구 테스트', () {
|
||||
test('전체 진단 실행', () async {
|
||||
print('\n=== 로그인 진단 시작 ===\n');
|
||||
|
||||
final diagnostics = await LoginDiagnostics.runFullDiagnostics();
|
||||
final report = LoginDiagnostics.formatDiagnosticsReport(diagnostics);
|
||||
|
||||
print(report);
|
||||
|
||||
// 진단 결과 검증
|
||||
expect(diagnostics, isNotNull);
|
||||
expect(diagnostics['environment'], isNotNull);
|
||||
expect(diagnostics['serialization'], isNotNull);
|
||||
|
||||
// 직렬화 테스트 결과 확인
|
||||
final serialization = diagnostics['serialization'] as Map<String, dynamic>;
|
||||
expect(serialization['loginRequestValid'], true);
|
||||
expect(serialization['format1Valid'], true);
|
||||
expect(serialization['format2Valid'], true);
|
||||
});
|
||||
|
||||
test('DebugLogger 기능 테스트', () {
|
||||
// API 요청 로깅
|
||||
DebugLogger.logApiRequest(
|
||||
method: 'POST',
|
||||
url: '/auth/login',
|
||||
data: {'email': 'test@example.com', 'password': '***'},
|
||||
);
|
||||
|
||||
// API 응답 로깅
|
||||
DebugLogger.logApiResponse(
|
||||
url: '/auth/login',
|
||||
statusCode: 200,
|
||||
data: {'success': true},
|
||||
);
|
||||
|
||||
// 에러 로깅
|
||||
DebugLogger.logError(
|
||||
'API 호출 실패',
|
||||
error: Exception('Network error'),
|
||||
additionalData: {'endpoint': '/auth/login'},
|
||||
);
|
||||
|
||||
// JSON 파싱 로깅
|
||||
final testJson = {
|
||||
'id': 1,
|
||||
'name': 'Test',
|
||||
};
|
||||
|
||||
final result = DebugLogger.parseJsonWithLogging(
|
||||
testJson,
|
||||
(json) => json,
|
||||
objectName: 'TestObject',
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result, equals(testJson));
|
||||
});
|
||||
});
|
||||
}
|
||||
339
test/api/auth_api_integration_test.dart
Normal file
339
test/api/auth_api_integration_test.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
import 'package:superport/data/datasources/remote/auth_remote_datasource.dart';
|
||||
import 'package:superport/data/models/auth/login_request.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
|
||||
import 'auth_api_integration_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([Dio])
|
||||
void main() {
|
||||
group('Auth API 통합 테스트 - 실제 API 동작 시뮬레이션', () {
|
||||
late MockDio mockDio;
|
||||
late ApiClient apiClient;
|
||||
late AuthRemoteDataSource authDataSource;
|
||||
|
||||
setUp(() {
|
||||
mockDio = MockDio();
|
||||
// ApiClient를 직접 생성하는 대신 mockDio를 주입
|
||||
apiClient = ApiClient();
|
||||
// Reflection을 사용해 private _dio 필드에 접근 (테스트 목적)
|
||||
// 실제로는 ApiClient에 테스트용 생성자를 추가하는 것이 좋음
|
||||
authDataSource = AuthRemoteDataSourceImpl(apiClient);
|
||||
});
|
||||
|
||||
test('Case 1: API가 success/data 형식으로 응답하는 경우', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
final apiResponse = {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'jwt_token_123456',
|
||||
'refresh_token': 'refresh_token_789',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '시스템 관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 1: success/data 래핑 형식 ===');
|
||||
print('요청 데이터: ${request.toJson()}');
|
||||
print('예상 응답: $apiResponse');
|
||||
|
||||
// 실제 API 호출 시뮬레이션
|
||||
try {
|
||||
// AuthRemoteDataSourceImpl의 로직 검증
|
||||
final responseData = apiResponse;
|
||||
|
||||
if (responseData['success'] == true && responseData['data'] != null) {
|
||||
print('✅ 응답 형식 1 감지 (success/data 래핑)');
|
||||
|
||||
final loginData = responseData['data'] as Map<String, dynamic>;
|
||||
final loginResponse = LoginResponse.fromJson(loginData);
|
||||
|
||||
print('파싱 성공:');
|
||||
print(' - Access Token: ${loginResponse.accessToken}');
|
||||
print(' - User Email: ${loginResponse.user.email}');
|
||||
print(' - User Role: ${loginResponse.user.role}');
|
||||
|
||||
expect(loginResponse.accessToken, 'jwt_token_123456');
|
||||
expect(loginResponse.user.email, 'admin@superport.com');
|
||||
expect(loginResponse.user.role, 'ADMIN');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print('❌ 파싱 실패: $e');
|
||||
print('스택 트레이스: $stackTrace');
|
||||
fail('success/data 형식 파싱에 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
test('Case 2: API가 직접 LoginResponse 형식으로 응답하는 경우', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
final apiResponse = {
|
||||
'access_token': 'direct_token_456',
|
||||
'refresh_token': 'direct_refresh_789',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 7200,
|
||||
'user': {
|
||||
'id': 2,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '일반 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 2: 직접 응답 형식 ===');
|
||||
print('요청 데이터: ${request.toJson()}');
|
||||
print('예상 응답: $apiResponse');
|
||||
|
||||
try {
|
||||
// 직접 응답 형식 처리
|
||||
if (apiResponse.containsKey('access_token')) {
|
||||
print('✅ 응답 형식 2 감지 (직접 응답)');
|
||||
|
||||
final loginResponse = LoginResponse.fromJson(apiResponse);
|
||||
|
||||
print('파싱 성공:');
|
||||
print(' - Access Token: ${loginResponse.accessToken}');
|
||||
print(' - User Username: ${loginResponse.user.username}');
|
||||
print(' - User Role: ${loginResponse.user.role}');
|
||||
|
||||
expect(loginResponse.accessToken, 'direct_token_456');
|
||||
expect(loginResponse.user.username, 'testuser');
|
||||
expect(loginResponse.user.role, 'USER');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print('❌ 파싱 실패: $e');
|
||||
print('스택 트레이스: $stackTrace');
|
||||
fail('직접 응답 형식 파싱에 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
test('Case 3: camelCase 필드명 사용 시 에러', () async {
|
||||
// Arrange
|
||||
final apiResponse = {
|
||||
'accessToken': 'camel_token_123', // camelCase
|
||||
'refreshToken': 'camel_refresh_456',
|
||||
'tokenType': 'Bearer',
|
||||
'expiresIn': 3600,
|
||||
'user': {
|
||||
'id': 3,
|
||||
'username': 'cameluser',
|
||||
'email': 'camel@test.com',
|
||||
'name': 'Camel User',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 3: camelCase 필드명 에러 ===');
|
||||
print('예상 응답: $apiResponse');
|
||||
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(apiResponse);
|
||||
fail('camelCase 응답이 성공하면 안됩니다');
|
||||
} catch (e) {
|
||||
print('✅ 예상된 에러 발생: $e');
|
||||
expect(e.toString(), contains('type \'Null\' is not a subtype of type \'String\''));
|
||||
}
|
||||
});
|
||||
|
||||
test('Case 4: 401 인증 실패 응답', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'wrong@email.com',
|
||||
password: 'wrongpassword',
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 4: 401 인증 실패 ===');
|
||||
print('요청 데이터: ${request.toJson()}');
|
||||
|
||||
// DioException 시뮬레이션
|
||||
final dioError = DioException(
|
||||
response: Response(
|
||||
statusCode: 401,
|
||||
data: {
|
||||
'message': 'Invalid credentials',
|
||||
'error': 'Unauthorized',
|
||||
},
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
),
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
type: DioExceptionType.badResponse,
|
||||
);
|
||||
|
||||
print('응답 상태: 401 Unauthorized');
|
||||
print('에러 메시지: Invalid credentials');
|
||||
|
||||
// AuthRemoteDataSourceImpl의 에러 처리 로직 검증
|
||||
expect(dioError.response?.statusCode, 401);
|
||||
|
||||
// 예상되는 Failure 타입
|
||||
print('✅ AuthenticationFailure로 변환되어야 함');
|
||||
});
|
||||
|
||||
test('Case 5: 네트워크 타임아웃', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
password: 'password',
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 5: 네트워크 타임아웃 ===');
|
||||
print('요청 데이터: ${request.toJson()}');
|
||||
|
||||
final dioError = DioException(
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
message: 'Connection timeout',
|
||||
);
|
||||
|
||||
print('에러 타입: ${dioError.type}');
|
||||
print('에러 메시지: ${dioError.message}');
|
||||
|
||||
// 예상되는 Failure 타입
|
||||
print('✅ NetworkFailure로 변환되어야 함');
|
||||
});
|
||||
|
||||
test('Case 6: 잘못된 JSON 응답', () async {
|
||||
// Arrange
|
||||
final apiResponse = {
|
||||
'error': 'Invalid request',
|
||||
'status': 'failed',
|
||||
// access_token 등 필수 필드 누락
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
print('\n=== Case 6: 잘못된 JSON 응답 ===');
|
||||
print('예상 응답: $apiResponse');
|
||||
|
||||
try {
|
||||
final loginResponse = LoginResponse.fromJson(apiResponse);
|
||||
fail('잘못된 JSON이 파싱되면 안됩니다');
|
||||
} catch (e) {
|
||||
print('✅ 예상된 에러 발생: $e');
|
||||
expect(e.toString(), contains('type \'Null\' is not a subtype'));
|
||||
}
|
||||
});
|
||||
|
||||
test('Case 7: ResponseInterceptor 동작 검증', () async {
|
||||
// ResponseInterceptor가 다양한 응답을 어떻게 처리하는지 검증
|
||||
print('\n=== Case 7: ResponseInterceptor 동작 검증 ===');
|
||||
|
||||
final testCases = [
|
||||
{
|
||||
'name': '이미 정규화된 응답',
|
||||
'input': {
|
||||
'success': true,
|
||||
'data': {'access_token': 'token1'},
|
||||
},
|
||||
'expected': {
|
||||
'success': true,
|
||||
'data': {'access_token': 'token1'},
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': '직접 데이터 응답 (access_token)',
|
||||
'input': {
|
||||
'access_token': 'token2',
|
||||
'user': {'id': 1},
|
||||
},
|
||||
'expected': {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'token2',
|
||||
'user': {'id': 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (final testCase in testCases) {
|
||||
print('\n테스트: ${testCase['name']}');
|
||||
print('입력: ${testCase['input']}');
|
||||
print('예상 출력: ${testCase['expected']}');
|
||||
|
||||
// ResponseInterceptor 로직 시뮬레이션
|
||||
final input = testCase['input'] as Map<String, dynamic>;
|
||||
Map<String, dynamic> output;
|
||||
|
||||
if (input.containsKey('success') && input.containsKey('data')) {
|
||||
output = input; // 이미 정규화됨
|
||||
} else if (input.containsKey('access_token') || input.containsKey('accessToken')) {
|
||||
output = {
|
||||
'success': true,
|
||||
'data': input,
|
||||
};
|
||||
} else {
|
||||
output = input;
|
||||
}
|
||||
|
||||
print('실제 출력: $output');
|
||||
expect(output, equals(testCase['expected']));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('에러 메시지 및 스택 트레이스 분석', () {
|
||||
test('실제 에러 시나리오 재현', () {
|
||||
print('\n=== 실제 에러 시나리오 재현 ===\n');
|
||||
|
||||
// 테스트에서 발생한 실제 에러들
|
||||
final errors = [
|
||||
{
|
||||
'scenario': 'Future.timeout 타입 에러',
|
||||
'error': "type '() => Left<Failure, LoginResponse>' is not a subtype of type '(() => FutureOr<Right<Failure, LoginResponse>>)?'",
|
||||
'cause': 'timeout의 onTimeout 콜백이 잘못된 타입을 반환',
|
||||
'solution': 'onTimeout이 Future<Either<Failure, LoginResponse>>를 반환하도록 수정',
|
||||
},
|
||||
{
|
||||
'scenario': 'JSON 파싱 null 에러',
|
||||
'error': "type 'Null' is not a subtype of type 'String' in type cast",
|
||||
'cause': 'snake_case 필드명 기대하지만 camelCase로 전달됨',
|
||||
'solution': 'API 응답 형식 확인 및 모델 수정',
|
||||
},
|
||||
{
|
||||
'scenario': '위젯 테스트 tap 실패',
|
||||
'error': "could not be tapped on because it has not been laid out yet",
|
||||
'cause': '위젯이 아직 렌더링되지 않은 상태에서 tap 시도',
|
||||
'solution': 'await tester.pumpAndSettle() 추가',
|
||||
},
|
||||
];
|
||||
|
||||
for (final error in errors) {
|
||||
print('시나리오: ${error['scenario']}');
|
||||
print('에러: ${error['error']}');
|
||||
print('원인: ${error['cause']}');
|
||||
print('해결책: ${error['solution']}');
|
||||
print('---\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
836
test/api/auth_api_integration_test.mocks.dart
Normal file
836
test/api/auth_api_integration_test.mocks.dart
Normal file
@@ -0,0 +1,836 @@
|
||||
// Mocks generated by Mockito 5.4.5 from annotations
|
||||
// in superport/test/api/auth_api_integration_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i8;
|
||||
|
||||
import 'package:dio/src/adapter.dart' as _i3;
|
||||
import 'package:dio/src/cancel_token.dart' as _i9;
|
||||
import 'package:dio/src/dio.dart' as _i7;
|
||||
import 'package:dio/src/dio_mixin.dart' as _i5;
|
||||
import 'package:dio/src/options.dart' as _i2;
|
||||
import 'package:dio/src/response.dart' as _i6;
|
||||
import 'package:dio/src/transformer.dart' as _i4;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
|
||||
class _FakeBaseOptions_0 extends _i1.SmartFake implements _i2.BaseOptions {
|
||||
_FakeBaseOptions_0(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeHttpClientAdapter_1 extends _i1.SmartFake
|
||||
implements _i3.HttpClientAdapter {
|
||||
_FakeHttpClientAdapter_1(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeTransformer_2 extends _i1.SmartFake implements _i4.Transformer {
|
||||
_FakeTransformer_2(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeInterceptors_3 extends _i1.SmartFake implements _i5.Interceptors {
|
||||
_FakeInterceptors_3(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeResponse_4<T1> extends _i1.SmartFake implements _i6.Response<T1> {
|
||||
_FakeResponse_4(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeDio_5 extends _i1.SmartFake implements _i7.Dio {
|
||||
_FakeDio_5(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
/// A class which mocks [Dio].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockDio extends _i1.Mock implements _i7.Dio {
|
||||
MockDio() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.BaseOptions get options => (super.noSuchMethod(
|
||||
Invocation.getter(#options),
|
||||
returnValue: _FakeBaseOptions_0(
|
||||
this,
|
||||
Invocation.getter(#options),
|
||||
),
|
||||
) as _i2.BaseOptions);
|
||||
|
||||
@override
|
||||
set options(_i2.BaseOptions? _options) => super.noSuchMethod(
|
||||
Invocation.setter(
|
||||
#options,
|
||||
_options,
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i3.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod(
|
||||
Invocation.getter(#httpClientAdapter),
|
||||
returnValue: _FakeHttpClientAdapter_1(
|
||||
this,
|
||||
Invocation.getter(#httpClientAdapter),
|
||||
),
|
||||
) as _i3.HttpClientAdapter);
|
||||
|
||||
@override
|
||||
set httpClientAdapter(_i3.HttpClientAdapter? _httpClientAdapter) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.setter(
|
||||
#httpClientAdapter,
|
||||
_httpClientAdapter,
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i4.Transformer get transformer => (super.noSuchMethod(
|
||||
Invocation.getter(#transformer),
|
||||
returnValue: _FakeTransformer_2(
|
||||
this,
|
||||
Invocation.getter(#transformer),
|
||||
),
|
||||
) as _i4.Transformer);
|
||||
|
||||
@override
|
||||
set transformer(_i4.Transformer? _transformer) => super.noSuchMethod(
|
||||
Invocation.setter(
|
||||
#transformer,
|
||||
_transformer,
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i5.Interceptors get interceptors => (super.noSuchMethod(
|
||||
Invocation.getter(#interceptors),
|
||||
returnValue: _FakeInterceptors_3(
|
||||
this,
|
||||
Invocation.getter(#interceptors),
|
||||
),
|
||||
) as _i5.Interceptors);
|
||||
|
||||
@override
|
||||
void close({bool? force = false}) => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#close,
|
||||
[],
|
||||
{#force: force},
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> head<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#head,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#head,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> headUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#headUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#headUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> get<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#get,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#get,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> getUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#getUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> post<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#post,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#post,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> postUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#postUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#postUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> put<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#put,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#put,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> putUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#putUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#putUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> patch<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> patchUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#patchUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#patchUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> delete<T>(
|
||||
String? path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> deleteUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
_i9.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#deleteUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#deleteUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<dynamic>> download(
|
||||
String? urlPath,
|
||||
dynamic savePath, {
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i9.CancelToken? cancelToken,
|
||||
bool? deleteOnError = true,
|
||||
_i2.FileAccessMode? fileAccessMode = _i2.FileAccessMode.write,
|
||||
String? lengthHeader = 'content-length',
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#download,
|
||||
[
|
||||
urlPath,
|
||||
savePath,
|
||||
],
|
||||
{
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#queryParameters: queryParameters,
|
||||
#cancelToken: cancelToken,
|
||||
#deleteOnError: deleteOnError,
|
||||
#fileAccessMode: fileAccessMode,
|
||||
#lengthHeader: lengthHeader,
|
||||
#data: data,
|
||||
#options: options,
|
||||
},
|
||||
),
|
||||
returnValue:
|
||||
_i8.Future<_i6.Response<dynamic>>.value(_FakeResponse_4<dynamic>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#download,
|
||||
[
|
||||
urlPath,
|
||||
savePath,
|
||||
],
|
||||
{
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#queryParameters: queryParameters,
|
||||
#cancelToken: cancelToken,
|
||||
#deleteOnError: deleteOnError,
|
||||
#fileAccessMode: fileAccessMode,
|
||||
#lengthHeader: lengthHeader,
|
||||
#data: data,
|
||||
#options: options,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<dynamic>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<dynamic>> downloadUri(
|
||||
Uri? uri,
|
||||
dynamic savePath, {
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
_i9.CancelToken? cancelToken,
|
||||
bool? deleteOnError = true,
|
||||
_i2.FileAccessMode? fileAccessMode = _i2.FileAccessMode.write,
|
||||
String? lengthHeader = 'content-length',
|
||||
Object? data,
|
||||
_i2.Options? options,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#downloadUri,
|
||||
[
|
||||
uri,
|
||||
savePath,
|
||||
],
|
||||
{
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#cancelToken: cancelToken,
|
||||
#deleteOnError: deleteOnError,
|
||||
#fileAccessMode: fileAccessMode,
|
||||
#lengthHeader: lengthHeader,
|
||||
#data: data,
|
||||
#options: options,
|
||||
},
|
||||
),
|
||||
returnValue:
|
||||
_i8.Future<_i6.Response<dynamic>>.value(_FakeResponse_4<dynamic>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#downloadUri,
|
||||
[
|
||||
uri,
|
||||
savePath,
|
||||
],
|
||||
{
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#cancelToken: cancelToken,
|
||||
#deleteOnError: deleteOnError,
|
||||
#fileAccessMode: fileAccessMode,
|
||||
#lengthHeader: lengthHeader,
|
||||
#data: data,
|
||||
#options: options,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<dynamic>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> request<T>(
|
||||
String? url, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.Options? options,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#request,
|
||||
[url],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#cancelToken: cancelToken,
|
||||
#options: options,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#request,
|
||||
[url],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#cancelToken: cancelToken,
|
||||
#options: options,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> requestUri<T>(
|
||||
Uri? uri, {
|
||||
Object? data,
|
||||
_i9.CancelToken? cancelToken,
|
||||
_i2.Options? options,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#requestUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#cancelToken: cancelToken,
|
||||
#options: options,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#requestUri,
|
||||
[uri],
|
||||
{
|
||||
#data: data,
|
||||
#cancelToken: cancelToken,
|
||||
#options: options,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i8.Future<_i6.Response<T>> fetch<T>(_i2.RequestOptions? requestOptions) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[requestOptions],
|
||||
),
|
||||
returnValue: _i8.Future<_i6.Response<T>>.value(_FakeResponse_4<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[requestOptions],
|
||||
),
|
||||
)),
|
||||
) as _i8.Future<_i6.Response<T>>);
|
||||
|
||||
@override
|
||||
_i7.Dio clone({
|
||||
_i2.BaseOptions? options,
|
||||
_i5.Interceptors? interceptors,
|
||||
_i3.HttpClientAdapter? httpClientAdapter,
|
||||
_i4.Transformer? transformer,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#clone,
|
||||
[],
|
||||
{
|
||||
#options: options,
|
||||
#interceptors: interceptors,
|
||||
#httpClientAdapter: httpClientAdapter,
|
||||
#transformer: transformer,
|
||||
},
|
||||
),
|
||||
returnValue: _FakeDio_5(
|
||||
this,
|
||||
Invocation.method(
|
||||
#clone,
|
||||
[],
|
||||
{
|
||||
#options: options,
|
||||
#interceptors: interceptors,
|
||||
#httpClientAdapter: httpClientAdapter,
|
||||
#transformer: transformer,
|
||||
},
|
||||
),
|
||||
),
|
||||
) as _i7.Dio);
|
||||
}
|
||||
373
test/integration/auth_integration_test_fixed.dart
Normal file
373
test/integration/auth_integration_test_fixed.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
import 'package:superport/data/datasources/remote/auth_remote_datasource.dart';
|
||||
import 'package:superport/data/models/auth/login_request.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:superport/core/config/environment.dart' as env;
|
||||
|
||||
import 'auth_integration_test_fixed.mocks.dart';
|
||||
|
||||
@GenerateMocks([ApiClient, FlutterSecureStorage])
|
||||
void main() {
|
||||
group('로그인 통합 테스트 (수정본)', () {
|
||||
late MockApiClient mockApiClient;
|
||||
late MockFlutterSecureStorage mockSecureStorage;
|
||||
late AuthRemoteDataSource authRemoteDataSource;
|
||||
late AuthService authService;
|
||||
|
||||
setUpAll(() async {
|
||||
// 테스트를 위한 환경 초기화
|
||||
dotenv.testLoad(mergeWith: {
|
||||
'USE_API': 'true',
|
||||
'API_BASE_URL': 'https://superport.naturebridgeai.com/api/v1',
|
||||
'API_TIMEOUT': '30000',
|
||||
'ENABLE_LOGGING': 'false',
|
||||
});
|
||||
await env.Environment.initialize('test');
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockApiClient = MockApiClient();
|
||||
mockSecureStorage = MockFlutterSecureStorage();
|
||||
authRemoteDataSource = AuthRemoteDataSourceImpl(mockApiClient);
|
||||
|
||||
// AuthServiceImpl에 mock dependencies 주입
|
||||
authService = AuthServiceImpl(authRemoteDataSource, mockSecureStorage);
|
||||
|
||||
// 기본 mock 설정
|
||||
when(mockSecureStorage.write(key: anyNamed('key'), value: anyNamed('value')))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
when(mockSecureStorage.read(key: anyNamed('key')))
|
||||
.thenAnswer((_) async => null);
|
||||
when(mockSecureStorage.delete(key: anyNamed('key')))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
});
|
||||
|
||||
group('성공적인 로그인 시나리오', () {
|
||||
test('API가 success/data 형식으로 응답하는 경우', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
// API 응답 모킹 - snake_case 필드명 사용
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'test_token_123',
|
||||
'refresh_token': 'refresh_token_456',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenAnswer((_) async => mockResponse);
|
||||
|
||||
// Act
|
||||
final result = await authRemoteDataSource.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
result.fold(
|
||||
(failure) => fail('로그인이 실패하면 안됩니다: ${failure.message}'),
|
||||
(loginResponse) {
|
||||
expect(loginResponse.accessToken, 'test_token_123');
|
||||
expect(loginResponse.refreshToken, 'refresh_token_456');
|
||||
expect(loginResponse.user.email, 'admin@superport.com');
|
||||
expect(loginResponse.user.role, 'ADMIN');
|
||||
},
|
||||
);
|
||||
|
||||
// Verify API 호출
|
||||
verify(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: request.toJson(),
|
||||
)).called(1);
|
||||
});
|
||||
|
||||
test('API가 직접 LoginResponse 형식으로 응답하는 경우', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
// 직접 응답 형식 - snake_case 필드명 사용
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'access_token': 'direct_token_789',
|
||||
'refresh_token': 'direct_refresh_123',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 7200,
|
||||
'user': {
|
||||
'id': 2,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenAnswer((_) async => mockResponse);
|
||||
|
||||
// Act
|
||||
final result = await authRemoteDataSource.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
result.fold(
|
||||
(failure) => fail('로그인이 실패하면 안됩니다: ${failure.message}'),
|
||||
(loginResponse) {
|
||||
expect(loginResponse.accessToken, 'direct_token_789');
|
||||
expect(loginResponse.refreshToken, 'direct_refresh_123');
|
||||
expect(loginResponse.user.username, 'testuser');
|
||||
expect(loginResponse.user.role, 'USER');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('실패 시나리오', () {
|
||||
test('401 인증 실패 응답', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'wrong@email.com',
|
||||
password: 'wrongpassword',
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenThrow(DioException(
|
||||
response: Response(
|
||||
statusCode: 401,
|
||||
statusMessage: 'Unauthorized',
|
||||
data: {'message': 'Invalid credentials'},
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
),
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
type: DioExceptionType.badResponse,
|
||||
));
|
||||
|
||||
// Act
|
||||
final result = await authRemoteDataSource.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<AuthenticationFailure>());
|
||||
expect(failure.message, contains('올바르지 않습니다'));
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
|
||||
test('네트워크 타임아웃', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
password: 'password',
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenThrow(DioException(
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
message: 'Connection timeout',
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
));
|
||||
|
||||
// Act
|
||||
final result = await authRemoteDataSource.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<ServerFailure>());
|
||||
expect(failure.message, contains('오류가 발생했습니다'));
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
|
||||
test('잘못된 응답 형식', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
password: 'password',
|
||||
);
|
||||
|
||||
// 잘못된 형식의 응답
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'error': 'Invalid request',
|
||||
'status': 'failed',
|
||||
// 필수 필드들이 누락됨
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenAnswer((_) async => mockResponse);
|
||||
|
||||
// Act
|
||||
final result = await authRemoteDataSource.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<ServerFailure>());
|
||||
expect(failure.message, contains('잘못된 응답 형식'));
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthService 통합 테스트', () {
|
||||
test('로그인 성공 시 토큰 저장 확인', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'saved_token_123',
|
||||
'refresh_token': 'saved_refresh_456',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post(
|
||||
'/auth/login',
|
||||
data: anyNamed('data'),
|
||||
queryParameters: anyNamed('queryParameters'),
|
||||
options: anyNamed('options'),
|
||||
cancelToken: anyNamed('cancelToken'),
|
||||
onSendProgress: anyNamed('onSendProgress'),
|
||||
onReceiveProgress: anyNamed('onReceiveProgress'),
|
||||
)).thenAnswer((_) async => mockResponse);
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
|
||||
// 토큰 저장 확인
|
||||
verify(mockSecureStorage.write(key: 'access_token', value: 'saved_token_123')).called(1);
|
||||
verify(mockSecureStorage.write(key: 'refresh_token', value: 'saved_refresh_456')).called(1);
|
||||
verify(mockSecureStorage.write(key: 'user', value: anyNamed('value'))).called(1);
|
||||
verify(mockSecureStorage.write(key: 'token_expiry', value: anyNamed('value'))).called(1);
|
||||
});
|
||||
|
||||
test('토큰 조회 테스트', () async {
|
||||
// Arrange
|
||||
when(mockSecureStorage.read(key: 'access_token'))
|
||||
.thenAnswer((_) async => 'test_access_token');
|
||||
|
||||
// Act
|
||||
final token = await authService.getAccessToken();
|
||||
|
||||
// Assert
|
||||
expect(token, 'test_access_token');
|
||||
verify(mockSecureStorage.read(key: 'access_token')).called(1);
|
||||
});
|
||||
|
||||
test('현재 사용자 조회 테스트', () async {
|
||||
// Arrange
|
||||
final userJson = '{"id":1,"username":"testuser","email":"test@example.com","name":"테스트 사용자","role":"USER"}';
|
||||
when(mockSecureStorage.read(key: 'user'))
|
||||
.thenAnswer((_) async => userJson);
|
||||
|
||||
// Act
|
||||
final user = await authService.getCurrentUser();
|
||||
|
||||
// Assert
|
||||
expect(user, isNotNull);
|
||||
expect(user!.id, 1);
|
||||
expect(user.username, 'testuser');
|
||||
expect(user.email, 'test@example.com');
|
||||
expect(user.name, '테스트 사용자');
|
||||
expect(user.role, 'USER');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
694
test/integration/auth_integration_test_fixed.mocks.dart
Normal file
694
test/integration/auth_integration_test_fixed.mocks.dart
Normal file
@@ -0,0 +1,694 @@
|
||||
// Mocks generated by Mockito 5.4.5 from annotations
|
||||
// in superport/test/integration/auth_integration_test_fixed.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i5;
|
||||
|
||||
import 'package:dio/dio.dart' as _i2;
|
||||
import 'package:flutter/foundation.dart' as _i6;
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i3;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:superport/data/datasources/remote/api_client.dart' as _i4;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
|
||||
class _FakeDio_0 extends _i1.SmartFake implements _i2.Dio {
|
||||
_FakeDio_0(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeResponse_1<T1> extends _i1.SmartFake implements _i2.Response<T1> {
|
||||
_FakeResponse_1(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeIOSOptions_2 extends _i1.SmartFake implements _i3.IOSOptions {
|
||||
_FakeIOSOptions_2(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeAndroidOptions_3 extends _i1.SmartFake
|
||||
implements _i3.AndroidOptions {
|
||||
_FakeAndroidOptions_3(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeLinuxOptions_4 extends _i1.SmartFake implements _i3.LinuxOptions {
|
||||
_FakeLinuxOptions_4(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeWindowsOptions_5 extends _i1.SmartFake
|
||||
implements _i3.WindowsOptions {
|
||||
_FakeWindowsOptions_5(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeWebOptions_6 extends _i1.SmartFake implements _i3.WebOptions {
|
||||
_FakeWebOptions_6(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeMacOsOptions_7 extends _i1.SmartFake implements _i3.MacOsOptions {
|
||||
_FakeMacOsOptions_7(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
/// A class which mocks [ApiClient].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockApiClient extends _i1.Mock implements _i4.ApiClient {
|
||||
MockApiClient() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i2.Dio get dio => (super.noSuchMethod(
|
||||
Invocation.getter(#dio),
|
||||
returnValue: _FakeDio_0(
|
||||
this,
|
||||
Invocation.getter(#dio),
|
||||
),
|
||||
) as _i2.Dio);
|
||||
|
||||
@override
|
||||
void updateAuthToken(String? token) => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#updateAuthToken,
|
||||
[token],
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeAuthToken() => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#removeAuthToken,
|
||||
[],
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> get<T>(
|
||||
String? path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i2.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#get,
|
||||
[path],
|
||||
{
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#get,
|
||||
[path],
|
||||
{
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> post<T>(
|
||||
String? path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i2.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#post,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#post,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> put<T>(
|
||||
String? path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i2.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#put,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#put,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> patch<T>(
|
||||
String? path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i2.CancelToken? cancelToken,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
#onSendProgress: onSendProgress,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> delete<T>(
|
||||
String? path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
_i2.Options? options,
|
||||
_i2.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[path],
|
||||
{
|
||||
#data: data,
|
||||
#queryParameters: queryParameters,
|
||||
#options: options,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<T>> uploadFile<T>(
|
||||
String? path, {
|
||||
required String? filePath,
|
||||
required String? fileFieldName,
|
||||
Map<String, dynamic>? additionalData,
|
||||
_i2.ProgressCallback? onSendProgress,
|
||||
_i2.CancelToken? cancelToken,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#uploadFile,
|
||||
[path],
|
||||
{
|
||||
#filePath: filePath,
|
||||
#fileFieldName: fileFieldName,
|
||||
#additionalData: additionalData,
|
||||
#onSendProgress: onSendProgress,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<_i2.Response<T>>.value(_FakeResponse_1<T>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#uploadFile,
|
||||
[path],
|
||||
{
|
||||
#filePath: filePath,
|
||||
#fileFieldName: fileFieldName,
|
||||
#additionalData: additionalData,
|
||||
#onSendProgress: onSendProgress,
|
||||
#cancelToken: cancelToken,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<T>>);
|
||||
|
||||
@override
|
||||
_i5.Future<_i2.Response<dynamic>> downloadFile(
|
||||
String? path, {
|
||||
required String? savePath,
|
||||
_i2.ProgressCallback? onReceiveProgress,
|
||||
_i2.CancelToken? cancelToken,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#downloadFile,
|
||||
[path],
|
||||
{
|
||||
#savePath: savePath,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#cancelToken: cancelToken,
|
||||
#queryParameters: queryParameters,
|
||||
},
|
||||
),
|
||||
returnValue:
|
||||
_i5.Future<_i2.Response<dynamic>>.value(_FakeResponse_1<dynamic>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#downloadFile,
|
||||
[path],
|
||||
{
|
||||
#savePath: savePath,
|
||||
#onReceiveProgress: onReceiveProgress,
|
||||
#cancelToken: cancelToken,
|
||||
#queryParameters: queryParameters,
|
||||
},
|
||||
),
|
||||
)),
|
||||
) as _i5.Future<_i2.Response<dynamic>>);
|
||||
}
|
||||
|
||||
/// A class which mocks [FlutterSecureStorage].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockFlutterSecureStorage extends _i1.Mock
|
||||
implements _i3.FlutterSecureStorage {
|
||||
MockFlutterSecureStorage() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i3.IOSOptions get iOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#iOptions),
|
||||
returnValue: _FakeIOSOptions_2(
|
||||
this,
|
||||
Invocation.getter(#iOptions),
|
||||
),
|
||||
) as _i3.IOSOptions);
|
||||
|
||||
@override
|
||||
_i3.AndroidOptions get aOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#aOptions),
|
||||
returnValue: _FakeAndroidOptions_3(
|
||||
this,
|
||||
Invocation.getter(#aOptions),
|
||||
),
|
||||
) as _i3.AndroidOptions);
|
||||
|
||||
@override
|
||||
_i3.LinuxOptions get lOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#lOptions),
|
||||
returnValue: _FakeLinuxOptions_4(
|
||||
this,
|
||||
Invocation.getter(#lOptions),
|
||||
),
|
||||
) as _i3.LinuxOptions);
|
||||
|
||||
@override
|
||||
_i3.WindowsOptions get wOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#wOptions),
|
||||
returnValue: _FakeWindowsOptions_5(
|
||||
this,
|
||||
Invocation.getter(#wOptions),
|
||||
),
|
||||
) as _i3.WindowsOptions);
|
||||
|
||||
@override
|
||||
_i3.WebOptions get webOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#webOptions),
|
||||
returnValue: _FakeWebOptions_6(
|
||||
this,
|
||||
Invocation.getter(#webOptions),
|
||||
),
|
||||
) as _i3.WebOptions);
|
||||
|
||||
@override
|
||||
_i3.MacOsOptions get mOptions => (super.noSuchMethod(
|
||||
Invocation.getter(#mOptions),
|
||||
returnValue: _FakeMacOsOptions_7(
|
||||
this,
|
||||
Invocation.getter(#mOptions),
|
||||
),
|
||||
) as _i3.MacOsOptions);
|
||||
|
||||
@override
|
||||
void registerListener({
|
||||
required String? key,
|
||||
required _i6.ValueChanged<String?>? listener,
|
||||
}) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#registerListener,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#listener: listener,
|
||||
},
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void unregisterListener({
|
||||
required String? key,
|
||||
required _i6.ValueChanged<String?>? listener,
|
||||
}) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#unregisterListener,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#listener: listener,
|
||||
},
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void unregisterAllListenersForKey({required String? key}) =>
|
||||
super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#unregisterAllListenersForKey,
|
||||
[],
|
||||
{#key: key},
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void unregisterAllListeners() => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#unregisterAllListeners,
|
||||
[],
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i5.Future<void> write({
|
||||
required String? key,
|
||||
required String? value,
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#write,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#value: value,
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<String?> read({
|
||||
required String? key,
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#read,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<String?>.value(),
|
||||
) as _i5.Future<String?>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool> containsKey({
|
||||
required String? key,
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#containsKey,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<bool>.value(false),
|
||||
) as _i5.Future<bool>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> delete({
|
||||
required String? key,
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[],
|
||||
{
|
||||
#key: key,
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<Map<String, String>> readAll({
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#readAll,
|
||||
[],
|
||||
{
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<Map<String, String>>.value(<String, String>{}),
|
||||
) as _i5.Future<Map<String, String>>);
|
||||
|
||||
@override
|
||||
_i5.Future<void> deleteAll({
|
||||
_i3.IOSOptions? iOptions,
|
||||
_i3.AndroidOptions? aOptions,
|
||||
_i3.LinuxOptions? lOptions,
|
||||
_i3.WebOptions? webOptions,
|
||||
_i3.MacOsOptions? mOptions,
|
||||
_i3.WindowsOptions? wOptions,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#deleteAll,
|
||||
[],
|
||||
{
|
||||
#iOptions: iOptions,
|
||||
#aOptions: aOptions,
|
||||
#lOptions: lOptions,
|
||||
#webOptions: webOptions,
|
||||
#mOptions: mOptions,
|
||||
#wOptions: wOptions,
|
||||
},
|
||||
),
|
||||
returnValue: _i5.Future<void>.value(),
|
||||
returnValueForMissingStub: _i5.Future<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
|
||||
@override
|
||||
_i5.Future<bool?> isCupertinoProtectedDataAvailable() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#isCupertinoProtectedDataAvailable,
|
||||
[],
|
||||
),
|
||||
returnValue: _i5.Future<bool?>.value(),
|
||||
) as _i5.Future<bool?>);
|
||||
}
|
||||
317
test/integration/login_integration_test.dart
Normal file
317
test/integration/login_integration_test.dart
Normal file
@@ -0,0 +1,317 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||
import 'package:superport/data/datasources/remote/auth_remote_datasource.dart';
|
||||
import 'package:superport/data/models/auth/login_request.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
import 'login_integration_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([ApiClient, FlutterSecureStorage, Dio])
|
||||
void main() {
|
||||
group('로그인 통합 테스트', () {
|
||||
late MockApiClient mockApiClient;
|
||||
late MockFlutterSecureStorage mockSecureStorage;
|
||||
late AuthRemoteDataSource authRemoteDataSource;
|
||||
late AuthService authService;
|
||||
|
||||
setUp(() {
|
||||
mockApiClient = MockApiClient();
|
||||
mockSecureStorage = MockFlutterSecureStorage();
|
||||
authRemoteDataSource = AuthRemoteDataSourceImpl(mockApiClient);
|
||||
authService = AuthServiceImpl(authRemoteDataSource, mockSecureStorage);
|
||||
});
|
||||
|
||||
group('로그인 프로세스 전체 테스트', () {
|
||||
test('성공적인 로그인 - 이메일 사용', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'success': true,
|
||||
'data': {
|
||||
'access_token': 'test_access_token',
|
||||
'refresh_token': 'test_refresh_token',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post('/auth/login', data: request.toJson()))
|
||||
.thenAnswer((_) async => mockResponse);
|
||||
|
||||
when(mockSecureStorage.write(key: anyNamed('key'), value: anyNamed('value')))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
result.fold(
|
||||
(failure) => fail('로그인이 실패하면 안됩니다'),
|
||||
(loginResponse) {
|
||||
expect(loginResponse.accessToken, 'test_access_token');
|
||||
expect(loginResponse.refreshToken, 'test_refresh_token');
|
||||
expect(loginResponse.user.email, 'admin@superport.com');
|
||||
expect(loginResponse.user.role, 'ADMIN');
|
||||
},
|
||||
);
|
||||
|
||||
// 토큰이 올바르게 저장되었는지 확인
|
||||
verify(mockSecureStorage.write(key: 'access_token', value: 'test_access_token')).called(1);
|
||||
verify(mockSecureStorage.write(key: 'refresh_token', value: 'test_refresh_token')).called(1);
|
||||
verify(mockSecureStorage.write(key: 'user', value: anyNamed('value'))).called(1);
|
||||
});
|
||||
|
||||
test('성공적인 로그인 - 직접 LoginResponse 형태', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'access_token': 'test_access_token',
|
||||
'refresh_token': 'test_refresh_token',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'admin',
|
||||
'email': 'admin@superport.com',
|
||||
'name': '관리자',
|
||||
'role': 'ADMIN',
|
||||
},
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post('/auth/login', data: request.toJson()))
|
||||
.thenAnswer((_) async => mockResponse);
|
||||
|
||||
when(mockSecureStorage.write(key: anyNamed('key'), value: anyNamed('value')))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isRight(), true);
|
||||
result.fold(
|
||||
(failure) => fail('로그인이 실패하면 안됩니다'),
|
||||
(loginResponse) {
|
||||
expect(loginResponse.accessToken, 'test_access_token');
|
||||
expect(loginResponse.user.email, 'admin@superport.com');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('로그인 실패 - 잘못된 인증 정보', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'wrongpassword',
|
||||
);
|
||||
|
||||
when(mockApiClient.post('/auth/login', data: request.toJson()))
|
||||
.thenThrow(DioException(
|
||||
response: Response(
|
||||
statusCode: 401,
|
||||
statusMessage: 'Unauthorized',
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
),
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
));
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<AuthenticationFailure>());
|
||||
expect(failure.message, contains('올바르지 않습니다'));
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
|
||||
test('로그인 실패 - 네트워크 오류', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
when(mockApiClient.post('/auth/login', data: request.toJson()))
|
||||
.thenThrow(DioException(
|
||||
type: DioExceptionType.connectionTimeout,
|
||||
message: 'Connection timeout',
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
));
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<ServerFailure>());
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
|
||||
test('로그인 실패 - 잘못된 응답 형식', () async {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'admin@superport.com',
|
||||
password: 'admin123',
|
||||
);
|
||||
|
||||
final mockResponse = Response(
|
||||
data: {
|
||||
'wrongFormat': true,
|
||||
},
|
||||
statusCode: 200,
|
||||
requestOptions: RequestOptions(path: '/auth/login'),
|
||||
);
|
||||
|
||||
when(mockApiClient.post('/auth/login', data: request.toJson()))
|
||||
.thenAnswer((_) async => mockResponse);
|
||||
|
||||
// Act
|
||||
final result = await authService.login(request);
|
||||
|
||||
// Assert
|
||||
expect(result.isLeft(), true);
|
||||
result.fold(
|
||||
(failure) {
|
||||
expect(failure, isA<ServerFailure>());
|
||||
expect(failure.message, contains('잘못된 응답 형식'));
|
||||
},
|
||||
(_) => fail('로그인이 성공하면 안됩니다'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('JSON 파싱 테스트', () {
|
||||
test('LoginResponse fromJson 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'access_token': 'test_token',
|
||||
'refresh_token': 'refresh_token',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
final loginResponse = LoginResponse.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(loginResponse.accessToken, 'test_token');
|
||||
expect(loginResponse.refreshToken, 'refresh_token');
|
||||
expect(loginResponse.tokenType, 'Bearer');
|
||||
expect(loginResponse.expiresIn, 3600);
|
||||
expect(loginResponse.user.id, 1);
|
||||
expect(loginResponse.user.username, 'testuser');
|
||||
expect(loginResponse.user.email, 'test@example.com');
|
||||
expect(loginResponse.user.name, '테스트 사용자');
|
||||
expect(loginResponse.user.role, 'USER');
|
||||
});
|
||||
|
||||
test('AuthUser fromJson 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
};
|
||||
|
||||
// Act
|
||||
final authUser = AuthUser.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(authUser.id, 1);
|
||||
expect(authUser.username, 'testuser');
|
||||
expect(authUser.email, 'test@example.com');
|
||||
expect(authUser.name, '테스트 사용자');
|
||||
expect(authUser.role, 'USER');
|
||||
});
|
||||
});
|
||||
|
||||
group('토큰 저장 및 검색 테스트', () {
|
||||
test('액세스 토큰 저장 및 검색', () async {
|
||||
// Arrange
|
||||
const testToken = 'test_access_token';
|
||||
when(mockSecureStorage.read(key: 'access_token'))
|
||||
.thenAnswer((_) async => testToken);
|
||||
|
||||
// Act
|
||||
final token = await authService.getAccessToken();
|
||||
|
||||
// Assert
|
||||
expect(token, testToken);
|
||||
verify(mockSecureStorage.read(key: 'access_token')).called(1);
|
||||
});
|
||||
|
||||
test('현재 사용자 정보 저장 및 검색', () async {
|
||||
// Arrange
|
||||
final testUser = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
when(mockSecureStorage.read(key: 'user'))
|
||||
.thenAnswer((_) async => '{"id":1,"username":"testuser","email":"test@example.com","name":"테스트 사용자","role":"USER"}');
|
||||
|
||||
// Act
|
||||
final user = await authService.getCurrentUser();
|
||||
|
||||
// Assert
|
||||
expect(user, isNotNull);
|
||||
expect(user!.id, testUser.id);
|
||||
expect(user.email, testUser.email);
|
||||
expect(user.name, testUser.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
1481
test/integration/login_integration_test.mocks.dart
Normal file
1481
test/integration/login_integration_test.mocks.dart
Normal file
File diff suppressed because it is too large
Load Diff
383
test/unit/models/auth_models_test.dart
Normal file
383
test/unit/models/auth_models_test.dart
Normal file
@@ -0,0 +1,383 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:superport/data/models/auth/login_request.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
|
||||
void main() {
|
||||
group('Auth Models 단위 테스트', () {
|
||||
group('LoginRequest 모델 테스트', () {
|
||||
test('이메일로 LoginRequest 생성', () {
|
||||
// Arrange & Act
|
||||
final request = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(request.email, 'test@example.com');
|
||||
expect(request.username, isNull);
|
||||
expect(request.password, 'password123');
|
||||
});
|
||||
|
||||
test('username으로 LoginRequest 생성', () {
|
||||
// Arrange & Act
|
||||
final request = LoginRequest(
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(request.email, isNull);
|
||||
expect(request.username, 'testuser');
|
||||
expect(request.password, 'password123');
|
||||
});
|
||||
|
||||
test('LoginRequest toJson 테스트', () {
|
||||
// Arrange
|
||||
final request = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = request.toJson();
|
||||
|
||||
// Assert
|
||||
expect(json['email'], 'test@example.com');
|
||||
expect(json['password'], 'password123');
|
||||
// null 값도 JSON에 포함됨
|
||||
expect(json.containsKey('username'), isTrue);
|
||||
expect(json['username'], isNull);
|
||||
});
|
||||
|
||||
test('LoginRequest fromJson 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'email': 'test@example.com',
|
||||
'password': 'password123',
|
||||
};
|
||||
|
||||
// Act
|
||||
final request = LoginRequest.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(request.email, 'test@example.com');
|
||||
expect(request.password, 'password123');
|
||||
});
|
||||
|
||||
test('LoginRequest 직렬화/역직렬화 라운드트립', () {
|
||||
// Arrange
|
||||
final original = LoginRequest(
|
||||
email: 'test@example.com',
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = original.toJson();
|
||||
final restored = LoginRequest.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(restored.email, original.email);
|
||||
expect(restored.username, original.username);
|
||||
expect(restored.password, original.password);
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthUser 모델 테스트', () {
|
||||
test('AuthUser 생성 및 속성 확인', () {
|
||||
// Arrange & Act
|
||||
final user = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(user.id, 1);
|
||||
expect(user.username, 'testuser');
|
||||
expect(user.email, 'test@example.com');
|
||||
expect(user.name, '테스트 사용자');
|
||||
expect(user.role, 'USER');
|
||||
});
|
||||
|
||||
test('AuthUser toJson 테스트', () {
|
||||
// Arrange
|
||||
final user = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = user.toJson();
|
||||
|
||||
// Assert
|
||||
expect(json['id'], 1);
|
||||
expect(json['username'], 'testuser');
|
||||
expect(json['email'], 'test@example.com');
|
||||
expect(json['name'], '테스트 사용자');
|
||||
expect(json['role'], 'USER');
|
||||
});
|
||||
|
||||
test('AuthUser fromJson 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
};
|
||||
|
||||
// Act
|
||||
final user = AuthUser.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(user.id, 1);
|
||||
expect(user.username, 'testuser');
|
||||
expect(user.email, 'test@example.com');
|
||||
expect(user.name, '테스트 사용자');
|
||||
expect(user.role, 'USER');
|
||||
});
|
||||
|
||||
test('AuthUser 직렬화/역직렬화 라운드트립', () {
|
||||
// Arrange
|
||||
final original = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = original.toJson();
|
||||
final restored = AuthUser.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(restored, original);
|
||||
expect(restored.id, original.id);
|
||||
expect(restored.username, original.username);
|
||||
expect(restored.email, original.email);
|
||||
expect(restored.name, original.name);
|
||||
expect(restored.role, original.role);
|
||||
});
|
||||
|
||||
test('AuthUser copyWith 테스트', () {
|
||||
// Arrange
|
||||
final original = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
// Act
|
||||
final modified = original.copyWith(
|
||||
name: '수정된 사용자',
|
||||
role: 'ADMIN',
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(modified.id, original.id);
|
||||
expect(modified.username, original.username);
|
||||
expect(modified.email, original.email);
|
||||
expect(modified.name, '수정된 사용자');
|
||||
expect(modified.role, 'ADMIN');
|
||||
});
|
||||
});
|
||||
|
||||
group('LoginResponse 모델 테스트', () {
|
||||
test('LoginResponse 생성 및 속성 확인', () {
|
||||
// Arrange & Act
|
||||
final authUser = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
final response = LoginResponse(
|
||||
accessToken: 'test_access_token',
|
||||
refreshToken: 'test_refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: authUser,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(response.accessToken, 'test_access_token');
|
||||
expect(response.refreshToken, 'test_refresh_token');
|
||||
expect(response.tokenType, 'Bearer');
|
||||
expect(response.expiresIn, 3600);
|
||||
expect(response.user, authUser);
|
||||
});
|
||||
|
||||
test('LoginResponse toJson 테스트', () {
|
||||
// Arrange
|
||||
final authUser = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
final response = LoginResponse(
|
||||
accessToken: 'test_access_token',
|
||||
refreshToken: 'test_refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: authUser,
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = response.toJson();
|
||||
|
||||
// Assert - snake_case 필드명 사용
|
||||
expect(json['access_token'], 'test_access_token');
|
||||
expect(json['refresh_token'], 'test_refresh_token');
|
||||
expect(json['token_type'], 'Bearer');
|
||||
expect(json['expires_in'], 3600);
|
||||
expect(json['user'], authUser); // user는 AuthUser 객체로 포함됨
|
||||
});
|
||||
|
||||
test('LoginResponse fromJson 테스트', () {
|
||||
// Arrange - snake_case 필드명 사용
|
||||
final json = {
|
||||
'access_token': 'test_access_token',
|
||||
'refresh_token': 'test_refresh_token',
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
final response = LoginResponse.fromJson(json);
|
||||
|
||||
// Assert
|
||||
expect(response.accessToken, 'test_access_token');
|
||||
expect(response.refreshToken, 'test_refresh_token');
|
||||
expect(response.tokenType, 'Bearer');
|
||||
expect(response.expiresIn, 3600);
|
||||
expect(response.user.email, 'test@example.com');
|
||||
});
|
||||
|
||||
test('LoginResponse 직렬화/역직렬화 라운드트립', () {
|
||||
// Arrange
|
||||
final authUser = AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
);
|
||||
|
||||
final original = LoginResponse(
|
||||
accessToken: 'test_access_token',
|
||||
refreshToken: 'test_refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: authUser,
|
||||
);
|
||||
|
||||
// Act
|
||||
final json = original.toJson();
|
||||
// toJson은 user를 AuthUser 객체로 반환하므로 직렬화 필요
|
||||
final jsonWithSerializedUser = {
|
||||
...json,
|
||||
'user': (json['user'] as AuthUser).toJson(),
|
||||
};
|
||||
final restored = LoginResponse.fromJson(jsonWithSerializedUser);
|
||||
|
||||
// Assert
|
||||
expect(restored.accessToken, original.accessToken);
|
||||
expect(restored.refreshToken, original.refreshToken);
|
||||
expect(restored.tokenType, original.tokenType);
|
||||
expect(restored.expiresIn, original.expiresIn);
|
||||
expect(restored.user.id, original.user.id);
|
||||
expect(restored.user.email, original.user.email);
|
||||
});
|
||||
|
||||
test('camelCase 필드명 호환성 테스트', () {
|
||||
// Arrange - API가 camelCase를 사용하는 경우
|
||||
final json = {
|
||||
'accessToken': 'test_access_token',
|
||||
'refreshToken': 'test_refresh_token',
|
||||
'tokenType': 'Bearer',
|
||||
'expiresIn': 3600,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
},
|
||||
};
|
||||
|
||||
// Act & Assert - camelCase는 지원되지 않음
|
||||
expect(
|
||||
() => LoginResponse.fromJson(json),
|
||||
throwsA(isA<TypeError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('타입 안정성 테스트', () {
|
||||
test('null 값 처리 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'id': null,
|
||||
'username': null,
|
||||
'email': 'test@example.com',
|
||||
'name': null,
|
||||
'role': null,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
expect(() => AuthUser.fromJson(json), throwsA(isA<TypeError>()));
|
||||
});
|
||||
|
||||
test('잘못된 타입 처리 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'id': '문자열ID', // 숫자여야 함
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
expect(() => AuthUser.fromJson(json), throwsA(isA<TypeError>()));
|
||||
});
|
||||
|
||||
test('필수 필드 누락 테스트', () {
|
||||
// Arrange
|
||||
final json = {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
// email 누락
|
||||
'name': '테스트 사용자',
|
||||
'role': 'USER',
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
expect(() => AuthUser.fromJson(json), throwsA(isA<TypeError>()));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
399
test/widget/login_widget_test.bak
Normal file
399
test/widget/login_widget_test.bak
Normal file
@@ -0,0 +1,399 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/screens/login/widgets/login_view_redesign.dart';
|
||||
import 'package:superport/screens/login/controllers/login_controller.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
|
||||
import 'login_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([AuthService])
|
||||
void main() {
|
||||
late MockAuthService mockAuthService;
|
||||
late GetIt getIt;
|
||||
|
||||
setUp(() {
|
||||
mockAuthService = MockAuthService();
|
||||
getIt = GetIt.instance;
|
||||
|
||||
// GetIt 초기화
|
||||
if (getIt.isRegistered<AuthService>()) {
|
||||
getIt.unregister<AuthService>();
|
||||
}
|
||||
getIt.registerSingleton<AuthService>(mockAuthService);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
getIt.reset();
|
||||
});
|
||||
|
||||
group('로그인 화면 위젯 테스트', () {
|
||||
testWidgets('로그인 화면 초기 렌더링', (WidgetTester tester) async {
|
||||
// Arrange & Act
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: LoginController(),
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(find.text('로그인'), findsOneWidget);
|
||||
expect(find.byType(TextFormField), findsNWidgets(2)); // ID와 비밀번호 필드
|
||||
expect(find.text('아이디/이메일'), findsOneWidget);
|
||||
expect(find.text('비밀번호'), findsOneWidget);
|
||||
expect(find.text('아이디 저장'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('입력 필드 유효성 검사', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act - 빈 상태로 로그인 시도
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.errorMessage, contains('입력해주세요'));
|
||||
});
|
||||
|
||||
testWidgets('로그인 성공 시나리오', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
final mockResponse = LoginResponse(
|
||||
accessToken: 'test_token',
|
||||
refreshToken: 'refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
),
|
||||
);
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Right(mockResponse));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
// ID 입력
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
// 비밀번호 입력
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
// 로그인 버튼 탭
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
// 비동기 작업 대기
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(controller.isLoading, false);
|
||||
expect(controller.errorMessage, isNull);
|
||||
});
|
||||
|
||||
testWidgets('로그인 실패 시나리오', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Left(AuthenticationFailure(
|
||||
message: '이메일 또는 비밀번호가 올바르지 않습니다.',
|
||||
)));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'wrong@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'wrongpassword');
|
||||
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.errorMessage, contains('올바르지 않습니다'));
|
||||
});
|
||||
|
||||
testWidgets('로딩 상태 표시', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
// 지연된 응답 시뮬레이션
|
||||
when(mockAuthService.login(any)).thenAnswer((_) async {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return Right(LoginResponse(
|
||||
accessToken: 'test_token',
|
||||
refreshToken: 'refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
// 로딩 상태 확인
|
||||
await tester.pump();
|
||||
|
||||
// Assert - 로딩 중
|
||||
expect(controller.isLoading, true);
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
// 로딩 완료 대기
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.pump();
|
||||
|
||||
// Assert - 로딩 완료
|
||||
expect(controller.isLoading, false);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('비밀번호 표시/숨기기 토글', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: LoginController(),
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
// 초기 상태 - 비밀번호 숨김
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'testpassword');
|
||||
|
||||
// 비밀번호 표시 아이콘 찾기
|
||||
final visibilityIcon = find.byIcon(Icons.visibility_off);
|
||||
expect(visibilityIcon, findsOneWidget);
|
||||
|
||||
// 아이콘 탭하여 비밀번호 표시
|
||||
await tester.tap(visibilityIcon);
|
||||
await tester.pump();
|
||||
|
||||
// 비밀번호 표시 상태 확인
|
||||
expect(find.byIcon(Icons.visibility), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('아이디 저장 체크박스 동작', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
// 초기 상태
|
||||
expect(controller.saveId, false);
|
||||
|
||||
// 체크박스 탭
|
||||
final checkbox = find.byType(Checkbox);
|
||||
await tester.tap(checkbox);
|
||||
await tester.pump();
|
||||
|
||||
// 상태 변경 확인
|
||||
expect(controller.saveId, true);
|
||||
|
||||
// 다시 탭하여 해제
|
||||
await tester.tap(checkbox);
|
||||
await tester.pump();
|
||||
|
||||
expect(controller.saveId, false);
|
||||
});
|
||||
|
||||
testWidgets('이메일 형식 검증', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act - 이메일 형식 입력
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
// LoginRequest 생성 시 이메일로 처리되는지 확인
|
||||
expect(controller.idController.text, 'test@example.com');
|
||||
|
||||
// Act - username 형식 입력
|
||||
await tester.enterText(idField, 'testuser');
|
||||
|
||||
// username으로 처리되는지 확인
|
||||
expect(controller.idController.text, 'testuser');
|
||||
});
|
||||
});
|
||||
|
||||
group('로그인 컨트롤러 단위 테스트', () {
|
||||
test('입력 검증 - 빈 아이디', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
controller.idController.text = '';
|
||||
controller.pwController.text = 'password';
|
||||
|
||||
// Act
|
||||
final result = await controller.login();
|
||||
|
||||
// Assert
|
||||
expect(result, false);
|
||||
expect(controller.errorMessage, contains('아이디 또는 이메일을 입력해주세요'));
|
||||
});
|
||||
|
||||
test('입력 검증 - 빈 비밀번호', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
controller.idController.text = 'test@example.com';
|
||||
controller.pwController.text = '';
|
||||
|
||||
// Act
|
||||
final result = await controller.login();
|
||||
|
||||
// Assert
|
||||
expect(result, false);
|
||||
expect(controller.errorMessage, contains('비밀번호를 입력해주세요'));
|
||||
});
|
||||
|
||||
test('이메일/username 구분', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
// Test 1: 이메일 형식
|
||||
controller.idController.text = 'test@example.com';
|
||||
controller.pwController.text = 'password';
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Right(LoginResponse(
|
||||
accessToken: 'token',
|
||||
refreshToken: 'refresh',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'test',
|
||||
email: 'test@example.com',
|
||||
name: 'Test',
|
||||
role: 'USER',
|
||||
),
|
||||
)));
|
||||
|
||||
// Act
|
||||
await controller.login();
|
||||
|
||||
// Assert
|
||||
final capturedRequest = verify(mockAuthService.login(captureAny)).captured.single;
|
||||
expect(capturedRequest.email, 'test@example.com');
|
||||
expect(capturedRequest.username, isNull);
|
||||
|
||||
// Test 2: Username 형식
|
||||
controller.idController.text = 'testuser';
|
||||
|
||||
// Act
|
||||
await controller.login();
|
||||
|
||||
// Assert
|
||||
final capturedRequest2 = verify(mockAuthService.login(captureAny)).captured.single;
|
||||
expect(capturedRequest2.email, isNull);
|
||||
expect(capturedRequest2.username, 'testuser');
|
||||
});
|
||||
});
|
||||
}
|
||||
401
test/widget/login_widget_test.dart
Normal file
401
test/widget/login_widget_test.dart
Normal file
@@ -0,0 +1,401 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/screens/login/widgets/login_view_redesign.dart';
|
||||
import 'package:superport/screens/login/controllers/login_controller.dart';
|
||||
import 'package:superport/data/models/auth/login_response.dart';
|
||||
import 'package:superport/data/models/auth/auth_user.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
|
||||
import 'login_widget_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([AuthService])
|
||||
void main() {
|
||||
late MockAuthService mockAuthService;
|
||||
late GetIt getIt;
|
||||
|
||||
setUp(() {
|
||||
mockAuthService = MockAuthService();
|
||||
getIt = GetIt.instance;
|
||||
|
||||
// GetIt 초기화
|
||||
if (getIt.isRegistered<AuthService>()) {
|
||||
getIt.unregister<AuthService>();
|
||||
}
|
||||
getIt.registerSingleton<AuthService>(mockAuthService);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
getIt.reset();
|
||||
});
|
||||
|
||||
group('로그인 화면 위젯 테스트', () {
|
||||
testWidgets('로그인 화면 초기 렌더링', (WidgetTester tester) async {
|
||||
// Arrange & Act
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: LoginController(),
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(find.text('로그인'), findsOneWidget);
|
||||
expect(find.byType(TextFormField), findsNWidgets(2)); // ID와 비밀번호 필드
|
||||
expect(find.text('아이디/이메일'), findsOneWidget);
|
||||
expect(find.text('비밀번호'), findsOneWidget);
|
||||
expect(find.text('아이디 저장'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('입력 필드 유효성 검사', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act - 빈 상태로 로그인 시도
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.errorMessage, contains('입력해주세요'));
|
||||
});
|
||||
|
||||
testWidgets('로그인 성공 시나리오', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
final mockResponse = LoginResponse(
|
||||
accessToken: 'test_token',
|
||||
refreshToken: 'refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
),
|
||||
);
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Right(mockResponse));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
// ID 입력
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
// 비밀번호 입력
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
// 로그인 버튼 탭
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
// 비동기 작업 대기
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(controller.isLoading, false);
|
||||
expect(controller.errorMessage, isNull);
|
||||
});
|
||||
|
||||
testWidgets('로그인 실패 시나리오', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Left(AuthenticationFailure(
|
||||
message: '이메일 또는 비밀번호가 올바르지 않습니다.',
|
||||
)));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'wrong@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'wrongpassword');
|
||||
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(controller.errorMessage, isNotNull);
|
||||
expect(controller.errorMessage, contains('올바르지 않습니다'));
|
||||
});
|
||||
|
||||
testWidgets('로딩 상태 표시', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
// 지연된 응답 시뮬레이션
|
||||
when(mockAuthService.login(any)).thenAnswer((_) async {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return Right(LoginResponse(
|
||||
accessToken: 'test_token',
|
||||
refreshToken: 'refresh_token',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
name: '테스트 사용자',
|
||||
role: 'USER',
|
||||
),
|
||||
));
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
final loginButton = find.widgetWithText(ElevatedButton, '로그인');
|
||||
await tester.tap(loginButton);
|
||||
|
||||
// 로딩 상태 확인
|
||||
await tester.pump();
|
||||
|
||||
// Assert - 로딩 중
|
||||
expect(controller.isLoading, true);
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
// 로딩 완료 대기
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.pump();
|
||||
|
||||
// Assert - 로딩 완료
|
||||
expect(controller.isLoading, false);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('비밀번호 표시/숨기기 토글', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: LoginController(),
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
// 초기 상태 - 비밀번호 숨김
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'testpassword');
|
||||
|
||||
// 비밀번호 표시 아이콘 찾기
|
||||
final visibilityIcon = find.byIcon(Icons.visibility_off);
|
||||
expect(visibilityIcon, findsOneWidget);
|
||||
|
||||
// 아이콘 탭하여 비밀번호 표시
|
||||
await tester.tap(visibilityIcon);
|
||||
await tester.pump();
|
||||
|
||||
// 비밀번호 표시 상태 확인
|
||||
expect(find.byIcon(Icons.visibility), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('아이디 저장 체크박스 동작', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
// 초기 상태
|
||||
expect(controller.saveId, false);
|
||||
|
||||
// 체크박스를 찾아서 탭
|
||||
await tester.pumpAndSettle(); // 위젯이 완전히 렌더링될 때까지 대기
|
||||
final checkbox = find.byType(Checkbox);
|
||||
expect(checkbox, findsOneWidget);
|
||||
await tester.tap(checkbox);
|
||||
await tester.pump();
|
||||
|
||||
// 상태 변경 확인
|
||||
expect(controller.saveId, true);
|
||||
|
||||
// 다시 탭하여 해제
|
||||
await tester.tap(find.byType(Checkbox));
|
||||
await tester.pump();
|
||||
|
||||
expect(controller.saveId, false);
|
||||
});
|
||||
|
||||
testWidgets('이메일 형식 검증', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: LoginViewRedesign(
|
||||
controller: controller,
|
||||
onLoginSuccess: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Act - 이메일 형식 입력
|
||||
final idField = find.byType(TextFormField).first;
|
||||
await tester.enterText(idField, 'test@example.com');
|
||||
|
||||
final passwordField = find.byType(TextFormField).last;
|
||||
await tester.enterText(passwordField, 'password123');
|
||||
|
||||
// LoginRequest 생성 시 이메일로 처리되는지 확인
|
||||
expect(controller.idController.text, 'test@example.com');
|
||||
|
||||
// Act - username 형식 입력
|
||||
await tester.enterText(idField, 'testuser');
|
||||
|
||||
// username으로 처리되는지 확인
|
||||
expect(controller.idController.text, 'testuser');
|
||||
});
|
||||
});
|
||||
|
||||
group('로그인 컨트롤러 단위 테스트', () {
|
||||
test('입력 검증 - 빈 아이디', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
controller.idController.text = '';
|
||||
controller.pwController.text = 'password';
|
||||
|
||||
// Act
|
||||
final result = await controller.login();
|
||||
|
||||
// Assert
|
||||
expect(result, false);
|
||||
expect(controller.errorMessage, contains('아이디 또는 이메일을 입력해주세요'));
|
||||
});
|
||||
|
||||
test('입력 검증 - 빈 비밀번호', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
controller.idController.text = 'test@example.com';
|
||||
controller.pwController.text = '';
|
||||
|
||||
// Act
|
||||
final result = await controller.login();
|
||||
|
||||
// Assert
|
||||
expect(result, false);
|
||||
expect(controller.errorMessage, contains('비밀번호를 입력해주세요'));
|
||||
});
|
||||
|
||||
test('이메일/username 구분', () async {
|
||||
// Arrange
|
||||
final controller = LoginController();
|
||||
|
||||
// Test 1: 이메일 형식
|
||||
controller.idController.text = 'test@example.com';
|
||||
controller.pwController.text = 'password';
|
||||
|
||||
when(mockAuthService.login(any))
|
||||
.thenAnswer((_) async => Right(LoginResponse(
|
||||
accessToken: 'token',
|
||||
refreshToken: 'refresh',
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: 3600,
|
||||
user: AuthUser(
|
||||
id: 1,
|
||||
username: 'test',
|
||||
email: 'test@example.com',
|
||||
name: 'Test',
|
||||
role: 'USER',
|
||||
),
|
||||
)));
|
||||
|
||||
// Act
|
||||
await controller.login();
|
||||
|
||||
// Assert
|
||||
final capturedRequest = verify(mockAuthService.login(captureAny)).captured.single;
|
||||
expect(capturedRequest.email, 'test@example.com');
|
||||
expect(capturedRequest.username, isNull);
|
||||
|
||||
// Test 2: Username 형식
|
||||
controller.idController.text = 'testuser';
|
||||
|
||||
// Act
|
||||
await controller.login();
|
||||
|
||||
// Assert
|
||||
final capturedRequest2 = verify(mockAuthService.login(captureAny)).captured.single;
|
||||
expect(capturedRequest2.email, isNull);
|
||||
expect(capturedRequest2.username, 'testuser');
|
||||
});
|
||||
});
|
||||
}
|
||||
153
test/widget/login_widget_test.mocks.dart
Normal file
153
test/widget/login_widget_test.mocks.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
// Mocks generated by Mockito 5.4.5 from annotations
|
||||
// in superport/test/widget/login_widget_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i4;
|
||||
|
||||
import 'package:dartz/dartz.dart' as _i2;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:superport/core/errors/failures.dart' as _i5;
|
||||
import 'package:superport/data/models/auth/auth_user.dart' as _i9;
|
||||
import 'package:superport/data/models/auth/login_request.dart' as _i7;
|
||||
import 'package:superport/data/models/auth/login_response.dart' as _i6;
|
||||
import 'package:superport/data/models/auth/token_response.dart' as _i8;
|
||||
import 'package:superport/services/auth_service.dart' as _i3;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
|
||||
class _FakeEither_0<L, R> extends _i1.SmartFake implements _i2.Either<L, R> {
|
||||
_FakeEither_0(
|
||||
Object parent,
|
||||
Invocation parentInvocation,
|
||||
) : super(
|
||||
parent,
|
||||
parentInvocation,
|
||||
);
|
||||
}
|
||||
|
||||
/// A class which mocks [AuthService].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockAuthService extends _i1.Mock implements _i3.AuthService {
|
||||
MockAuthService() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i4.Stream<bool> get authStateChanges => (super.noSuchMethod(
|
||||
Invocation.getter(#authStateChanges),
|
||||
returnValue: _i4.Stream<bool>.empty(),
|
||||
) as _i4.Stream<bool>);
|
||||
|
||||
@override
|
||||
_i4.Future<_i2.Either<_i5.Failure, _i6.LoginResponse>> login(
|
||||
_i7.LoginRequest? request) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#login,
|
||||
[request],
|
||||
),
|
||||
returnValue:
|
||||
_i4.Future<_i2.Either<_i5.Failure, _i6.LoginResponse>>.value(
|
||||
_FakeEither_0<_i5.Failure, _i6.LoginResponse>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#login,
|
||||
[request],
|
||||
),
|
||||
)),
|
||||
) as _i4.Future<_i2.Either<_i5.Failure, _i6.LoginResponse>>);
|
||||
|
||||
@override
|
||||
_i4.Future<_i2.Either<_i5.Failure, void>> logout() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#logout,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<_i2.Either<_i5.Failure, void>>.value(
|
||||
_FakeEither_0<_i5.Failure, void>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#logout,
|
||||
[],
|
||||
),
|
||||
)),
|
||||
) as _i4.Future<_i2.Either<_i5.Failure, void>>);
|
||||
|
||||
@override
|
||||
_i4.Future<_i2.Either<_i5.Failure, _i8.TokenResponse>> refreshToken() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#refreshToken,
|
||||
[],
|
||||
),
|
||||
returnValue:
|
||||
_i4.Future<_i2.Either<_i5.Failure, _i8.TokenResponse>>.value(
|
||||
_FakeEither_0<_i5.Failure, _i8.TokenResponse>(
|
||||
this,
|
||||
Invocation.method(
|
||||
#refreshToken,
|
||||
[],
|
||||
),
|
||||
)),
|
||||
) as _i4.Future<_i2.Either<_i5.Failure, _i8.TokenResponse>>);
|
||||
|
||||
@override
|
||||
_i4.Future<bool> isLoggedIn() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#isLoggedIn,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<bool>.value(false),
|
||||
) as _i4.Future<bool>);
|
||||
|
||||
@override
|
||||
_i4.Future<_i9.AuthUser?> getCurrentUser() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getCurrentUser,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<_i9.AuthUser?>.value(),
|
||||
) as _i4.Future<_i9.AuthUser?>);
|
||||
|
||||
@override
|
||||
_i4.Future<String?> getAccessToken() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getAccessToken,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<String?>.value(),
|
||||
) as _i4.Future<String?>);
|
||||
|
||||
@override
|
||||
_i4.Future<String?> getRefreshToken() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getRefreshToken,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<String?>.value(),
|
||||
) as _i4.Future<String?>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> clearSession() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#clearSession,
|
||||
[],
|
||||
),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
) as _i4.Future<void>);
|
||||
}
|
||||
Reference in New Issue
Block a user