setUpAuthenticatedUser(); // 결재 양식 $this->form = ApprovalForm::create([ 'tenant_id' => $this->tenant->id, 'name' => '품의서', 'code' => 'REQ-'.uniqid(), 'category' => ApprovalForm::CATEGORY_REQUEST, 'template' => '{}', 'body_template' => '', 'is_active' => true, 'created_by' => $this->user->id, ]); // 결재자 (별도 사용자) $approverId = 'approver'.uniqid(); $this->approver = User::create([ 'user_id' => $approverId, 'name' => '결재자', 'email' => $approverId.'@example.com', 'password' => bcrypt('password123'), ]); \App\Models\Members\UserTenant::create([ 'user_id' => $this->approver->id, 'tenant_id' => $this->tenant->id, 'is_active' => true, 'is_default' => true, ]); // 부서 (있으면 사용, 없으면 생성) $this->department = Department::where('tenant_id', $this->tenant->id)->first(); if (! $this->department) { $this->department = Department::create([ 'tenant_id' => $this->tenant->id, 'name' => '개발팀', 'code' => 'DEV', 'is_active' => true, 'created_by' => $this->user->id, ]); } // 결재선 템플릿 $this->line = ApprovalLine::create([ 'tenant_id' => $this->tenant->id, 'name' => '기본 결재선', 'steps' => json_encode([ [ 'step_order' => 1, 'step_type' => ApprovalLine::STEP_TYPE_APPROVAL, 'approver_id' => $this->approver->id, ], ]), 'is_default' => true, 'created_by' => $this->user->id, ]); } // ==================== 기안 (CRUD) ==================== public function test_결재_문서_생성_임시저장(): void { $response = $this->api('post', '/api/v1/approvals', [ 'form_id' => $this->form->id, 'title' => '테스트 품의서', 'content' => ['body' => '테스트 내용입니다.'], ]); $response->assertStatus(200); $this->assertTrue($response->json('success')); $this->assertDatabaseHas('approvals', [ 'title' => '테스트 품의서', 'status' => Approval::STATUS_DRAFT, 'tenant_id' => $this->tenant->id, ]); } public function test_기안함_목록_조회(): void { $this->createApproval(); $response = $this->api('get', '/api/v1/approvals/drafts'); $response->assertStatus(200) ->assertJsonStructure(['success', 'data']); } public function test_기안함_현황_카드(): void { $this->createApproval(); $response = $this->api('get', '/api/v1/approvals/drafts/summary'); $response->assertStatus(200); } public function test_결재_문서_상세_조회(): void { $approval = $this->createApproval(); $response = $this->api('get', "/api/v1/approvals/{$approval->id}"); $this->assertApiSuccess($response); $data = $response->json('data'); $this->assertEquals($approval->id, $data['id']); $this->assertEquals('테스트 품의서', $data['title']); } public function test_결재_문서_수정(): void { $approval = $this->createApproval(); $response = $this->api('patch', "/api/v1/approvals/{$approval->id}", [ 'title' => '수정된 품의서', 'content' => ['body' => '수정된 내용'], ]); $response->assertStatus(200); $this->assertDatabaseHas('approvals', [ 'id' => $approval->id, 'title' => '수정된 품의서', ]); } public function test_결재_문서_삭제(): void { $approval = $this->createApproval(); $response = $this->api('delete', "/api/v1/approvals/{$approval->id}"); $response->assertStatus(200); $this->assertSoftDeleted('approvals', ['id' => $approval->id]); } // ==================== 워크플로우 (상신/승인/반려) ==================== public function test_결재_상신(): void { $approval = $this->createApproval(); $response = $this->api('post', "/api/v1/approvals/{$approval->id}/submit", [ 'steps' => [ [ 'step_order' => 1, 'step_type' => 'approval', 'approver_id' => $this->approver->id, ], ], ]); $response->assertStatus(200); $approval->refresh(); $this->assertEquals(Approval::STATUS_PENDING, $approval->status); } public function test_결재_승인(): void { $approval = $this->createAndSubmitApproval(); // 결재자로 로그인 $approverToken = $this->loginAs($this->approver); $response = $this->withHeaders([ 'X-API-KEY' => $this->apiKey, 'Authorization' => 'Bearer '.$approverToken, 'Accept' => 'application/json', ])->postJson("/api/v1/approvals/{$approval->id}/approve", [ 'comment' => '승인합니다.', ]); $response->assertStatus(200); $approval->refresh(); $this->assertEquals(Approval::STATUS_APPROVED, $approval->status); } public function test_결재_반려(): void { $approval = $this->createAndSubmitApproval(); // 결재자로 로그인 $approverToken = $this->loginAs($this->approver); $response = $this->withHeaders([ 'X-API-KEY' => $this->apiKey, 'Authorization' => 'Bearer '.$approverToken, 'Accept' => 'application/json', ])->postJson("/api/v1/approvals/{$approval->id}/reject", [ 'comment' => '수정 후 재상신 바랍니다.', ]); $response->assertStatus(200); $approval->refresh(); $this->assertEquals(Approval::STATUS_REJECTED, $approval->status); } public function test_결재_회수_기안자만_가능(): void { $approval = $this->createAndSubmitApproval(); // 기안자가 회수 $response = $this->api('post', "/api/v1/approvals/{$approval->id}/cancel", [ 'reason' => '내용 수정 필요', ]); $response->assertStatus(200); $approval->refresh(); $this->assertEquals(Approval::STATUS_CANCELLED, $approval->status); } // ==================== 결재함/참조함/완료함 ==================== public function test_결재함_목록_조회(): void { $response = $this->api('get', '/api/v1/approvals/inbox'); $response->assertStatus(200) ->assertJsonStructure(['success', 'data']); } public function test_참조함_목록_조회(): void { $response = $this->api('get', '/api/v1/approvals/reference'); $response->assertStatus(200) ->assertJsonStructure(['success', 'data']); } public function test_완료함_목록_조회(): void { $response = $this->api('get', '/api/v1/approvals/completed'); $response->assertStatus(200) ->assertJsonStructure(['success', 'data']); } public function test_뱃지_미처리_건수(): void { $response = $this->api('get', '/api/v1/approvals/badge-counts'); $response->assertStatus(200) ->assertJsonStructure(['success', 'data']); } // ==================== 인증 ==================== public function test_미인증_요청시_401(): void { $response = $this->withHeaders([ 'X-API-KEY' => $this->apiKey, 'Accept' => 'application/json', ])->getJson('/api/v1/approvals/drafts'); $response->assertStatus(401); } // ==================== 헬퍼 ==================== private function createApproval(): Approval { return Approval::create([ 'tenant_id' => $this->tenant->id, 'document_number' => 'AP-'.uniqid(), 'form_id' => $this->form->id, 'line_id' => $this->line->id, 'title' => '테스트 품의서', 'content' => '테스트 내용', 'status' => Approval::STATUS_DRAFT, 'drafter_id' => $this->user->id, 'department_id' => $this->department->id, 'drafted_at' => now(), 'current_step' => 0, 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ]); } private function createAndSubmitApproval(): Approval { $approval = $this->createApproval(); // 상신 $this->api('post', "/api/v1/approvals/{$approval->id}/submit", [ 'steps' => [ [ 'step_order' => 1, 'step_type' => 'approval', 'approver_id' => $this->approver->id, ], ], ]); $approval->refresh(); return $approval; } private function loginAs(User $user): string { $response = $this->withHeaders([ 'X-API-KEY' => $this->apiKey, 'Accept' => 'application/json', ])->postJson('/api/v1/login', [ 'user_id' => $user->user_id, 'user_pwd' => 'password123', ]); return $response->json('access_token'); } }