690419:1831 feat: update CI/CD to use SSH key authentication #05
CI / CD Pipeline / build (push) Failing after 4m57s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-04-19 18:31:30 +07:00
parent 733f3c3987
commit 13745e5874
61 changed files with 6709 additions and 1241 deletions
@@ -91,9 +91,39 @@ describe('useWorkflowAction — T027a error handling (Clarify Q1+Q2)', () => {
);
});
it('403: should show unauthorized toast', async () => {
it('M1 (403): should use backend-provided message instead of hardcoded string', async () => {
// backend ส่ง message แบบ contextual (cross-contract) — frontend ต้อง preserve
vi.mocked(workflowEngineService.transition).mockRejectedValue(
makeApiError(403, 'ไม่มีสิทธิ์', ['ติดต่อ Admin'])
makeApiError(
403,
'คุณไม่มีสิทธิ์เข้าถึง Workflow ของสัญญานี้',
['ตรวจสอบสิทธิ์กับ Project Admin']
)
);
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useWorkflowAction('inst-1'), { wrapper });
await act(async () => {
result.current.mutate({ action: 'APPROVE' });
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// ✓ toast ต้องใช้ backend message (ไม่ใช่ "คุณไม่มีสิทธิ์ดำเนินการในขั้นตอนนี้" generic)
expect(toast.error).toHaveBeenCalledWith(
'คุณไม่มีสิทธิ์เข้าถึง Workflow ของสัญญานี้',
expect.objectContaining({
description: 'ตรวจสอบสิทธิ์กับ Project Admin',
})
);
});
it('M1 (403): should fallback to generic message when backend message missing', async () => {
vi.mocked(workflowEngineService.transition).mockRejectedValue(
makeApiError(403, '', ['ติดต่อ Admin'])
);
const { wrapper } = createTestQueryClient();
@@ -115,6 +145,81 @@ describe('useWorkflowAction — T027a error handling (Clarify Q1+Q2)', () => {
);
});
it('M3 (409): should reset idempotency key after 409 so retry uses fresh key', async () => {
// First call → 409
vi.mocked(workflowEngineService.transition).mockRejectedValueOnce(
makeApiError(409, 'ไม่สามารถอัปโหลดในสถานะนี้ได้', ['รีเฟรชหน้า'])
);
// Second call → success
vi.mocked(workflowEngineService.transition).mockResolvedValueOnce({
success: true,
nextState: 'APPROVED',
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useWorkflowAction('inst-1'), { wrapper });
// First mutate → 409
await act(async () => {
result.current.mutate({ action: 'APPROVE' });
});
await waitFor(() => expect(result.current.isError).toBe(true));
const firstKey = vi
.mocked(workflowEngineService.transition)
.mock.calls[0][2];
// Second mutate → success
await act(async () => {
result.current.mutate({ action: 'APPROVE' });
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const secondKey = vi
.mocked(workflowEngineService.transition)
.mock.calls[1][2];
// ✓ Key ต้องแตกต่างกัน (reset แล้วหลัง 409)
expect(firstKey).not.toBe(secondKey);
expect(firstKey).toMatch(/^[0-9a-f-]{36}$/);
expect(secondKey).toMatch(/^[0-9a-f-]{36}$/);
});
it('M3 (503): should NOT reset idempotency key (user can retry with same key)', async () => {
// First call → 503
vi.mocked(workflowEngineService.transition).mockRejectedValueOnce(
makeApiError(503, 'ระบบยุ่ง')
);
// Second call → success
vi.mocked(workflowEngineService.transition).mockResolvedValueOnce({
success: true,
});
const { wrapper } = createTestQueryClient();
const { result } = renderHook(() => useWorkflowAction('inst-1'), { wrapper });
await act(async () => {
result.current.mutate({ action: 'APPROVE' });
});
await waitFor(() => expect(result.current.isError).toBe(true));
const firstKey = vi
.mocked(workflowEngineService.transition)
.mock.calls[0][2];
await act(async () => {
result.current.mutate({ action: 'APPROVE' });
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const secondKey = vi
.mocked(workflowEngineService.transition)
.mock.calls[1][2];
// ✓ Key ต้องเหมือนเดิม (503 = retryable, same intent)
expect(firstKey).toBe(secondKey);
});
it('should show success toast on 200', async () => {
vi.mocked(workflowEngineService.transition).mockResolvedValue({
success: true,