diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts new file mode 100644 index 0000000000..e95cee11d0 --- /dev/null +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 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. + */ +type storageOptions = { + projectId?: string; + keyFilename?: string; +}; + +class Bucket { + private readonly bucketName; + + constructor(bucketName: string) { + this.bucketName = bucketName; + } + + getMetadata() { + return new Promise(resolve => { + resolve(''); + }); + } + + upload(source: string, { destination }) { + return new Promise(resolve => { + resolve({ source, destination }); + }); + } +} + +export class Storage { + private readonly projectId; + private readonly keyFilename; + + constructor(options: storageOptions) { + this.projectId = options.projectId; + this.keyFilename = options.keyFilename; + } + + bucket(bucketName) { + return new Bucket(bucketName); + } +} diff --git a/packages/techdocs-common/__mocks__/klaw.ts b/packages/techdocs-common/__mocks__/klaw.ts new file mode 100644 index 0000000000..ce2ce90a5f --- /dev/null +++ b/packages/techdocs-common/__mocks__/klaw.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 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 { EventEmitter } from 'events'; + +const walk = () => { + const emitter = new EventEmitter(); + setTimeout(() => { + emitter.emit('end'); + }, 10); + return emitter; +}; + +export default walk; diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 7cd53c69c1..eb917edc2b 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -54,7 +54,12 @@ "winston": "^3.2.1" }, "devDependencies": { - "@types/klaw": "^3.0.1", - "@backstage/cli": "^0.4.0" + "@backstage/cli": "^0.4.0", + "@types/klaw": "^3.0.1" + }, + "jest": { + "roots": [ + ".." + ] } } diff --git a/packages/techdocs-common/src/helpers.test.ts b/packages/techdocs-common/src/helpers.test.ts index 10518df74f..88c4d8829e 100644 --- a/packages/techdocs-common/src/helpers.test.ts +++ b/packages/techdocs-common/src/helpers.test.ts @@ -15,10 +15,108 @@ */ import { Readable } from 'stream'; -import { getDocFilesFromRepository } from './helpers'; +import { + getDocFilesFromRepository, + getLocationForEntity, + parseReferenceAnnotation, +} from './helpers'; import { UrlReader, ReadTreeResponse } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; +const entityBase: Entity = { + metadata: { + namespace: 'default', + name: 'mytestcomponent', + description: 'A component for testing', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'documentation', + lifecycle: 'experimental', + owner: 'testuser', + }, +}; + +const metadataBase = { + namespace: 'default', + name: 'mytestcomponent', + description: 'A component for testing', +}; + +const goodAnnotation = { + annotations: { + 'backstage.io/techdocs-ref': + 'url:https://github.com/backstage/backstage/blob/master/subfolder/', + }, +}; + +const mockEntityWithAnnotation: Entity = { + ...entityBase, + ...{ + metadata: { + ...metadataBase, + ...goodAnnotation, + }, + }, +}; + +const badAnnotation = { + annotations: { + 'backstage.io/techdocs-ref': 'bad-annotation', + }, +}; + +const mockEntityWithBadAnnotation: Entity = { + ...entityBase, + ...{ + metadata: { + ...metadataBase, + ...badAnnotation, + }, + }, +}; + +describe('parseReferenceAnnotation', () => { + it('should parse annotation', () => { + const parsedLocationAnnotation = parseReferenceAnnotation( + 'backstage.io/techdocs-ref', + mockEntityWithAnnotation, + ); + expect(parsedLocationAnnotation.type).toBe('url'); + expect(parsedLocationAnnotation.target).toBe( + 'https://github.com/backstage/backstage/blob/master/subfolder/', + ); + }); + + it('should throw error without annotation', () => { + expect(() => { + parseReferenceAnnotation('backstage.io/techdocs-ref', entityBase); + }).toThrow(/No location annotation/); + }); + + it('should throw error with bad annotation', () => { + expect(() => { + parseReferenceAnnotation( + 'backstage.io/techdocs-ref', + mockEntityWithBadAnnotation, + ); + }).toThrow(/Failure to parse/); + }); +}); + +describe('getLocationForEntity', () => { + it('should get location for entity', () => { + const parsedLocationAnnotation = getLocationForEntity( + mockEntityWithAnnotation, + ); + expect(parsedLocationAnnotation.type).toBe('url'); + expect(parsedLocationAnnotation.target).toBe( + 'https://github.com/backstage/backstage/blob/master/subfolder/', + ); + }); +}); + describe('getDocFilesFromRepository', () => { it('should read a remote directory using UrlReader.readTree', async () => { class MockUrlReader implements UrlReader { @@ -41,28 +139,9 @@ describe('getDocFilesFromRepository', () => { } } - const mockEntity: Entity = { - metadata: { - namespace: 'default', - annotations: { - 'backstage.io/techdocs-ref': - 'url:https://github.com/backstage/backstage/blob/master/subfolder/', - }, - name: 'mytestcomponent', - description: 'A component for testing', - }, - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - spec: { - type: 'documentation', - lifecycle: 'experimental', - owner: 'testuser', - }, - }; - const output = await getDocFilesFromRepository( new MockUrlReader(), - mockEntity, + mockEntityWithAnnotation, ); expect(output).toBe('/tmp/testfolder'); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts new file mode 100644 index 0000000000..e618f7edc7 --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 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 mockFs from 'mock-fs'; +import * as winston from 'winston'; +import { ConfigReader } from '@backstage/config'; +import { GoogleGCSPublish } from './googleStorage'; +import { PublisherBase } from './types'; + +const createMockEntity = (annotations = {}) => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + annotations: { + ...annotations, + }, + }, + }; +}; + +const logger = winston.createLogger(); +jest.spyOn(logger, 'info').mockReturnValue(logger); + +let publisher: PublisherBase; + +beforeEach(() => { + mockFs({ + '/path/to/google-application-credentials.json': '{}', + }); + + const mockConfig = ConfigReader.fromConfigs([ + { + context: '', + data: { + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'google_gcs', + google: { + pathToKey: '/path/to/google-application-credentials.json', + projectId: 'gcp-project-id', + bucketName: 'bucketName', + }, + }, + }, + }, + }, + ]); + + publisher = GoogleGCSPublish.fromConfig(mockConfig, logger); +}); + +afterEach(() => { + mockFs.restore(); +}); + +describe('GoogleGCSPublish', () => { + it('should publish a directory', () => { + mockFs({ + '/path/to/generatedDirectory': { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + + const entity = createMockEntity(); + return expect( + publisher.publish({ entity, directory: '/path/to/generatedDirectory' }), + ).resolves.toStrictEqual({}); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/packages/techdocs-common/src/stages/publish/publish.test.ts new file mode 100644 index 0000000000..002719997b --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/publish.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 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 mockFs from 'mock-fs'; +import { + getVoidLogger, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { Publisher } from './publish'; +import { LocalPublish } from './local'; +import { GoogleGCSPublish } from './googleStorage'; + +const logger = getVoidLogger(); +const testDiscovery: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValueOnce('http://localhost:7000'), + getExternalBaseUrl: jest.fn(), +}; + +describe('Publisher', () => { + it('should create local publisher by default', () => { + const mockConfig = ConfigReader.fromConfigs([ + { + context: '', + data: { + techdocs: { + requestUrl: 'http://localhost:7000', + }, + }, + }, + ]); + + const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + expect(publisher).toBeInstanceOf(LocalPublish); + }); + + it('should create local publisher from config', () => { + const mockConfig = ConfigReader.fromConfigs([ + { + context: '', + data: { + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'local', + }, + }, + }, + }, + ]); + + const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + expect(publisher).toBeInstanceOf(LocalPublish); + }); + + it('should create google gcs publisher from config', () => { + mockFs({ + '/path/to/google-application-credentials.json': '{}', + }); + + const mockConfig = ConfigReader.fromConfigs([ + { + context: '', + data: { + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'google_gcs', + google: { + pathToKey: '/path/to/google-application-credentials.json', + projectId: 'gcp-project-id', + bucketName: 'bucketName', + }, + }, + }, + }, + }, + ]); + + const publisher = Publisher.fromConfig(mockConfig, logger, testDiscovery); + expect(publisher).toBeInstanceOf(GoogleGCSPublish); + + mockFs.restore(); + }); +});