binder = $binder ?? new VariableBinder; $this->resolver = $resolver ?? new DependencyResolver; $this->validator = $validator ?? new ResponseValidator; $this->httpClient = $httpClient ?? new HttpClient; $this->logCapturer = $logCapturer ?? new ApiLogCapturer; $this->conditionEvaluator = $conditionEvaluator ?? new ConditionEvaluator($this->binder); } /** * 플로우 실행 * * @param array $flowDefinition 플로우 정의 JSON * @param array $inputVariables 입력 변수 (실행 시 주입) * @return array 실행 결과 */ public function execute(array $flowDefinition, array $inputVariables = []): array { $startTime = microtime(true); $this->reset(); // API 로그 캡처 시작 $this->logCapturer->start(); try { // 1. 플로우 정의 검증 $this->validateFlowDefinition($flowDefinition); // 2. 설정 적용 $this->applyConfig($flowDefinition['config'] ?? []); // 3. 전역 변수 초기화 $variables = array_merge( $flowDefinition['variables'] ?? [], $inputVariables ); $this->binder->setVariables($variables); // 4. 스텝 정보 추출 $steps = $flowDefinition['steps'] ?? []; $this->totalSteps = count($steps); if ($this->totalSteps === 0) { return $this->buildResult($startTime, 'SUCCESS', 'No steps to execute'); } // 5. 의존성 정렬 $orderedStepIds = $this->resolver->resolve($steps); $stepMap = $this->buildStepMap($steps); // 6. 실행 상태 변경 $this->status = 'RUNNING'; // 7. 각 단계 실행 $stopOnFailure = $flowDefinition['config']['stopOnFailure'] ?? true; foreach ($orderedStepIds as $stepId) { $step = $stepMap[$stepId]; $stepName = $step['name'] ?? $stepId; // 1. 의존성 스텝 성공 여부 확인 $dependencyCheck = $this->checkDependencies($step); if (! $dependencyCheck['canRun']) { // 의존성 실패로 스킵 $skipResult = $this->buildStepResult($stepId, $stepName, microtime(true), false, [ 'skipped' => true, 'skipReason' => $dependencyCheck['reason'], 'skipType' => 'dependency', 'failedDependencies' => $dependencyCheck['failedDeps'], ]); $this->executionLog[] = $skipResult; $this->stepSuccessMap[$stepId] = false; $this->registerStepResult($stepId, $skipResult); continue; } // 2. 조건(condition) 평가 - 분기 처리 if (isset($step['condition'])) { $conditionResult = $this->conditionEvaluator->evaluate($step['condition']); if (! $conditionResult['passed']) { // 조건 불만족으로 스킵 (실패가 아님, 정상 스킵) $skipResult = $this->buildStepResult($stepId, $stepName, microtime(true), true, [ 'skipped' => true, 'skipReason' => 'Condition not met: '.$conditionResult['reason'], 'skipType' => 'condition', 'conditionEvaluation' => $conditionResult, ]); $this->executionLog[] = $skipResult; // 조건 스킵은 성공으로 처리 (분기에서 선택되지 않은 것일 뿐) $this->stepSuccessMap[$stepId] = true; $this->completedSteps++; $this->registerStepResult($stepId, $skipResult); continue; } } // 3. Bearer 토큰 동적 업데이트 (login 스텝 이후) $this->updateBearerToken(); // 4. 스텝 실행 $stepResult = $this->executeStep($step); $this->executionLog[] = $stepResult; $this->stepSuccessMap[$stepId] = $stepResult['success']; $this->registerStepResult($stepId, $stepResult); if ($stepResult['success']) { $this->completedSteps++; } else { if (! ($step['continueOnFailure'] ?? false) && $stopOnFailure) { $this->status = 'FAILED'; return $this->buildResult( $startTime, 'FAILED', "Step '{$stepId}' failed: ".($stepResult['error'] ?? 'Unknown error'), $stepId ); } // continueOnFailure가 true면 계속 진행 } } // 8. 최종 상태 결정 $this->status = $this->completedSteps === $this->totalSteps ? 'SUCCESS' : 'PARTIAL'; return $this->buildResult($startTime, $this->status); } catch (Exception $e) { $this->status = 'FAILED'; return $this->buildResult($startTime, 'FAILED', $e->getMessage()); } } /** * 의존성 스텝 성공 여부 확인 * * 조건부 의존성 지원: * - 단순 문자열: "step_id" - 해당 스텝이 성공해야 실행 * - 조건부 객체: {"step": "step_id", "onlyIf": "success|failure|executed|skipped|any"} * * @param array $step 실행할 스텝 * @return array ['canRun' => bool, 'reason' => string|null, 'failedDeps' => array] */ private function checkDependencies(array $step): array { $dependencies = $step['dependsOn'] ?? []; if (empty($dependencies)) { return ['canRun' => true, 'reason' => null, 'failedDeps' => []]; } $failedDeps = []; foreach ($dependencies as $dep) { // 조건부 의존성 파싱 $parsed = $this->resolver->parseDependency($dep); $depId = $parsed['stepId']; $condition = $parsed['condition']; // 의존 스텝 결과 조회 $stepResults = $this->conditionEvaluator->getStepResults(); $depResult = $stepResults[$depId] ?? null; // 의존 스텝이 아직 실행되지 않은 경우 (순서 문제) if ($depResult === null && ! isset($this->stepSuccessMap[$depId])) { $failedDeps[] = "{$depId} (not executed yet)"; continue; } // 조건에 따른 의존성 체크 $conditionMet = $this->checkDependencyCondition($depId, $condition, $depResult); if (! $conditionMet) { $failedDeps[] = "{$depId} (condition '{$condition}' not met)"; } } if (! empty($failedDeps)) { return [ 'canRun' => false, 'reason' => 'Dependency condition failed: '.implode(', ', $failedDeps), 'failedDeps' => $failedDeps, ]; } return ['canRun' => true, 'reason' => null, 'failedDeps' => []]; } /** * 의존성 조건 체크 * * @param string $depId 의존 스텝 ID * @param string $condition 조건 (success, failure, executed, skipped, any) * @param array|null $depResult 의존 스텝 실행 결과 * @return bool 조건 충족 여부 */ private function checkDependencyCondition(string $depId, string $condition, ?array $depResult): bool { // stepSuccessMap에서 기본 정보 조회 $isSuccess = $this->stepSuccessMap[$depId] ?? false; $isSkipped = $depResult['skipped'] ?? false; $isExecuted = isset($this->stepSuccessMap[$depId]) && ! $isSkipped; return match ($condition) { 'success' => $isSuccess && ! $isSkipped, 'failure', 'failed' => ! $isSuccess && ! $isSkipped && $isExecuted, 'executed' => $isExecuted, 'skipped' => $isSkipped, 'any' => true, // 순서만 보장, 결과 무관 default => $isSuccess, // 기본값은 success }; } /** * 단일 스텝 실행 */ private function executeStep(array $step): array { $stepId = $step['id']; $stepName = $step['name'] ?? $stepId; $startTime = microtime(true); try { // 0. useSessionAuth 옵션 처리 - 세션 인증 정보로 대체 if (! empty($step['useSessionAuth'])) { return $this->executeSessionAuthStep($step, $stepId, $stepName, $startTime); } // 1. 딜레이 적용 $delay = $step['delay'] ?? 0; if ($delay > 0) { usleep($delay * 1000); // ms → μs } // 2. 변수 바인딩 $endpoint = $this->binder->bind($step['endpoint']); $headers = $this->binder->bind($step['headers'] ?? []); $body = $this->binder->bind($step['body'] ?? []); $query = $this->binder->bind($step['query'] ?? []); // 3. HTTP 요청 실행 $method = strtoupper($step['method']); $response = $this->httpClient->request($method, $endpoint, [ 'headers' => $headers, 'body' => $body, 'query' => $query, ]); // 4. HTTP 에러 체크 if ($response['error']) { return $this->buildStepResult($stepId, $stepName, $startTime, false, [ 'error' => $response['error'], 'request' => [ 'method' => $method, 'endpoint' => $endpoint, 'headers' => $headers, 'body' => $body, 'query' => $query, ], ]); } // 5. 응답 검증 (expect 또는 assertions 둘 다 지원) $expect = $step['expect'] ?? []; if (empty($expect) && isset($step['assertions'])) { $expect = $this->convertAssertionsToExpect($step['assertions']); } // expect가 비어있으면 기본적으로 2xx 상태 코드 기대 if (empty($expect) || ! isset($expect['status'])) { $expect['status'] = [200, 201, 204]; } // expect 값에도 변수 바인딩 적용 (jsonPath 값의 {{변수}} 치환) $expect = $this->bindExpectVariables($expect); $validation = $this->validator->validate($response, $expect); // 6. 변수 추출 $extracted = []; if (isset($step['extract'])) { $extracted = $this->validator->extractValues($response['body'], $step['extract']); $this->binder->setStepResult($stepId, $extracted, $response['body']); } // 7. 결과 구성 $success = $validation['success']; // 성공/실패 이유 설명 생성 $reason = $this->buildResultReason($success, $expect, $response['status'], $step); return $this->buildStepResult($stepId, $stepName, $startTime, $success, [ 'description' => $step['description'] ?? null, 'reason' => $reason, 'expect' => $expect, 'request' => [ 'method' => $method, 'endpoint' => $endpoint, 'headers' => $headers, 'body' => $body, 'query' => $query, ], 'response' => [ 'status' => $response['status'], 'body' => $response['body'], 'duration' => $response['duration'], ], 'extracted' => $extracted, 'validation' => $validation, 'error' => $success ? null : implode('; ', $validation['errors']), ]); } catch (Exception $e) { return $this->buildStepResult($stepId, $stepName, $startTime, false, [ 'error' => $e->getMessage(), ]); } } /** * 세션 인증 스텝 실행 * * useSessionAuth: true 옵션이 있는 login 스텝에서 사용 * - 세션 토큰이 있으면 → 세션 토큰 사용 (API 호출 스킵) * - 세션 토큰이 없으면 → .env 크레덴셜로 실제 API 로그인 * * @param array $step 스텝 정의 * @param string $stepId 스텝 ID * @param string $stepName 스텝 이름 * @param float $startTime 시작 시간 * @return array 스텝 결과 */ private function executeSessionAuthStep(array $step, string $stepId, string $stepName, float $startTime): array { // 세션 인증 정보 조회 $sessionAuth = $this->binder->getSessionAuth(); // 세션 토큰이 있으면 사용 (API 호출 스킵) if (! empty($sessionAuth['token'])) { $extracted = [ 'token' => $sessionAuth['token'], 'access_token' => $sessionAuth['token'], ]; if ($sessionAuth['user']) { $extracted['user_id'] = $sessionAuth['user']['id']; $extracted['user_name'] = $sessionAuth['user']['name']; $extracted['user_email'] = $sessionAuth['user']['email']; } if ($sessionAuth['tenant_id']) { $extracted['tenant_id'] = $sessionAuth['tenant_id']; } $this->binder->setStepResult($stepId, $extracted, [ 'message' => '세션 인증 사용', 'user' => $sessionAuth['user'], 'tenant_id' => $sessionAuth['tenant_id'], ]); return $this->buildStepResult($stepId, $stepName, $startTime, true, [ 'description' => $step['description'] ?? '세션 인증 정보 사용', 'reason' => '✓ 세션 인증 사용 (API 호출 생략)', 'useSessionAuth' => true, 'sessionUser' => $sessionAuth['user'], 'extracted' => $extracted, 'response' => [ 'status' => 200, 'body' => [ 'message' => '세션 인증 정보를 사용합니다.', 'access_token' => substr($sessionAuth['token'], 0, 20).'...', 'user' => $sessionAuth['user'], 'tenant_id' => $sessionAuth['tenant_id'], ], 'duration' => 0, ], ]); } // 세션 토큰이 없으면 .env로 폴백하여 실제 API 로그인 // useSessionAuth 옵션을 제거하고 일반 스텝처럼 실행 $fallbackStep = $step; unset($fallbackStep['useSessionAuth']); $fallbackStep['description'] = '.env 크레덴셜로 API 로그인 (세션 토큰 없음)'; return $this->executeLoginStep($fallbackStep, $stepId, $stepName, $startTime); } /** * 일반 로그인 스텝 실행 (실제 API 호출) * * @param array $step 스텝 정의 * @param string $stepId 스텝 ID * @param string $stepName 스텝 이름 * @param float $startTime 시작 시간 * @return array 스텝 결과 */ private function executeLoginStep(array $step, string $stepId, string $stepName, float $startTime): array { try { // 딜레이 적용 $delay = $step['delay'] ?? 0; if ($delay > 0) { usleep($delay * 1000); } // 변수 바인딩 $endpoint = $this->binder->bind($step['endpoint']); $headers = $this->binder->bind($step['headers'] ?? []); $body = $this->binder->bind($step['body'] ?? []); $query = $this->binder->bind($step['query'] ?? []); // HTTP 요청 실행 $method = strtoupper($step['method']); $response = $this->httpClient->request($method, $endpoint, [ 'headers' => $headers, 'body' => $body, 'query' => $query, ]); // HTTP 에러 체크 if ($response['error']) { return $this->buildStepResult($stepId, $stepName, $startTime, false, [ 'error' => $response['error'], 'reason' => '✗ .env 폴백 로그인 실패: '.$response['error'], 'request' => [ 'method' => $method, 'endpoint' => $endpoint, 'headers' => $headers, 'body' => $body, ], ]); } // 응답 검증 $expect = $step['expect'] ?? ['status' => [200, 201]]; $validation = $this->validator->validate($response, $expect); // 변수 추출 $extracted = []; if (isset($step['extract'])) { $extracted = $this->validator->extractValues($response['body'], $step['extract']); $this->binder->setStepResult($stepId, $extracted, $response['body']); } $success = $validation['success']; return $this->buildStepResult($stepId, $stepName, $startTime, $success, [ 'description' => $step['description'] ?? '.env 크레덴셜로 API 로그인', 'reason' => $success ? '✓ .env 폴백 로그인 성공' : '✗ .env 폴백 로그인 실패: '.implode('; ', $validation['errors']), 'fallbackLogin' => true, 'request' => [ 'method' => $method, 'endpoint' => $endpoint, 'headers' => $headers, 'body' => $body, ], 'response' => [ 'status' => $response['status'], 'body' => $response['body'], 'duration' => $response['duration'], ], 'extracted' => $extracted, 'validation' => $validation, 'error' => $success ? null : implode('; ', $validation['errors']), ]); } catch (Exception $e) { return $this->buildStepResult($stepId, $stepName, $startTime, false, [ 'error' => $e->getMessage(), 'reason' => '✗ .env 폴백 로그인 예외: '.$e->getMessage(), ]); } } /** * 플로우 정의 검증 */ private function validateFlowDefinition(array $definition): void { // steps 필드 필수 if (! isset($definition['steps']) || ! is_array($definition['steps'])) { throw new Exception('Flow definition must have a "steps" array'); } // 각 스텝 검증 foreach ($definition['steps'] as $index => $step) { if (! isset($step['id'])) { throw new Exception("Step at index {$index} must have an 'id' field"); } if (! isset($step['method'])) { throw new Exception("Step '{$step['id']}' must have a 'method' field"); } if (! isset($step['endpoint'])) { throw new Exception("Step '{$step['id']}' must have an 'endpoint' field"); } } // 의존성 검증 $validation = $this->resolver->validate($definition['steps']); if (! $validation['valid']) { throw new Exception('Dependency validation failed: '.implode('; ', $validation['errors'])); } } /** * 설정 적용 */ private function applyConfig(array $config): void { // config 저장 (bearerToken 동적 업데이트용) $this->flowConfig = $config; // Base URL 결정 - JSON에 있으면 사용, 없으면 .env에서 $baseUrl = $this->resolveBaseUrl($config['baseUrl'] ?? null); if ($baseUrl) { // Docker 환경에서 외부 URL을 내부 URL로 변환 if ($this->isDockerEnvironment()) { $parsedUrl = parse_url($baseUrl); $host = $parsedUrl['host'] ?? ''; // *.sam.kr 도메인을 nginx 컨테이너로 라우팅 if (str_ends_with($host, '.sam.kr') || $host === 'sam.kr') { $this->httpClient->setHostHeader($host); $this->httpClient->withoutVerifying(); // URL을 https://nginx/... 로 변환 $internalUrl = preg_replace( '#https?://[^/]+#', 'https://nginx', $baseUrl ); $baseUrl = $internalUrl; } } $this->httpClient->setBaseUrl($baseUrl); } // Timeout if (isset($config['timeout'])) { $timeout = (int) ($config['timeout'] / 1000); // ms → s $this->httpClient->setTimeout(max(1, $timeout)); } // 기본 헤더 if (isset($config['headers'])) { $this->httpClient->setDefaultHeaders($config['headers']); } // 인증 - JSON에 없으면 .env에서 자동 로드 $apiKey = $config['apiKey'] ?? null; if (empty($apiKey)) { $apiKey = env('FLOW_TESTER_API_KEY'); } else { // 플레이스홀더 치환 ({{$auth.apiKey}} 등) $apiKey = $this->binder->bind($apiKey); } if (! empty($apiKey)) { $this->httpClient->setApiKey($apiKey); } $bearerToken = $config['bearerToken'] ?? null; if (empty($bearerToken)) { // .env 또는 로그인 사용자의 토큰 사용 $bearerToken = $this->getDefaultBearerToken(); } else { // 플레이스홀더 치환 ({{$auth.token}} 등) $bearerToken = $this->binder->bind($bearerToken); } if (! empty($bearerToken)) { $this->httpClient->setBearerToken($bearerToken); } } /** * 기본 Bearer 토큰 조회 * * 세션 토큰만 사용 (API 서버 로그인으로 발급받은 토큰) * .env 폴백은 MNG 토큰이 API 서버에서 인식되지 않으므로 제거됨 */ private function getDefaultBearerToken(): ?string { // 세션에 저장된 토큰 (API 서버 로그인으로 발급받은 토큰) return session('api_explorer_token') ?: null; } /** * Bearer 토큰 동적 업데이트 * * login 스텝에서 추출된 token을 후속 요청에서 사용할 수 있도록 * 각 스텝 실행 전에 config.bearerToken을 다시 바인딩합니다. */ private function updateBearerToken(): void { $bearerToken = $this->flowConfig['bearerToken'] ?? null; if (! empty($bearerToken)) { // 플레이스홀더 치환 ({{login.token}} 등) $resolvedToken = $this->binder->bind($bearerToken); if (! empty($resolvedToken) && $resolvedToken !== $bearerToken) { // 바인딩된 토큰이 있으면 httpClient에 설정 $this->httpClient->setBearerToken($resolvedToken); } } } /** * Base URL 결정 * * 우선순위: * 1. JSON에 완전한 URL (도메인 포함) → 그대로 사용 * 2. JSON에 경로만 있거나 없음 → .env의 FLOW_TESTER_API_BASE_URL + 경로 * * @param string|null $configBaseUrl JSON config의 baseUrl * @return string|null 최종 Base URL */ private function resolveBaseUrl(?string $configBaseUrl): ?string { // 환경변수에서 기본 도메인 가져오기 $envBaseUrl = env('FLOW_TESTER_API_BASE_URL'); // JSON에 baseUrl이 없는 경우 if (empty($configBaseUrl)) { return $envBaseUrl ?: null; } // JSON에 완전한 URL이 있는지 확인 (http:// 또는 https://로 시작) if (preg_match('#^https?://#', $configBaseUrl)) { // 완전한 URL이면 그대로 사용 return $configBaseUrl; } // 상대 경로만 있는 경우 (.env 도메인 + 상대 경로) if ($envBaseUrl) { // .env URL 끝의 슬래시 제거 $envBaseUrl = rtrim($envBaseUrl, '/'); // 상대 경로 시작의 슬래시 보장 $configBaseUrl = '/'.ltrim($configBaseUrl, '/'); return $envBaseUrl.$configBaseUrl; } // .env도 없고 상대 경로만 있으면 그대로 반환 (실패할 수 있음) return $configBaseUrl; } /** * Docker 환경인지 확인 */ private function isDockerEnvironment(): bool { // Docker 컨테이너 내부에서 실행 중인지 확인 return file_exists('/.dockerenv') || (getenv('DOCKER_CONTAINER') === 'true'); } /** * 스텝 맵 구성 (id → step) */ private function buildStepMap(array $steps): array { $map = []; foreach ($steps as $step) { $map[$step['id']] = $step; } return $map; } /** * 스텝 결과 구성 */ private function buildStepResult( string $stepId, string $stepName, float $startTime, bool $success, array $details = [] ): array { $duration = (int) ((microtime(true) - $startTime) * 1000); return array_merge([ 'stepId' => $stepId, 'stepName' => $stepName, 'success' => $success, 'duration' => $duration, 'timestamp' => date('Y-m-d H:i:s'), ], $details); } /** * 최종 결과 구성 */ private function buildResult( float $startTime, string $status, ?string $errorMessage = null, ?string $failedStep = null ): array { $duration = (int) ((microtime(true) - $startTime) * 1000); // API 로그 캡처 $this->apiLogs = $this->logCapturer->capture(); return [ 'status' => $status, 'duration' => $duration, 'totalSteps' => $this->totalSteps, 'completedSteps' => $this->completedSteps, 'failedStep' => $failedStep, 'errorMessage' => $errorMessage, 'executionLog' => $this->executionLog, 'apiLogs' => $this->apiLogs, 'startedAt' => date('Y-m-d H:i:s', (int) $startTime), 'completedAt' => date('Y-m-d H:i:s'), ]; } /** * 상태 초기화 */ private function reset(): void { $this->executionLog = []; $this->status = 'PENDING'; $this->completedSteps = 0; $this->totalSteps = 0; $this->stepSuccessMap = []; $this->apiLogs = []; $this->binder->reset(); $this->conditionEvaluator->reset(); } /** * 스텝 결과를 ConditionEvaluator에 등록 * * 다음 스텝의 조건 평가에서 이전 스텝 결과를 참조할 수 있도록 합니다. * * @param string $stepId 스텝 ID * @param array $result 스텝 실행 결과 */ private function registerStepResult(string $stepId, array $result): void { $this->conditionEvaluator->setStepResult($stepId, $result); } /** * 현재 진행 상황 조회 */ public function getProgress(): array { return [ 'status' => $this->status, 'completedSteps' => $this->completedSteps, 'totalSteps' => $this->totalSteps, 'percent' => $this->totalSteps > 0 ? round(($this->completedSteps / $this->totalSteps) * 100, 1) : 0, ]; } /** * 실행 로그 조회 */ public function getExecutionLog(): array { return $this->executionLog; } /** * 현재 상태 조회 */ public function getStatus(): string { return $this->status; } /** * 성공/실패 이유 설명 생성 * * @param bool $success 성공 여부 * @param array $expect 예상 조건 * @param int $actualStatus 실제 HTTP 상태 코드 * @param array $step 스텝 정의 * @return string 이유 설명 */ private function buildResultReason(bool $success, array $expect, int $actualStatus, array $step): string { $expectedStatuses = $expect['status'] ?? [200, 201, 204]; if (! is_array($expectedStatuses)) { $expectedStatuses = [$expectedStatuses]; } // 예상 상태 코드 문자열 생성 $expectedStr = implode(', ', $expectedStatuses); // 400, 404 등 에러 코드가 예상값인 경우 (부정 테스트) $isNegativeTest = ! empty(array_filter($expectedStatuses, fn ($s) => $s >= 400)); if ($success) { if ($isNegativeTest) { // 부정 테스트 성공 설명 $reason = "✓ 예상대로 {$actualStatus} 반환"; if ($actualStatus === 400) { $reason .= ' (유효성 검증 실패 확인)'; } elseif ($actualStatus === 404) { $reason .= ' (리소스 미존재 확인)'; } elseif ($actualStatus === 409) { $reason .= ' (충돌 상태 확인)'; } elseif ($actualStatus === 403) { $reason .= ' (권한 없음 확인)'; } // 스텝 설명이 있으면 추가 if (! empty($step['description'])) { $reason .= ' - '.$step['description']; } return $reason; } // 일반 성공 return "✓ 정상 응답 ({$actualStatus})"; } // 실패 설명 $reason = "✗ 예상: [{$expectedStr}], 실제: {$actualStatus}"; return $reason; } /** * expect 값에 변수 바인딩 적용 * * jsonPath 값들의 {{변수}} 플레이스홀더를 치환합니다. * 예: "$.data.client_code": "{{test_client_code}}" → "TEST_CLIENT_123" */ private function bindExpectVariables(array $expect): array { // jsonPath 값들에 변수 바인딩 적용 if (isset($expect['jsonPath']) && is_array($expect['jsonPath'])) { foreach ($expect['jsonPath'] as $path => $expected) { // 문자열이고 {{}} 플레이스홀더가 있는 경우만 바인딩 if (is_string($expected) && str_contains($expected, '{{')) { $expect['jsonPath'][$path] = $this->binder->bind($expected); } } } return $expect; } /** * assertions 배열 형식을 expect 객체 형식으로 변환 * * Postman 스타일: * [ * {"type": "status", "expected": 200}, * {"type": "jsonPath", "path": "data.id", "operator": "exists"}, * {"type": "jsonPath", "path": "data.name", "expected": "테스트"} * ] * * → 내부 형식: * { * "status": 200, * "jsonPath": { * "data.id": "@exists", * "data.name": "테스트" * } * } */ private function convertAssertionsToExpect(array $assertions): array { $expect = []; foreach ($assertions as $assertion) { $type = $assertion['type'] ?? ''; switch ($type) { case 'status': $expect['status'] = $assertion['expected'] ?? 200; break; case 'jsonPath': $path = $assertion['path'] ?? ''; if (empty($path)) { continue 2; } if (! isset($expect['jsonPath'])) { $expect['jsonPath'] = []; } // operator가 있으면 @operator 형식으로 변환 if (isset($assertion['operator'])) { $expect['jsonPath'][$path] = '@'.$assertion['operator']; } elseif (isset($assertion['expected'])) { // expected 값이 있으면 직접 비교 $expect['jsonPath'][$path] = $assertion['expected']; } break; } } return $expect; } }