diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@gitbeaker/node/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@gitbeaker/node/index.ts deleted file mode 100644 index 7166dc0f63..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@gitbeaker/node/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export const mockGitlabClient = { - Namespaces: { - show: jest.fn(), - }, - Projects: { - create: jest.fn(), - }, - Users: { - current: jest.fn(), - }, -}; - -export class Gitlab { - constructor() { - return mockGitlabClient; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/azure-devops-node-api/GitApi/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/azure-devops-node-api/GitApi/index.ts deleted file mode 100644 index 6b6e313cae..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/azure-devops-node-api/GitApi/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export const mockGitApi = { - createRepository: jest.fn(), -}; - -export class GitApi { - constructor() { - return mockGitApi; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/nodegit/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/nodegit/index.ts deleted file mode 100644 index 79a32c3050..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/nodegit/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export const mockIndex = { - addAll: jest.fn(), - write: jest.fn(), - writeTree: jest.fn().mockResolvedValue('mockoid'), -}; - -export const mockRepo = { - refreshIndex: jest.fn().mockResolvedValue(mockIndex), - createCommit: jest.fn(), -}; - -export const mockRemote = { - push: jest.fn(), -}; - -const Repository = { init: jest.fn().mockResolvedValue(mockRepo) }; -const Remote = { create: jest.fn().mockResolvedValue(mockRemote) }; -const Signature = { now: jest.fn() }; -const Cred = { - userpassPlaintextNew: jest.fn(), -}; - -export { Repository, Remote, Signature, Cred }; diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts deleted file mode 100644 index de320ef498..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 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. - */ - -export const mockGithubClient = { - rest: { - actions: { - createWorkflowDispatch: jest.fn(), - }, - repos: { - createInOrg: jest.fn(), - createForAuthenticatedUser: jest.fn(), - createWebhook: jest.fn(), - addCollaborator: jest.fn(), - replaceAllTopics: jest.fn(), - }, - users: { - getByUsername: jest.fn(), - }, - teams: { - addOrUpdateRepoPermissionsInOrg: jest.fn(), - }, - }, -}; - -export class Octokit { - constructor() { - return mockGithubClient; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts index 7850163e9d..0782ea40cd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -jest.mock('octokit'); import { TemplateAction } from '../../types'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; @@ -27,6 +26,21 @@ import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; +const mockOctokit = { + rest: { + actions: { + createWorkflowDispatch: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + describe('github:actions:dispatch', () => { const config = new ConfigReader({ integrations: { @@ -54,8 +68,6 @@ describe('github:actions:dispatch', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('octokit'); - beforeEach(() => { jest.resetAllMocks(); githubCredentialsProvider = @@ -67,7 +79,7 @@ describe('github:actions:dispatch', () => { }); it('should call the githubApis for creating WorkflowDispatch without an input object', async () => { - mockGithubClient.rest.actions.createWorkflowDispatch.mockResolvedValue({ + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ data: { foo: 'bar', }, @@ -82,7 +94,7 @@ describe('github:actions:dispatch', () => { await action.handler(ctx); expect( - mockGithubClient.rest.actions.createWorkflowDispatch, + mockOctokit.rest.actions.createWorkflowDispatch, ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', @@ -92,7 +104,7 @@ describe('github:actions:dispatch', () => { }); it('should call the githubApis for creating WorkflowDispatch with an input object', async () => { - mockGithubClient.rest.actions.createWorkflowDispatch.mockResolvedValue({ + mockOctokit.rest.actions.createWorkflowDispatch.mockResolvedValue({ data: { foo: 'bar', }, @@ -108,7 +120,7 @@ describe('github:actions:dispatch', () => { await action.handler(ctx); expect( - mockGithubClient.rest.actions.createWorkflowDispatch, + mockOctokit.rest.actions.createWorkflowDispatch, ).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts index b80691fdcc..0798b04a93 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -jest.mock('octokit'); - import { createGithubWebhookAction } from './githubWebhook'; import { ScmIntegrations, @@ -27,6 +25,21 @@ import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import { TemplateAction } from '../..'; +const mockOctokit = { + rest: { + repos: { + createWebhook: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + describe('github:repository:webhook:create', () => { const config = new ConfigReader({ integrations: { @@ -65,8 +78,6 @@ describe('github:repository:webhook:create', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('octokit'); - it('should call the githubApi for creating repository Webhook', async () => { const repoUrl = 'github.com?repo=repo&owner=owner'; const webhookUrl = 'https://example.com/payload'; @@ -75,7 +86,7 @@ describe('github:repository:webhook:create', () => { }); await action.handler(ctx); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -97,7 +108,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -118,7 +129,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push', 'pull_request'], @@ -139,7 +150,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -160,7 +171,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -181,7 +192,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -202,7 +213,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index ec564e93dc..4e835657a3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -17,7 +17,6 @@ import { TemplateAction } from '../../types'; jest.mock('../helpers'); -jest.mock('octokit'); import { createPublishGithubAction } from './github'; import { @@ -34,6 +33,30 @@ import { } from '../helpers'; import { when } from 'jest-when'; +const mockOctokit = { + rest: { + users: { + getByUsername: jest.fn(), + }, + repos: { + addCollaborator: jest.fn(), + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + replaceAllTopics: jest.fn(), + }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + describe('publish:github', () => { const config = new ConfigReader({ integrations: { @@ -62,8 +85,6 @@ describe('publish:github', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('octokit'); - beforeEach(() => { jest.resetAllMocks(); githubCredentialsProvider = @@ -76,14 +97,14 @@ describe('publish:github', () => { }); it('should call the githubApis with the correct values for createInOrg', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, }); - mockGithubClient.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); - expect(mockGithubClient.rest.repos.createInOrg).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ description: 'description', name: 'repo', org: 'owner', @@ -102,7 +123,7 @@ describe('publish:github', () => { repoVisibility: 'public', }, }); - expect(mockGithubClient.rest.repos.createInOrg).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ description: 'description', name: 'repo', org: 'owner', @@ -116,17 +137,17 @@ describe('publish:github', () => { }); it('should call the githubApis with the correct values for createForAuthenticatedUser', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: {}, }); await action.handler(mockContext); expect( - mockGithubClient.rest.repos.createForAuthenticatedUser, + mockOctokit.rest.repos.createForAuthenticatedUser, ).toHaveBeenCalledWith({ description: 'description', name: 'repo', @@ -145,7 +166,7 @@ describe('publish:github', () => { }, }); expect( - mockGithubClient.rest.repos.createForAuthenticatedUser, + mockOctokit.rest.repos.createForAuthenticatedUser, ).toHaveBeenCalledWith({ description: 'description', name: 'repo', @@ -158,11 +179,11 @@ describe('publish:github', () => { }); it('should call initRepoAndPush with the correct values', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -182,11 +203,11 @@ describe('publish:github', () => { }); it('should call initRepoAndPush with the correct defaultBranch main', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -235,11 +256,11 @@ describe('publish:github', () => { githubCredentialsProvider, }); - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -279,11 +300,11 @@ describe('publish:github', () => { githubCredentialsProvider, }); - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -304,11 +325,11 @@ describe('publish:github', () => { }); it('should add access for the team when it starts with the owner', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -318,7 +339,7 @@ describe('publish:github', () => { await action.handler(mockContext); expect( - mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg, + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg, ).toHaveBeenCalledWith({ org: 'owner', team_slug: 'blam', @@ -329,11 +350,11 @@ describe('publish:github', () => { }); it('should add outside collaborators when provided', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -348,7 +369,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.rest.repos.addCollaborator).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.addCollaborator).toHaveBeenCalledWith({ username: 'outsidecollaborator', owner: 'owner', repo: 'repo', @@ -357,11 +378,11 @@ describe('publish:github', () => { }); it('should add multiple collaborators when provided', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -392,7 +413,7 @@ describe('publish:github', () => { }; expect( - mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1], + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1], ).toEqual([ { ...commonProperties, @@ -402,7 +423,7 @@ describe('publish:github', () => { ]); expect( - mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], ).toEqual([ { ...commonProperties, @@ -413,18 +434,18 @@ describe('publish:github', () => { }); it('should ignore failures when adding multiple collaborators', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - when(mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg) + when(mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg) .calledWith({ org: 'owner', owner: 'owner', @@ -452,7 +473,7 @@ describe('publish:github', () => { }); expect( - mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + mockOctokit.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], ).toEqual([ { org: 'owner', @@ -465,18 +486,18 @@ describe('publish:github', () => { }); it('should add topics when provided', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - mockGithubClient.rest.repos.replaceAllTopics.mockResolvedValue({ + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ data: { names: ['node.js'], }, @@ -490,7 +511,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', names: ['node.js'], @@ -498,18 +519,18 @@ describe('publish:github', () => { }); it('should lowercase topics when provided', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - mockGithubClient.rest.repos.replaceAllTopics.mockResolvedValue({ + mockOctokit.rest.repos.replaceAllTopics.mockResolvedValue({ data: { names: ['backstage'], }, @@ -523,7 +544,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ + expect(mockOctokit.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', names: ['backstage'], @@ -531,11 +552,11 @@ describe('publish:github', () => { }); it('should call output with the remoteUrl and the repoContentsUrl', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -555,11 +576,11 @@ describe('publish:github', () => { }); it('should use main as default branch', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -585,11 +606,11 @@ describe('publish:github', () => { }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { - mockGithubClient.rest.users.getByUsername.mockResolvedValue({ + mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { name: 'repository', }, @@ -599,7 +620,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', - client: mockGithubClient, + client: mockOctokit, repoName: 'repository', logger: mockContext.logger, defaultBranch: 'master', @@ -616,7 +637,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', - client: mockGithubClient, + client: mockOctokit, repoName: 'repository', logger: mockContext.logger, defaultBranch: 'master', @@ -633,7 +654,7 @@ describe('publish:github', () => { expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ owner: 'owner', - client: mockGithubClient, + client: mockOctokit, repoName: 'repository', logger: mockContext.logger, defaultBranch: 'master', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index db4ad5a019..ed8df4e5e8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ jest.mock('../helpers'); -jest.mock('@gitbeaker/node'); import { createPublishGitlabAction } from './gitlab'; import { ScmIntegrations } from '@backstage/integration'; @@ -23,6 +22,25 @@ import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import { initRepoAndPush } from '../helpers'; +const mockGitlabClient = { + Namespaces: { + show: jest.fn(), + }, + Projects: { + create: jest.fn(), + }, + Users: { + current: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + describe('publish:gitlab', () => { const config = new ConfigReader({ integrations: { @@ -54,8 +72,6 @@ describe('publish:gitlab', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGitlabClient } = require('@gitbeaker/node'); - beforeEach(() => { jest.resetAllMocks(); }); diff --git a/plugins/techdocs-node/__mocks__/@azure/identity.ts b/plugins/techdocs-node/__mocks__/@azure/identity.ts deleted file mode 100644 index 6aeb738963..0000000000 --- a/plugins/techdocs-node/__mocks__/@azure/identity.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 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. - */ -export class DefaultAzureCredential { - /** - * Creates an instance of the DefaultAzureCredential class. - */ -} diff --git a/plugins/techdocs-node/__mocks__/@azure/storage-blob.ts b/plugins/techdocs-node/__mocks__/@azure/storage-blob.ts deleted file mode 100644 index 1d63e7266a..0000000000 --- a/plugins/techdocs-node/__mocks__/@azure/storage-blob.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2020 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. - */ -import { - BlobUploadCommonResponse, - ContainerGetPropertiesResponse, -} from '@azure/storage-blob'; -import { EventEmitter } from 'events'; -import { IStorageFilesMock } from '../../src/testUtils/types'; - -const storage = global.storageFilesMock as IStorageFilesMock; - -export class BlockBlobClient { - private readonly blobName; - - constructor(blobName: string) { - this.blobName = blobName; - } - - uploadFile(source: string): Promise { - storage.writeFile(this.blobName, source); - return Promise.resolve({ - _response: { - request: { - url: `https://example.blob.core.windows.net`, - } as any, - status: 200, - headers: {} as any, - }, - }); - } - - exists() { - return storage.fileExists(this.blobName); - } - - download() { - const emitter = new EventEmitter(); - setTimeout(() => { - if (storage.fileExists(this.blobName)) { - emitter.emit('data', storage.readFile(this.blobName)); - emitter.emit('end'); - } else { - emitter.emit( - 'error', - new Error(`The file ${this.blobName} does not exist!`), - ); - } - }, 0); - return Promise.resolve({ - readableStreamBody: emitter, - }); - } -} - -class BlockBlobClientFailUpload extends BlockBlobClient { - uploadFile(): Promise { - return Promise.resolve({ - _response: { - request: { - url: `https://example.blob.core.windows.net`, - } as any, - status: 500, - headers: {} as any, - }, - }); - } -} - -class ContainerClientIterator { - private containerName: string; - - constructor(containerName) { - this.containerName = containerName; - } - - async next() { - if ( - this.containerName === 'delete_stale_files_success' || - this.containerName === 'delete_stale_files_error' - ) { - return { - value: { - segment: { - blobItems: [{ name: `stale_file.png` }], - }, - }, - }; - } - return { - value: { - segment: { - blobItems: [], - }, - }, - }; - } -} - -export class ContainerClient { - getProperties(): Promise { - return Promise.resolve({ - _response: { - request: { - url: `https://example.blob.core.windows.net`, - } as any, - status: 200, - headers: {} as any, - parsedHeaders: {}, - }, - }); - } - - getBlockBlobClient(blobName: string) { - return new BlockBlobClient(blobName); - } - - listBlobsFlat() { - return { - byPage: () => { - return new ContainerClientIterator(this.containerName); - }, - }; - } - - deleteBlob() { - if (this.containerName === 'delete_stale_files_error') { - throw new Error('Message'); - } - } -} - -class ContainerClientFailGetProperties extends ContainerClient { - getProperties(): Promise { - return Promise.resolve({ - _response: { - request: { - url: `https://example.blob.core.windows.net`, - } as any, - status: 404, - headers: {} as any, - parsedHeaders: {}, - }, - }); - } -} - -class ContainerClientFailUpload extends ContainerClient { - getBlockBlobClient(blobName: string) { - return new BlockBlobClientFailUpload(blobName); - } -} - -export class BlobServiceClient { - private readonly url; - private readonly credential; - - constructor(url: string, credential?: StorageSharedKeyCredential) { - storage.emptyFiles(); - this.url = url; - this.credential = credential; - } - - getContainerClient(containerName: string) { - if (containerName === 'bad_container') { - return new ContainerClientFailGetProperties(); - } - if (this.credential.accountName === 'bad_account_credentials') { - return new ContainerClientFailUpload(); - } - return new ContainerClient(); - } -} - -export class StorageSharedKeyCredential { - private readonly accountName; - private readonly accountKey; - - constructor(accountName: string, accountKey: string) { - this.accountName = accountName; - this.accountKey = accountKey; - } -} diff --git a/plugins/techdocs-node/__mocks__/@google-cloud/storage.ts b/plugins/techdocs-node/__mocks__/@google-cloud/storage.ts deleted file mode 100644 index e87ccb69c1..0000000000 --- a/plugins/techdocs-node/__mocks__/@google-cloud/storage.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 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. - */ -import { Readable } from 'stream'; -import { IStorageFilesMock } from '../../src/testUtils/types'; - -const storage = global.storageFilesMock as IStorageFilesMock; - -class GCSFile { - private readonly path: string; - - constructor(path: string) { - this.path = path; - } - - exists() { - return new Promise(async (resolve, reject) => { - if (storage.fileExists(this.path)) { - resolve([true]); - } else { - reject(); - } - }); - } - - createReadStream() { - const readable = new Readable(); - readable._read = () => {}; - - process.nextTick(() => { - if (storage.fileExists(this.path)) { - if (readable.eventNames().includes('pipe')) { - readable.emit('pipe'); - } - readable.emit('data', storage.readFile(this.path)); - readable.emit('end'); - } else { - readable.emit( - 'error', - new Error(`The file ${this.path} does not exist!`), - ); - } - }); - - return readable; - } - - delete() { - return Promise.resolve(); - } -} - -class Bucket { - private readonly bucketName; - - constructor(bucketName: string) { - this.bucketName = bucketName; - } - - async getMetadata() { - if (this.bucketName === 'bad_bucket_name') { - throw Error('Bucket does not exist'); - } - return ''; - } - - upload(source: string, { destination }) { - return new Promise(async resolve => { - storage.writeFile(destination, source); - resolve(null); - }); - } - - file(destinationFilePath: string) { - if (this.bucketName === 'delete_stale_files_error') { - throw Error('Message'); - } - return new GCSFile(destinationFilePath); - } - - getFilesStream() { - const readable = new Readable(); - readable._read = () => {}; - - process.nextTick(() => { - if ( - this.bucketName === 'delete_stale_files_success' || - this.bucketName === 'delete_stale_files_error' - ) { - readable.emit('data', { name: 'stale-file.png' }); - } - readable.emit('end'); - }); - - return readable; - } -} - -export class Storage { - constructor() { - storage.emptyFiles(); - } - - bucket(bucketName) { - return new Bucket(bucketName); - } -} diff --git a/plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts b/plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts deleted file mode 100644 index 0f11950a66..0000000000 --- a/plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2020 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. - */ -import fs from 'fs-extra'; -import os from 'os'; -import path from 'path'; -import { - ContainerMetaResponse, - DownloadResponse, - NotFound, - ObjectMetaResponse, - UploadResponse, -} from '@trendyol-js/openstack-swift-sdk'; -import { Stream, Readable } from 'stream'; - -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; - -const checkFileExists = async (Key: string): Promise => { - // Key will always have / as file separator irrespective of OS since cloud providers expects /. - // Normalize Key to OS specific path before checking if file exists. - const filePath = path.join(rootDir, Key); - - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - return false; - } -}; - -const streamToBuffer = (stream: Stream | Readable): Promise => { - return new Promise((resolve, reject) => { - try { - const chunks: any[] = []; - stream.on('data', chunk => chunks.push(chunk)); - stream.on('error', reject); - stream.on('end', () => resolve(Buffer.concat(chunks))); - } catch (e) { - throw new Error(`Unable to parse the response data ${e.message}`); - } - }); -}; - -export class SwiftClient { - async getMetadata(_containerName: string, file: string) { - const fileExists = await checkFileExists(file); - if (fileExists) { - return new ObjectMetaResponse({ - fullPath: file, - }); - } - return new NotFound(); - } - - async getContainerMetadata(containerName: string) { - if (containerName === 'mock') { - return new ContainerMetaResponse({ - size: 10, - }); - } - return new NotFound(); - } - - async upload(_containerName: string, destination: string, stream: Readable) { - try { - const filePath = path.join(rootDir, destination); - const fileBuffer = await streamToBuffer(stream); - - await fs.writeFile(filePath, fileBuffer); - const fileExists = await checkFileExists(destination); - - if (fileExists) { - return new UploadResponse(filePath); - } - const errorMessage = `Unable to upload file(s) to OpenStack Swift.`; - throw new Error(errorMessage); - } catch (error) { - const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${error}`; - throw new Error(errorMessage); - } - } - - async download(_containerName: string, file: string) { - const filePath = path.join(rootDir, file); - const fileExists = await checkFileExists(file); - if (!fileExists) { - return new NotFound(); - } - return new DownloadResponse([], fs.createReadStream(filePath)); - } -} diff --git a/plugins/techdocs-node/__mocks__/aws-sdk.ts b/plugins/techdocs-node/__mocks__/aws-sdk.ts deleted file mode 100644 index ebb3c6840b..0000000000 --- a/plugins/techdocs-node/__mocks__/aws-sdk.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020 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. - */ -import { EventEmitter } from 'events'; -import { ReadStream } from 'fs'; -import { IStorageFilesMock } from '../src/testUtils/types'; - -export { Credentials } from 'aws-sdk'; - -const storage = global.storageFilesMock as IStorageFilesMock; - -export class S3 { - constructor() { - storage.emptyFiles(); - } - - headObject({ Key }: { Key: string }) { - return { - promise: async () => { - if (!storage.fileExists(Key)) { - throw new Error('File does not exist'); - } - }, - }; - } - - getObject({ Key }: { Key: string }) { - return { - promise: async () => storage.fileExists(Key), - createReadStream: () => { - const emitter = new EventEmitter(); - process.nextTick(() => { - if (storage.fileExists(Key)) { - emitter.emit('data', Buffer.from(storage.readFile(Key))); - emitter.emit('end'); - } else { - emitter.emit('error', new Error(`The file ${Key} does not exist!`)); - } - }); - return emitter; - }, - }; - } - - headBucket({ Bucket }) { - return { - promise: async () => { - if (Bucket === 'errorBucket') { - throw new Error('Bucket does not exist'); - } - return {}; - }, - }; - } - - upload({ Key, Body }: { Key: string; Body: ReadStream }) { - return { - promise: () => - new Promise(async resolve => { - const chunks = []; - Body.on('data', chunk => { - chunks.push(chunk); - }); - Body.once('end', () => { - storage.writeFile(Key, Buffer.concat(chunks)); - resolve(null); - }); - }), - }; - } - - listObjectsV2({ Bucket }) { - return { - promise: () => { - if ( - Bucket === 'delete_stale_files_success' || - Bucket === 'delete_stale_files_error' - ) { - return Promise.resolve({ - Contents: [{ Key: 'stale_file.png' }], - }); - } - return Promise.resolve({}); - }, - }; - } - - deleteObject({ Bucket }) { - return { - promise: () => { - if (Bucket === 'delete_stale_files_error') { - throw new Error('Message'); - } - return Promise.resolve(); - }, - }; - } -} - -export default { - S3, -}; diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 5f711765ff..540061a4f6 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -72,10 +72,5 @@ "@types/recursive-readdir": "^2.2.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" - }, - "jest": { - "roots": [ - ".." - ] } } diff --git a/plugins/techdocs-node/src/setupTests.ts b/plugins/techdocs-node/src/setupTests.ts deleted file mode 100644 index d342cfb26a..0000000000 --- a/plugins/techdocs-node/src/setupTests.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 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. - */ - -import { StorageFilesMock } from './testUtils/StorageFilesMock'; - -(global as any).rootDir = StorageFilesMock.rootDir; -(global as any).storageFilesMock = new StorageFilesMock(); diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts index 537fc52b27..6372093c4b 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -21,12 +21,112 @@ import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; import path from 'path'; -import fs from 'fs-extra'; +import fs, { ReadStream } from 'fs-extra'; +import { EventEmitter } from 'events'; import { AwsS3Publish } from './awsS3'; +import { storageRootDir } from '../../testUtils/StorageFilesMock'; -// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock aws-sdk client library +jest.mock('aws-sdk', () => { + const { StorageFilesMock } = require('../../testUtils/StorageFilesMock'); + const storage = new StorageFilesMock(); -const rootDir = (global as any).rootDir; // Set by setupTests.ts + return { + __esModule: true, + Credentials: jest.requireActual('aws-sdk').Credentials, + default: { + S3: class { + constructor() { + storage.emptyFiles(); + } + + headObject({ Key }: { Key: string }) { + return { + promise: async () => { + if (!storage.fileExists(Key)) { + throw new Error('File does not exist'); + } + }, + }; + } + + getObject({ Key }: { Key: string }) { + return { + promise: async () => storage.fileExists(Key), + createReadStream: () => { + const emitter = new EventEmitter(); + process.nextTick(() => { + if (storage.fileExists(Key)) { + emitter.emit('data', Buffer.from(storage.readFile(Key))); + emitter.emit('end'); + } else { + emitter.emit( + 'error', + new Error(`The file ${Key} does not exist!`), + ); + } + }); + return emitter; + }, + }; + } + + headBucket({ Bucket }: { Bucket: string }) { + return { + promise: async () => { + if (Bucket === 'errorBucket') { + throw new Error('Bucket does not exist'); + } + return {}; + }, + }; + } + + upload({ Key, Body }: { Key: string; Body: ReadStream }) { + return { + promise: () => + new Promise(async resolve => { + const chunks = new Array(); + Body.on('data', chunk => { + chunks.push(chunk as Buffer); + }); + Body.once('end', () => { + storage.writeFile(Key, Buffer.concat(chunks)); + resolve(null); + }); + }), + }; + } + + listObjectsV2({ Bucket }: { Bucket: string }) { + return { + promise: () => { + if ( + Bucket === 'delete_stale_files_success' || + Bucket === 'delete_stale_files_error' + ) { + return Promise.resolve({ + Contents: [{ Key: 'stale_file.png' }], + }); + } + return Promise.resolve({}); + }, + }; + } + + deleteObject({ Bucket }: { Bucket: string }) { + return { + promise: () => { + if (Bucket === 'delete_stale_files_error') { + throw new Error('Message'); + } + return Promise.resolve(); + }, + }; + } + }, + }, + }; +}); const getEntityRootDir = (entity: Entity) => { const { @@ -34,7 +134,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); @@ -213,7 +313,7 @@ describe('AwsS3Publish', () => { it('should fail to publish a directory', async () => { const wrongPathToGeneratedDirectory = path.join( - rootDir, + storageRootDir, 'wrong', 'path', 'to', diff --git a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts index 7af5338d88..6d5848b0d8 100644 --- a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts @@ -23,10 +23,207 @@ import mockFs from 'mock-fs'; import path from 'path'; import fs from 'fs-extra'; import { AzureBlobStoragePublish } from './azureBlobStorage'; +import { EventEmitter } from 'events'; +import { + BlobUploadCommonResponse, + ContainerGetPropertiesResponse, +} from '@azure/storage-blob'; +import { + storageRootDir, + StorageFilesMock, +} from '../../testUtils/StorageFilesMock'; -// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Azure client library +jest.mock('@azure/identity', () => ({ + __esModule: true, + DefaultAzureCredential: class {}, +})); -const rootDir = (global as any).rootDir; // Set by setupTests.ts +jest.mock('@azure/storage-blob', () => { + class BlockBlobClient { + constructor( + private readonly blobName: string, + private readonly storage: StorageFilesMock, + ) {} + + uploadFile(source: string): Promise { + this.storage.writeFile(this.blobName, source); + return Promise.resolve({ + _response: { + request: { + url: `https://example.blob.core.windows.net`, + } as any, + status: 200, + headers: {} as any, + }, + }); + } + + exists() { + return this.storage.fileExists(this.blobName); + } + + download() { + const emitter = new EventEmitter(); + setTimeout(() => { + if (this.storage.fileExists(this.blobName)) { + emitter.emit('data', this.storage.readFile(this.blobName)); + emitter.emit('end'); + } else { + emitter.emit( + 'error', + new Error(`The file ${this.blobName} does not exist!`), + ); + } + }, 0); + return Promise.resolve({ + readableStreamBody: emitter, + }); + } + } + + class BlockBlobClientFailUpload extends BlockBlobClient { + uploadFile(): Promise { + return Promise.resolve({ + _response: { + request: { + url: `https://example.blob.core.windows.net`, + } as any, + status: 500, + headers: {} as any, + }, + }); + } + } + + class ContainerClientIterator { + private containerName: string; + + constructor(containerName: string) { + this.containerName = containerName; + } + + async next() { + if ( + this.containerName === 'delete_stale_files_success' || + this.containerName === 'delete_stale_files_error' + ) { + return { + value: { + segment: { + blobItems: [{ name: `stale_file.png` }], + }, + }, + }; + } + return { + value: { + segment: { + blobItems: [], + }, + }, + }; + } + } + + class ContainerClient { + constructor( + private readonly containerName: string, + protected readonly storage: StorageFilesMock, + ) {} + + getProperties(): Promise { + return Promise.resolve({ + _response: { + request: { + url: `https://example.blob.core.windows.net`, + } as any, + status: 200, + headers: {} as any, + parsedHeaders: {}, + }, + }); + } + + getBlockBlobClient(blobName: string) { + return new BlockBlobClient(blobName, this.storage); + } + + listBlobsFlat() { + return { + byPage: () => { + return new ContainerClientIterator(this.containerName); + }, + }; + } + + deleteBlob() { + if (this.containerName === 'delete_stale_files_error') { + throw new Error('Message'); + } + } + } + + class ContainerClientFailGetProperties extends ContainerClient { + getProperties(): Promise { + return Promise.resolve({ + _response: { + request: { + url: `https://example.blob.core.windows.net`, + } as any, + status: 404, + headers: {} as any, + parsedHeaders: {}, + }, + }); + } + } + + class ContainerClientFailUpload extends ContainerClient { + getBlockBlobClient(blobName: string) { + return new BlockBlobClientFailUpload(blobName, this.storage); + } + } + + class BlobServiceClient { + storage = new StorageFilesMock(); + + constructor( + public readonly url: string, + private readonly credential?: StorageSharedKeyCredential, + ) { + this.storage.emptyFiles(); + } + + getContainerClient(containerName: string) { + if (containerName === 'bad_container') { + return new ContainerClientFailGetProperties( + containerName, + this.storage, + ); + } + if (this.credential?.accountName === 'bad_account_credentials') { + return new ContainerClientFailUpload(containerName, this.storage); + } + return new ContainerClient(containerName, this.storage); + } + } + + class StorageSharedKeyCredential { + readonly accountName; + readonly accountKey; + + constructor(accountName: string, accountKey: string) { + this.accountName = accountName; + this.accountKey = accountKey; + } + } + + return { + __esModule: true, + BlobServiceClient, + StorageSharedKeyCredential, + }; +}); const getEntityRootDir = (entity: Entity) => { const { @@ -34,7 +231,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); @@ -178,7 +375,7 @@ describe('AzureBlobStoragePublish', () => { it('should fail to publish a directory', async () => { const wrongPathToGeneratedDirectory = path.join( - rootDir, + storageRootDir, 'wrong', 'path', 'to', diff --git a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts index 6471d89a06..b347f185c6 100644 --- a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts @@ -22,11 +22,119 @@ import request from 'supertest'; import mockFs from 'mock-fs'; import path from 'path'; import fs from 'fs-extra'; +import { Readable } from 'stream'; import { GoogleGCSPublish } from './googleStorage'; +import { + storageRootDir, + StorageFilesMock, +} from '../../testUtils/StorageFilesMock'; -// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Google Cloud Storage client library +jest.mock('@google-cloud/storage', () => { + class GCSFile { + constructor( + private readonly filePath: string, + private readonly storage: StorageFilesMock, + ) {} -const rootDir = (global as any).rootDir; // Set by setupTests.ts + exists() { + return new Promise(async (resolve, reject) => { + if (this.storage.fileExists(this.filePath)) { + resolve([true]); + } else { + reject(); + } + }); + } + + createReadStream() { + const readable = new Readable(); + readable._read = () => {}; + + process.nextTick(() => { + if (this.storage.fileExists(this.filePath)) { + if (readable.eventNames().includes('pipe')) { + readable.emit('pipe'); + } + readable.emit('data', this.storage.readFile(this.filePath)); + readable.emit('end'); + } else { + readable.emit( + 'error', + new Error(`The file ${this.filePath} does not exist!`), + ); + } + }); + + return readable; + } + + delete() { + return Promise.resolve(); + } + } + + class Bucket { + constructor( + private readonly bucketName: string, + private readonly storage: StorageFilesMock, + ) {} + + async getMetadata() { + if (this.bucketName === 'bad_bucket_name') { + throw Error('Bucket does not exist'); + } + return ''; + } + + upload(source: string, { destination }: { destination: string }) { + return new Promise(async resolve => { + this.storage.writeFile(destination, source); + resolve(null); + }); + } + + file(destinationFilePath: string) { + if (this.bucketName === 'delete_stale_files_error') { + throw Error('Message'); + } + return new GCSFile(destinationFilePath, this.storage); + } + + getFilesStream() { + const readable = new Readable(); + readable._read = () => {}; + + process.nextTick(() => { + if ( + this.bucketName === 'delete_stale_files_success' || + this.bucketName === 'delete_stale_files_error' + ) { + readable.emit('data', { name: 'stale-file.png' }); + } + readable.emit('end'); + }); + + return readable; + } + } + + class Storage { + storage = new StorageFilesMock(); + + constructor() { + this.storage.emptyFiles(); + } + + bucket(bucketName: string) { + return new Bucket(bucketName, this.storage); + } + } + + return { + __esModule: true, + Storage, + }; +}); const getEntityRootDir = (entity: Entity) => { const { @@ -34,7 +142,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); @@ -193,7 +301,7 @@ describe('GoogleGCSPublish', () => { it('should fail to publish a directory', async () => { const wrongPathToGeneratedDirectory = path.join( - rootDir, + storageRootDir, 'wrong', 'path', 'to', diff --git a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts index 619431fdd7..51b67d6167 100644 --- a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts @@ -24,12 +24,106 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; -import os from 'os'; +import fs from 'fs-extra'; import path from 'path'; import { OpenStackSwiftPublish } from './openStackSwift'; import { PublisherBase, TechDocsMetadata } from './types'; +import { storageRootDir } from '../../testUtils/StorageFilesMock'; +import { Stream, Readable } from 'stream'; -// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock @trendyol-js/openstack-swift-sdk client library +jest.mock('@trendyol-js/openstack-swift-sdk', () => { + const { + ContainerMetaResponse, + DownloadResponse, + NotFound, + ObjectMetaResponse, + UploadResponse, + }: typeof import('@trendyol-js/openstack-swift-sdk') = jest.requireActual( + '@trendyol-js/openstack-swift-sdk', + ); + + const checkFileExists = async (Key: string): Promise => { + // Key will always have / as file separator irrespective of OS since cloud providers expects /. + // Normalize Key to OS specific path before checking if file exists. + const filePath = path.join(storageRootDir, Key); + + try { + await fs.access(filePath, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } + }; + + const streamToBuffer = (stream: Stream | Readable): Promise => { + return new Promise((resolve, reject) => { + try { + const chunks: any[] = []; + stream.on('data', chunk => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks))); + } catch (e) { + throw new Error(`Unable to parse the response data ${e.message}`); + } + }); + }; + + return { + __esModule: true, + SwiftClient: class { + async getMetadata(_containerName: string, file: string) { + const fileExists = await checkFileExists(file); + if (fileExists) { + return new ObjectMetaResponse({ + fullPath: file, + }); + } + return new NotFound(); + } + + async getContainerMetadata(containerName: string) { + if (containerName === 'mock') { + return new ContainerMetaResponse({ + size: 10, + }); + } + return new NotFound(); + } + + async upload( + _containerName: string, + destination: string, + stream: Readable, + ) { + try { + const filePath = path.join(storageRootDir, destination); + const fileBuffer = await streamToBuffer(stream); + + await fs.writeFile(filePath, fileBuffer); + const fileExists = await checkFileExists(destination); + + if (fileExists) { + return new UploadResponse(filePath); + } + const errorMessage = `Unable to upload file(s) to OpenStack Swift.`; + throw new Error(errorMessage); + } catch (error) { + const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${error}`; + throw new Error(errorMessage); + } + } + + async download(_containerName: string, file: string) { + const filePath = path.join(storageRootDir, file); + const fileExists = await checkFileExists(file); + if (!fileExists) { + return new NotFound(); + } + return new DownloadResponse([], fs.createReadStream(filePath)); + } + }, + }; +}); const createMockEntity = (annotations = {}): Entity => { return { @@ -51,15 +145,13 @@ const createMockEntityName = (): CompoundEntityRef => ({ namespace: 'test-namespace', }); -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; - const getEntityRootDir = (entity: Entity) => { const { kind, metadata: { namespace, name }, } = entity; - return path.join(rootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; const getPosixEntityRootDir = (entity: Entity) => { @@ -179,7 +271,7 @@ describe('OpenStackSwiftPublish', () => { it('should fail to publish a directory', async () => { const wrongPathToGeneratedDirectory = path.join( - rootDir, + storageRootDir, 'wrong', 'path', 'to', diff --git a/plugins/techdocs-node/src/stages/publish/publish.test.ts b/plugins/techdocs-node/src/stages/publish/publish.test.ts index 4537313edf..ab300412a4 100644 --- a/plugins/techdocs-node/src/stages/publish/publish.test.ts +++ b/plugins/techdocs-node/src/stages/publish/publish.test.ts @@ -31,6 +31,11 @@ const discovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; +jest.mock('@azure/identity', () => ({ + __esModule: true, + DefaultAzureCredential: class {}, +})); + describe('Publisher', () => { beforeEach(() => { jest.resetModules(); // clear the cache diff --git a/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts b/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts index 4a0e183bdb..dd28110d00 100644 --- a/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts +++ b/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts @@ -19,12 +19,13 @@ import path from 'path'; import fs from 'fs-extra'; import { IStorageFilesMock } from './types'; -const rootDir: string = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +export const storageRootDir: string = + os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const encoding = 'utf8'; export class StorageFilesMock implements IStorageFilesMock { - static rootDir = rootDir; + static rootDir = storageRootDir; private files: Record; @@ -37,20 +38,20 @@ export class StorageFilesMock implements IStorageFilesMock { } public fileExists(targetPath: string): boolean { - const filePath = path.join(rootDir, targetPath); + const filePath = path.join(storageRootDir, targetPath); const posixPath = filePath.split(path.posix.sep).join(path.sep); return this.files[posixPath] !== undefined; } public readFile(targetPath: string): Buffer { - const filePath = path.join(rootDir, targetPath); + const filePath = path.join(storageRootDir, targetPath); return Buffer.from(this.files[filePath] ?? '', encoding); } public writeFile(targetPath: string, sourcePath: string): void; public writeFile(targetPath: string, sourceBuffer: Buffer): void; public writeFile(targetPath: string, source: string | Buffer): void { - const filePath = path.join(rootDir, targetPath); + const filePath = path.join(storageRootDir, targetPath); if (typeof source === 'string') { this.files[filePath] = fs.readFileSync(source).toString(encoding); } else {