diff --git a/packages/synchronizer/src/__tests__/fixtures/github-kubeshop-monokle-core.policy.yaml b/packages/synchronizer/src/__tests__/fixtures/github-kubeshop-monokle-core.policy.yaml new file mode 100644 index 000000000..31e0f9afa --- /dev/null +++ b/packages/synchronizer/src/__tests__/fixtures/github-kubeshop-monokle-core.policy.yaml @@ -0,0 +1,8 @@ +plugins: + open-policy-agent: false + resource-links: true + yaml-syntax: true + kubernetes-schema: true + pod-security-standards: true + practices: true + metadata: false diff --git a/packages/synchronizer/src/__tests__/synchronizer.spec.ts b/packages/synchronizer/src/__tests__/synchronizer.spec.ts new file mode 100644 index 000000000..92c314727 --- /dev/null +++ b/packages/synchronizer/src/__tests__/synchronizer.spec.ts @@ -0,0 +1,329 @@ +import {dirname, resolve} from 'path'; +import {fileURLToPath} from 'url'; +import {rm, mkdir, cp} from 'fs/promises'; +import sinon from 'sinon'; +import {assert} from 'chai'; +import {createDefaultMonokleSynchronizer} from '../createDefaultMonokleSynchronizer.js'; +import {StorageHandlerPolicy} from '../handlers/storageHandlerPolicy.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('Synchronizer Tests', () => { + const stubs: sinon.SinonStub[] = []; + + before(async () => { + await cleanupTmpConfigDir(); + }); + + afterEach(async () => { + if (stubs.length) { + stubs.forEach(stub => stub.restore()); + } + + await cleanupTmpConfigDir(); + }); + + describe('getPolicy', () => { + it('returns no policy if there is no policy file (from path)', async () => { + const storagePath = await createTmpConfigDir(); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const policy = await authenticator.getPolicy(storagePath); + + assert.isObject(policy); + assert.isFalse(policy.valid); + assert.isEmpty(policy.path); + assert.isEmpty(policy.policy); + }); + + it('returns no policy if there is no policy file (from git data)', async () => { + const storagePath = await createTmpConfigDir(); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const policy = await authenticator.getPolicy({ + provider: 'github', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }); + + assert.isObject(policy); + assert.isFalse(policy.valid); + assert.isEmpty(policy.path); + assert.isEmpty(policy.policy); + }); + + it('returns policy if there is policy file (from path)', async () => { + const storagePath = await createTmpConfigDir('github-kubeshop-monokle-core.policy.yaml'); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const getRepoRemoteDataStub = sinon.stub((authenticator as any).gitHandler, 'getRepoRemoteData').resolves({ + provider: 'github', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }); + stubs.push(getRepoRemoteDataStub); + + const policy = await authenticator.getPolicy(storagePath); + + assert.isObject(policy); + assert.isTrue(policy.valid); + assert.isNotEmpty(policy.path); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); + assert.isNotEmpty(policy.policy); + }); + + it('returns policy if there is policy file (from git data)', async () => { + const storagePath = await createTmpConfigDir('github-kubeshop-monokle-core.policy.yaml'); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const policy = await authenticator.getPolicy({ + provider: 'github', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }); + + assert.isObject(policy); + assert.isTrue(policy.valid); + assert.isNotEmpty(policy.path); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); + assert.isNotEmpty(policy.policy); + assert.isNotEmpty(policy.policy.plugins); + }); + + it('throws error when no access token provided with forceRefetch set', async () => { + try { + const storagePath = await createTmpConfigDir(); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + await authenticator.getPolicy(storagePath, true); + + assert.fail('Should have thrown error.'); + } catch (err: any) { + assert.match(err.message, /Cannot force refetch without access token/); + } + }); + + it('refetches policy when forceRefetch set', async() => { + const storagePath = await createTmpConfigDir(); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((authenticator as any).apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 5, + email: 'user5@kubeshop.io', + projects: [ + { + project: { + id: 5000, + slug: 'user5-proj', + name: 'User5 Project', + repositories: [ + { + id: "user5-proj-policy-id", + projectId: 5000, + provider: "GITHUB", + owner: "kubeshop", + name: "monokle-core", + prChecks: false, + canEnablePrChecks: true + } + ], + }, + }, + ], + }, + }, + }; + } + + if (query.includes('query getPolicy')) { + return { + data: { + getProject: { + id: 5000, + policy: { + id: "user5-proj-policy-id", + json: { + plugins: { + "pod-security-standards": true, + "yaml-syntax": false, + "resource-links": false, + "kubernetes-schema": false, + practices: true + }, + rules: { + "pod-security-standards/host-process": "err", + }, + settings: { + "kubernetes-schema": { + schemaVersion: "v1.27.1" + } + } + } + } + } + } + } + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'github', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }; + + const policy = await authenticator.getPolicy(repoData); + + assert.isFalse(policy.valid); + + const newPolicy = await authenticator.getPolicy(repoData, true, 'SAMPLE_ACCESS_TOKEN'); + + assert.isObject(newPolicy); + assert.isTrue(newPolicy.valid); + assert.isNotEmpty(newPolicy.path); + assert.match(newPolicy.path, /github-kubeshop-monokle-core.policy.yaml$/); + assert.isNotEmpty(newPolicy.policy); + assert.isNotEmpty(newPolicy.policy.plugins); + assert.isNotEmpty(newPolicy.policy.rules); + assert.isNotEmpty(newPolicy.policy.settings); + + const getPolicyResult = await authenticator.getPolicy(repoData); + + assert.deepEqual(newPolicy, getPolicyResult); + }); + + it('emits synchronize event after policy is fetched', async () => { + const storagePath = await createTmpConfigDir(); + const authenticator = createDefaultMonokleSynchronizer(new StorageHandlerPolicy(storagePath)); + + const queryApiStub = sinon.stub((authenticator as any).apiHandler, 'queryApi').callsFake(async (...args) => { + const query = args[0] as string; + + if (query.includes('query getUser')) { + return { + data: { + me: { + id: 5, + email: 'user5@kubeshop.io', + projects: [ + { + project: { + id: 5000, + slug: 'user5-proj', + name: 'User5 Project', + repositories: [ + { + id: "user5-proj-policy-id", + projectId: 5000, + provider: "GITHUB", + owner: "kubeshop", + name: "monokle-core", + prChecks: false, + canEnablePrChecks: true + } + ], + }, + }, + ], + }, + }, + }; + } + + if (query.includes('query getPolicy')) { + return { + data: { + getProject: { + id: 5000, + policy: { + id: "user5-proj-policy-id", + json: { + plugins: { + "pod-security-standards": true, + "yaml-syntax": false, + "resource-links": false, + "kubernetes-schema": false, + practices: true + }, + rules: { + "pod-security-standards/host-process": "err", + }, + settings: { + "kubernetes-schema": { + schemaVersion: "v1.27.1" + } + } + } + } + } + } + } + } + + return {}; + }); + stubs.push(queryApiStub); + + const repoData = { + provider: 'github', + remote: 'origin', + owner: 'kubeshop', + name: 'monokle-core', + }; + + const result = new Promise(resolve => { + authenticator.on('synchronize', (policy) => { + assert.isObject(policy); + assert.isTrue(policy.valid); + assert.isNotEmpty(policy.path); + assert.match(policy.path, /github-kubeshop-monokle-core.policy.yaml$/); + assert.isNotEmpty(policy.policy); + assert.isNotEmpty(policy.policy.plugins); + assert.isNotEmpty(policy.policy.rules); + assert.isNotEmpty(policy.policy.settings); + + resolve(policy); + }); + }); + + await authenticator.getPolicy(repoData, true, 'SAMPLE_ACCESS_TOKEN'); + + return result; + }); + }); +}); + +async function createTmpConfigDir(copyPolicyFixture = '') { + const testDir = resolve(__dirname, './'); + const testTmpDir = resolve(testDir, './tmp'); + const fixturesSourceDir = resolve(testDir, '../../src/__tests__/fixtures'); + + await mkdir(testTmpDir, {recursive: true}); + + if (copyPolicyFixture) { + await cp(resolve(fixturesSourceDir, copyPolicyFixture), resolve(testTmpDir, copyPolicyFixture)); + } + + return testTmpDir; +} + +async function cleanupTmpConfigDir() { + const testDir = resolve(__dirname, './'); + const testTmpDir = resolve(testDir, './tmp'); + + await rm(testTmpDir, {recursive: true, force: true}); +} diff --git a/packages/synchronizer/src/index.ts b/packages/synchronizer/src/index.ts index 4423d4293..73ba4cef9 100644 --- a/packages/synchronizer/src/index.ts +++ b/packages/synchronizer/src/index.ts @@ -1,12 +1,14 @@ -export * from './handlers/apiHandler'; -export * from './handlers/deviceFlowHandler'; -export * from './handlers/gitHandler'; -export * from './handlers/storageHandler'; -export * from './handlers/storageHandlerAuth'; -export * from './handlers/storageHandlerPolicy'; +export * from './handlers/apiHandler.js'; +export * from './handlers/deviceFlowHandler.js'; +export * from './handlers/gitHandler.js'; +export * from './handlers/storageHandler.js'; +export * from './handlers/storageHandlerAuth.js'; +export * from './handlers/storageHandlerPolicy.js'; -export * from './utils/authenticator'; -export * from './utils/synchronizer'; +export * from './utils/authenticator.js'; +export * from './utils/synchronizer.js'; -export * from './createDefaultMonokleAuthenticator'; -export * from './createDefaultMonokleSynchronizer'; +export * from './constants.js'; + +export * from './createDefaultMonokleAuthenticator.js'; +export * from './createDefaultMonokleSynchronizer.js'; diff --git a/packages/synchronizer/src/utils/synchronizer.ts b/packages/synchronizer/src/utils/synchronizer.ts index ff9d0574b..2b5782edf 100644 --- a/packages/synchronizer/src/utils/synchronizer.ts +++ b/packages/synchronizer/src/utils/synchronizer.ts @@ -28,13 +28,17 @@ export class Synchronizer extends EventEmitter { super(); } - async getPolicy(rootPath: string, accessToken: string, forceRefetch: boolean): Promise; - async getPolicy(repoData: RepoRemoteData, accessToken: string, forceRefetch: boolean): Promise; + async getPolicy(rootPath: string, forceRefetch?: boolean, accessToken?: string): Promise; + async getPolicy(repoData: RepoRemoteData, forceRefetch?: boolean, accessToken?: string): Promise; async getPolicy( repoDataOrRootPath: RepoRemoteData | string, - accessToken: string, - forceRefetch = false + forceRefetch = false, + accessToken = '', ): Promise { + if (forceRefetch && (!accessToken || accessToken?.length === 0)) { + throw new Error('Cannot force refetch without access token.'); + } + const repoData = typeof repoDataOrRootPath === 'string' ? await this.getRootGitData(repoDataOrRootPath) : repoDataOrRootPath; @@ -42,12 +46,13 @@ export class Synchronizer extends EventEmitter { await this.synchronize(repoData, accessToken); } - const policyContent = await this.readPolicy(repoData); + const policyContent = await this.readPolicy(repoData) ?? {}; + const isValidPolicy = Object.keys(policyContent).length > 0; return { - valid: Boolean(policyContent), - path: this.getPolicyPath(repoData), - policy: policyContent ?? {}, + valid: isValidPolicy, + path: isValidPolicy ? this.getPolicyPath(repoData) : '', + policy: policyContent, }; }