diff --git a/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts b/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts index 1fbb6a3d47..efd6a05fcf 100644 --- a/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts +++ b/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts @@ -27,7 +27,7 @@ import { LocationAnalyzer, } from './types'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { GitHubLocationAnalyzer } from './GitHubLocationAnalyzer'; +import { GitHubLocationAnalyzer } from './analyzers/GitHubLocationAnalyzer'; export class RepoLocationAnalyzer implements LocationAnalyzer { private readonly logger: Logger; @@ -85,8 +85,6 @@ export class RepoLocationAnalyzer implements LocationAnalyzer { if (analyzer) { const existingEntityFiles = await analyzer.analyze( - owner, - name, request.location.target, ); if (existingEntityFiles.length > 0) { diff --git a/plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.test.ts b/plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.test.ts new file mode 100644 index 0000000000..11bb3137c7 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +const octokit = { + search: { + code: jest.fn(), + }, + repos: { + get: jest.fn(), + }, +}; + +jest.mock('@octokit/rest', () => { + class Octokit { + constructor() { + return octokit; + } + } + return { Octokit }; +}); + +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { GitHubLocationAnalyzer } from './GitHubLocationAnalyzer'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; +import { GitHubIntegration } from '@backstage/integration'; + +const server = setupServer(); + +describe('GitHubLocationAnalyzer', () => { + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), + getExternalBaseUrl: jest.fn(), + }; + const integration = new GitHubIntegration({ + host: 'h.com', + apiBaseUrl: 'a', + rawBaseUrl: 'r', + token: 't', + }); + + setupRequestMockHandlers(server); + + beforeEach(() => { + server.use( + rest.post('http://localhost:7007/locations', async (req, res, ctx) => { + return res( + ctx.status(201), + ctx.json({ + location: 'test', + exists: false, + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'test-entity', + }, + spec: { + type: 'url', + target: 'whatever', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity-2', + description: 'The expected description 2', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, + ], + }), + ); + }), + ); + }); + + it('should analyze', async () => { + octokit.search.code.mockImplementation((opts: { q: string }) => { + if (opts.q === 'filename:catalog-info.yaml repo:foo/bar') { + return Promise.resolve({ + data: { items: [{ path: 'catalog-info.yaml' }], total_count: 1 }, + }); + } + return Promise.reject(); + }); + + octokit.repos.get.mockResolvedValue({ + data: { default_branch: 'my_default_branch' }, + }); + + const analyzer = new GitHubLocationAnalyzer({ + discovery: mockDiscoveryApi, + integration, + }); + const result = await analyzer.analyze('https://github.com/foo/bar'); + + expect(result[0].isRegistered).toBeFalsy(); + expect(result[0].location).toEqual({ + type: 'url', + target: + 'https://github.com/foo/bar/blob/my_default_branch/catalog-info.yaml', + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/GitHubLocationAnalyzer.ts b/plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.ts similarity index 67% rename from plugins/catalog-backend/src/ingestion/GitHubLocationAnalyzer.ts rename to plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.ts index 46fa87cd92..2e42f202f9 100644 --- a/plugins/catalog-backend/src/ingestion/GitHubLocationAnalyzer.ts +++ b/plugins/catalog-backend/src/ingestion/analyzers/GitHubLocationAnalyzer.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { GitHubIntegration } from '@backstage/integration'; import { DiscoveryApi } from '@backstage/plugin-permission-common'; import { Octokit } from '@octokit/rest'; import { trimEnd } from 'lodash'; -import { AnalyzeLocationExistingEntity, ScmLocationAnalyzer } from './types'; +import parseGitUrl from 'git-url-parse'; +import { AnalyzeLocationExistingEntity, ScmLocationAnalyzer } from '../types'; export type GitHubLocationAnalyzerOptions = { integration: GitHubIntegration; @@ -27,45 +28,45 @@ export type GitHubLocationAnalyzerOptions = { discovery: DiscoveryApi; }; export class GitHubLocationAnalyzer implements ScmLocationAnalyzer { - private readonly integration: GitHubIntegration; private readonly catalogFilename: string; private readonly discovery: DiscoveryApi; + private readonly octokitClient: Octokit; + private readonly catalogClient: CatalogApi; constructor(options: GitHubLocationAnalyzerOptions) { - this.integration = options.integration; this.catalogFilename = options.catalogFilename || 'catalog-info.yaml'; this.discovery = options.discovery; + this.octokitClient = new Octokit({ + auth: options.integration.config.token, + baseUrl: options.integration.config.apiBaseUrl, + }); + this.catalogClient = new CatalogClient({ discoveryApi: this.discovery }); } - async analyze( - owner: string, - repo: string, - url: string, - ): Promise { - const octo = new Octokit({ - auth: this.integration.config.token, - baseUrl: this.integration.config.apiBaseUrl, - }); - const query = `filename:${this.catalogFilename} repo:${owner}/${repo} `; + async analyze(url: string): Promise { + const { owner, name: repo } = parseGitUrl(url); + const query = `filename:${this.catalogFilename} repo:${owner}/${repo}`; - const catalogClient = new CatalogClient({ discoveryApi: this.discovery }); - - const searchResult = await octo.search.code({ q: query }).catch(e => { - throw new Error(`Couldn't search repository for metadata file, ${e}`); - }); + const searchResult = await this.octokitClient.search + .code({ q: query }) + .catch(e => { + throw new Error(`Couldn't search repository for metadata file, ${e}`); + }); const exists = searchResult.data.total_count > 0; if (exists) { - const repoInformation = await octo.repos.get({ owner, repo }).catch(e => { - throw new Error(`Couldn't fetch repo data, ${e}`); - }); + const repoInformation = await this.octokitClient.repos + .get({ owner, repo }) + .catch(e => { + throw new Error(`Couldn't fetch repo data, ${e}`); + }); const defaultBranch = repoInformation.data.default_branch; const result = await Promise.all( searchResult.data.items .map(i => `${trimEnd(url, '/')}/blob/${defaultBranch}/${i.path}`) .map(async target => { - const addLocationResult = await catalogClient.addLocation({ + const addLocationResult = await this.catalogClient.addLocation({ type: 'url', target, dryRun: true,