Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
type JobRequest,
JobsApi,
type RebootServicesRequest,
ServiceMainCallsApi,
type Status,
TerraformActionsApi,
type TerraformAdvancedSettings,
Expand Down Expand Up @@ -136,6 +137,8 @@ const customDomainHelmApi = new HelmCustomDomainApi()

const deploymentQueueActionsApi = new DeploymentQueueActionsApi()

const serviceMainCallsApi = new ServiceMainCallsApi()

// Prefer this type in param instead of ServiceTypeEnum
// to suppport string AND enum as param.
// ServiceTypeEnum still exist mainly for compatibility reason (to use redux and react-query fetched services in data-access).
Expand Down Expand Up @@ -600,6 +603,13 @@ export const services = createQueryKeys('services', {
return response.data.results
},
}),
gitWebhookStatus: (serviceId: string) => ({
queryKey: [serviceId],
async queryFn() {
const response = await serviceMainCallsApi.getServiceGitWebhookStatus(serviceId)
return response.data
},
}),
})

type CloneServiceRequest = {
Expand Down Expand Up @@ -1240,6 +1250,10 @@ export const mutations = {
const response = await environmentActionApi.cleanFailedJobs(environmentId, payload)
return response.data
},
async syncGitWebhook({ serviceId }: { serviceId: string }) {
const response = await serviceMainCallsApi.syncServiceGitWebhook(serviceId)
return response.data
},
}

export type ServicesKeys = inferQueryKeys<typeof services>
4 changes: 4 additions & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from './lib/auto-deploy-setting/auto-deploy-setting'
export * from './lib/auto-deploy-section/auto-deploy-section'
export * from './lib/git-webhook-status-badge/git-webhook-status-badge'
export * from './lib/hooks/use-git-webhook-status/use-git-webhook-status'
export * from './lib/hooks/use-sync-git-webhook/use-sync-git-webhook'
export * from './lib/advanced-settings/advanced-settings'
export * from './lib/general-setting/general-setting'
export * from './lib/build-settings/build-settings'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { useGitWebhookStatus } from '../hooks/use-git-webhook-status/use-git-webhook-status'
import { useSyncGitWebhook } from '../hooks/use-sync-git-webhook/use-sync-git-webhook'
import { AutoDeploySection } from './auto-deploy-section'

jest.mock('../hooks/use-git-webhook-status/use-git-webhook-status')
jest.mock('../hooks/use-sync-git-webhook/use-sync-git-webhook')
jest.mock('../auto-deploy-setting/auto-deploy-setting', () => ({
AutoDeploySetting: () => <div data-testid="auto-deploy-setting">AutoDeploySetting</div>,
}))
jest.mock('../git-webhook-status-badge/git-webhook-status-badge', () => ({
GitWebhookStatusBadge: () => <div data-testid="webhook-status-badge">WebhookStatusBadge</div>,
}))

const mockUseGitWebhookStatus = useGitWebhookStatus as jest.MockedFunction<typeof useGitWebhookStatus>
const mockUseSyncGitWebhook = useSyncGitWebhook as jest.MockedFunction<typeof useSyncGitWebhook>

describe('AutoDeploySection', () => {
const mockMutate = jest.fn()

beforeEach(() => {
jest.clearAllMocks()
mockUseSyncGitWebhook.mockReturnValue({
mutate: mockMutate,
isLoading: false,
} as unknown as ReturnType<typeof useSyncGitWebhook>)
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'ACTIVE' },
isLoading: false,
isError: false,
refetch: jest.fn(),
} as ReturnType<typeof useGitWebhookStatus>)
})

it('renders auto-deploy setting', () => {
renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.getByTestId('auto-deploy-setting')).toBeInTheDocument()
})

it('shows webhook section when auto-deploy is enabled and source is GIT', () => {
renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.getByTestId('webhook-status-badge')).toBeInTheDocument()
})

it('hides webhook section when auto-deploy is disabled', () => {
renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: false },
})
)

expect(screen.queryByTestId('webhook-status-badge')).not.toBeInTheDocument()
})

it('hides webhook section for CONTAINER_REGISTRY source', () => {
renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="CONTAINER_REGISTRY" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.queryByTestId('webhook-status-badge')).not.toBeInTheDocument()
})

it('shows "Update Webhook" button when webhook status is NOT_CONFIGURED', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'NOT_CONFIGURED' },
isLoading: false,
isError: false,
refetch: jest.fn(),
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.getByRole('button', { name: /update webhook/i })).toBeInTheDocument()
})

it('shows "Update Webhook" button when webhook status is MISCONFIGURED', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'MISCONFIGURED' },
isLoading: false,
isError: false,
refetch: jest.fn(),
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.getByRole('button', { name: /update webhook/i })).toBeInTheDocument()
})

it('hides "Update Webhook" button when webhook status is ACTIVE', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'ACTIVE' },
isLoading: false,
isError: false,
refetch: jest.fn(),
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

expect(screen.queryByRole('button', { name: /update webhook/i })).not.toBeInTheDocument()
})

it('calls syncWebhook when clicking "Update Webhook" button', async () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'NOT_CONFIGURED' },
isLoading: false,
isError: false,
refetch: jest.fn(),
} as ReturnType<typeof useGitWebhookStatus>)

const { userEvent } = renderWithProviders(
wrapWithReactHookForm(<AutoDeploySection serviceId="service-123" source="GIT" />, {
defaultValues: { auto_deploy: true },
})
)

await userEvent.click(screen.getByRole('button', { name: /update webhook/i }))

expect(mockMutate).toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useFormContext } from 'react-hook-form'
import { Button } from '@qovery/shared/ui'
import { AutoDeploySetting, type AutoDeploySettingProps } from '../auto-deploy-setting/auto-deploy-setting'
import { GitWebhookStatusBadge } from '../git-webhook-status-badge/git-webhook-status-badge'
import { useGitWebhookStatus } from '../hooks/use-git-webhook-status/use-git-webhook-status'
import { useSyncGitWebhook } from '../hooks/use-sync-git-webhook/use-sync-git-webhook'

export interface AutoDeploySectionProps extends AutoDeploySettingProps {
serviceId: string
}

export function AutoDeploySection({ serviceId, source, className }: AutoDeploySectionProps) {
const { watch } = useFormContext()
const autoDeployEnabled = watch('auto_deploy')
const supportsWebhook = source !== 'CONTAINER_REGISTRY'

const { data: webhookStatus } = useGitWebhookStatus({ serviceId, enabled: supportsWebhook })
const { mutate: syncWebhook, isLoading: isSyncing } = useSyncGitWebhook({ serviceId })

const shouldShowWebhook = supportsWebhook && autoDeployEnabled
const showSyncButton = webhookStatus && webhookStatus.status !== 'ACTIVE'

return (
<div className="overflow-hidden rounded-md border border-neutral-200">
<div className="p-4">
<AutoDeploySetting source={source} className={className} />
</div>
{shouldShowWebhook && (
<div className="flex items-center justify-between border-t border-neutral-200 bg-neutral-100 px-4 py-3">
<GitWebhookStatusBadge serviceId={serviceId} />
{showSyncButton && (
<Button
type="button"
variant="surface"
color="neutral"
size="sm"
onClick={() => syncWebhook()}
loading={isSyncing}
>
Update Webhook
</Button>
)}
</div>
)}
</div>
)
}

export default AutoDeploySection
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { useGitWebhookStatus } from '../hooks/use-git-webhook-status/use-git-webhook-status'
import { GitWebhookStatusBadge } from './git-webhook-status-badge'

jest.mock('../hooks/use-git-webhook-status/use-git-webhook-status')

const mockUseGitWebhookStatus = useGitWebhookStatus as jest.MockedFunction<typeof useGitWebhookStatus>

describe('GitWebhookStatusBadge', () => {
const mockRefetch = jest.fn()

beforeEach(() => {
jest.clearAllMocks()
})

it('shows loading state with "Checking..." badge', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Checking...')).toBeInTheDocument()
})

it('shows error state with error badge', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Error')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})

it('calls refetch when clicking error retry button', async () => {
mockUseGitWebhookStatus.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

const { userEvent } = renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

await userEvent.click(screen.getByRole('button'))

expect(mockRefetch).toHaveBeenCalled()
})

it('shows green "Working" badge for ACTIVE status', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'ACTIVE' },
isLoading: false,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Working')).toBeInTheDocument()
})

it('shows red "Not Configured" badge for NOT_CONFIGURED status', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'NOT_CONFIGURED' },
isLoading: false,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Not Configured')).toBeInTheDocument()
})

it('shows yellow "Misconfigured" badge for MISCONFIGURED status', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'MISCONFIGURED', missing_events: ['push', 'pull_request'] },
isLoading: false,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Misconfigured')).toBeInTheDocument()
})

it('shows neutral "Unable to Verify" badge for UNABLE_TO_VERIFY status', () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'UNABLE_TO_VERIFY' },
isLoading: false,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

expect(screen.getByText('Unable to Verify')).toBeInTheDocument()
})

it('has tooltip present for status badge', async () => {
mockUseGitWebhookStatus.mockReturnValue({
data: { status: 'ACTIVE' },
isLoading: false,
isError: false,
refetch: mockRefetch,
} as ReturnType<typeof useGitWebhookStatus>)

const { userEvent } = renderWithProviders(<GitWebhookStatusBadge serviceId="service-123" />)

await userEvent.hover(screen.getByText('Working'))

expect(await screen.findByRole('tooltip')).toBeInTheDocument()
})
})
Loading