From c8ad3c908c49d5b5f7549231a285b0b96a25c558 Mon Sep 17 00:00:00 2001 From: hskwon Date: Mon, 22 Dec 2025 19:51:52 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=EB=85=BC=EB=A6=AC=EA=B4=80=EA=B3=84?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=B4=88=EB=8C=80=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LOGICAL_RELATIONSHIPS.md | 2 +- docs/flow-tests/user-invitation-flow.json | 926 ++++++++++++++++++++++ 2 files changed, 927 insertions(+), 1 deletion(-) create mode 100644 docs/flow-tests/user-invitation-flow.json diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index 33201e5..062e475 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,6 +1,6 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2025-12-22 17:39:00 +> **자동 생성**: 2025-12-22 19:39:30 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 diff --git a/docs/flow-tests/user-invitation-flow.json b/docs/flow-tests/user-invitation-flow.json new file mode 100644 index 0000000..4229113 --- /dev/null +++ b/docs/flow-tests/user-invitation-flow.json @@ -0,0 +1,926 @@ +{ + "name": "사용자 초대 플로우 테스트", + "description": "사용자 초대 전체 플로우 (발송/목록/수락/취소/재발송) 및 분기 상황 테스트", + "version": "1.0", + "config": { + "baseUrl": "https://api.sam.kr/api/v1", + "apiKey": "{{$env.FLOW_TESTER_API_KEY}}", + "timeout": 30000, + "stopOnFailure": false + }, + "variables": { + "user_id": "{{$env.FLOW_TESTER_USER_ID}}", + "user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}", + "test_email_prefix": "flowtest", + "test_domain": "example.com" + }, + "setup": { + "description": "테스트 전 초기화 - 기존 테스트 초대 정리는 수동으로 진행" + }, + "flows": [ + { + "id": "flow_1", + "name": "🔴 P1: 기본 초대 플로우 (role=user)", + "description": "일반 사용자 역할로 초대 발송 → 목록 확인 → 취소", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.access_token": "@isString" + } + } + }, + { + "id": "invite_user_role", + "name": "초대 발송 (role=user)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_user_{{$timestamp}}@{{variables.test_domain}}", + "role": "user", + "message": "SAM 시스템에 합류해 주세요!" + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id", + "invitationToken": "$.data.token", + "invitedEmail": "$.data.email" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending", + "$.data.email": "@isString" + } + } + }, + { + "id": "list_invitations", + "name": "초대 목록 조회", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "status": "pending", + "per_page": 10 + }, + "dependsOn": ["invite_user_role"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + }, + { + "id": "cancel_invitation", + "name": "초대 취소", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite_user_role.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["list_invitations"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] + }, + { + "id": "flow_2", + "name": "🔴 P1: 관리자 역할 초대 (role=admin)", + "description": "관리자 역할로 초대 발송", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_admin_role", + "name": "초대 발송 (role=admin)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_admin_{{$timestamp}}@{{variables.test_domain}}", + "role": "admin", + "message": "관리자로 초대합니다." + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending" + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite_admin_role.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["invite_admin_role"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_3", + "name": "🔴 P1: 매니저 역할 초대 (role=manager)", + "description": "매니저 역할로 초대 발송", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_manager_role", + "name": "초대 발송 (role=manager)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_manager_{{$timestamp}}@{{variables.test_domain}}", + "role": "manager" + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending" + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite_manager_role.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["invite_manager_role"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_4", + "name": "🔴 P1: 초대 재발송 플로우", + "description": "초대 발송 → 재발송 → 취소", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite", + "name": "초대 발송", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_resend_{{$timestamp}}@{{variables.test_domain}}", + "role": "user" + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id", + "originalExpiresAt": "$.data.expires_at" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "resend", + "name": "초대 재발송", + "method": "POST", + "endpoint": "/users/invitations/{{invite.invitationId}}/resend", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["invite"], + "extract": { + "newExpiresAt": "$.data.expires_at" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.status": "pending" + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["resend"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_5", + "name": "🔴 P1: 에러 케이스 - 중복 이메일 초대", + "description": "동일 이메일로 중복 초대 시도 (대기 중 초대 존재)", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "first_invite", + "name": "첫 번째 초대 발송", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_dup_{{$timestamp}}@{{variables.test_domain}}", + "role": "user" + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id", + "testEmail": "$.data.email" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "duplicate_invite", + "name": "중복 초대 발송 (실패 예상)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{first_invite.testEmail}}", + "role": "user" + }, + "dependsOn": ["first_invite"], + "expect": { + "status": [400, 422], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{first_invite.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["duplicate_invite"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_6", + "name": "🟡 P2: 목록 필터링 - 상태별", + "description": "초대 목록을 상태별로 필터링", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "list_pending", + "name": "대기 중 목록 조회", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "status": "pending" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "list_accepted", + "name": "수락된 목록 조회", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "status": "accepted" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "list_expired", + "name": "만료된 목록 조회", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "status": "expired" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "list_cancelled", + "name": "취소된 목록 조회", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "status": "cancelled" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] + }, + { + "id": "flow_7", + "name": "🟡 P2: 목록 정렬 옵션", + "description": "다양한 정렬 기준으로 목록 조회", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "sort_created_at_desc", + "name": "생성일 내림차순", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "sort_by": "created_at", + "sort_dir": "desc" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "sort_expires_at_asc", + "name": "만료일 오름차순", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "sort_by": "expires_at", + "sort_dir": "asc" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + }, + { + "id": "sort_email_asc", + "name": "이메일 오름차순", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "sort_by": "email", + "sort_dir": "asc" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true + } + } + } + ] + }, + { + "id": "flow_8", + "name": "🟡 P2: 이메일 검색", + "description": "이메일 키워드로 초대 검색", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "search_by_email", + "name": "이메일 검색", + "method": "GET", + "endpoint": "/users/invitations", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "query": { + "search": "flowtest" + }, + "dependsOn": ["login"], + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.data": "@isArray" + } + } + } + ] + }, + { + "id": "flow_9", + "name": "🟢 P3: 만료 기간 지정 옵션", + "description": "만료 기간을 명시적으로 지정하여 초대", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_with_expires", + "name": "14일 만료 기간으로 초대", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_expires_{{$timestamp}}@{{variables.test_domain}}", + "role": "user", + "expires_days": 14 + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.expires_at": "@isString" + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite_with_expires.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["invite_with_expires"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_10", + "name": "🟢 P3: role_id로 초대 (숫자 ID 사용)", + "description": "role 문자열 대신 role_id 숫자로 초대", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_with_role_id", + "name": "role_id=2로 초대", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_roleid_{{$timestamp}}@{{variables.test_domain}}", + "role_id": 2 + }, + "dependsOn": ["login"], + "extract": { + "invitationId": "$.data.id" + }, + "expect": { + "status": [200], + "jsonPath": { + "$.success": true, + "$.data.role_id": 2 + } + } + }, + { + "id": "cleanup", + "name": "초대 취소 (정리)", + "method": "DELETE", + "endpoint": "/users/invitations/{{invite_with_role_id.invitationId}}", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["invite_with_role_id"], + "expect": { + "status": [200] + } + } + ] + }, + { + "id": "flow_11", + "name": "🔴 P1: 에러 케이스 - 잘못된 역할", + "description": "존재하지 않는 role 값으로 초대 시도", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_invalid_role", + "name": "잘못된 role로 초대 (실패 예상)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "{{variables.test_email_prefix}}_invalid_{{$timestamp}}@{{variables.test_domain}}", + "role": "superadmin" + }, + "dependsOn": ["login"], + "expect": { + "status": [400, 422], + "jsonPath": { + "$.success": false + } + } + } + ] + }, + { + "id": "flow_12", + "name": "🔴 P1: 에러 케이스 - 이메일 형식 오류", + "description": "잘못된 이메일 형식으로 초대 시도", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "invite_invalid_email", + "name": "잘못된 이메일로 초대 (실패 예상)", + "method": "POST", + "endpoint": "/users/invite", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "body": { + "email": "invalid-email-format", + "role": "user" + }, + "dependsOn": ["login"], + "expect": { + "status": [422], + "jsonPath": { + "$.success": false + } + } + } + ] + }, + { + "id": "flow_13", + "name": "🔴 P1: 에러 케이스 - 권한 없이 접근", + "description": "인증 없이 초대 API 접근 시도", + "steps": [ + { + "id": "invite_no_auth", + "name": "인증 없이 초대 시도 (실패 예상)", + "method": "POST", + "endpoint": "/users/invite", + "body": { + "email": "noauth@example.com", + "role": "user" + }, + "expect": { + "status": [401], + "jsonPath": { + "$.success": false + } + } + }, + { + "id": "list_no_auth", + "name": "인증 없이 목록 조회 (실패 예상)", + "method": "GET", + "endpoint": "/users/invitations", + "expect": { + "status": [401], + "jsonPath": { + "$.success": false + } + } + } + ] + }, + { + "id": "flow_14", + "name": "🔴 P1: 에러 케이스 - 존재하지 않는 초대 취소", + "description": "존재하지 않는 초대 ID로 취소 시도", + "steps": [ + { + "id": "login", + "name": "로그인", + "method": "POST", + "endpoint": "/login", + "body": { + "user_id": "{{variables.user_id}}", + "user_pwd": "{{variables.user_pwd}}" + }, + "extract": { + "accessToken": "$.access_token" + }, + "expect": { + "status": [200] + } + }, + { + "id": "cancel_not_found", + "name": "존재하지 않는 초대 취소 (실패 예상)", + "method": "DELETE", + "endpoint": "/users/invitations/999999", + "headers": { + "Authorization": "Bearer {{login.accessToken}}" + }, + "dependsOn": ["login"], + "expect": { + "status": [404], + "jsonPath": { + "$.success": false + } + } + } + ] + } + ], + "summary": { + "total_flows": 14, + "priority_breakdown": { + "P1_critical": 8, + "P2_important": 3, + "P3_recommended": 3 + }, + "coverage": { + "endpoints": [ + "POST /users/invite", + "GET /users/invitations", + "DELETE /users/invitations/{id}", + "POST /users/invitations/{id}/resend" + ], + "not_covered": [ + "POST /users/invitations/{token}/accept (별도 Flow 필요: 실제 이메일 수신 필요)" + ] + }, + "branches_tested": { + "role_types": ["admin", "manager", "user"], + "role_methods": ["role (string)", "role_id (integer)"], + "status_filters": ["pending", "accepted", "expired", "cancelled"], + "sort_options": ["created_at", "expires_at", "email"], + "error_cases": ["duplicate_email", "invalid_role", "invalid_email", "no_auth", "not_found"] + } + } +}