feat: [AH-969]: add new package type NPM (#3463)

* feat: [AH-969]: add new package type NPM
* feat: [AH-969]: Support NPM in create/edit registry and create/edit upstream registry
jobatzil/login/xforwardedfor
Shivanand Sonnad 2025-02-24 07:53:45 +00:00 committed by Harness
parent 3a1348893d
commit 620a6cbb25
17 changed files with 2035 additions and 9 deletions

View File

@ -51,7 +51,7 @@
"@codemirror/view": "^6.9.6",
"@harnessio/design-system": "^2.1.1",
"@harnessio/icons": "^2.1.9",
"@harnessio/react-har-service-client": "^0.10.0",
"@harnessio/react-har-service-client": "^0.12.0",
"@harnessio/react-ssca-manager-client": "^0.65.0",
"@harnessio/uicore": "^4.1.2",
"@tanstack/react-query": "4.20.4",

View File

@ -133,5 +133,6 @@ export interface MFEAppProps {
}
export enum FeatureFlags {
HAR_TRIGGERS = 'HAR_TRIGGERS'
HAR_TRIGGERS = 'HAR_TRIGGERS',
HAR_NPM_PACKAGE_TYPE_ENABLED = 'HAR_NPM_PACKAGE_TYPE_ENABLED'
}

View File

@ -15,7 +15,7 @@
*/
import type { IconName } from '@harnessio/icons'
import type { FeatureFlags } from '@ar/MFEAppTypes'
import { FeatureFlags } from '@ar/MFEAppTypes'
import type { StringsMap } from '@ar/frameworks/strings'
import { RepositoryPackageType } from '@ar/common/types'
import { useFeatureFlags } from './useFeatureFlag'
@ -70,6 +70,7 @@ const RepositoryTypes: RepositoryTypeListItem[] = [
value: RepositoryPackageType.NPM,
icon: 'npm-repository-type',
tooltip: 'Coming Soon!',
featureFlag: FeatureFlags.HAR_NPM_PACKAGE_TYPE_ENABLED,
disabled: true
},
{

View File

@ -16,7 +16,7 @@
import type { IconName } from '@harnessio/icons'
import type { FeatureFlags } from '@ar/MFEAppTypes'
import { FeatureFlags } from '@ar/MFEAppTypes'
import type { StringsMap } from '@ar/frameworks/strings'
import { UpstreamProxyPackageType } from '@ar/pages/upstream-proxy-details/types'
@ -61,5 +61,13 @@ export const UpstreamProxyPackageTypeList: UpstreamRepositoryPackageTypeListItem
label: 'repositoryTypes.maven',
value: UpstreamProxyPackageType.MAVEN,
icon: 'maven-repository-type'
},
{
label: 'repositoryTypes.npm',
value: UpstreamProxyPackageType.NPM,
icon: 'npm-repository-type',
tooltip: 'Coming Soon!',
featureFlag: FeatureFlags.HAR_NPM_PACKAGE_TYPE_ENABLED,
disabled: true
}
]

View File

@ -0,0 +1,130 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import type { IconName } from '@harnessio/icons'
import { RepositoryConfigType, RepositoryPackageType } from '@ar/common/types'
import UpstreamProxyActions from '@ar/pages/upstream-proxy-details/components/UpstreamProxyActions/UpstreamProxyActions'
import UpstreamProxyConfigurationForm from '@ar/pages/upstream-proxy-details/components/Forms/UpstreamProxyConfigurationForm'
import UpstreamProxyCreateFormContent from '@ar/pages/upstream-proxy-details/components/FormContent/UpstreamProxyCreateFormContent'
import UpstreamProxyDetailsHeader from '@ar/pages/upstream-proxy-details/components/UpstreamProxyDetailsHeader/UpstreamProxyDetailsHeader'
import {
type CreateRepositoryFormProps,
type RepositoryActionsProps,
type RepositoryConfigurationFormProps,
type RepositoryDetailsHeaderProps,
RepositoryStep,
type RepositoySetupClientProps
} from '@ar/frameworks/RepositoryStep/Repository'
import {
UpstreamProxyAuthenticationMode,
type UpstreamRegistryRequest,
UpstreamRepositoryURLInputSource
} from '@ar/pages/upstream-proxy-details/types'
import type { Repository, VirtualRegistryRequest } from '../types'
import RepositoryActions from '../components/Actions/RepositoryActions'
import RedirectPageView from '../components/RedirectPageView/RedirectPageView'
import SetupClientContent from '../components/SetupClientContent/SetupClientContent'
import RepositoryConfigurationForm from '../components/Forms/RepositoryConfigurationForm'
import RepositoryCreateFormContent from '../components/FormContent/RepositoryCreateFormContent'
import RepositoryDetailsHeader from '../components/RepositoryDetailsHeader/RepositoryDetailsHeader'
export class NpmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
protected packageType = RepositoryPackageType.NPM
protected repositoryName = 'NPM Repository'
protected repositoryIcon: IconName = 'npm-repository-type'
protected supportedScanners = []
protected supportsUpstreamProxy = true
protected supportedUpstreamURLSources = [
UpstreamRepositoryURLInputSource.NpmJS,
UpstreamRepositoryURLInputSource.Custom
]
protected defaultValues: VirtualRegistryRequest = {
packageType: RepositoryPackageType.NPM,
identifier: '',
config: {
type: RepositoryConfigType.VIRTUAL
},
scanners: []
}
protected defaultUpstreamProxyValues: UpstreamRegistryRequest = {
packageType: RepositoryPackageType.NPM,
identifier: '',
config: {
type: RepositoryConfigType.UPSTREAM,
source: UpstreamRepositoryURLInputSource.NpmJS,
authType: UpstreamProxyAuthenticationMode.ANONYMOUS,
url: ''
},
cleanupPolicy: [],
scanners: []
}
renderCreateForm(props: CreateRepositoryFormProps): JSX.Element {
const { type } = props
if (type === RepositoryConfigType.VIRTUAL) {
return <RepositoryCreateFormContent isEdit={false} />
} else {
return <UpstreamProxyCreateFormContent isEdit={false} readonly={false} />
}
}
renderCofigurationForm(props: RepositoryConfigurationFormProps<Repository>): JSX.Element {
const { type } = props
if (type === RepositoryConfigType.VIRTUAL) {
return <RepositoryConfigurationForm ref={props.formikRef} readonly={props.readonly} />
} else {
return <UpstreamProxyConfigurationForm ref={props.formikRef} readonly={props.readonly} />
}
}
renderActions(props: RepositoryActionsProps<Repository>): JSX.Element {
if (props.type === RepositoryConfigType.VIRTUAL) {
return <RepositoryActions data={props.data} readonly={props.readonly} pageType={props.pageType} />
}
return <UpstreamProxyActions data={props.data} readonly={props.readonly} pageType={props.pageType} />
}
renderSetupClient(props: RepositoySetupClientProps): JSX.Element {
const { repoKey, onClose, artifactKey, versionKey } = props
return (
<SetupClientContent
repoKey={repoKey}
artifactKey={artifactKey}
versionKey={versionKey}
onClose={onClose}
packageType={RepositoryPackageType.NPM}
/>
)
}
renderRepositoryDetailsHeader(props: RepositoryDetailsHeaderProps<Repository>): JSX.Element {
const { type } = props
if (type === RepositoryConfigType.VIRTUAL) {
return <RepositoryDetailsHeader data={props.data} />
} else {
return <UpstreamProxyDetailsHeader data={props.data} />
}
}
renderRedirectPage(): JSX.Element {
return <RedirectPageView />
}
}

View File

@ -0,0 +1,244 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import userEvent from '@testing-library/user-event'
import { fireEvent, getByTestId, getByText, render, screen, waitFor } from '@testing-library/react'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
import RepositoryListPage from '@ar/pages/repository-list/RepositoryListPage'
import { queryByNameAttribute } from 'utils/test/testUtils'
import { MockGetNpmRegistryResponseWithAllData } from './__mockData__'
import '../../RepositoryFactory'
const createRegistryFn = jest.fn().mockImplementation(() => Promise.resolve(MockGetNpmRegistryResponseWithAllData))
const showSuccessToast = jest.fn()
const showErrorToast = jest.fn()
const mockHistoryPush = jest.fn()
jest.mock('@harnessio/uicore', () => ({
...jest.requireActual('@harnessio/uicore'),
useToaster: jest.fn().mockImplementation(() => ({
showSuccess: showSuccessToast,
showError: showErrorToast,
clear: jest.fn()
}))
}))
// eslint-disable-next-line jest-no-mock
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush
})
}))
jest.mock('@harnessio/react-har-service-client', () => ({
useGetAllRegistriesQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
data: { content: { data: { registries: [] }, status: 'SUCCESS' } },
refetch: jest.fn(),
error: null
})),
useCreateRegistryMutation: jest.fn().mockImplementation(() => ({
mutateAsync: createRegistryFn
}))
}))
describe('Verify create npm registry flow', () => {
test('Verify Modal header', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
const pageSubHeader = getByTestId(container, 'page-subheader')
const createRegistryButton = getByText(pageSubHeader, 'repositoryList.newRepository')
expect(createRegistryButton).toBeInTheDocument()
await userEvent.click(createRegistryButton)
const modal = document.getElementsByClassName('bp3-dialog')[0]
expect(modal).toBeInTheDocument()
const dialogHeader = screen.getByTestId('modaldialog-header')
expect(dialogHeader).toHaveTextContent('repositoryDetails.repositoryForm.modalTitle')
expect(dialogHeader).toHaveTextContent('repositoryDetails.repositoryForm.modalSubTitle')
const closeButton = modal.querySelector('button[aria-label="Close"]')
expect(closeButton).toBeInTheDocument()
await userEvent.click(closeButton!)
expect(modal).not.toBeInTheDocument()
})
test('verify registry type selector', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
const pageSubHeader = getByTestId(container, 'page-subheader')
const createRegistryButton = getByText(pageSubHeader, 'repositoryList.newRepository')
expect(createRegistryButton).toBeInTheDocument()
await userEvent.click(createRegistryButton)
const modal = document.getElementsByClassName('bp3-dialog')[0]
expect(modal).toBeInTheDocument()
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
expect(dialogBody).toHaveTextContent('repositoryDetails.repositoryForm.selectRepoType')
const registryTypeOption = dialogBody.querySelector('input[type="checkbox"][name=packageType][value="NPM"]')
expect(registryTypeOption).not.toBeDisabled()
fireEvent.change(registryTypeOption!, { target: { checked: true } })
expect(registryTypeOption).toBeChecked()
})
test('verify npm registry create form with success scenario', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
const pageSubHeader = getByTestId(container, 'page-subheader')
const createRegistryButton = getByText(pageSubHeader, 'repositoryList.newRepository')
expect(createRegistryButton).toBeInTheDocument()
await userEvent.click(createRegistryButton)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
expect(dialogBody).toHaveTextContent('repositoryDetails.repositoryForm.selectRepoType')
const registryTypeOption = dialogBody.querySelector('input[type="checkbox"][name=packageType][value="NPM"]')
expect(registryTypeOption).not.toBeDisabled()
await userEvent.click(registryTypeOption!)
fireEvent.change(registryTypeOption!, { target: { checked: true } })
expect(registryTypeOption).toBeChecked()
expect(dialogBody).toHaveTextContent('repositoryDetails.repositoryForm.title')
const formData = MockGetNpmRegistryResponseWithAllData.content.data
const nameField = queryByNameAttribute('identifier', dialogBody)
expect(nameField).toBeInTheDocument()
expect(nameField).not.toBeDisabled()
fireEvent.change(nameField!, { target: { value: formData.identifier } })
const descriptionEditButton = getByTestId(dialogBody, 'description-edit')
expect(descriptionEditButton).toBeInTheDocument()
await userEvent.click(descriptionEditButton)
const descriptionField = queryByNameAttribute('description', dialogBody)
expect(descriptionField).toBeInTheDocument()
expect(descriptionField).not.toBeDisabled()
fireEvent.change(descriptionField!, { target: { value: formData.description } })
const dialogFooter = screen.getByTestId('modaldialog-footer')
expect(dialogFooter).toBeInTheDocument()
const createButton = dialogFooter.querySelector(
'button[type="submit"][aria-label="repositoryDetails.repositoryForm.create"]'
)
expect(createButton).toBeInTheDocument()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenCalledWith({
body: {
cleanupPolicy: [],
config: { type: 'VIRTUAL', upstreamProxies: [] },
description: 'custom description',
identifier: 'npm-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showSuccessToast).toHaveBeenCalledWith('repositoryDetails.repositoryForm.repositoryCreated')
expect(mockHistoryPush).toHaveBeenCalledWith('/registries/npm-repo/configuration')
})
})
test('verify npm registry create form with failure scenario', async () => {
createRegistryFn.mockImplementation(() => Promise.reject({ message: 'error message' }))
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
const pageSubHeader = getByTestId(container, 'page-subheader')
const createRegistryButton = getByText(pageSubHeader, 'repositoryList.newRepository')
expect(createRegistryButton).toBeInTheDocument()
await userEvent.click(createRegistryButton)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
expect(dialogBody).toHaveTextContent('repositoryDetails.repositoryForm.selectRepoType')
const registryTypeOption = dialogBody.querySelector('input[type="checkbox"][name=packageType][value="NPM"]')
expect(registryTypeOption).not.toBeDisabled()
await userEvent.click(registryTypeOption!)
fireEvent.change(registryTypeOption!, { target: { checked: true } })
expect(registryTypeOption).toBeChecked()
expect(dialogBody).toHaveTextContent('repositoryDetails.repositoryForm.title')
const formData = MockGetNpmRegistryResponseWithAllData.content.data
const nameField = queryByNameAttribute('identifier', dialogBody)
expect(nameField).toBeInTheDocument()
expect(nameField).not.toBeDisabled()
fireEvent.change(nameField!, { target: { value: formData.identifier } })
const dialogFooter = screen.getByTestId('modaldialog-footer')
expect(dialogFooter).toBeInTheDocument()
const createButton = dialogFooter.querySelector(
'button[type="submit"][aria-label="repositoryDetails.repositoryForm.create"]'
)
expect(createButton).toBeInTheDocument()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenCalledWith({
body: {
cleanupPolicy: [],
config: { type: 'VIRTUAL', upstreamProxies: [] },
identifier: 'npm-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showErrorToast).toHaveBeenCalledWith('error message')
})
})
})

View File

@ -0,0 +1,338 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import userEvent from '@testing-library/user-event'
import { render, screen, waitFor } from '@testing-library/react'
import type { Registry } from '@harnessio/react-har-service-client'
import { Parent } from '@ar/common/types'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
import RepositoryListPage from '@ar/pages/repository-list/RepositoryListPage'
import { MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData } from './__mockData__'
import upstreamProxyUtils from '../../__tests__/utils'
import '../../RepositoryFactory'
const createRegistryFn = jest
.fn()
.mockImplementation(() => Promise.resolve(MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData))
const showSuccessToast = jest.fn()
const showErrorToast = jest.fn()
const mockHistoryPush = jest.fn()
jest.mock('@harnessio/uicore', () => ({
...jest.requireActual('@harnessio/uicore'),
useToaster: jest.fn().mockImplementation(() => ({
showSuccess: showSuccessToast,
showError: showErrorToast,
clear: jest.fn()
}))
}))
// eslint-disable-next-line jest-no-mock
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush
})
}))
jest.mock('@harnessio/react-har-service-client', () => ({
useGetAllRegistriesQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
data: { content: { data: { registries: [] }, status: 'SUCCESS' } },
refetch: jest.fn(),
error: null
})),
useCreateRegistryMutation: jest.fn().mockImplementation(() => ({
mutateAsync: createRegistryFn
}))
}))
describe('Verify create npm upstream registry flow', () => {
test('Verify Modal header', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
const modal = await upstreamProxyUtils.openModal(container)
const dialogHeader = screen.getByTestId('modaldialog-header')
expect(dialogHeader).toHaveTextContent('upstreamProxyDetails.createForm.title')
const closeButton = modal.querySelector('button[aria-label="Close"]')
expect(closeButton).toBeInTheDocument()
await userEvent.click(closeButton!)
expect(modal).not.toBeInTheDocument()
})
test('verify registry type selector', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
await upstreamProxyUtils.openModal(container)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
await upstreamProxyUtils.verifyPackageTypeSelector(dialogBody, 'NPM')
})
test('verify NPM registry create form with success scenario > Source as NpmJs > Anonymous', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
await upstreamProxyUtils.openModal(container)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
await upstreamProxyUtils.verifyPackageTypeSelector(dialogBody, 'NPM')
expect(dialogBody).toHaveTextContent('upstreamProxyDetails.form.title')
const formData = MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data as Registry
await upstreamProxyUtils.verifyUpstreamProxyCreateForm(
dialogBody,
formData,
'NpmJs',
'Anonymous',
'NpmJs',
'Anonymous'
)
const createButton = await upstreamProxyUtils.getSubmitButton()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenLastCalledWith({
body: {
cleanupPolicy: [],
config: { auth: null, authType: 'Anonymous', source: 'NpmJs', type: 'UPSTREAM', url: '' },
description: 'test description',
identifier: 'npm-up-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showSuccessToast).toHaveBeenLastCalledWith(
'upstreamProxyDetails.actions.createUpdateModal.createSuccessMessage'
)
expect(mockHistoryPush).toHaveBeenLastCalledWith('/registries/npm-up-repo/configuration')
})
})
test('verify NPM registry create form with success scenario > Source as NpmJs > UserPassword', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}
parent={Parent.OSS}>
<RepositoryListPage />
</ArTestWrapper>
)
await upstreamProxyUtils.openModal(container)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
await upstreamProxyUtils.verifyPackageTypeSelector(dialogBody, 'NPM')
expect(dialogBody).toHaveTextContent('upstreamProxyDetails.form.title')
const formData = MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data as Registry
await upstreamProxyUtils.verifyUpstreamProxyCreateForm(
dialogBody,
formData,
'NpmJs',
'UserPassword',
'NpmJs',
'Anonymous'
)
const createButton = await upstreamProxyUtils.getSubmitButton()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenLastCalledWith({
body: {
cleanupPolicy: [],
config: {
auth: { authType: 'UserPassword', secretIdentifier: 'password', userName: 'username' },
authType: 'UserPassword',
source: 'NpmJs',
type: 'UPSTREAM',
url: ''
},
description: 'test description',
identifier: 'npm-up-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showSuccessToast).toHaveBeenLastCalledWith(
'upstreamProxyDetails.actions.createUpdateModal.createSuccessMessage'
)
expect(mockHistoryPush).toHaveBeenLastCalledWith('/registries/npm-up-repo/configuration')
})
})
test('verify npm registry create form with success scenario > Source as Custom > Anonymous', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryListPage />
</ArTestWrapper>
)
await upstreamProxyUtils.openModal(container)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
await upstreamProxyUtils.verifyPackageTypeSelector(dialogBody, 'NPM')
expect(dialogBody).toHaveTextContent('upstreamProxyDetails.form.title')
const formData = MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data as Registry
await upstreamProxyUtils.verifyUpstreamProxyCreateForm(
dialogBody,
formData,
'Custom',
'Anonymous',
'NpmJs',
'Anonymous'
)
const createButton = await upstreamProxyUtils.getSubmitButton()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenLastCalledWith({
body: {
cleanupPolicy: [],
config: {
auth: null,
authType: 'Anonymous',
source: 'Custom',
type: 'UPSTREAM',
url: 'https://custom.docker.com'
},
description: 'test description',
identifier: 'npm-up-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showSuccessToast).toHaveBeenLastCalledWith(
'upstreamProxyDetails.actions.createUpdateModal.createSuccessMessage'
)
expect(mockHistoryPush).toHaveBeenLastCalledWith('/registries/npm-up-repo/configuration')
})
})
test('verify NPM registry create form with success scenario > Source as Custom > Username Password', async () => {
const { container } = render(
<ArTestWrapper
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}
parent={Parent.OSS}>
<RepositoryListPage />
</ArTestWrapper>
)
await upstreamProxyUtils.openModal(container)
const dialogBody = screen.getByTestId('modaldialog-body')
expect(dialogBody).toBeInTheDocument()
await upstreamProxyUtils.verifyPackageTypeSelector(dialogBody, 'NPM')
expect(dialogBody).toHaveTextContent('upstreamProxyDetails.form.title')
const formData = MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data as Registry
await upstreamProxyUtils.verifyUpstreamProxyCreateForm(
dialogBody,
formData,
'Custom',
'UserPassword',
'NpmJs',
'Anonymous'
)
const createButton = await upstreamProxyUtils.getSubmitButton()
await userEvent.click(createButton!)
await waitFor(() => {
expect(createRegistryFn).toHaveBeenLastCalledWith({
body: {
cleanupPolicy: [],
config: {
auth: { authType: 'UserPassword', secretIdentifier: 'password', userName: 'username' },
authType: 'UserPassword',
source: 'Custom',
type: 'UPSTREAM',
url: 'https://custom.docker.com'
},
description: 'test description',
identifier: 'npm-up-repo',
packageType: 'NPM',
parentRef: 'undefined',
scanners: []
},
queryParams: { space_ref: 'undefined' }
})
expect(showSuccessToast).toHaveBeenLastCalledWith(
'upstreamProxyDetails.actions.createUpdateModal.createSuccessMessage'
)
expect(mockHistoryPush).toHaveBeenLastCalledWith('/registries/npm-up-repo/configuration')
})
})
})

View File

@ -0,0 +1,367 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import userEvent from '@testing-library/user-event'
import { fireEvent, getByTestId, getByText, queryByTestId, render, waitFor } from '@testing-library/react'
import { useGetClientSetupDetailsQuery, useGetRegistryQuery } from '@harnessio/react-har-service-client'
import { DEFAULT_DATE_TIME_FORMAT } from '@ar/constants'
import { getReadableDateTime } from '@ar/common/dateUtils'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
import { queryByNameAttribute } from 'utils/test/testUtils'
import RepositoryDetailsPage from '../../RepositoryDetailsPage'
import {
MockGetNpmArtifactsByRegistryResponse,
MockGetNpmRegistryResponseWithAllData,
MockGetNpmSetupClientOnRegistryConfigPageResponse
} from './__mockData__'
import '../../RepositoryFactory'
const modifyRepository = jest.fn().mockImplementation(
() =>
new Promise(onSuccess => {
onSuccess({ content: { status: 'SUCCESS' } })
})
)
const mockHistoryPush = jest.fn()
// eslint-disable-next-line jest-no-mock
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush
})
}))
jest.mock('@harnessio/react-har-service-client', () => ({
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
refetch: jest.fn(),
error: false,
data: MockGetNpmRegistryResponseWithAllData
})),
useGetAllArtifactsByRegistryQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
refetch: jest.fn(),
error: false,
data: MockGetNpmArtifactsByRegistryResponse
})),
useGetClientSetupDetailsQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
refetch: jest.fn(),
error: false,
data: MockGetNpmSetupClientOnRegistryConfigPageResponse
})),
useDeleteRegistryMutation: jest.fn().mockImplementation(() => ({
isLoading: false,
mutateAsync: jest.fn()
})),
useModifyRegistryMutation: jest.fn().mockImplementation(() => ({
isLoading: false,
mutateAsync: modifyRepository
})),
useGetAllRegistriesQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
data: { content: { data: { registries: [] }, status: 'SUCCESS' } },
refetch: jest.fn(),
error: null
}))
}))
describe('Verify header section for NPM artifact registry', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('Verify breadcrumbs', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = container.querySelector('div[data-testid=page-header]')
expect(pageHeader).toBeInTheDocument()
const breadcrumbsSection = pageHeader?.querySelector('div[class*=PageHeader--breadcrumbsDiv--]')
expect(breadcrumbsSection).toBeInTheDocument()
expect(breadcrumbsSection).toHaveTextContent('breadcrumbs.repositories')
})
test('Verify registry icon, registry name, tag, lables, description and last updated', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'registry-header-container')
expect(pageHeader).toBeInTheDocument()
expect(pageHeader?.querySelector('span[data-icon=npm-repository-type]')).toBeInTheDocument()
const data = MockGetNpmRegistryResponseWithAllData.content.data
const title = getByTestId(container, 'registry-title')
expect(title).toHaveTextContent(data.identifier)
const description = getByTestId(container, 'registry-description')
expect(description).toHaveTextContent(data.description)
expect(pageHeader?.querySelector('svg[data-icon=tag]')).toBeInTheDocument()
const lastModifiedAt = getByTestId(container, 'registry-last-modified-at')
expect(lastModifiedAt).toHaveTextContent(getReadableDateTime(Number(data.modifiedAt), DEFAULT_DATE_TIME_FORMAT))
})
test('Verify registry setup client action', async () => {
const { container } = render(
<ArTestWrapper path="/registries/abcd/:tab" pathParams={{ tab: 'configuration' }}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'registry-header-container')
const setupClientBtn = pageHeader.querySelector('button[aria-label="actions.setupClient"]')
expect(setupClientBtn).toBeInTheDocument()
await userEvent.click(setupClientBtn!)
await waitFor(() => {
expect(useGetClientSetupDetailsQuery).toHaveBeenLastCalledWith({
queryParams: { artifact: undefined, version: undefined },
registry_ref: 'undefined/npm-repo/+'
})
})
})
test('Verify other registry actions', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'registry-header-container')
const actions3DotsBtn = pageHeader.querySelector('span[data-icon=Options')
expect(actions3DotsBtn).toBeInTheDocument()
await userEvent.click(actions3DotsBtn!)
const dialogs = document.getElementsByClassName('bp3-popover')
await waitFor(() => expect(dialogs).toHaveLength(1))
const selectPopover = dialogs[0] as HTMLElement
const items = selectPopover.getElementsByClassName('bp3-menu-item')
for (let idx = 0; idx < items.length; idx++) {
const actionItem = items[idx]
expect(actionItem.querySelector('span[data-icon=code-delete]')).toBeInTheDocument()
expect(actionItem).toHaveTextContent('actions.delete')
}
})
test('Verify tab selection status', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/:repositoryIdentifier/:tab"
pathParams={{ repositoryIdentifier: 'abcd', tab: 'packages' }}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const tabList = container.querySelector('div[role=tablist]')
expect(tabList).toBeInTheDocument()
const artifactsTab = tabList?.querySelector('div[data-tab-id=packages][aria-selected=true]')
expect(artifactsTab).toBeInTheDocument()
const configurationTab = tabList?.querySelector('div[data-tab-id=configuration][aria-selected=false]')
expect(configurationTab).toBeInTheDocument()
await userEvent.click(configurationTab!)
expect(mockHistoryPush).toHaveBeenCalledWith('/registries/abcd/configuration')
})
})
describe('Verify configuration form', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('should render form correctly with all data prefilled', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
// Artifact registry defination section
const registryDefinitionSection = getByTestId(container, 'registry-definition')
const nameField = queryByNameAttribute('identifier', registryDefinitionSection)
expect(nameField).toBeInTheDocument()
expect(nameField).toBeDisabled()
expect(nameField).toHaveAttribute('value', MockGetNpmRegistryResponseWithAllData.content.data.identifier)
const descriptionField = queryByNameAttribute('description', registryDefinitionSection)
expect(descriptionField).toBeInTheDocument()
expect(descriptionField).not.toBeDisabled()
expect(descriptionField).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.description)
const tags = registryDefinitionSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
tags.forEach((each, idx) => {
expect(each).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.labels[idx])
})
// Security scan section
const securityScanSection = queryByTestId(container, 'security-section')
expect(securityScanSection).toBeInTheDocument()
// artifact filtering rules
const filteringRulesSection = getByTestId(container, 'include-exclude-patterns-section')
expect(filteringRulesSection).toBeInTheDocument()
const allowedPatternsSection = filteringRulesSection.querySelectorAll('div.bp3-form-group')[0]
const allowedPatterns = allowedPatternsSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
allowedPatterns.forEach((each, idx) => {
expect(each).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.allowedPattern[idx])
})
const blockedPatternsSection = filteringRulesSection.querySelectorAll('div.bp3-form-group')[1]
const blockedPatterns = blockedPatternsSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
blockedPatterns.forEach((each, idx) => {
expect(each).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.blockedPattern[idx])
})
// upstream proxy section
const upstreamProxySection = getByTestId(container, 'upstream-proxy-section')
expect(upstreamProxySection).toBeInTheDocument()
const selectedItemList = upstreamProxySection.querySelectorAll('ul[aria-label=orderable-list] .bp3-menu-item')
selectedItemList.forEach((each, idx) => {
expect(each).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.config.upstreamProxies[idx])
})
// cleanup policy section
const cleanupPoliciesSection = getByTestId(container, 'cleanup-policy-section')
expect(cleanupPoliciesSection).toBeInTheDocument()
const addCleanupPolicyBtn = cleanupPoliciesSection.querySelector(
'a[role=button][aria-label="cleanupPolicy.addBtn"]'
)
expect(addCleanupPolicyBtn).toBeInTheDocument()
expect(addCleanupPolicyBtn).toHaveAttribute('disabled', '')
// action buttons
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).toBeDisabled()
})
test('should able to submit the form with updated data', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const descriptionField = queryByNameAttribute('description', container)
fireEvent.change(descriptionField!, { target: { value: 'updated description' } })
expect(descriptionField).toHaveTextContent('updated description')
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).not.toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).not.toBeDisabled()
await userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenCalledWith({
body: {
...MockGetNpmRegistryResponseWithAllData.content.data,
description: 'updated description'
},
registry_ref: 'undefined/abcd/+'
})
})
})
test('should able to discard the changes', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const descriptionField = queryByNameAttribute('description', container)
fireEvent.change(descriptionField!, { target: { value: 'updated description' } })
expect(descriptionField).toHaveTextContent('updated description')
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).not.toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).not.toBeDisabled()
await userEvent.click(discardBtn!)
await waitFor(() => {
expect(saveBtn).toBeDisabled()
expect(discardBtn).toBeDisabled()
expect(descriptionField).toHaveTextContent(MockGetNpmRegistryResponseWithAllData.content.data.description)
})
})
test('should render retry if failed to load get registry api', async () => {
const refetchFn = jest.fn()
;(useGetRegistryQuery as jest.Mock).mockImplementation(() => ({
isFetching: false,
isError: true,
error: { message: 'failed to load registry' },
data: null,
refetch: refetchFn
}))
const { container } = render(
<ArTestWrapper
queryParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
expect(getByText(container, 'failed to load registry')).toBeInTheDocument()
const retryBtn = container.querySelector('button[aria-label=Retry]')
expect(retryBtn).toBeInTheDocument()
await userEvent.click(retryBtn!)
await waitFor(() => {
expect(refetchFn).toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,566 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import userEvent from '@testing-library/user-event'
import { fireEvent, getByTestId, getByText, queryByTestId, render, waitFor } from '@testing-library/react'
import { Parent } from '@ar/common/types'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
import { queryByNameAttribute } from 'utils/test/testUtils'
import RepositoryDetailsPage from '../../RepositoryDetailsPage'
import {
MockGetNpmArtifactsByRegistryResponse,
MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData
} from './__mockData__'
import upstreamProxyUtils from '../../__tests__/utils'
const modifyRepository = jest.fn().mockImplementation(
() =>
new Promise(onSuccess => {
onSuccess({ content: { status: 'SUCCESS' } })
})
)
const deleteRegistry = jest.fn().mockImplementation(
() =>
new Promise(onSuccess => {
onSuccess({ content: { status: 'SUCCESS' } })
})
)
const showSuccessToast = jest.fn()
const showErrorToast = jest.fn()
jest.mock('@harnessio/uicore', () => ({
...jest.requireActual('@harnessio/uicore'),
useToaster: jest.fn().mockImplementation(() => ({
showSuccess: showSuccessToast,
showError: showErrorToast,
clear: jest.fn()
}))
}))
const mockHistoryPush = jest.fn()
// eslint-disable-next-line jest-no-mock
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush
})
}))
jest.mock('@harnessio/react-har-service-client', () => ({
useGetRegistryQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
refetch: jest.fn(),
error: false,
data: MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData
})),
useGetAllArtifactsByRegistryQuery: jest.fn().mockImplementation(() => ({
isFetching: false,
refetch: jest.fn(),
error: false,
data: MockGetNpmArtifactsByRegistryResponse
})),
useDeleteRegistryMutation: jest.fn().mockImplementation(() => ({
isLoading: false,
mutateAsync: deleteRegistry
})),
useModifyRegistryMutation: jest.fn().mockImplementation(() => ({
isLoading: false,
mutateAsync: modifyRepository
}))
}))
describe('Verify header section for docker artifact registry', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('Verify breadcrumbs', async () => {
const { container } = render(
<ArTestWrapper parent={Parent.OSS}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = container.querySelector('div[data-testid=page-header]')
expect(pageHeader).toBeInTheDocument()
const breadcrumbsSection = pageHeader?.querySelector('div[class*=PageHeader--breadcrumbsDiv--]')
expect(breadcrumbsSection).toBeInTheDocument()
expect(breadcrumbsSection).toHaveTextContent('breadcrumbs.repositories')
})
test('Verify registry icon, registry name, tag, lables, description and last updated', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'upstream-registry-header-container')
expect(pageHeader).toBeInTheDocument()
expect(pageHeader?.querySelector('span[data-icon=npm-repository-type]')).toBeInTheDocument()
const data = MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data
expect(pageHeader).toHaveTextContent(data.identifier)
expect(pageHeader).toHaveTextContent('na')
})
test('Verify registry setup client action not visible', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'upstream-registry-header-container')
const setupClientBtn = pageHeader.querySelector('button[aria-label="actions.setupClient"]')
expect(setupClientBtn).not.toBeInTheDocument()
})
test('Verify other registry actions', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'upstream-registry-header-container')
const actions3DotsBtn = pageHeader.querySelector('span[data-icon=Options')
expect(actions3DotsBtn).toBeInTheDocument()
await userEvent.click(actions3DotsBtn!)
const dialogs = document.getElementsByClassName('bp3-popover')
await waitFor(() => expect(dialogs).toHaveLength(1))
const selectPopover = dialogs[0] as HTMLElement
const items = selectPopover.getElementsByClassName('bp3-menu-item')
for (let idx = 0; idx < items.length; idx++) {
const actionItem = items[idx]
expect(actionItem.querySelector('span[data-icon=code-delete]')).toBeInTheDocument()
expect(actionItem).toHaveTextContent('actions.delete')
}
})
test('verify delete action: Success', async () => {
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'upstream-registry-header-container')
const actions3DotsBtn = pageHeader.querySelector('span[data-icon=Options')
expect(actions3DotsBtn).toBeInTheDocument()
await userEvent.click(actions3DotsBtn!)
const selectPopover = document.getElementsByClassName('bp3-popover')[0] as HTMLElement
const items = selectPopover.getElementsByClassName('bp3-menu-item')
const actionItem = items[0]
expect(actionItem.querySelector('span[data-icon=code-delete]')).toBeInTheDocument()
expect(actionItem).toHaveTextContent('actions.delete')
await userEvent.click(actionItem)
let deleteDialog = document.getElementsByClassName('bp3-dialog')[0]
expect(deleteDialog).toBeInTheDocument()
expect(deleteDialog).toHaveTextContent('upstreamProxyDetails.actions.delete.title')
expect(deleteDialog).toHaveTextContent('upstreamProxyDetails.actions.delete.contentText')
const cancelButton = deleteDialog.querySelector('button[aria-label=cancel]')
expect(cancelButton).toBeInTheDocument()
await userEvent.click(cancelButton!)
expect(deleteDialog).not.toBeInTheDocument()
await userEvent.click(actionItem!)
deleteDialog = document.getElementsByClassName('bp3-dialog')[0]
expect(deleteDialog).toBeInTheDocument()
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
expect(deleteBtn).toBeInTheDocument()
await userEvent.click(deleteBtn!)
await waitFor(() => {
expect(deleteRegistry).toHaveBeenLastCalledWith({ registry_ref: 'undefined/npm-up-repo/+' })
expect(mockHistoryPush).toHaveBeenCalledWith('/registries')
expect(showSuccessToast).toHaveBeenCalledWith('upstreamProxyDetails.actions.delete.repositoryDeleted')
})
expect(deleteDialog).not.toBeInTheDocument()
})
test('verify delete action: Failure', async () => {
deleteRegistry.mockImplementationOnce(
() =>
new Promise((_, onReject) => {
onReject({ message: 'error message' })
})
)
const { container } = render(
<ArTestWrapper>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const pageHeader = getByTestId(container, 'upstream-registry-header-container')
const actions3DotsBtn = pageHeader.querySelector('span[data-icon=Options')
expect(actions3DotsBtn).toBeInTheDocument()
await userEvent.click(actions3DotsBtn!)
const selectPopover = document.getElementsByClassName('bp3-popover')[0] as HTMLElement
const items = selectPopover.getElementsByClassName('bp3-menu-item')
const actionItem = items[0]
expect(actionItem.querySelector('span[data-icon=code-delete]')).toBeInTheDocument()
expect(actionItem).toHaveTextContent('actions.delete')
await userEvent.click(actionItem)
const deleteDialog = document.getElementsByClassName('bp3-dialog')[0]
expect(deleteDialog).toBeInTheDocument()
expect(deleteDialog).toHaveTextContent('upstreamProxyDetails.actions.delete.title')
expect(deleteDialog).toHaveTextContent('upstreamProxyDetails.actions.delete.contentText')
const deleteBtn = deleteDialog.querySelector('button[aria-label=delete]')
expect(deleteBtn).toBeInTheDocument()
await userEvent.click(deleteBtn!)
await waitFor(() => {
expect(deleteRegistry).toHaveBeenLastCalledWith({ registry_ref: 'undefined/npm-up-repo/+' })
expect(mockHistoryPush).not.toHaveBeenCalledWith('/registries')
expect(showErrorToast).toHaveBeenLastCalledWith('error message')
})
expect(deleteDialog).not.toBeInTheDocument()
})
test('Verify tab selection status', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/:repositoryIdentifier/:tab"
pathParams={{ repositoryIdentifier: 'abcd', tab: 'packages' }}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const tabList = container.querySelector('div[role=tablist]')
expect(tabList).toBeInTheDocument()
const artifactsTab = tabList?.querySelector('div[data-tab-id=packages][aria-selected=true]')
expect(artifactsTab).toBeInTheDocument()
const configurationTab = tabList?.querySelector('div[data-tab-id=configuration][aria-selected=false]')
expect(configurationTab).toBeInTheDocument()
await userEvent.click(configurationTab!)
expect(mockHistoryPush).toHaveBeenCalledWith('/registries/abcd/configuration')
})
})
describe('Verify configuration form', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('should render form correctly with all data prefilled', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
// Artifact registry defination section
const registryDefinitionSection = getByTestId(container, 'upstream-registry-definition')
const nameField = queryByNameAttribute('identifier', registryDefinitionSection)
expect(nameField).toBeInTheDocument()
expect(nameField).toBeDisabled()
expect(nameField).toHaveAttribute(
'value',
MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data.identifier
)
const descriptionField = queryByNameAttribute('description', registryDefinitionSection)
expect(descriptionField).toBeInTheDocument()
expect(descriptionField).not.toBeDisabled()
expect(descriptionField).toHaveTextContent(
MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data.description
)
const tags = registryDefinitionSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
tags.forEach((each, idx) => {
expect(each).toHaveTextContent(MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data.labels[idx])
})
// verify source selection
const sourceAuthSection = getByTestId(container, 'upstream-source-auth-definition')
const sourceSection = sourceAuthSection.querySelector('input[type=radio][name="config.source"][value=NpmJs]')
expect(sourceSection).toBeChecked()
expect(sourceSection).not.toBeDisabled()
// verify auth type selection
const authTypeSelection = sourceAuthSection.querySelector(
'input[type=radio][name="config.authType"][value=Anonymous]'
)
expect(authTypeSelection).toBeChecked()
expect(authTypeSelection).not.toBeDisabled()
// Security scan section
const securityScanSection = queryByTestId(container, 'security-section')
expect(securityScanSection).toBeInTheDocument()
// artifact filtering rules
const filteringRulesSection = getByTestId(container, 'include-exclude-patterns-section')
expect(filteringRulesSection).toBeInTheDocument()
const allowedPatternsSection = filteringRulesSection.querySelectorAll('div.bp3-form-group')[0]
const allowedPatterns = allowedPatternsSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
allowedPatterns.forEach((each, idx) => {
expect(each).toHaveTextContent(
MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data.allowedPattern[idx]
)
})
const blockedPatternsSection = filteringRulesSection.querySelectorAll('div.bp3-form-group')[1]
const blockedPatterns = blockedPatternsSection.querySelectorAll('div.bp3-tag-input-values .bp3-tag')
blockedPatterns.forEach((each, idx) => {
expect(each).toHaveTextContent(
MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data.blockedPattern[idx]
)
})
const advancedOptionTitle = getByText(container, 'repositoryDetails.repositoryForm.advancedOptionsTitle')
expect(advancedOptionTitle).toBeInTheDocument()
await userEvent.click(advancedOptionTitle)
// cleanup policy section
const cleanupPoliciesSection = getByTestId(container, 'cleanup-policy-section')
expect(cleanupPoliciesSection).toBeInTheDocument()
const addCleanupPolicyBtn = cleanupPoliciesSection.querySelector(
'a[role=button][aria-label="cleanupPolicy.addBtn"]'
)
expect(addCleanupPolicyBtn).toBeInTheDocument()
expect(addCleanupPolicyBtn).toHaveAttribute('disabled', '')
// action buttons
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).toBeDisabled()
})
test('should able to submit the form with updated data: Success Scenario', async () => {
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const descriptionField = queryByNameAttribute('description', container)
fireEvent.change(descriptionField!, { target: { value: 'updated description' } })
expect(descriptionField).toHaveTextContent('updated description')
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).not.toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).not.toBeDisabled()
await userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenLastCalledWith({
body: {
...MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data,
description: 'updated description'
},
registry_ref: 'undefined/abcd/+'
})
expect(showSuccessToast).toHaveBeenLastCalledWith(
'upstreamProxyDetails.actions.createUpdateModal.updateSuccessMessage'
)
})
})
test('Verify source and auth section with multiple scenarios', async () => {
const { container } = render(
<ArTestWrapper
parent={Parent.OSS}
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const saveBtn = container.querySelector('button[aria-label=save]')
const sourceAuthSection = getByTestId(container, 'upstream-source-auth-definition')
// verify NpmJs, UserPassword
{
await upstreamProxyUtils.verifySourceAndAuthSection(
sourceAuthSection,
'NpmJs',
'UserPassword',
'NpmJs',
'Anonymous'
)
userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenLastCalledWith({
body: {
allowedPattern: ['test1', 'test2'],
blockedPattern: ['test3', 'test4'],
config: {
auth: { authType: 'UserPassword', secretIdentifier: 'password', userName: 'username' },
authType: 'UserPassword',
source: 'NpmJs',
type: 'UPSTREAM',
url: ''
},
createdAt: '1738516362995',
description: 'test description',
identifier: 'npm-up-repo',
labels: ['label1', 'label2', 'label3', 'label4'],
packageType: 'NPM',
url: ''
},
registry_ref: 'undefined/abcd/+'
})
})
}
// verify Custom, Anonymous
{
await upstreamProxyUtils.verifySourceAndAuthSection(
sourceAuthSection,
'Custom',
'Anonymous',
'NpmJs',
'Anonymous'
)
userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenLastCalledWith({
body: {
allowedPattern: ['test1', 'test2'],
blockedPattern: ['test3', 'test4'],
config: {
auth: null,
authType: 'Anonymous',
source: 'Custom',
type: 'UPSTREAM',
url: 'https://custom.docker.com'
},
createdAt: '1738516362995',
description: 'test description',
identifier: 'npm-up-repo',
labels: ['label1', 'label2', 'label3', 'label4'],
packageType: 'NPM',
url: ''
},
registry_ref: 'undefined/abcd/+'
})
})
}
// verify Custom, UserPassword
{
await upstreamProxyUtils.verifySourceAndAuthSection(
sourceAuthSection,
'Custom',
'UserPassword',
'NpmJs',
'Anonymous'
)
userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenLastCalledWith({
body: {
allowedPattern: ['test1', 'test2'],
blockedPattern: ['test3', 'test4'],
config: {
auth: { authType: 'UserPassword', secretIdentifier: 'password', userName: 'username' },
authType: 'UserPassword',
source: 'Custom',
type: 'UPSTREAM',
url: 'https://custom.docker.com'
},
createdAt: '1738516362995',
description: 'test description',
identifier: 'npm-up-repo',
labels: ['label1', 'label2', 'label3', 'label4'],
packageType: 'NPM',
url: ''
},
registry_ref: 'undefined/abcd/+'
})
})
}
})
test('should able to submit the form with updated data: Failure Scenario', async () => {
modifyRepository.mockImplementationOnce(
() =>
new Promise((_, onReject) => {
onReject({ message: 'error message' })
})
)
const { container } = render(
<ArTestWrapper
path="/registries/abcd/:tab"
pathParams={{ tab: 'configuration' }}
featureFlags={{
HAR_NPM_PACKAGE_TYPE_ENABLED: true
}}>
<RepositoryDetailsPage />
</ArTestWrapper>
)
const descriptionField = queryByNameAttribute('description', container)
fireEvent.change(descriptionField!, { target: { value: 'updated description' } })
expect(descriptionField).toHaveTextContent('updated description')
const saveBtn = container.querySelector('button[aria-label=save]')
expect(saveBtn).not.toBeDisabled()
const discardBtn = container.querySelector('button[aria-label=discard]')
expect(discardBtn).not.toBeDisabled()
await userEvent.click(saveBtn!)
await waitFor(() => {
expect(modifyRepository).toHaveBeenLastCalledWith({
body: {
...MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData.content.data,
description: 'updated description'
},
registry_ref: 'undefined/abcd/+'
})
expect(showErrorToast).toHaveBeenLastCalledWith('error message')
})
})
})

View File

@ -0,0 +1,357 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { GetAllArtifactsByRegistryOkResponse } from '@harnessio/react-har-service-client'
export const MockGetNpmRegistryResponseWithAllData = {
content: {
data: {
config: {
type: 'VIRTUAL',
upstreamProxies: ['npm-js-proxy']
},
createdAt: '1738040653409',
description: 'custom description',
identifier: 'npm-repo',
labels: ['label1', 'label2', 'label3', 'label4'],
modifiedAt: '1738040653409',
packageType: 'NPM',
url: 'http://host.docker.internal:3000/artifact-registry/maven-repo',
allowedPattern: ['test1', 'test2'],
blockedPattern: ['test3', 'test4']
},
status: 'SUCCESS'
}
}
export const MockGetNpmArtifactsByRegistryResponse: GetAllArtifactsByRegistryOkResponse = {
content: {
data: {
artifacts: [
{
downloadsCount: 0,
lastModified: '1738048875014',
latestVersion: 'v1',
name: 'artifact',
packageType: 'NPM',
registryIdentifier: 'npm-repo',
registryPath: ''
}
],
itemCount: 0,
pageCount: 0,
pageIndex: 0,
pageSize: 50
},
status: 'SUCCESS'
}
}
export const MockGetNpmSetupClientOnRegistryConfigPageResponse = {
content: {
data: {
mainHeader: 'Maven Client Setup',
secHeader: 'Follow these instructions to install/use Maven artifacts or compatible packages.',
sections: [
{
header: '1. Generate Identity Token',
secHeader: 'An identity token will serve as the password for uploading and downloading artifacts.',
steps: [
{
header: 'Generate an identity token',
type: 'GenerateToken'
}
],
type: 'INLINE'
},
{
tabs: [
{
header: 'Maven',
sections: [
{
header: '2. Pull a Maven Package',
secHeader: 'Set default repository in your pom.xml file.',
steps: [
{
commands: [
{
value:
'\u003crepositories\u003e\n \u003crepository\u003e\n \u003cid\u003emaven-dev\u003c/id\u003e\n \u003curl\u003ehttp://host.docker.internal:3000/maven/artifact-registry/maven-repo\u003c/url\u003e\n \u003creleases\u003e\n \u003cenabled\u003etrue\u003c/enabled\u003e\n \u003cupdatePolicy\u003ealways\u003c/updatePolicy\u003e\n \u003c/releases\u003e\n \u003csnapshots\u003e\n \u003cenabled\u003etrue\u003c/enabled\u003e\n \u003cupdatePolicy\u003ealways\u003c/updatePolicy\u003e\n \u003c/snapshots\u003e\n \u003c/repository\u003e\n\u003c/repositories\u003e'
}
],
header: 'To set default registry in your pom.xml file by adding the following:',
type: 'Static'
},
{
commands: [
{
value:
'\u003csettings\u003e\n \u003cservers\u003e\n \u003cserver\u003e\n \u003cid\u003emaven-dev\u003c/id\u003e\n \u003cusername\u003eadmin@gitness.io\u003c/username\u003e\n \u003cpassword\u003eidentity-token\u003c/password\u003e\n \u003c/server\u003e\n \u003c/servers\u003e\n\u003c/settings\u003e'
}
],
header:
'Copy the following your ~/ .m2/settings.xml file for MacOs, or $USERPROFILE$\\ .m2\\settings.xml for Windows to authenticate with token to pull from your Maven registry.',
type: 'Static'
},
{
commands: [
{
value:
'\u003cdependency\u003e\n \u003cgroupId\u003e\u003cGROUP_ID\u003e\u003c/groupId\u003e\n \u003cartifactId\u003e\u003cARTIFACT_ID\u003e\u003c/artifactId\u003e\n \u003cversion\u003e\u003cVERSION\u003e\u003c/version\u003e\n\u003c/dependency\u003e'
}
],
header:
"Add a dependency to the project's pom.xml (replace \u003cGROUP_ID\u003e, \u003cARTIFACT_ID\u003e \u0026 \u003cVERSION\u003e with your own):",
type: 'Static'
},
{
commands: [
{
value: 'mvn install'
}
],
header: 'Install dependencies in pom.xml file',
type: 'Static'
}
],
type: 'INLINE'
},
{
header: '3. Push a Maven Package',
secHeader: 'Set default repository in your pom.xml file.',
steps: [
{
commands: [
{
value:
'\u003cdistributionManagement\u003e\n \u003csnapshotRepository\u003e\n \u003cid\u003emaven-dev\u003c/id\u003e\n \u003curl\u003ehttp://host.docker.internal:3000/maven/artifact-registry/maven-repo\u003c/url\u003e\n \u003c/snapshotRepository\u003e\n \u003crepository\u003e\n \u003cid\u003emaven-dev\u003c/id\u003e\n \u003curl\u003ehttp://host.docker.internal:3000/maven/artifact-registry/maven-repo\u003c/url\u003e\n \u003c/repository\u003e\n\u003c/distributionManagement\u003e'
}
],
header: 'To set default registry in your pom.xml file by adding the following:',
type: 'Static'
},
{
commands: [
{
value:
'\u003csettings\u003e\n \u003cservers\u003e\n \u003cserver\u003e\n \u003cid\u003emaven-dev\u003c/id\u003e\n \u003cusername\u003eadmin@gitness.io\u003c/username\u003e\n \u003cpassword\u003eidentity-token\u003c/password\u003e\n \u003c/server\u003e\n \u003c/servers\u003e\n\u003c/settings\u003e'
}
],
header:
'Copy the following your ~/ .m2/setting.xml file for MacOs, or $USERPROFILE$\\ .m2\\settings.xml for Windows to authenticate with token to push to your Maven registry.',
type: 'Static'
},
{
commands: [
{
value: 'mvn deploy'
}
],
header: 'Publish package to your Maven registry.',
type: 'Static'
}
],
type: 'INLINE'
}
]
},
{
header: 'Gradle',
sections: [
{
header: '2. Pull a Gradle Package',
secHeader: 'Set default repository in your build.gradle file.',
steps: [
{
commands: [
{
value:
'repositories{\n maven{\n url “http://host.docker.internal:3000/maven/artifact-registry/maven-repo”\n\n credentials {\n username “admin@gitness.io”\n password “identity-token”\n }\n }\n}'
}
],
header: 'Set the default registry in your projects build.gradle by adding the following:',
type: 'Static'
},
{
commands: [
{
value: 'repositoryUser=admin@gitness.io\nrepositoryPassword={{identity-token}}'
}
],
header:
'As this is a private registry, youll need to authenticate. Create or add to the ~/.gradle/gradle.properties file with the following:',
type: 'Static'
},
{
commands: [
{
value:
'dependencies {\n implementation \u003cGROUP_ID\u003e:\u003cARTIFACT_ID\u003e:\u003cVERSION\u003e\n}'
}
],
header: 'Add a dependency to the projects build.gradle',
type: 'Static'
},
{
commands: [
{
value: 'gradlew build // Linux or OSX\n gradlew.bat build // Windows'
}
],
header: 'Install dependencies in build.gradle file',
type: 'Static'
}
],
type: 'INLINE'
},
{
header: '3. Push a Gradle Package',
secHeader: 'Set default repository in your build.gradle file.',
steps: [
{
commands: [
{
value:
"publishing {\n publications {\n maven(MavenPublication) {\n groupId = '\u003cGROUP_ID\u003e'\n artifactId = '\u003cARTIFACT_ID\u003e'\n version = '\u003cVERSION\u003e'\n\n from components.java\n }\n }\n}"
}
],
header: 'Add a maven publish plugin configuration to the projects build.gradle.',
type: 'Static'
},
{
commands: [
{
value: 'gradlew publish'
}
],
header: 'Publish package to your Maven registry.',
type: 'Static'
}
],
type: 'INLINE'
}
]
},
{
header: 'Sbt/Scala',
sections: [
{
header: '2. Pull a Sbt/Scala Package',
secHeader: 'Set default repository in your build.sbt file.',
steps: [
{
commands: [
{
value:
'resolver += “Harness Registry” at “http://host.docker.internal:3000/maven/artifact-registry/maven-repo”\ncredentials += Credentials(Path.userHome / “.sbt” / “.Credentials”)'
}
],
header: 'Set the default registry in your projects build.sbt by adding the following:',
type: 'Static'
},
{
commands: [
{
value:
'realm=Harness Registry\nhost=host.docker.internal:3000\nuser=admin@gitness.io\npassword={{identity-token}}'
}
],
header:
'As this is a private registry, youll need to authenticate. Create or add to the ~/.sbt/.credentials file with the following:',
type: 'Static'
},
{
commands: [
{
value:
'libraryDependencies += “\u003cGROUP_ID\u003e” % “\u003cARTIFACT_ID\u003e” % “\u003cVERSION\u003e”'
}
],
header: 'Add a dependency to the projects build.sbt',
type: 'Static'
},
{
commands: [
{
value: 'sbt update'
}
],
header: 'Install dependencies in build.sbt file',
type: 'Static'
}
],
type: 'INLINE'
},
{
header: '3. Push a Sbt/Scala Package',
secHeader: 'Set default repository in your build.sbt file.',
steps: [
{
commands: [
{
value:
'publishTo := Some("Harness Registry" at "http://host.docker.internal:3000/maven/artifact-registry/maven-repo")'
}
],
header: 'Add publish configuration to the projects build.sbt.',
type: 'Static'
},
{
commands: [
{
value: 'sbt publish'
}
],
header: 'Publish package to your Maven registry.',
type: 'Static'
}
],
type: 'INLINE'
}
]
}
],
type: 'TABS'
}
]
},
status: 'SUCCESS'
}
}
export const MockGetNpmUpstreamRegistryResponseWithNpmJsSourceAllData = {
content: {
data: {
allowedPattern: ['test1', 'test2'],
blockedPattern: ['test3', 'test4'],
config: {
auth: null,
authType: 'Anonymous',
source: 'NpmJs',
type: 'UPSTREAM',
url: ''
},
createdAt: '1738516362995',
identifier: 'npm-up-repo',
description: 'test description',
packageType: 'NPM',
labels: ['label1', 'label2', 'label3', 'label4'],
url: ''
},
status: 'SUCCESS'
}
}

View File

@ -19,8 +19,10 @@ import { DockerRepositoryType } from './DockerRepository/DockerRepositoryType'
import { MavenRepositoryType } from './MavenRepository/MavenRepository'
import { HelmRepositoryType } from './HelmRepository/HelmRepositoryType'
import { GenericRepositoryType } from './GenericRepository/GenericRepositoryType'
import { NpmRepositoryType } from './NpmRepository/NpmRepositoryType'
repositoryFactory.registerStep(new DockerRepositoryType())
repositoryFactory.registerStep(new HelmRepositoryType())
repositoryFactory.registerStep(new GenericRepositoryType())
repositoryFactory.registerStep(new MavenRepositoryType())
repositoryFactory.registerStep(new NpmRepositoryType())

View File

@ -58,5 +58,9 @@ export const URLSourceToSupportedAuthTypesMapping: Record<
[UpstreamRepositoryURLInputSource.MavenCentral]: [
UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
UpstreamProxyAuthenticationMode.ANONYMOUS
],
[UpstreamRepositoryURLInputSource.NpmJS]: [
UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
UpstreamProxyAuthenticationMode.ANONYMOUS
]
}

View File

@ -37,5 +37,9 @@ export const UpstreamURLSourceConfig: Record<UpstreamRepositoryURLInputSource, R
[UpstreamRepositoryURLInputSource.MavenCentral]: {
label: 'upstreamProxyDetails.createForm.source.mavenCentral',
value: UpstreamRepositoryURLInputSource.MavenCentral
},
[UpstreamRepositoryURLInputSource.NpmJS]: {
label: 'upstreamProxyDetails.createForm.source.npmjs',
value: UpstreamRepositoryURLInputSource.NpmJS
}
}

View File

@ -13,6 +13,7 @@ createForm:
ecr: AWS ECR
custom: Custom
mavenCentral: Maven Central
npmjs: npmjs
authentication:
title: Authentication
userNameAndPassword: Username and Password

View File

@ -21,12 +21,14 @@ export enum UpstreamProxyPackageType {
DOCKER = 'DOCKER',
HELM = 'HELM',
GENERIC = 'GENERIC',
MAVEN = 'MAVEN'
MAVEN = 'MAVEN',
NPM = 'NPM'
}
export enum UpstreamRepositoryURLInputSource {
Dockerhub = 'Dockerhub',
MavenCentral = 'MavenCentral',
NpmJS = 'NpmJs',
AwsEcr = 'AwsEcr',
Custom = 'Custom'
}

View File

@ -143,6 +143,7 @@ export interface StringsMap {
'upstreamProxyDetails.createForm.source.dockerHub': string
'upstreamProxyDetails.createForm.source.ecr': string
'upstreamProxyDetails.createForm.source.mavenCentral': string
'upstreamProxyDetails.createForm.source.npmjs': string
'upstreamProxyDetails.createForm.source.title': string
'upstreamProxyDetails.createForm.title': string
'upstreamProxyDetails.createForm.url': string

View File

@ -1945,10 +1945,10 @@
yargs "^17.6.2"
zod "^3.19.1"
"@harnessio/react-har-service-client@^0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.10.0.tgz#84a9a89ec3f02b35313dae02857789ec25a095dc"
integrity sha512-TC84QK0mas2F3dtN7EK4yJkjJYPCq0uEtXCdMCyvX36Mw/4mY+pEcbfhQhaP+cugqWJT2/eZ21TrPrx07rG/ig==
"@harnessio/react-har-service-client@^0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.12.0.tgz#9ad2dec1a4ba2d4150e9c4dea67e2f867d9141ed"
integrity sha512-bBDrVL/OkX14SdG3XS5/h7W5pQ0BH6tXhK1w2F9eWf5qupyZe2lwaDpVRzxi2fkzr0wojf2wd/e3p3IiB47X0g==
dependencies:
"@harnessio/oats-cli" "^3.0.0"