diff --git a/.changeset/eleven-tables-tease.md b/.changeset/eleven-tables-tease.md
new file mode 100644
index 0000000000..177211b4db
--- /dev/null
+++ b/.changeset/eleven-tables-tease.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-import': patch
+---
+
+Modifying import functionality to register existing catalog-info.yaml if one exists in given Github repository
diff --git a/plugins/catalog-import/src/components/ImportComponentForm.test.tsx b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx
new file mode 100644
index 0000000000..dcacfcc76f
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportComponentForm.test.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * 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 { renderInTestApp } from '@backstage/test-utils';
+import { RegisterComponentForm } from './ImportComponentForm';
+import {
+ ApiProvider,
+ ApiRegistry,
+ DiscoveryApi,
+ errorApiRef,
+} from '@backstage/core';
+import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
+import { catalogImportApiRef, CatalogImportClient } from '../api';
+import { fireEvent, waitFor, screen } from '@testing-library/react';
+
+describe('', () => {
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ apis = ApiRegistry.from([
+ [catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })],
+ [
+ catalogImportApiRef,
+ new CatalogImportClient({
+ discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
+ githubAuthApi: {
+ getAccessToken: (_, __) => Promise.resolve('token'),
+ },
+ configApi: {} as any,
+ }),
+ ],
+ [errorApiRef, mockErrorApi],
+ ]);
+ });
+
+ async function renderSUT(
+ nextStep: () => void = () => {},
+ saveConfig: () => void = () => {},
+ ) {
+ return await renderInTestApp(
+
+
+ ,
+ );
+ }
+
+ it('Renders without exploding', async () => {
+ await renderSUT();
+ expect(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ ).toBeInTheDocument();
+ });
+
+ it('Should have basic URL validation for input', async () => {
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: 'not a url' } },
+ );
+ });
+ await waitFor(() => {
+ fireEvent.click(screen.getByText('Next'));
+ });
+ expect(screen.getByText('Must start with https://.')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/components/ImportComponentPage.test.tsx b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx
new file mode 100644
index 0000000000..0e03a467e5
--- /dev/null
+++ b/plugins/catalog-import/src/components/ImportComponentPage.test.tsx
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * 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 { msw, renderInTestApp } from '@backstage/test-utils';
+import { ImportComponentPage } from './ImportComponentPage';
+import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
+import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
+import { catalogImportApiRef, CatalogImportClient } from '../api';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
+
+import { rest } from 'msw';
+import { setupServer } from 'msw/node';
+
+let codeSearchMockResponse: () => Promise<{
+ data: {
+ total_count: number;
+ items: Array<{ path: string }>;
+ };
+}>;
+
+jest.mock('@backstage/integration', () => ({
+ readGitHubIntegrationConfigs: () => ({
+ find: () => ({
+ host: 'test.localhost',
+ owner: 'someuser',
+ }),
+ }),
+}));
+
+jest.mock('@octokit/rest', () => ({
+ Octokit: jest.fn().mockImplementation(() => {
+ return {
+ repos: {
+ get: () =>
+ Promise.resolve({
+ data: {
+ default_branch: 'main',
+ },
+ }),
+ },
+ search: {
+ code: codeSearchMockResponse,
+ },
+ };
+ }),
+}));
+
+describe('', () => {
+ const server = setupServer();
+ msw.setupDefaultHandlers(server);
+
+ beforeEach(() => {
+ server.use(
+ rest.post('https://backend.localhost/locations', (_, res, ctx) => {
+ return res(
+ ctx.status(201),
+ ctx.json(require('../mocks/locations-POST-response.json')),
+ );
+ }),
+ rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => {
+ return res(
+ ctx.json(require('../mocks/analyze-location-POST-response.json')),
+ );
+ }),
+ );
+ });
+ beforeAll(() => server.listen());
+ afterEach(() => server.resetHandlers());
+ afterAll(() => server.close());
+
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ const getBaseUrl = () => Promise.resolve('https://backend.localhost');
+ apis = ApiRegistry.from([
+ [
+ catalogApiRef,
+ new CatalogClient({
+ discoveryApi: { getBaseUrl },
+ }),
+ ],
+ [
+ catalogImportApiRef,
+ new CatalogImportClient({
+ discoveryApi: { getBaseUrl },
+ githubAuthApi: {
+ getAccessToken: (_, __) => Promise.resolve('token'),
+ },
+ configApi: {} as any,
+ }),
+ ],
+ [errorApiRef, mockErrorApi],
+ ]);
+ });
+
+ async function renderSUT() {
+ return await renderInTestApp(
+
+
+ ,
+ );
+ }
+
+ it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => {
+ codeSearchMockResponse = () =>
+ Promise.resolve({
+ data: {
+ total_count: 3,
+ items: [
+ { path: 'simple/path/catalog-info.yaml' },
+ { path: 'co/mple/x/path/catalog-info.yaml' },
+ { path: 'catalog-info.yaml' },
+ ],
+ },
+ });
+ await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: 'https://test.localhost/someuser/somerepo' } },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml',
+ ),
+ ).toBeInTheDocument();
+
+ const pullReqText = screen.queryByText('pull request');
+ expect(pullReqText).not.toBeInTheDocument();
+ });
+ });
+
+ it('Should indicate a pull request creation when no yaml file found in the repo', async () => {
+ codeSearchMockResponse = () =>
+ Promise.resolve({
+ data: {
+ total_count: 0,
+ items: [],
+ },
+ });
+ const { container } = await renderSUT();
+ await waitFor(() => {
+ fireEvent.input(
+ screen.getByPlaceholderText('https://github.com/backstage/backstage'),
+ { target: { value: 'https://test.localhost/someuser/somerepo' } },
+ );
+ });
+
+ fireEvent.click(screen.getByText('Next'));
+ await waitFor(() => {
+ expect(
+ screen.getByText('https://test.localhost/someuser/somerepo'),
+ ).toBeInTheDocument();
+ });
+ const textNode = container
+ .querySelector('a[href="https://test.localhost/someuser/somerepo"]')
+ ?.closest('p');
+ expect(textNode?.innerHTML).toContain(
+ 'Following config object will be submitted in a pull request to the repository',
+ );
+ expect(
+ screen.queryByText(
+ 'https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml',
+ ),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-import/src/mocks/analyze-location-POST-response.json b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json
new file mode 100644
index 0000000000..eaadf1054a
--- /dev/null
+++ b/plugins/catalog-import/src/mocks/analyze-location-POST-response.json
@@ -0,0 +1,22 @@
+{
+ "existingEntityFiles": [],
+ "generateEntities": [
+ {
+ "entity": {
+ "apiVersion": "backstage.io/v1alpha1",
+ "kind": "Component",
+ "metadata": {
+ "name": "somerepo",
+ "annotations": {
+ "github.com/project-slug": "someuser/somerepo"
+ }
+ },
+ "spec": {
+ "type": "other",
+ "lifecycle": "unknown"
+ }
+ },
+ "fields": []
+ }
+ ]
+}
diff --git a/plugins/catalog-import/src/mocks/locations-POST-response.json b/plugins/catalog-import/src/mocks/locations-POST-response.json
new file mode 100644
index 0000000000..20e4e1582c
--- /dev/null
+++ b/plugins/catalog-import/src/mocks/locations-POST-response.json
@@ -0,0 +1,39 @@
+{
+ "location": {
+ "id": "d4a64359-a709-4c91-a9de-0905a033bf22",
+ "type": "url",
+ "target": "https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml"
+ },
+ "entities": [
+ {
+ "metadata": {
+ "namespace": "default",
+ "annotations": {
+ "backstage.io/managed-by-location": "url:https://test.localhost/someusername/somerepo/blob/master/src/catalog-info.yaml",
+ "github.com/project-slug": "someusername/somerepo"
+ },
+ "name": "somerepo",
+ "uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146",
+ "etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0",
+ "generation": 1
+ },
+ "apiVersion": "backstage.io/v1alpha1",
+ "kind": "Component",
+ "spec": {
+ "type": "other",
+ "lifecycle": "unknown",
+ "owner": "unknown"
+ },
+ "relations": [
+ {
+ "target": {
+ "kind": "group",
+ "namespace": "default",
+ "name": "unknown"
+ },
+ "type": "ownedBy"
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts
index 825bcd4115..fba7d7a957 100644
--- a/plugins/catalog-import/src/setupTests.ts
+++ b/plugins/catalog-import/src/setupTests.ts
@@ -15,3 +15,6 @@
*/
import '@testing-library/jest-dom';
+import fetch from 'cross-fetch';
+
+global.fetch = fetch;