From e89b62c2b2a387263a5fb2aee60eb9ef35282621 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Thu, 28 Nov 2024 10:28:47 +0530 Subject: [PATCH 01/19] test: added apiKey and AuthProvider test Signed-off-by: Abinand P --- tests/apiKey.test.tsx | 44 ++++++++++++++ tests/components/AuthProvider.test.tsx | 82 ++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 tests/apiKey.test.tsx create mode 100644 tests/components/AuthProvider.test.tsx 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..6de1c19 --- /dev/null +++ b/tests/components/AuthProvider.test.tsx @@ -0,0 +1,82 @@ +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.mockResolvedValueOnce(new Promise(() => {})); + + const { lastFrame } = render( + + Child Component + , + ); + + expect(lastFrame()).toContain('Loading Token'); + }); + it('should display error message if loading token fails', async () => { + loadAuthToken.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.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.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', () => { + try { + const ChildComponent = () => { + const { authToken } = useAuth(); + return {authToken || 'No token'}; + }; + render(); + } catch (error: any) { + expect(error.message).toMatch( + /useAuth must be used within an AuthProvider/, + ); + return; + } + }); +}); From e447a0dd36264b0773ef4108089ef87e5160722b Mon Sep 17 00:00:00 2001 From: Abinand P Date: Thu, 28 Nov 2024 13:26:07 +0530 Subject: [PATCH 02/19] test:added index and PDPCheck Signed-off-by: Abinand P --- tests/PDPCheck.test.tsx | 94 ++++++++++++++++++++++++++ tests/components/AuthProvider.test.tsx | 34 ++++++---- tests/index.test.tsx | 14 ++++ 3 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 tests/PDPCheck.test.tsx create mode 100644 tests/index.test.tsx 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/components/AuthProvider.test.tsx b/tests/components/AuthProvider.test.tsx index 6de1c19..bd6e285 100644 --- a/tests/components/AuthProvider.test.tsx +++ b/tests/components/AuthProvider.test.tsx @@ -12,7 +12,7 @@ vi.mock('../../source/lib/auth.js', () => ({ describe('AuthProvider', () => { it('should display loading text while loading token', async () => { - loadAuthToken.mockResolvedValueOnce(new Promise(() => {})); + (loadAuthToken as any).mockResolvedValueOnce(new Promise(() => {})); const { lastFrame } = render( @@ -23,7 +23,9 @@ describe('AuthProvider', () => { expect(lastFrame()).toContain('Loading Token'); }); it('should display error message if loading token fails', async () => { - loadAuthToken.mockRejectedValueOnce(new Error('Failed to load token')); + (loadAuthToken as any).mockRejectedValueOnce( + new Error('Failed to load token'), + ); const { lastFrame } = render( @@ -36,7 +38,7 @@ describe('AuthProvider', () => { }); it('should display children when token is loaded successfully', async () => { - loadAuthToken.mockResolvedValueOnce('mocked-token'); + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); const { lastFrame } = render( @@ -53,7 +55,7 @@ describe('AuthProvider', () => { return {authToken || 'No token'}; }; - loadAuthToken.mockResolvedValueOnce('mocked-token'); + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); const { lastFrame } = render( @@ -66,17 +68,19 @@ describe('AuthProvider', () => { }); it('should throw an error when useAuth is called outside of AuthProvider', () => { - try { - const ChildComponent = () => { + const ChildComponent = () => { + let apiKey: string; + try { const { authToken } = useAuth(); - return {authToken || 'No token'}; - }; - render(); - } catch (error: any) { - expect(error.message).toMatch( - /useAuth must be used within an AuthProvider/, - ); - return; - } + 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/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/, + ); + }); +}); From 34e4e7e6975a46314aa6b3eb588be5201bae0b0a Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 17:00:02 +0530 Subject: [PATCH 03/19] test:added unit test for pdpd command and api Signed-off-by: Abinand P --- source/lib/auth.ts | 1 - tests/OPAPolicy.test.tsx | 49 ++++++++++++++++++++++++++++ tests/PDPRun.test.tsx | 11 +++++++ tests/components/PDPCommand.test.tsx | 37 +++++++++++++++++++++ tests/lib/api.test.ts | 17 ++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests/OPAPolicy.test.tsx create mode 100644 tests/PDPRun.test.tsx create mode 100644 tests/components/PDPCommand.test.tsx create mode 100644 tests/lib/api.test.ts diff --git a/source/lib/auth.ts b/source/lib/auth.ts index 4f2be4b..10a67aa 100644 --- a/source/lib/auth.ts +++ b/source/lib/auth.ts @@ -27,7 +27,6 @@ export const tokenType = (token: string): TokenType => { if (token.split('.').length === 3) { return TokenType.AccessToken; } - return TokenType.Invalid; }; diff --git a/tests/OPAPolicy.test.tsx b/tests/OPAPolicy.test.tsx new file mode 100644 index 0000000..cabecd4 --- /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/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/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/lib/api.test.ts b/tests/lib/api.test.ts new file mode 100644 index 0000000..6adfb54 --- /dev/null +++ b/tests/lib/api.test.ts @@ -0,0 +1,17 @@ +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({}); + }) +}) \ No newline at end of file From f7560d63ded623c07dc38f0480279e9587ad693e Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 18:40:48 +0530 Subject: [PATCH 04/19] test:added gitops/util and modified some existing tests Signed-off-by: Abinand P --- source/lib/gitops/utils.ts | 2 +- tests/OPAPolicy.test.tsx | 16 +-- tests/lib/api.test.ts | 38 ++++--- tests/lib/gitops_utils.test.ts | 185 +++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 25 deletions(-) create mode 100644 tests/lib/gitops_utils.test.ts diff --git a/source/lib/gitops/utils.ts b/source/lib/gitops/utils.ts index 8f2c064..b814b75 100644 --- a/source/lib/gitops/utils.ts +++ b/source/lib/gitops/utils.ts @@ -87,7 +87,7 @@ async function configurePermit( status: gitConfigResponse.status, }; } else { - throw new Error('Invalid Configuration ' + response); + throw new Error('Invalid Configuration '); } } diff --git a/tests/OPAPolicy.test.tsx b/tests/OPAPolicy.test.tsx index cabecd4..d961e96 100644 --- a/tests/OPAPolicy.test.tsx +++ b/tests/OPAPolicy.test.tsx @@ -11,15 +11,16 @@ describe('OPA Policy Command', () => { const options = { serverUrl: 'http://localhost:8181', keyAccount: 'testAccount', - apiKey: "permit_key_".concat("a".repeat(97)), + apiKey: 'permit_key_'.concat('a'.repeat(97)), }; (fetch as any).mockResolvedValueOnce({ - ok:true, - json:async ()=> ({ - result:[ - { id: 'policy1', name: 'policyName1' }, - { id: 'policy2', name: 'policyName2' }, - ]}), + ok: true, + json: async () => ({ + result: [ + { id: 'policy1', name: 'policyName1' }, + { id: 'policy2', name: 'policyName2' }, + ], + }), status: 200, }); const { stdin, lastFrame } = render(); @@ -31,7 +32,6 @@ describe('OPA Policy Command', () => { expect(lastFrame()?.toString()).toMatch('policy1'); expect(lastFrame()?.toString()).toMatch('policy2'); stdin.write(enter); - }); it('should render the policy command with error', async () => { const options = { diff --git a/tests/lib/api.test.ts b/tests/lib/api.test.ts index 6adfb54..5ad413e 100644 --- a/tests/lib/api.test.ts +++ b/tests/lib/api.test.ts @@ -1,17 +1,23 @@ -import { describe, vi, it, expect } from "vitest"; -import * as api from "../../source/lib/api"; +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({}); - }) -}) \ No newline at end of file +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/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 '); + }); +}); From acc865bc09b64c8c520fe3233f25383d81a534ca Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 18:54:35 +0530 Subject: [PATCH 05/19] test: improved the coverage of the githubComponent Signed-off-by: Abinand P --- tests/github.test.tsx | 77 ++++++++++++++++++++++++++++++++++++++++++ tests/lib/auth.test.ts | 0 2 files changed, 77 insertions(+) create mode 100644 tests/lib/auth.test.ts diff --git a/tests/github.test.tsx b/tests/github.test.tsx index 6d29cc4..98ff6a8 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -385,4 +385,81 @@ describe('GiHub Complete Flow', () => { const frameString = lastFrame()?.toString() ?? ''; expect(frameString).toMatch(/GitOps Configuration Wizard - GitHub/); }); + it('should display Error message for invalid status of the repo', async () => { + (configurePermit as any).mockResolvedValueOnce({ + id: '1', + status: 'invalid', + key: 'repo3', + }); + const { stdin, lastFrame } = render( + , + ); + const frameString = lastFrame()?.toString() ?? ''; + expect(frameString).toMatch(/Loading Token/); + await delay(50); + 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( + '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/lib/auth.test.ts b/tests/lib/auth.test.ts new file mode 100644 index 0000000..e69de29 From 0a020589d8522d3b3c407104a5f2b1715765cb96 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 20:37:13 +0530 Subject: [PATCH 06/19] test:added tests for the cleanAuthToken, loadAuthToken, saveAuthToken, tokenType Signed-off-by: Abinand P --- source/lib/auth.ts | 1 + tests/lib/auth.test.ts | 88 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/source/lib/auth.ts b/source/lib/auth.ts index 10a67aa..4f2be4b 100644 --- a/source/lib/auth.ts +++ b/source/lib/auth.ts @@ -27,6 +27,7 @@ export const tokenType = (token: string): TokenType => { if (token.split('.').length === 3) { return TokenType.AccessToken; } + return TokenType.Invalid; }; diff --git a/tests/lib/auth.test.ts b/tests/lib/auth.test.ts index e69de29..9d94504 100644 --- a/tests/lib/auth.test.ts +++ b/tests/lib/auth.test.ts @@ -0,0 +1,88 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as auth from '../../source/lib/auth'; +import pkg from 'keytar'; +import delay from 'delay'; +import { + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, +} from '../../source/config'; +import http from 'http'; +import open from 'open'; +import { randomBytes, createHash } from 'crypto'; + +vi.mock('node:http', () => ({ + createServer: 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')), + })), +})); + +global.fetch = vi.fn(); + +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(); + }); +}); From c7f832e883d1cca6ccaa80c241a663ab0e113f45 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 21:12:41 +0530 Subject: [PATCH 07/19] test: added test for browser auth open Signed-off-by: Abinand P --- tests/lib/auth.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/lib/auth.test.ts b/tests/lib/auth.test.ts index 9d94504..c1e1d8e 100644 --- a/tests/lib/auth.test.ts +++ b/tests/lib/auth.test.ts @@ -1,17 +1,18 @@ import { describe, vi, it, expect } from 'vitest'; import * as auth from '../../source/lib/auth'; import pkg from 'keytar'; -import delay from 'delay'; +import * as http from 'http'; import { KEYSTORE_PERMIT_SERVICE_NAME, DEFAULT_PERMIT_KEYSTORE_ACCOUNT, } from '../../source/config'; -import http from 'http'; import open from 'open'; -import { randomBytes, createHash } from 'crypto'; -vi.mock('node:http', () => ({ - createServer: vi.fn(), +vi.mock('http', () => ({ + createServer: vi.fn().mockReturnValue({ + listen: vi.fn(), + close: vi.fn(), + }), })); vi.mock('open', () => ({ default: vi.fn(), @@ -25,8 +26,6 @@ vi.mock('node:crypto', () => ({ })), })); -global.fetch = vi.fn(); - describe('Token Type', () => { it('Should return correct token type', async () => { const demoToken = 'permit_key_'.concat('a'.repeat(97)); @@ -86,3 +85,9 @@ describe('Clean Auth Token', () => { expect(result).toBeNull(); }); }); +describe('Browser Auth', () => { + it('Should open browser', async () => { + await auth.browserAuth(); + expect(open).toHaveBeenCalled(); + }); +}); From a806c2c2334180cc1998d17e0114974d170f6a96 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 29 Nov 2024 21:44:05 +0530 Subject: [PATCH 08/19] docs: added details about the test --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 84b661a..16023c4 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ Based on [Pastel](https://github.com/vadimdemedes/create-pastel-app) - run `npm install` - run `npx tsx ./source/cli.tsx` +### Writing Tests +Permit CLI uses [`vitest`](https://vitest.dev/) as a tool for writing tests. It also uses [`ink-testing-library`](https://github.com/vadimdemedes/ink-testing-library) to render the `Ink` components. + +- run `npx vitest` for testing +- run `npx vitest --coverage` for code coverage. + + ## CLI ``` From 659672a59130d5f348ac06e22d558992a06bac8a Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 13 Dec 2024 21:08:18 +0530 Subject: [PATCH 09/19] test:removed the tests that are redundant for pdp check Signed-off-by: Abinand P --- tests/PDPCheck.test.tsx | 17 ++---- tests/e2e/check.test.ts | 122 ---------------------------------------- tests/lib/api.test.ts | 3 +- 3 files changed, 7 insertions(+), 135 deletions(-) delete mode 100644 tests/e2e/check.test.ts diff --git a/tests/PDPCheck.test.tsx b/tests/PDPCheck.test.tsx index 85bb8ad..726d35f 100644 --- a/tests/PDPCheck.test.tsx +++ b/tests/PDPCheck.test.tsx @@ -25,9 +25,7 @@ describe('PDP Check Component', () => { }; const { lastFrame } = render(); - expect(lastFrame()).toMatchInlineSnapshot(` - "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" - `); + expect(lastFrame()).toContain(`Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`); await delay(50); expect(lastFrame()?.toString()).toContain('ALLOWED'); }); @@ -45,9 +43,7 @@ describe('PDP Check Component', () => { }; const { lastFrame } = render(); - expect(lastFrame()).toMatchInlineSnapshot(` - "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" - `); + expect(lastFrame()).toContain(`Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`); await delay(50); expect(lastFrame()?.toString()).toContain('DENIED'); }); @@ -65,9 +61,7 @@ describe('PDP Check Component', () => { }; const { lastFrame } = render(); - expect(lastFrame()).toMatchInlineSnapshot(` - "Checking user="testUser" action=testAction resource=testResource at tenant=testTenant" - `); + expect(lastFrame()).toContain(`Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`); await delay(50); expect(lastFrame()?.toString()).toContain('Error'); }); @@ -85,9 +79,8 @@ describe('PDP Check Component', () => { }; const { lastFrame } = render(); - expect(lastFrame()).toMatchInlineSnapshot(` - "Checking user="testUser" action=testAction resource=testResourceType: testRecsourceKey at -tenant=testTenant"`); + expect(lastFrame()).toContain(`Checking user="testUser"action=testAction resource=testResourceType: testRecsourceKeyat +tenant=testTenant`); await delay(50); expect(lastFrame()?.toString()).toContain('ALLOWED'); }); diff --git a/tests/e2e/check.test.ts b/tests/e2e/check.test.ts deleted file mode 100644 index ef5f610..0000000 --- a/tests/e2e/check.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -// tests/e2e/check.test.ts -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { describe, it, expect } from 'vitest'; - -const execAsync = promisify(exec); -const CLI_COMMAND = 'npx tsx ./source/cli pdp check'; - -describe('pdp check command e2e', () => { - // Test original functionality remains intact - describe('backwards compatibility', () => { - it('should work with basic required parameters', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read` - ); - expect(stdout).toContain('user="testUser"'); - expect(stdout).toContain('action=read'); - expect(stdout).toContain('resource=testResource'); - },10000); - - it('should work with optional tenant parameter', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read "tenant" "customTenant"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should work with resource type:key format', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r "document:doc123" -a read` - ); - expect(stdout).toContain('resource=document:doc123'); - }); - }); - - // Test new attribute functionality - describe('user attributes', () => { - it('should handle single user attribute', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle multiple user attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin,department:IT,level:5"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle user attributes with different types', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "isAdmin:true,age:25,name:john"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('resource attributes', () => { - it('should handle single resource attribute', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "owner:john"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle multiple resource attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "owner:john,status:active,priority:high"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle resource attributes with different types', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "isPublic:true,size:1024,type:document"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('combined scenarios', () => { - it('should handle both user and resource attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin" -ra "status:active"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should work with all parameters combined', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r "document:doc123" -a write "tenant" customTenant -ua "role:admin,dept:IT" -ra "status:active,size:1024"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('error handling', () => { - it('should handle invalid user attribute format', async () => { - try{ - const { stderr } = await execAsync( - `${CLI_COMMAND} -u johnexample.com -r "document"`, - { encoding: 'utf8' } - );} - catch (error) { - expect(error.stderr).toContain(''); - } - }); - - it('should handle invalid resource attribute format', async () => { - try { - await execAsync( - `${CLI_COMMAND} -u johnexample.com -r "document"`, - - ); - } catch (error) { - expect(error.stderr).toContain(''); - } - },10000); - }); -}); \ No newline at end of file diff --git a/tests/lib/api.test.ts b/tests/lib/api.test.ts index 5ad413e..949422e 100644 --- a/tests/lib/api.test.ts +++ b/tests/lib/api.test.ts @@ -5,10 +5,11 @@ describe('API', () => { it('should call the apiCall', async () => { (fetch as any).mockResolvedValueOnce({ headers: {}, + ok:true, status: 200, json: async () => ({ id: 'testId', name: 'testName' }), }); - const response = await api.apiCall( + const response = await api.apiCall<{id:string,name:string}>( 'testEndpoint', 'testToken', 'testCookie', From 286f3fdd63cc9a4144f43119acd3c7dbabc4863b Mon Sep 17 00:00:00 2001 From: Abinand P Date: Fri, 13 Dec 2024 22:29:45 +0530 Subject: [PATCH 10/19] formatted the code Signed-off-by: Abinand P --- README.md | 4 +- tests/EnvCopy.test.tsx | 77 +++++++++------ tests/EnvMember.test.tsx | 48 +++++---- tests/EnvSelect.test.tsx | 16 ++- tests/LoginFlow.test.tsx | 40 +++++--- tests/PDPCheck.test.tsx | 15 ++- tests/SelectEnvironment.test.tsx | 16 +-- tests/SelectOrganisation.test.tsx | 55 +++++++---- tests/SelectProject.test.tsx | 16 +-- tests/lib/api.test.ts | 4 +- tests/login.test.tsx | 13 +++ tests/logout.test.tsx | 22 +++++ tests/utils/attributes.test.ts | 158 ++++++++++++++++-------------- 13 files changed, 310 insertions(+), 174 deletions(-) create mode 100644 tests/login.test.tsx create mode 100644 tests/logout.test.tsx diff --git a/README.md b/README.md index 16023c4..c95bc35 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ Based on [Pastel](https://github.com/vadimdemedes/create-pastel-app) - run `npx tsx ./source/cli.tsx` ### Writing Tests -Permit CLI uses [`vitest`](https://vitest.dev/) as a tool for writing tests. It also uses [`ink-testing-library`](https://github.com/vadimdemedes/ink-testing-library) to render the `Ink` components. + +Permit CLI uses [`vitest`](https://vitest.dev/) as a tool for writing tests. It also uses [`ink-testing-library`](https://github.com/vadimdemedes/ink-testing-library) to render the `Ink` components. - run `npx vitest` for testing - run `npx vitest --coverage` for code coverage. - ## CLI ``` diff --git a/tests/EnvCopy.test.tsx b/tests/EnvCopy.test.tsx index 8724ee9..b392952 100644 --- a/tests/EnvCopy.test.tsx +++ b/tests/EnvCopy.test.tsx @@ -26,7 +26,7 @@ vi.mock('../source/components/EnvironmentSelection.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -38,19 +38,23 @@ afterEach(() => { describe('Copy Component', () => { it('should handle successful environment copy flow using arguments', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useEnvironmentApi).mockReturnValue({ - copyEnvironment: vi.fn(() => Promise.resolve({ - error: null, - })), + copyEnvironment: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); // @ts-ignore @@ -63,7 +67,16 @@ describe('Copy Component', () => { return null; }); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(100); // Allow name input expect(lastFrame()).toMatch(/Environment copied successfully/); @@ -71,10 +84,12 @@ describe('Copy Component', () => { it('should handle invalid API key gracefully', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: false, - error: 'Invalid API Key', - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: false, + error: 'Invalid API Key', + }), + ), }); const { lastFrame } = render(); @@ -87,19 +102,23 @@ describe('Copy Component', () => { it('should handle successful environment copy flow using the wizard', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useEnvironmentApi).mockReturnValue({ - copyEnvironment: vi.fn(() => Promise.resolve({ - error: null, - })), + copyEnvironment: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { @@ -107,12 +126,14 @@ describe('Copy Component', () => { { label: 'Org1', value: 'org1' }, { label: 'Proj1', value: 'proj1' }, { label: 'Env1', value: 'env1' }, - 'secret' + 'secret', ); return null; }); - const { lastFrame, stdin } = render(); + const { lastFrame, stdin } = render( + , + ); await delay(50); // Allow environment selection diff --git a/tests/EnvMember.test.tsx b/tests/EnvMember.test.tsx index 39f8efa..7b83b23 100644 --- a/tests/EnvMember.test.tsx +++ b/tests/EnvMember.test.tsx @@ -26,7 +26,7 @@ vi.mock('../source/components/EnvironmentSelection.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -40,32 +40,38 @@ const enter = '\r'; describe('Member Component', () => { it('should handle successful member invite flow', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - organization_id: 'org1', - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + organization_id: 'org1', + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useMemberApi).mockReturnValue({ - inviteNewMember: vi.fn(() => Promise.resolve({ - error: null, - })), + inviteNewMember: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { onComplete( { label: 'Org1', value: 'org1' }, { label: 'Proj1', value: 'proj1' }, - { label: 'Env1', value: 'env1' } + { label: 'Env1', value: 'env1' }, ); return null; }); - const { lastFrame, stdin } = render(); + const { lastFrame, stdin } = render( + , + ); await delay(50); // Allow environment selection @@ -81,13 +87,17 @@ describe('Member Component', () => { it('should handle invalid API key gracefully', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: false, - error: 'Invalid API Key', - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: false, + error: 'Invalid API Key', + }), + ), }); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(50); // Allow async operations to complete diff --git a/tests/EnvSelect.test.tsx b/tests/EnvSelect.test.tsx index 38195d3..804dae8 100644 --- a/tests/EnvSelect.test.tsx +++ b/tests/EnvSelect.test.tsx @@ -31,7 +31,7 @@ vi.mock('../source/lib/auth.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -49,7 +49,9 @@ describe('Select Component', () => { it('should redirect to login when no API key is provided', async () => { // Mock the Login component - vi.mocked(Login).mockImplementation(() => Mocked Login Component); + vi.mocked(Login).mockImplementation(() => ( + Mocked Login Component + )); const { lastFrame } = render(); + const { lastFrame } = render( + ); + + await delay(100); // Allow async operations to complete + + expect(lastFrame()).toMatch(/Failed to save token/); + expect(process.exit).toHaveBeenCalledWith(1); + }); + it('handle login successs', async () => { + vi.mocked(useApiKeyApi).mockReturnValue({ + validateApiKey: vi.fn(() => true), + }); + vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { + onComplete( + { label: 'Org1', value: 'org1' }, + { label: 'Proj1', value: 'proj1' }, + { label: 'Env1', value: 'env1' }, + 'secret_token', + ); + return null; + }); + const { lastFrame } = render(); + await delay(100); // Allow async operations to complete + expect(lastFrame()).toMatch(/Environment: Env1 selected successfully/); + }); }); diff --git a/tests/cli.test.tsx b/tests/cli.test.tsx new file mode 100644 index 0000000..78033e3 --- /dev/null +++ b/tests/cli.test.tsx @@ -0,0 +1,18 @@ +import { describe, vi, expect, it } from 'vitest'; + +vi.mock('pastel', () => ({ + default: vi.fn().mockImplementation(() => ({ + run: vi.fn(() => Promise.resolve()), + })), +})); + +import Pastel from 'pastel'; + +describe('Cli script', () => { + it('Should run the pastel app', async () => { + await import('../source/cli'); + expect(Pastel).toHaveBeenCalled(); + const pastelInstance = Pastel.mock.results[0].value; + expect(pastelInstance.run).toHaveBeenCalled(); + }); +}); diff --git a/tests/components/EnvironmentSelection.test.tsx b/tests/components/EnvironmentSelection.test.tsx new file mode 100644 index 0000000..0dbac26 --- /dev/null +++ b/tests/components/EnvironmentSelection.test.tsx @@ -0,0 +1,95 @@ +import { vi, it, describe, expect, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import EnvironmentSelection from '../../source/components/EnvironmentSelection'; +import delay from 'delay'; +import { useAuthApi } from '../../source/hooks/useAuthApi'; +import { useApiKeyApi } from '../../source/hooks/useApiKeyApi'; +import { useEnvironmentApi } from '../../source/hooks/useEnvironmentApi'; +import { useOrganisationApi } from '../../source/hooks/useOrganisationApi'; +import { apiCall } from '../../source/lib/api'; +import { g } from 'vitest/dist/chunks/suite.B2jumIFP.js'; +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +vi.mock('../../source/hooks/useAuthApi', () => ({ + useAuthApi: () => ({ + authSwitchOrgs: vi.fn().mockResolvedValue({ + headers: { + getSetCookie: () => ['new-cookie'], + }, + error: null, + }), + }), +})); + +vi.mock('../../source/hooks/useEnvironmentApi', () => ({ + useEnvironmentApi: () => ({ + getEnvironment: vi.fn().mockResolvedValue({ + response: { + name: 'Test Env', + id: 'env1', + project_id: 'proj1', + }, + }), + }), +})); + +vi.mock('../../source/hooks/useOrganisationApi', () => ({ + useOrganisationApi: () => ({ + getOrg: vi.fn().mockResolvedValue({ + response: { + name: 'Test Org', + id: 'org1', + }, + }), + }), +})); + +vi.mock('../../source/hooks/useApiKeyApi', () => ({ + useApiKeyApi: () => ({ + getApiKeyScope: vi.fn().mockResolvedValue({ + response: { + environment_id: 'env1', + project_id: 'proj1', + organization_id: 'org1', + }, + error: null, + status: 200, + }), + getProjectEnvironmentApiKey: vi.fn().mockResolvedValue({ + response: { secret: 'test-secret' }, + error: null, + }), + }), +})); + +describe('EnvironmentSelection', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should handle the where the scope has environment_id and project_id', async () => { + await delay(100); + const onComplete = vi.fn(); + const onError = vi.fn(); + + render( + , + ); + + await delay(100); + expect(onError).not.toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith( + { label: 'Test Org', value: 'org1' }, + { label: '', value: 'proj1' }, + { label: 'Test Env', value: 'env1' }, + 'test-token', + ); + }); +}); From 5edd1f8f7980a72a9f38585dbd24e43b18acfa35 Mon Sep 17 00:00:00 2001 From: Gabriel Manor Date: Mon, 30 Dec 2024 10:28:56 +0200 Subject: [PATCH 14/19] Extend delay for API Key command --- tests/apiKey.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/apiKey.test.tsx b/tests/apiKey.test.tsx index 98af3fb..17870d2 100644 --- a/tests/apiKey.test.tsx +++ b/tests/apiKey.test.tsx @@ -27,7 +27,7 @@ describe('ApiKey', () => { const { lastFrame } = render( , ); - await delay(50); + await delay(100); expect(lastFrame()).toMatch(/permit_key_aaaaaaa/); }); it('Invalid Key', async () => { From 6b206dbb3eb82d9a28de4bbc0f31b3c5617b7f89 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Mon, 30 Dec 2024 09:04:10 +0000 Subject: [PATCH 15/19] test: mocked clipboardy functions Signed-off-by: Abinand P --- tests/github.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/github.test.tsx b/tests/github.test.tsx index 98ff6a8..148be1d 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -16,6 +16,12 @@ import { import { loadAuthToken } from '../source/lib/auth.js'; const demoPermitKey = 'permit_key_'.concat('a'.repeat(97)); + +vi.mock("clipboardy",()=> ({ + default:{ + writeSync: vi.fn(), + } +})) vi.mock('../source/lib/auth.js', () => ({ loadAuthToken: vi.fn(() => demoPermitKey), })); From 76c7db4182dcc5c0144c64fb169547de9855c557 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Mon, 30 Dec 2024 09:13:43 +0000 Subject: [PATCH 16/19] ci: added libsecrete in ci Signed-off-by: Abinand P --- .github/workflows/node.js.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 2da32dc..7c5d5c9 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -26,5 +26,9 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci + - name: install depdencies + run: | + sudo apt-get update + sudo apt-get install -y libsecret-1-dev - run: npm run build --if-present - run: npm run lint From b3cfe20fdfe9cadecc32d85b25e7d541322a173f Mon Sep 17 00:00:00 2001 From: Abinand P Date: Mon, 30 Dec 2024 09:16:54 +0000 Subject: [PATCH 17/19] ci: removed duplicate download Signed-off-by: Abinand P --- .github/workflows/node.js.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e60ae6b..f2bbd18 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -32,10 +32,6 @@ jobs: # Comment: Required for secure credential storage by certain Node.js packages - run: npm ci - - name: install depdencies - run: | - sudo apt-get update - sudo apt-get install -y libsecret-1-dev - run: npm run build --if-present - run: npm run lint - run: npm run test From 95a21fabd2f23f8add7af761f933a241f4c96139 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Mon, 30 Dec 2024 10:51:16 +0000 Subject: [PATCH 18/19] test: fixed the keytar depdency Signed-off-by: Abinand P --- source/commands/apiKey.tsx | 8 ++++-- source/commands/pdp/check.tsx | 6 ++++- source/components/LoginFlow.tsx | 2 +- source/lib/auth.ts | 2 +- tests/PDPCheck.test.tsx | 6 ++++- tests/apiKey.test.tsx | 13 ++++++++- tests/github.test.tsx | 9 +++---- tests/lib/auth.test.ts | 47 ++++++++++++++++++++++++++------- tests/logout.test.tsx | 7 +++++ 9 files changed, 79 insertions(+), 21 deletions(-) diff --git a/source/commands/apiKey.tsx b/source/commands/apiKey.tsx index fe4c932..c71484c 100644 --- a/source/commands/apiKey.tsx +++ b/source/commands/apiKey.tsx @@ -4,7 +4,7 @@ import zod from 'zod'; import { keyAccountOption } from '../options/keychain.js'; import { KEYSTORE_PERMIT_SERVICE_NAME } from '../config.js'; -import keytar from 'keytar'; +import * as keytar from 'keytar'; export const args = zod.tuple([ zod @@ -34,7 +34,11 @@ export default function ApiKey({ args, options }: Props) { keytar .getPassword(KEYSTORE_PERMIT_SERVICE_NAME, options.keyAccount) .then(value => setReadKey(value || '')) - .catch(reason => setReadKey(`-- Failed to read key- reason ${reason}`)); + .catch(reason => + setReadKey( + `-- Failed to read key- reason ${reason instanceof Error ? reason.message : String(reason)}`, + ), + ); } }, [action, options.keyAccount]); diff --git a/source/commands/pdp/check.tsx b/source/commands/pdp/check.tsx index fb8770f..fc111b8 100644 --- a/source/commands/pdp/check.tsx +++ b/source/commands/pdp/check.tsx @@ -162,7 +162,11 @@ export default function Check({ options }: Props) { queryPDP(apiKey); }) .catch(reason => { - setError(reason); + if (reason instanceof Error) { + setError(reason.message); + } else { + setError(String(reason)); + } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options.keyAccount]); diff --git a/source/components/LoginFlow.tsx b/source/components/LoginFlow.tsx index 3b33c17..132e42f 100644 --- a/source/components/LoginFlow.tsx +++ b/source/components/LoginFlow.tsx @@ -43,7 +43,7 @@ const LoginFlow: React.FC = ({ } onSuccess(token, headers.getSetCookie()[0] ?? ''); } catch (error: unknown) { - onError(`Unexpected error during authentication. ${error}`); + onError(`Unexpected error during authentication. ${error as string}`); return; } } diff --git a/source/lib/auth.ts b/source/lib/auth.ts index 4f2be4b..ea96fe3 100644 --- a/source/lib/auth.ts +++ b/source/lib/auth.ts @@ -1,7 +1,7 @@ import { createHash, randomBytes } from 'node:crypto'; import * as http from 'node:http'; import open from 'open'; -import pkg from 'keytar'; +import * as pkg from 'keytar'; import { DEFAULT_PERMIT_KEYSTORE_ACCOUNT, KEYSTORE_PERMIT_SERVICE_NAME, diff --git a/tests/PDPCheck.test.tsx b/tests/PDPCheck.test.tsx index 260c097..cfa1d99 100644 --- a/tests/PDPCheck.test.tsx +++ b/tests/PDPCheck.test.tsx @@ -3,9 +3,12 @@ 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'; +import * as keytar from 'keytar'; global.fetch = vi.fn(); - +vi.mock('keytar', () => ({ + getPassword: vi.fn().mockResolvedValue('permit_key_a'.concat('a').repeat(97)), +})); describe('PDP Check Component', () => { afterEach(() => { // Clear mock calls after each test @@ -29,6 +32,7 @@ describe('PDP Check Component', () => { `Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`, ); await delay(50); + console.log(lastFrame()); expect(lastFrame()?.toString()).toContain('ALLOWED'); }); it('should render with the given options', async () => { diff --git a/tests/apiKey.test.tsx b/tests/apiKey.test.tsx index 17870d2..ea87b1d 100644 --- a/tests/apiKey.test.tsx +++ b/tests/apiKey.test.tsx @@ -1,8 +1,15 @@ import React from 'react'; import { render } from 'ink-testing-library'; import ApiKey from '../source/commands/apiKey'; -import { describe, it, expect } from 'vitest'; +import { vi, describe, it, expect } from 'vitest'; import delay from 'delay'; +import * as keytar from 'keytar'; + +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), + deletePassword: vi.fn(), +})); describe('ApiKey', () => { it('Should save the key', () => { @@ -13,7 +20,9 @@ describe('ApiKey', () => { expect(lastFrame()).toMatch(/Key saved to secure key store./); }); it('Should validate the key', () => { + const { getPassword } = keytar; const permitKey = 'permit_key_'.concat('a'.repeat(97)); + (getPassword as any).mockResolvedValueOnce(permitKey); const { lastFrame } = render( { }); it('Should read the key', async () => { const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { getPassword } = keytar; + (getPassword as any).mockResolvedValueOnce(permitKey); const { lastFrame } = render( , ); diff --git a/tests/github.test.tsx b/tests/github.test.tsx index 148be1d..cbb20cc 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -16,12 +16,11 @@ import { import { loadAuthToken } from '../source/lib/auth.js'; const demoPermitKey = 'permit_key_'.concat('a'.repeat(97)); - -vi.mock("clipboardy",()=> ({ - default:{ +vi.mock('clipboardy', () => ({ + default: { writeSync: vi.fn(), - } -})) + }, +})); vi.mock('../source/lib/auth.js', () => ({ loadAuthToken: vi.fn(() => demoPermitKey), })); diff --git a/tests/lib/auth.test.ts b/tests/lib/auth.test.ts index c1e1d8e..947320d 100644 --- a/tests/lib/auth.test.ts +++ b/tests/lib/auth.test.ts @@ -1,13 +1,14 @@ 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'; +import * as pkg from 'keytar'; +// Mock dependencies vi.mock('http', () => ({ createServer: vi.fn().mockReturnValue({ listen: vi.fn(), @@ -26,17 +27,26 @@ vi.mock('node:crypto', () => ({ })), })); +// Correct mock for 'keytar' using named exports +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), // Mocked return value + deletePassword: vi.fn(), +})); + 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); @@ -47,47 +57,66 @@ describe('Token Type', () => { describe('Save Auth Token', () => { it('Should save token', async () => { const demoToken = 'permit_key_'.concat('a'.repeat(97)); + const { setPassword } = pkg; const result = await auth.saveAuthToken(demoToken); - expect(result).toBe(''); + expect(setPassword).toBeCalledWith( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + demoToken, + ); + expect(result).toBe(''); // Ensure the result is empty as expected }); + 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); + await auth.saveAuthToken(demoToken); // Save token first + const { getPassword } = pkg; + (getPassword as any).mockResolvedValueOnce( + 'permit_key_a'.concat('a'.repeat(97)), + ); const result = await auth.loadAuthToken(); - expect(result).toBe(demoToken); + expect(result).toBe('permit_key_a'.concat('a'.repeat(97))); // Mocked return value }); + it('Should throw error', async () => { - await pkg.deletePassword( + const { deletePassword } = await import('keytar'); + await deletePassword( KEYSTORE_PERMIT_SERVICE_NAME, DEFAULT_PERMIT_KEYSTORE_ACCOUNT, ); + try { await auth.loadAuthToken(); } catch (error) { - expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(Error); // Expect an error when the token is not found } }); }); + describe('Clean Auth Token', () => { it('Should clean token', async () => { + const { getPassword } = pkg; await auth.cleanAuthToken(); - const result = await pkg.getPassword( + (getPassword as any).mockResolvedValueOnce(null); + const result = await getPassword( KEYSTORE_PERMIT_SERVICE_NAME, DEFAULT_PERMIT_KEYSTORE_ACCOUNT, ); - expect(result).toBeNull(); + expect(result).toBeNull(); // Expect null after cleaning the token }); }); + describe('Browser Auth', () => { it('Should open browser', async () => { await auth.browserAuth(); - expect(open).toHaveBeenCalled(); + expect(open).toHaveBeenCalled(); // Ensure the browser opens }); }); diff --git a/tests/logout.test.tsx b/tests/logout.test.tsx index aa62f5a..5b14f22 100644 --- a/tests/logout.test.tsx +++ b/tests/logout.test.tsx @@ -3,6 +3,13 @@ import React from 'react'; import { render } from 'ink-testing-library'; import Logout from '../source/commands/logout'; import delay from 'delay'; +import * as keytar from 'keytar'; + +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), + deletePassword: vi.fn(), +})); describe('Logout', () => { beforeEach(() => { From 7daf0b951d2008d6e50fb62e6a665da226c9f1d2 Mon Sep 17 00:00:00 2001 From: Abinand P Date: Mon, 30 Dec 2024 11:01:55 +0000 Subject: [PATCH 19/19] test: fixed additional keytar depdency Signed-off-by: Abinand P --- tests/OPAPolicy.test.tsx | 6 ++++++ tests/PDPRun.test.tsx | 11 ++++++++++- tests/login.test.tsx | 8 ++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/OPAPolicy.test.tsx b/tests/OPAPolicy.test.tsx index d961e96..7b3427c 100644 --- a/tests/OPAPolicy.test.tsx +++ b/tests/OPAPolicy.test.tsx @@ -3,8 +3,14 @@ import { describe, it, expect, vi } from 'vitest'; import { render } from 'ink-testing-library'; import Policy from '../source/commands/opa/policy'; import delay from 'delay'; +import * as keytar from "keytar" global.fetch = vi.fn(); const enter = '\r'; +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) describe('OPA Policy Command', () => { it('should render the policy command', async () => { diff --git a/tests/PDPRun.test.tsx b/tests/PDPRun.test.tsx index d2a6b71..2723c8d 100644 --- a/tests/PDPRun.test.tsx +++ b/tests/PDPRun.test.tsx @@ -1,10 +1,19 @@ import React from 'react'; -import { describe, expect, it } from 'vitest'; +import {vi, describe, expect, it } from 'vitest'; import { render } from 'ink-testing-library'; import Run from '../source/commands/pdp/run'; +import * as keytar from "keytar" + +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) describe('PDP Run', () => { it('Should render the PDP Run command', () => { + const {getPassword} = keytar; + (getPassword as any).mockResolvedValueOnce("permit_key_".concat("a".repeat(97))) const { lastFrame } = render(); expect(lastFrame()?.toString()).toMatch(/Loading Token/); }); diff --git a/tests/login.test.tsx b/tests/login.test.tsx index 9a1f92e..635cc9f 100644 --- a/tests/login.test.tsx +++ b/tests/login.test.tsx @@ -3,6 +3,14 @@ import { vi, expect, it, describe } from 'vitest'; import { render } from 'ink-testing-library'; import Login from '../source/commands/login'; import delay from 'delay'; +import * as keytar from "keytar" + +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) + describe('Login Component', () => { it('Should render the login component', async () => { const { lastFrame } = render(