import 'package:flutter_test/flutter_test.dart'; import 'package:superport/services/company_service.dart'; import 'package:superport/models/company_model.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/data/models/company/company_dto.dart'; import 'screens/base/base_screen_test.dart'; import 'framework/models/test_models.dart'; import 'framework/models/error_models.dart'; import 'framework/models/report_models.dart' as report_models; /// 회사(Company) 화면 자동화 테스트 /// /// 이 테스트는 회사 관리 전체 프로세스를 자동으로 실행하고, /// 에러 발생 시 자동으로 진단하고 수정합니다. class CompanyAutomatedTest extends BaseScreenTest { late CompanyService companyService; CompanyAutomatedTest({ required super.apiClient, required super.getIt, required super.testContext, required super.errorDiagnostics, required super.autoFixer, required super.dataGenerator, required super.reportCollector, }); @override ScreenMetadata getScreenMetadata() { return ScreenMetadata( screenName: 'CompanyScreen', controllerType: CompanyService, relatedEndpoints: [ ApiEndpoint( path: '/api/v1/companies', method: 'POST', description: '회사 생성', ), ApiEndpoint( path: '/api/v1/companies', method: 'GET', description: '회사 목록 조회', ), ApiEndpoint( path: '/api/v1/companies/{id}', method: 'GET', description: '회사 상세 조회', ), ApiEndpoint( path: '/api/v1/companies/{id}', method: 'PUT', description: '회사 수정', ), ApiEndpoint( path: '/api/v1/companies/{id}', method: 'DELETE', description: '회사 삭제', ), ApiEndpoint( path: '/api/v1/companies/{id}/branches', method: 'POST', description: '지점 생성', ), ApiEndpoint( path: '/api/v1/companies/{id}/branches', method: 'GET', description: '지점 목록 조회', ), ApiEndpoint( path: '/api/v1/companies/check-duplicate', method: 'GET', description: '회사명 중복 확인', ), ], screenCapabilities: { 'company_management': { 'crud': true, 'branch_management': true, 'duplicate_check': true, 'search': true, 'pagination': true, }, }, ); } @override Future initializeServices() async { companyService = getIt(); } @override dynamic getService() => companyService; @override String getResourceType() => 'company'; @override Map getDefaultFilters() { return { 'isActive': true, }; } @override Future> detectCustomFeatures(ScreenMetadata metadata) async { final features = []; // 회사 관리 기능 테스트 features.add(TestableFeature( featureName: 'Company Management', type: FeatureType.custom, testCases: [ // 정상 회사 생성 시나리오 TestCase( name: 'Normal company creation', execute: (data) async { await performNormalCompanyCreation(data); }, verify: (data) async { await verifyNormalCompanyCreation(data); }, ), // 지점 관리 시나리오 TestCase( name: 'Branch management', execute: (data) async { await performBranchManagement(data); }, verify: (data) async { await verifyBranchManagement(data); }, ), // 중복 사업자번호 처리 시나리오 TestCase( name: 'Duplicate business number handling', execute: (data) async { await performDuplicateBusinessNumber(data); }, verify: (data) async { await verifyDuplicateBusinessNumber(data); }, ), // 필수 필드 누락 시나리오 TestCase( name: 'Missing required fields', execute: (data) async { await performMissingRequiredFields(data); }, verify: (data) async { await verifyMissingRequiredFields(data); }, ), // 잘못된 데이터 형식 시나리오 TestCase( name: 'Invalid data format', execute: (data) async { await performInvalidDataFormat(data); }, verify: (data) async { await verifyInvalidDataFormat(data); }, ), ], metadata: { 'description': '회사 관리 프로세스 자동화 테스트', }, )); return features; } /// 정상 회사 생성 프로세스 Future performNormalCompanyCreation(TestData data) async { _log('=== 정상 회사 생성 프로세스 시작 ==='); try { // 1. 회사 데이터 자동 생성 _log('회사 데이터 자동 생성 중...'); final companyData = await dataGenerator.generate( GenerationStrategy( dataType: CreateCompanyRequest, fields: [ FieldGeneration( fieldName: 'name', valueType: String, strategy: 'unique', prefix: 'AutoTest Company ', ), FieldGeneration( fieldName: 'contactName', valueType: String, strategy: 'realistic', pool: ['김철수', '이영희', '박민수', '최수진', '정대성'], ), FieldGeneration( fieldName: 'contactPosition', valueType: String, strategy: 'realistic', pool: ['대표이사', '부장', '차장', '과장', '팀장'], ), FieldGeneration( fieldName: 'contactPhone', valueType: String, strategy: 'pattern', format: '010-{RANDOM:4}-{RANDOM:4}', ), FieldGeneration( fieldName: 'contactEmail', valueType: String, strategy: 'pattern', format: '{FIRSTNAME}@{COMPANY}.com', ), ], relationships: [], constraints: {}, ), ); _log('생성된 회사 데이터: ${companyData.toJson()}'); // 2. 회사 생성 _log('회사 생성 API 호출 중...'); Company? createdCompany; try { // CreateCompanyRequest를 Company 객체로 변환 final companyReq = companyData.data as CreateCompanyRequest; final company = Company( id: 0, name: companyReq.name, address: Address( zipCode: '12345', region: '서울시', detailAddress: '강남구 테헤란로 123', ), contactName: companyReq.contactName, contactPosition: companyReq.contactPosition, contactPhone: companyReq.contactPhone, contactEmail: companyReq.contactEmail, companyTypes: companyReq.companyTypes.map((type) { if (type.contains('partner')) return CompanyType.partner; return CompanyType.customer; }).toList(), remark: companyReq.remark, ); createdCompany = await companyService.createCompany(company); _log('회사 생성 성공: ID=${createdCompany.id}'); testContext.addCreatedResourceId('company', createdCompany.id.toString()); } catch (e) { _log('회사 생성 실패: $e'); // 에러 진단 final diagnosis = await errorDiagnostics.diagnose( ApiError( endpoint: '/api/v1/companies', method: 'POST', statusCode: 400, message: e.toString(), requestBody: companyData.toJson(), timestamp: DateTime.now(), requestUrl: '/api/v1/companies', requestMethod: 'POST', ), ); _log('에러 진단 결과: ${diagnosis.errorType} - ${diagnosis.description}'); // 자동 수정 final fixResult = await autoFixer.attemptAutoFix(diagnosis); if (!fixResult.success) { throw Exception('자동 수정 실패: ${fixResult.error}'); } // 수정된 데이터로 재시도 _log('수정된 데이터로 재시도...'); final fixedReq = companyData.data as CreateCompanyRequest; final fixedCompany = Company( id: 0, name: fixedReq.name, address: Address( zipCode: '12345', region: '서울시', detailAddress: '강남구 테헤란로 123', ), contactName: '담당자', contactPosition: '직책', contactPhone: '010-0000-0000', contactEmail: 'contact@company.com', companyTypes: [CompanyType.customer], remark: fixedReq.remark, ); createdCompany = await companyService.createCompany(fixedCompany); _log('회사 생성 성공 (재시도): ID=${createdCompany.id}'); testContext.addCreatedResourceId('company', createdCompany.id.toString()); } // 3. 생성된 회사 조회 _log('생성된 회사 조회 중...'); final companyDetail = await companyService.getCompanyDetail(createdCompany.id!); _log('회사 상세 조회 성공: ${companyDetail.name}'); testContext.setData('createdCompany', createdCompany); testContext.setData('companyDetail', companyDetail); testContext.setData('processSuccess', true); } catch (e) { _log('예상치 못한 오류 발생: $e'); testContext.setData('processSuccess', false); testContext.setData('lastError', e.toString()); } } /// 정상 회사 생성 검증 Future verifyNormalCompanyCreation(TestData data) async { final processSuccess = testContext.getData('processSuccess') ?? false; expect(processSuccess, isTrue, reason: '회사 생성 프로세스가 실패했습니다'); final createdCompany = testContext.getData('createdCompany'); expect(createdCompany, isNotNull, reason: '회사가 생성되지 않았습니다'); final companyDetail = testContext.getData('companyDetail'); expect(companyDetail, isNotNull, reason: '회사 상세 정보를 조회할 수 없습니다'); // 생성된 회사와 조회된 회사 정보가 일치하는지 확인 expect(createdCompany.id, equals(companyDetail.id), reason: '회사 ID가 일치하지 않습니다'); expect(createdCompany.name, equals(companyDetail.name), reason: '회사명이 일치하지 않습니다'); _log('✓ 정상 회사 생성 프로세스 검증 완료'); } /// 지점 관리 시나리오 Future performBranchManagement(TestData data) async { _log('=== 지점 관리 시나리오 시작 ==='); // 먼저 회사 생성 await performNormalCompanyCreation(data); final company = testContext.getData('createdCompany') as Company; try { // 1. 지점 생성 _log('지점 생성 중...'); final branch = Branch( id: 0, companyId: company.id!, name: '강남지점', address: Address( zipCode: '06000', region: '서울시', detailAddress: '강남구 역삼동 123-45', ), contactName: '김지점장', contactPhone: '02-1234-5678', ); final createdBranch = await companyService.createBranch(company.id!, branch); _log('지점 생성 성공: ID=${createdBranch.id}'); testContext.setData('createdBranch', createdBranch); // 2. 지점 목록 조회 _log('지점 목록 조회 중...'); final branches = await companyService.getCompanyBranches(company.id!); _log('지점 목록 조회 성공: ${branches.length}개'); testContext.setData('branches', branches); // 3. 지점 수정 _log('지점 정보 수정 중...'); final updatedBranch = branch.copyWith( name: '강남지점 (수정됨)', contactName: '이지점장', ); final modifiedBranch = await companyService.updateBranch( company.id!, createdBranch.id!, updatedBranch, ); _log('지점 수정 성공'); testContext.setData('modifiedBranch', modifiedBranch); // 4. 지점 삭제 _log('지점 삭제 중...'); await companyService.deleteBranch(company.id!, createdBranch.id!); _log('지점 삭제 성공'); testContext.setData('branchManagementSuccess', true); } catch (e) { _log('지점 관리 중 오류 발생: $e'); testContext.setData('branchManagementSuccess', false); testContext.setData('branchError', e.toString()); } } /// 지점 관리 시나리오 검증 Future verifyBranchManagement(TestData data) async { final success = testContext.getData('branchManagementSuccess') ?? false; expect(success, isTrue, reason: '지점 관리가 실패했습니다'); final createdBranch = testContext.getData('createdBranch'); expect(createdBranch, isNotNull, reason: '지점이 생성되지 않았습니다'); final branches = testContext.getData('branches') as List?; expect(branches, isNotNull, reason: '지점 목록을 조회할 수 없습니다'); expect(branches!.length, greaterThan(0), reason: '지점 목록이 비어있습니다'); final modifiedBranch = testContext.getData('modifiedBranch'); expect(modifiedBranch, isNotNull, reason: '지점 수정이 실패했습니다'); expect(modifiedBranch.name, contains('수정됨'), reason: '지점명이 수정되지 않았습니다'); _log('✓ 지점 관리 시나리오 검증 완료'); } /// 중복 사업자번호 처리 시나리오 Future performDuplicateBusinessNumber(TestData data) async { _log('=== 중복 사업자번호 처리 시나리오 시작 ==='); // 첫 번째 회사 생성 final firstCompany = Company( id: 0, name: 'Duplicate Test Company 1', address: Address( zipCode: '12345', region: '서울시', detailAddress: '테스트 주소', ), contactName: '담당자1', contactPhone: '010-1111-1111', companyTypes: [CompanyType.customer], ); final created1 = await companyService.createCompany(firstCompany); testContext.addCreatedResourceId('company', created1.id.toString()); _log('첫 번째 회사 생성 성공: ${created1.name}'); // 같은 이름으로 두 번째 회사 생성 시도 try { // 중복 확인 _log('회사명 중복 확인 중...'); final isDuplicate = await companyService.checkDuplicateCompany(firstCompany.name); if (isDuplicate) { _log('중복된 회사명 감지됨'); // 자동으로 고유한 이름 생성 final uniqueName = '${firstCompany.name} - ${DateTime.now().millisecondsSinceEpoch}'; final secondCompany = firstCompany.copyWith( name: uniqueName, contactName: '담당자2', ); final created2 = await companyService.createCompany(secondCompany); testContext.addCreatedResourceId('company', created2.id.toString()); _log('고유한 이름으로 회사 생성 성공: ${created2.name}'); testContext.setData('duplicateHandled', true); testContext.setData('uniqueName', uniqueName); } else { // 시스템이 중복을 허용하는 경우 _log('경고: 시스템이 중복 회사명을 허용합니다'); testContext.setData('duplicateAllowed', true); } } catch (e) { _log('중복 처리 중 오류 발생: $e'); testContext.setData('duplicateError', e.toString()); } } /// 중복 사업자번호 처리 검증 Future verifyDuplicateBusinessNumber(TestData data) async { final duplicateHandled = testContext.getData('duplicateHandled') ?? false; final duplicateAllowed = testContext.getData('duplicateAllowed') ?? false; expect( duplicateHandled || duplicateAllowed, isTrue, reason: '중복 처리가 올바르게 수행되지 않았습니다', ); if (duplicateHandled) { final uniqueName = testContext.getData('uniqueName'); expect(uniqueName, isNotNull, reason: '고유한 이름이 생성되지 않았습니다'); _log('✓ 고유한 이름으로 회사 생성됨: $uniqueName'); } _log('✓ 중복 사업자번호 처리 시나리오 검증 완료'); } /// 필수 필드 누락 시나리오 Future performMissingRequiredFields(TestData data) async { _log('=== 필수 필드 누락 시나리오 시작 ==='); // 필수 필드가 누락된 회사 데이터 final incompleteCompany = Company( id: 0, name: '', // 빈 회사명 (필수 필드) address: Address( zipCode: '', region: '', detailAddress: '', ), companyTypes: [], // 빈 회사 타입 ); try { await companyService.createCompany(incompleteCompany); fail('필수 필드가 누락된 데이터로 회사가 생성되어서는 안 됩니다'); } catch (e) { _log('예상된 에러 발생: $e'); // 에러 진단 final diagnosis = await errorDiagnostics.diagnose( ApiError( endpoint: '/api/v1/companies', method: 'POST', statusCode: 400, message: e.toString(), requestBody: incompleteCompany.toJson(), timestamp: DateTime.now(), requestUrl: '/api/v1/companies', requestMethod: 'POST', ), ); expect(diagnosis.errorType, equals(ErrorType.missingRequiredField)); _log('진단 결과: ${diagnosis.missingFields?.length ?? 0}개 필드 누락'); // 자동 수정 final fixResult = await autoFixer.attemptAutoFix(diagnosis); if (!fixResult.success) { throw Exception('자동 수정 실패: ${fixResult.error}'); } // 수정된 데이터로 재시도 final fixedCompany = Company( id: 0, name: 'Auto-Fixed Company ${DateTime.now().millisecondsSinceEpoch}', address: Address( zipCode: '00000', region: '미지정', detailAddress: '자동 생성 주소', ), contactName: '미지정', contactPhone: '000-0000-0000', companyTypes: [CompanyType.customer], ); _log('수정된 데이터: ${fixedCompany.toJson()}'); final created = await companyService.createCompany(fixedCompany); testContext.addCreatedResourceId('company', created.id.toString()); testContext.setData('missingFieldsFixed', true); testContext.setData('fixedCompany', created); } } /// 필수 필드 누락 시나리오 검증 Future verifyMissingRequiredFields(TestData data) async { final missingFieldsFixed = testContext.getData('missingFieldsFixed') ?? false; expect(missingFieldsFixed, isTrue, reason: '필수 필드 누락 문제가 해결되지 않았습니다'); final fixedCompany = testContext.getData('fixedCompany'); expect(fixedCompany, isNotNull, reason: '수정된 회사가 생성되지 않았습니다'); _log('✓ 필수 필드 누락 시나리오 검증 완료'); } /// 잘못된 데이터 형식 시나리오 Future performInvalidDataFormat(TestData data) async { _log('=== 잘못된 데이터 형식 시나리오 시작 ==='); // 잘못된 형식의 데이터 final invalidCompany = Company( id: 0, name: 'Invalid Format Company', address: Address( zipCode: '12345', region: '서울시', detailAddress: '테스트 주소', ), contactEmail: 'invalid-email-format', // 잘못된 이메일 형식 contactPhone: '1234567890', // 잘못된 전화번호 형식 companyTypes: [CompanyType.customer], ); try { await companyService.createCompany(invalidCompany); // 일부 시스템은 형식 검증을 하지 않을 수 있음 _log('경고: 시스템이 데이터 형식을 검증하지 않습니다'); testContext.setData('formatValidationExists', false); } catch (e) { _log('예상된 형식 에러 발생: $e'); // 에러 진단 await errorDiagnostics.diagnose( ApiError( endpoint: '/api/v1/companies', method: 'POST', statusCode: 400, message: e.toString(), requestBody: invalidCompany.toJson(), timestamp: DateTime.now(), requestUrl: '/api/v1/companies', requestMethod: 'POST', ), ); // 올바른 형식으로 수정 final validCompany = Company( id: 0, name: invalidCompany.name, address: invalidCompany.address, contactEmail: 'contact@company.com', // 올바른 이메일 형식 contactPhone: '010-1234-5678', // 올바른 전화번호 형식 companyTypes: invalidCompany.companyTypes, ); _log('형식을 수정한 데이터로 재시도...'); final created = await companyService.createCompany(validCompany); testContext.addCreatedResourceId('company', created.id.toString()); testContext.setData('formatFixed', true); testContext.setData('validCompany', created); } } /// 잘못된 데이터 형식 시나리오 검증 Future verifyInvalidDataFormat(TestData data) async { final formatValidationExists = testContext.getData('formatValidationExists'); final formatFixed = testContext.getData('formatFixed') ?? false; if (formatValidationExists == false) { _log('⚠️ 경고: 시스템에 데이터 형식 검증이 구현되지 않았습니다'); } else { expect(formatFixed, isTrue, reason: '데이터 형식 문제가 해결되지 않았습니다'); final validCompany = testContext.getData('validCompany'); expect(validCompany, isNotNull, reason: '올바른 형식의 회사가 생성되지 않았습니다'); } _log('✓ 잘못된 데이터 형식 시나리오 검증 완료'); } // BaseScreenTest의 추상 메서드 구현 @override Future performCreateOperation(TestData data) async { final company = Company( id: 0, name: data.data['name'] ?? 'Test Company ${DateTime.now().millisecondsSinceEpoch}', address: Address( zipCode: data.data['zipCode'] ?? '12345', region: data.data['region'] ?? '서울시', detailAddress: data.data['address'] ?? '테스트 주소', ), contactName: data.data['contactName'], contactPosition: data.data['contactPosition'], contactPhone: data.data['contactPhone'], contactEmail: data.data['contactEmail'], companyTypes: [CompanyType.customer], remark: data.data['remark'], ); return await companyService.createCompany(company); } @override Future performReadOperation(TestData data) async { return await companyService.getCompanies( page: data.data['page'] ?? 1, perPage: data.data['perPage'] ?? 20, search: data.data['search'], isActive: data.data['isActive'], ); } @override Future performUpdateOperation(dynamic resourceId, Map updateData) async { final currentCompany = await companyService.getCompanyDetail(resourceId as int); final updatedCompany = currentCompany.copyWith( name: updateData['name'] ?? currentCompany.name, address: updateData['address'] != null ? Address.fromFullAddress(updateData['address']) : currentCompany.address, contactName: updateData['contactName'], contactPosition: updateData['contactPosition'], contactPhone: updateData['contactPhone'], contactEmail: updateData['contactEmail'], remark: updateData['remark'], ); return await companyService.updateCompany(resourceId, updatedCompany); } @override Future performDeleteOperation(dynamic resourceId) async { await companyService.deleteCompany(resourceId as int); } @override dynamic extractResourceId(dynamic resource) { return (resource as Company).id; } // 헬퍼 메서드 void _log(String message) { // Logging via report collector only // 리포트 수집기에도 로그 추가 reportCollector.addStep( report_models.StepReport( stepName: 'Company Management', timestamp: DateTime.now(), success: !message.contains('실패') && !message.contains('에러'), message: message, details: {}, ), ); } } // Branch 모델에 copyWith 메서드 추가 extension BranchExtension on Branch { Branch copyWith({ int? id, int? companyId, String? name, Address? address, String? contactName, String? contactPhone, String? remark, }) { return Branch( id: id ?? this.id, companyId: companyId ?? this.companyId, name: name ?? this.name, address: address ?? this.address, contactName: contactName ?? this.contactName, contactPhone: contactPhone ?? this.contactPhone, remark: remark ?? this.remark, ); } } // 테스트 실행을 위한 main 함수 void main() { group('Company Automated Test', () { test('This is a screen test class, not a standalone test', () { // 이 클래스는 BaseScreenTest를 상속받아 프레임워크를 통해 실행됩니다 // 직접 실행하려면 run_company_test.dart를 사용하세요 expect(true, isTrue); }); }); }