diff --git a/tests/OPAPolicy.test.tsx b/tests/OPAPolicy.test.tsx new file mode 100644 index 0000000..d961e96 --- /dev/null +++ b/tests/OPAPolicy.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import Policy from '../source/commands/opa/policy'; +import delay from 'delay'; +global.fetch = vi.fn(); +const enter = '\r'; + +describe('OPA Policy Command', () => { + it('should render the policy command', async () => { + const options = { + serverUrl: 'http://localhost:8181', + keyAccount: 'testAccount', + apiKey: 'permit_key_'.concat('a'.repeat(97)), + }; + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ + result: [ + { id: 'policy1', name: 'policyName1' }, + { id: 'policy2', name: 'policyName2' }, + ], + }), + status: 200, + }); + const { stdin, lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + 'Listing Policies on Opa Server=http://localhost:8181', + ); + await delay(50); + expect(lastFrame()?.toString()).toMatch('Showing 2 of 2 policies:'); + expect(lastFrame()?.toString()).toMatch('policy1'); + expect(lastFrame()?.toString()).toMatch('policy2'); + stdin.write(enter); + }); + it('should render the policy command with error', async () => { + const options = { + serverUrl: 'http://localhost:8181', + keyAccount: 'testAccount', + }; + (fetch as any).mockRejectedValueOnce(new Error('Error')); + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + 'Listing Policies on Opa Server=http://localhost:8181', + ); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Request failed:/); + }); +}); diff --git a/tests/PDPCheck.test.tsx b/tests/PDPCheck.test.tsx new file mode 100644 index 0000000..85bb8ad --- /dev/null +++ b/tests/PDPCheck.test.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { describe, vi, it, expect, afterEach } from 'vitest'; +import delay from 'delay'; +import Check from '../source/commands/pdp/check'; + +global.fetch = vi.fn(); + +describe('PDP Check Component', () => { + afterEach(() => { + // Clear mock calls after each test + vi.clearAllMocks(); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: true }), + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toMatchInlineSnapshot(` + "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" + `); + await delay(50); + expect(lastFrame()?.toString()).toContain('ALLOWED'); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: false }), + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toMatchInlineSnapshot(` + "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" + `); + await delay(50); + expect(lastFrame()?.toString()).toContain('DENIED'); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: false, + text: async () => 'Error', + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toMatchInlineSnapshot(` + "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" + `); + await delay(50); + expect(lastFrame()?.toString()).toContain('Error'); + }); + it('should render with the given options with multiple resource', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: true }), + }); + const options = { + user: 'testUser', + resource: 'testResourceType: testRecsourceKey', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toMatchInlineSnapshot(` + "Checking user="testUser" action=testAction resource=testResourceType: testRecsourceKey at +tenant=testTenant"`); + await delay(50); + expect(lastFrame()?.toString()).toContain('ALLOWED'); + }); +}); diff --git a/tests/PDPRun.test.tsx b/tests/PDPRun.test.tsx new file mode 100644 index 0000000..d2a6b71 --- /dev/null +++ b/tests/PDPRun.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from 'ink-testing-library'; +import Run from '../source/commands/pdp/run'; + +describe('PDP Run', () => { + it('Should render the PDP Run command', () => { + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch(/Loading Token/); + }); +}); diff --git a/tests/apiKey.test.tsx b/tests/apiKey.test.tsx new file mode 100644 index 0000000..98af3fb --- /dev/null +++ b/tests/apiKey.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import ApiKey from '../source/commands/apiKey'; +import { describe, it, expect } from 'vitest'; +import delay from 'delay'; + +describe('ApiKey', () => { + it('Should save the key', () => { + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatch(/Key saved to secure key store./); + }); + it('Should validate the key', () => { + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatch(/Key is valid./); + }); + it('Should read the key', async () => { + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + await delay(50); + expect(lastFrame()).toMatch(/permit_key_aaaaaaa/); + }); + it('Invalid Key', async () => { + const permitKey = 'permit_key'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + await delay(50); + expect(lastFrame()).toMatch(/Key is not valid./); + }); +}); diff --git a/tests/components/AuthProvider.test.tsx b/tests/components/AuthProvider.test.tsx new file mode 100644 index 0000000..bd6e285 --- /dev/null +++ b/tests/components/AuthProvider.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { AuthProvider, useAuth } from '../../source/components/AuthProvider.js'; +import { loadAuthToken } from '../../source/lib/auth.js'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Text } from 'ink'; +import delay from 'delay'; + +vi.mock('../../source/lib/auth.js', () => ({ + loadAuthToken: vi.fn(), +})); + +describe('AuthProvider', () => { + it('should display loading text while loading token', async () => { + (loadAuthToken as any).mockResolvedValueOnce(new Promise(() => {})); + + const { lastFrame } = render( + + Child Component + , + ); + + expect(lastFrame()).toContain('Loading Token'); + }); + it('should display error message if loading token fails', async () => { + (loadAuthToken as any).mockRejectedValueOnce( + new Error('Failed to load token'), + ); + + const { lastFrame } = render( + + Child Component + , + ); + + await delay(50); + expect(lastFrame()).toContain('Failed to load token'); + }); + + it('should display children when token is loaded successfully', async () => { + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); + + const { lastFrame } = render( + + Child Component + , + ); + + await delay(50); + expect(lastFrame()).toContain('Child Component'); + }); + it('should use the auth context successfully', async () => { + const ChildComponent = () => { + const { authToken } = useAuth(); + return {authToken || 'No token'}; + }; + + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); + + const { lastFrame } = render( + + + , + ); + + await delay(100); + expect(lastFrame()).toContain('mocked-token'); + }); + + it('should throw an error when useAuth is called outside of AuthProvider', () => { + const ChildComponent = () => { + let apiKey: string; + try { + const { authToken } = useAuth(); + apiKey = authToken; + } catch (error) { + return useAuth must be used within an AuthProvider; + } + return {apiKey || 'No token'}; + }; + const { lastFrame } = render(); + expect(lastFrame()).toContain( + 'useAuth must be used within an AuthProvider', + ); + }); +}); diff --git a/tests/components/PDPCommand.test.tsx b/tests/components/PDPCommand.test.tsx new file mode 100644 index 0000000..81deba9 --- /dev/null +++ b/tests/components/PDPCommand.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import PDPCommand from '../../source/components/PDPCommand'; +import { AuthProvider } from '../../source/components/AuthProvider'; +import delay from 'delay'; +import { loadAuthToken } from '../../source/lib/auth'; +vi.mock('../../source/lib/auth', () => ({ + loadAuthToken: vi.fn(), +})); +describe('PDP Component', () => { + it('should render the PDP component with auth token', async () => { + (loadAuthToken as any).mockResolvedValueOnce( + 'permit_key_'.concat('a'.repeat(97)), + ); + const { lastFrame } = render( + + + , + ); + expect(lastFrame()?.toString()).toMatch('Loading Token'); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + 'Run the following command from your terminal:', + ); + }); + it('should render the Spinner', async () => { + const { lastFrame } = render( + + + , + ); + expect(lastFrame()?.toString()).toMatch('Loading Token'); + await delay(50); + expect(lastFrame()?.toString()).toMatch('Loading command'); + }); +}); diff --git a/tests/github.test.tsx b/tests/github.test.tsx index 6d29cc4..623c980 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -384,5 +384,68 @@ describe('GiHub Complete Flow', () => { await delay(50); const frameString = lastFrame()?.toString() ?? ''; expect(frameString).toMatch(/GitOps Configuration Wizard - GitHub/); + + + await delay(50); + stdin.write(arrowDown); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Enter Your RepositoryKey :/); + await delay(50); + stdin.write('repo3'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/SSH Key Generated./); + await delay(50); + stdin.write('git@github.com:user/repository.git'); + await delay(50); + stdin.write(enter); + expect(lastFrame()?.toString()).toMatch(/Enter the Branch Name:/); + await delay(50); + stdin.write('main'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + 'Invalid configuration. Please check the configuration and try again.', + ); + }); + it('should work with inactive argument', async () => { + const { stdin, lastFrame } = render( + , + ); + const frameString = lastFrame()?.toString() ?? ''; + expect(frameString).toMatch(/Loading Token/); + await delay(100); + expect(lastFrame()?.toString()).toMatch( + /GitOps Configuration Wizard - GitHub/, + ); + await delay(50); + stdin.write(arrowDown); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Enter Your RepositoryKey :/); + await delay(50); + stdin.write('repo3'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/SSH Key Generated./); + await delay(50); + stdin.write('git@github.com:user/repository.git'); + await delay(50); + stdin.write(enter); + expect(lastFrame()?.toString()).toMatch(/Enter the Branch Name:/); + await delay(50); + stdin.write('main'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + /Your GitOps is configured succesffuly. To complete the setup, remember to activate it later/, + ); }); }); diff --git a/tests/index.test.tsx b/tests/index.test.tsx new file mode 100644 index 0000000..4005379 --- /dev/null +++ b/tests/index.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { describe, vi, expect, it } from 'vitest'; +import Index from '../source/commands/index'; +import delay from 'delay'; + +describe('index file', () => { + it('the index file should render', () => { + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + /Run this command with --help for more information/, + ); + }); +}); diff --git a/tests/lib/api.test.ts b/tests/lib/api.test.ts new file mode 100644 index 0000000..5ad413e --- /dev/null +++ b/tests/lib/api.test.ts @@ -0,0 +1,23 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as api from '../../source/lib/api'; +global.fetch = vi.fn(); +describe('API', () => { + it('should call the apiCall', async () => { + (fetch as any).mockResolvedValueOnce({ + headers: {}, + status: 200, + json: async () => ({ id: 'testId', name: 'testName' }), + }); + const response = await api.apiCall( + 'testEndpoint', + 'testToken', + 'testCookie', + 'GET', + 'testBody', + ); + expect(response.status).toBe(200); + expect(response.response.id).toBe('testId'); + expect(response.response.name).toBe('testName'); + expect(response.headers).toEqual({}); + }); +}); diff --git a/tests/lib/auth.test.ts b/tests/lib/auth.test.ts new file mode 100644 index 0000000..c1e1d8e --- /dev/null +++ b/tests/lib/auth.test.ts @@ -0,0 +1,93 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as auth from '../../source/lib/auth'; +import pkg from 'keytar'; +import * as http from 'http'; +import { + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, +} from '../../source/config'; +import open from 'open'; + +vi.mock('http', () => ({ + createServer: vi.fn().mockReturnValue({ + listen: vi.fn(), + close: vi.fn(), + }), +})); +vi.mock('open', () => ({ + default: vi.fn(), +})); + +vi.mock('node:crypto', () => ({ + randomBytes: vi.fn().mockReturnValue(Buffer.from('mock-verifier')), + createHash: vi.fn().mockImplementation(() => ({ + update: vi.fn().mockReturnThis(), + digest: vi.fn(() => Buffer.from('mock-hash')), + })), +})); + +describe('Token Type', () => { + it('Should return correct token type', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + const tokenType = auth.tokenType(demoToken); + expect(tokenType).toBe(auth.TokenType.APIToken); + }); + it('Should return type of JWT', async () => { + const demoJwtToken = 'demo1.demo2.demo3'; + const tokenType = auth.tokenType(demoJwtToken); + expect(tokenType).toBe(auth.TokenType.AccessToken); + }); + it('should return invalid token', async () => { + const demoInvalidToken = 'invalid token'; + const tokenType = auth.tokenType(demoInvalidToken); + expect(tokenType).toBe(auth.TokenType.Invalid); + }); +}); + +describe('Save Auth Token', () => { + it('Should save token', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + const result = await auth.saveAuthToken(demoToken); + expect(result).toBe(''); + }); + it('Should return invalid token', async () => { + const demoToken = 'invalid token'; + const result = await auth.saveAuthToken(demoToken); + expect(result).toBe('Invalid auth token'); + }); +}); +describe('Load Auth Token', () => { + it('Should load token', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + await auth.saveAuthToken(demoToken); + const result = await auth.loadAuthToken(); + expect(result).toBe(demoToken); + }); + it('Should throw error', async () => { + await pkg.deletePassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + try { + await auth.loadAuthToken(); + } catch (error) { + expect(error).toBeInstanceOf(Error); + } + }); +}); +describe('Clean Auth Token', () => { + it('Should clean token', async () => { + await auth.cleanAuthToken(); + const result = await pkg.getPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + expect(result).toBeNull(); + }); +}); +describe('Browser Auth', () => { + it('Should open browser', async () => { + await auth.browserAuth(); + expect(open).toHaveBeenCalled(); + }); +}); diff --git a/tests/lib/gitops_utils.test.ts b/tests/lib/gitops_utils.test.ts new file mode 100644 index 0000000..7826dd6 --- /dev/null +++ b/tests/lib/gitops_utils.test.ts @@ -0,0 +1,185 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as utils from '../../source/lib/gitops/utils'; +import { apiCall } from '../../source/lib/api'; +import ssh from 'micro-key-producer/ssh.js'; +import { randomBytes } from 'micro-key-producer/utils.js'; +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); +vi.mock('micro-key-producer/ssh.js', () => ({ + default: vi.fn(), +})); +vi.mock('micro-key-producer/utils.js', () => ({ + randomBytes: vi.fn(), +})); + +describe('getProjectList', () => { + it('should return a list of projects', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: [ + { + key: 'testKey', + urn_namespace: 'testNamespace', + id: 'testId', + organization_id: 'testOrgId', + created_at: 'testCreatedAt', + updated_at: 'testUpdatedAt', + name: 'testName', + }, + ], + }); + const projects = await utils.getProjectList( + 'permit_key_'.concat('a'.repeat(96)), + ); + expect(projects).toEqual([ + { + key: 'testKey', + urn_namespace: 'testNamespace', + id: 'testId', + organization_id: 'testOrgId', + created_at: 'testCreatedAt', + updated_at: 'testUpdatedAt', + name: 'testName', + }, + ]); + }); + it('should throw an error if the status is not 200', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 400, + response: 'testError', + }); + await expect( + utils.getProjectList('permit_key_'.concat('a'.repeat(96))), + ).rejects.toThrow('Failed to fetch projects: testError'); + }); +}); + +describe('getRepoList', () => { + it('should return a list of repos', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: [ + { status: 'valid', key: 'testKey' }, + { status: 'invalid', key: 'testKey2' }, + ], + }); + const repos = await utils.getRepoList( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + ); + expect(repos).toEqual([ + { status: 'valid', key: 'testKey' }, + { status: 'invalid', key: 'testKey2' }, + ]); + }); +}); + +describe('generateSSHKey', () => { + it('should generate an SSH key', () => { + (randomBytes as any).mockReturnValueOnce(new Uint8Array(32)); + (ssh as any).mockReturnValueOnce({ + publicKeyBytes: new Uint8Array(8), + publicKey: 'publicKey', + privateKey: 'privateKey', + fingerprint: 'testFingerprint', + }); + const key = utils.generateSSHKey(); + expect(key).toStrictEqual({ + publicKeyBytes: new Uint8Array(8), + publicKey: 'publicKey', + privateKey: 'privateKey', + fingerprint: 'testFingerprint', + }); + }); +}); + +describe('Configure Permit', async () => { + it('should configure permit', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + const response = await utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ); + expect(response).toStrictEqual({ + id: 'testId', + key: 'testKey', + status: 'valid', + }); + }); + it('should throw an error if the status is 422', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 422, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + await expect( + utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ), + ).rejects.toThrow('Validation Error in Configuring Permit'); + }); + it('should throw an error if the status is not 200', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 400, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + await expect( + utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ), + ).rejects.toThrow('Invalid Configuration '); + }); +});