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 @@ -393,6 +393,36 @@ describe('ContainerRegistryCreateEditModal', () => {
)
}, 30000)

it('should strip trailing slash from registry URL on submit', async () => {
const { userEvent } = renderWithProviders(<ContainerRegistryCreateEditModal {...props} />)

const inputType = screen.getByLabelText('Type')
await selectEvent.select(inputType, 'GENERIC_CR', {
container: document.body,
})

const inputName = screen.getByLabelText('Registry name')
await userEvent.clear(inputName)
await userEvent.type(inputName, 'registry-name')

const inputUrl = screen.getByLabelText('Registry url')
await userEvent.clear(inputUrl)
await userEvent.type(inputUrl, 'https://my-registry.example.com/')

const btn = screen.getByRole('button', { name: 'Create' })
expect(btn).toBeEnabled()

await userEvent.click(btn)

expect(useCreateContainerRegistryMockSpy().mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
containerRegistryRequest: expect.objectContaining({
url: 'https://my-registry.example.com',
}),
})
)
}, 30000)

it('should set default registry type based on existing registry config', () => {
expect(getContainerRegistryDefaultType(undefined)).toBe('STS')

Expand Down Expand Up @@ -572,4 +602,38 @@ describe('ContainerRegistryCreateEditModal', () => {

expect(props.onClose).toHaveBeenCalled()
})

it('should strip trailing slash from registry URL when editing a registry', async () => {
const { userEvent } = renderWithProviders(
<ContainerRegistryCreateEditModal
{...props}
isEdit
registry={{
id: '1111-1111-1111',
created_at: '',
updated_at: '',
name: 'my-registry',
url: 'https://my-registry.example.com',
kind: ContainerRegistryKindEnum.GENERIC_CR,
}}
/>
)

const inputUrl = screen.getByLabelText('Registry url')
await userEvent.clear(inputUrl)
await userEvent.type(inputUrl, 'https://my-registry.example.com/')

const btn = screen.getByRole('button', { name: 'Confirm' })
expect(btn).toBeEnabled()

await userEvent.click(btn)

expect(useEditContainerRegistryMockSpy().mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
containerRegistryRequest: expect.objectContaining({
url: 'https://my-registry.example.com',
}),
})
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEffect } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { P, match } from 'ts-pattern'
import { ExternalLink, ModalCrud, useModal } from '@qovery/shared/ui'
import { stripUrlTrailingSlash } from '@qovery/shared/util-js'
import ContainerRegistryForm from '../container-registry-form/container-registry-form'
import { useCreateContainerRegistry } from '../hooks/use-create-container-registry/use-create-container-registry'
import { useEditContainerRegistry } from '../hooks/use-edit-container-registry/use-edit-container-registry'
Expand Down Expand Up @@ -167,6 +168,7 @@ export function ContainerRegistryCreateEditModal({
type,
kind,
config: { login_type, ...config },
url,
...rest
} = containerRegistryRequest
try {
Expand All @@ -175,6 +177,8 @@ export function ContainerRegistryCreateEditModal({
containerRegistryRequest: {
...rest,
kind,
// A URL like `https://ghcr.io/` is rejected as invalid by the backend
url: url ? stripUrlTrailingSlash(url) : url,
config: getContainerRegistryPayloadConfig({
type,
kind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,81 @@ describe('HelmRepositoryCreateEditModal', () => {
})
})

it('should strip trailing slash from OCI URL on submit', async () => {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
props.repository = undefined

const { userEvent } = renderWithProviders(<HelmRepositoryCreateEditModal {...props} />)

const inputName = screen.getByTestId('input-name')
await userEvent.type(inputName, 'my-oci-repository')

const selectType = screen.getByLabelText('Kind')
await selectEvent.select(selectType, 'OCI_GENERIC_CR', { container: document.body })

const inputUrl = screen.getByTestId('input-url')
await userEvent.type(inputUrl, 'oci://docker.io/')

const button = await screen.findByRole('button', { name: /Create/i })
expect(button).toBeInTheDocument()
expect(button).toBeEnabled()

await userEvent.click(screen.getByTestId('submit-button'))

expect(useCreateHelmRepositoryMockSpy().mutateAsync).toHaveBeenCalledWith({
organizationId: '0000-0000-0000',
helmRepositoryRequest: {
name: 'my-oci-repository',
kind: 'OCI_GENERIC_CR',
description: undefined,
url: 'oci://docker.io',
config: {
access_key_id: undefined,
region: undefined,
scaleway_access_key: undefined,
scaleway_secret_key: undefined,
secret_access_key: undefined,
username: undefined,
password: undefined,
},
},
})
})

it('should strip trailing slash from OCI URL when editing a repository', async () => {
const { userEvent } = renderWithProviders(
<HelmRepositoryCreateEditModal
{...props}
isEdit
repository={{
id: '1111-1111-1111',
created_at: '',
updated_at: '',
name: 'my-oci-repository',
description: 'description',
url: 'oci://docker.io',
kind: 'OCI_GENERIC_CR',
}}
/>
)

const inputUrl = screen.getByTestId('input-url')
await userEvent.clear(inputUrl)
await userEvent.type(inputUrl, 'oci://docker.io/')

const btn = screen.getByRole('button', { name: 'Confirm' })
expect(btn).toBeEnabled()

await userEvent.click(btn)

expect(useEditHelmRepositoryMockSpy().mutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
helmRepositoryRequest: expect.objectContaining({
url: 'oci://docker.io',
}),
})
)
})

it('should submit the form to edit a repository', async () => {
const { userEvent } = renderWithProviders(
<HelmRepositoryCreateEditModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ModalCrud,
useModal,
} from '@qovery/shared/ui'
import { stripUrlTrailingSlash } from '@qovery/shared/util-js'
import { useAvailableHelmRepositories } from '../hooks/use-available-helm-repositories/use-available-helm-repositories'
import { useCreateHelmRepository } from '../hooks/use-create-helm-repository/use-create-helm-repository'
import { useEditHelmRepository } from '../hooks/use-edit-helm-repository/use-edit-helm-repository'
Expand Down Expand Up @@ -115,13 +116,17 @@ export function HelmRepositoryCreateEditModal({
}
}

// A URL like `oci://docker.io/` is rejected as invalid by the backend
const url = helmRepositoryRequest.url ? stripUrlTrailingSlash(helmRepositoryRequest.url) : helmRepositoryRequest.url

try {
if (repository) {
const response = await editHelmRepository({
organizationId: organizationId,
helmRepositoryId: repository.id,
helmRepositoryRequest: {
...helmRepositoryRequest,
url,
config: config,
},
})
Expand All @@ -131,6 +136,7 @@ export function HelmRepositoryCreateEditModal({
organizationId: organizationId,
helmRepositoryRequest: {
...helmRepositoryRequest,
url,
config: config,
},
})
Expand Down
1 change: 1 addition & 0 deletions libs/shared/util-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './lib/scroll-parent-to-child'
export * from './lib/short-to-long-id'
export * from './lib/sort-by-key'
export * from './lib/status-actions-available'
export * from './lib/strip-url-trailing-slash'
export * from './lib/to-short-qovery-id'
export * from './lib/trim-id'
export * from './lib/uppercase-first-letter'
Expand Down
31 changes: 31 additions & 0 deletions libs/shared/util-js/src/lib/strip-url-trailing-slash.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { stripUrlTrailingSlash } from './strip-url-trailing-slash'

describe('stripUrlTrailingSlash', () => {
it('should strip a single trailing slash', () => {
expect(stripUrlTrailingSlash('oci://docker.io/')).toBe('oci://docker.io')
})

it('should strip multiple trailing slashes', () => {
expect(stripUrlTrailingSlash('oci://docker.io///')).toBe('oci://docker.io')
})

it('should leave a URL without a trailing slash untouched', () => {
expect(stripUrlTrailingSlash('oci://docker.io')).toBe('oci://docker.io')
})

it('should preserve a meaningful path', () => {
expect(stripUrlTrailingSlash('oci://aaa.bbb.ccc.tech/qovery')).toBe('oci://aaa.bbb.ccc.tech/qovery')
})

it('should strip a trailing slash from the path while preserving a query string', () => {
expect(stripUrlTrailingSlash('https://docker.io/?foo=bar')).toBe('https://docker.io?foo=bar')
})

it('should strip a trailing slash from the path while preserving a fragment', () => {
expect(stripUrlTrailingSlash('https://docker.io/#section')).toBe('https://docker.io#section')
})

it('should not touch a trailing slash inside a query value', () => {
expect(stripUrlTrailingSlash('https://docker.io?redirect=/foo/')).toBe('https://docker.io?redirect=/foo/')
})
})
10 changes: 10 additions & 0 deletions libs/shared/util-js/src/lib/strip-url-trailing-slash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Strips trailing slash(es) from the path of a URL, leaving any query string or fragment untouched.
// Backends (Helm repositories, container registries) reject a URL like `oci://docker.io/` because
// the trailing slash makes the path non-empty, so we normalize it before submitting the form.
export function stripUrlTrailingSlash(url: string): string {
const suffixIndex = url.search(/[?#]/)
const path = suffixIndex === -1 ? url : url.slice(0, suffixIndex)
const suffix = suffixIndex === -1 ? '' : url.slice(suffixIndex)

return path.replace(/\/+$/, '') + suffix
}
Loading