forked from permitio/permit-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Related to permitio#12 Add unit tests and integrate Jest for 90% code coverage. * **Add Jest and Update Configuration** - Add Jest as a dev dependency in `package.json`. - Update the `test` script to use Jest. - Add Jest configuration to `package.json`. * **Update GitHub Actions Workflow** - Add a step to run Jest tests in `.github/workflows/node.js.yml`. - Add a step to generate coverage reports. * **Add Unit Tests** - Add unit tests for `ApiKey` component in `source/commands/apiKey.test.tsx`. - Add unit tests for `Login` component in `source/commands/login.test.tsx`. - Add unit tests for `Logout` component in `source/commands/logout.test.tsx`. - Add unit tests for `Policy` component in `source/commands/opa/policy.test.tsx`. - Add unit tests for `Check` component in `source/commands/pdp/check.test.tsx`. - Add unit tests for `Run` component in `source/commands/pdp/run.test.tsx`. - Add unit tests for `AuthProvider` component in `source/components/AuthProvider.test.tsx`. - Add unit tests for `PDPCommand` component in `source/components/PDPCommand.test.tsx`. * **Update Documentation** - Add instructions for running tests locally in `README.md`. - Add instructions for writing new tests in `README.md`.
- Loading branch information
1 parent
f70b563
commit 31983b6
Showing
11 changed files
with
480 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
"dev": " NODE_NO_WARNINGS=1 tsc --watch", | ||
"lint": "eslint \"source/**/*.{js,ts,tsx}\"", | ||
"lint:fix": "eslint \"source/**/*.{js,ts,tsx}\" --fix", | ||
"test": "prettier --check ./source && npx tsx --test test*.tsx", | ||
"test": "jest --coverage", | ||
"simple-check": "npx tsx ./source/cli.tsx pdp check -u [email protected] -a create -r task" | ||
}, | ||
"files": [ | ||
|
@@ -56,6 +56,15 @@ | |
"prettier": "^3.3.3", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.6.3", | ||
"typescript-eslint": "^8.11.0" | ||
"typescript-eslint": "^8.11.0", | ||
"jest": "^29.0.0", | ||
"ts-jest": "^29.0.0" | ||
}, | ||
"jest": { | ||
"preset": "ts-jest", | ||
"testEnvironment": "node", | ||
"collectCoverage": true, | ||
"coverageDirectory": "coverage", | ||
"coverageReporters": ["text", "lcov"] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React from 'react'; | ||
import { render } from 'ink-testing-library'; | ||
import { test, expect } from '@jest/globals'; | ||
import ApiKey, { args, options } from './apiKey'; | ||
import keytar from 'keytar'; | ||
|
||
jest.mock('keytar'); | ||
|
||
const mockGetPassword = keytar.getPassword as jest.Mock; | ||
const mockSetPassword = keytar.setPassword as jest.Mock; | ||
|
||
const defaultProps = { | ||
args: ['save', 'permit_key_1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567'], | ||
options: { | ||
keyAccount: 'testAccount', | ||
}, | ||
}; | ||
|
||
test('ApiKey component - save valid key', () => { | ||
const { lastFrame } = render(<ApiKey {...defaultProps} />); | ||
expect(lastFrame()).toContain('Key saved to secure key store.'); | ||
expect(mockSetPassword).toHaveBeenCalledWith('Permit.io', 'testAccount', 'permit_key_1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567'); | ||
}); | ||
|
||
test('ApiKey component - validate valid key', () => { | ||
const props = { | ||
...defaultProps, | ||
args: ['validate', 'permit_key_1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567'], | ||
}; | ||
const { lastFrame } = render(<ApiKey {...props} />); | ||
expect(lastFrame()).toContain('Key is valid.'); | ||
}); | ||
|
||
test('ApiKey component - read key', async () => { | ||
mockGetPassword.mockResolvedValue('permit_key_1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567'); | ||
const props = { | ||
...defaultProps, | ||
args: ['read', ''], | ||
}; | ||
const { lastFrame } = render(<ApiKey {...props} />); | ||
await new Promise(resolve => setTimeout(resolve, 0)); // Wait for useEffect | ||
expect(lastFrame()).toContain('permit_key_1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567'); | ||
expect(mockGetPassword).toHaveBeenCalledWith('Permit.io', 'testAccount'); | ||
}); | ||
|
||
test('ApiKey component - invalid key', () => { | ||
const props = { | ||
...defaultProps, | ||
args: ['save', 'invalid_key'], | ||
}; | ||
const { lastFrame } = render(<ApiKey {...props} />); | ||
expect(lastFrame()).toContain('Key is not valid.'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import React from 'react'; | ||
import { render, act } from 'ink-testing-library'; | ||
import Login from './login'; | ||
import { apiCall } from '../lib/api'; | ||
import { authCallbackServer, browserAuth, saveAuthToken, TokenType, tokenType } from '../lib/auth'; | ||
|
||
jest.mock('../lib/api'); | ||
jest.mock('../lib/auth'); | ||
|
||
describe('Login Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should display login message initially', () => { | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
expect(lastFrame()).toContain('Login to Permit'); | ||
}); | ||
|
||
it('should display logging in message when logging in', async () => { | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Logging in...'); | ||
}); | ||
|
||
it('should display organizations after logging in', async () => { | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'org1', name: 'Org 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(browserAuth as jest.Mock).mockResolvedValueOnce('verifier'); | ||
(authCallbackServer as jest.Mock).mockResolvedValueOnce('token'); | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Select an organization'); | ||
}); | ||
|
||
it('should display projects after selecting an organization', async () => { | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'org1', name: 'Org 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'project1', name: 'Project 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(browserAuth as jest.Mock).mockResolvedValueOnce('verifier'); | ||
(authCallbackServer as jest.Mock).mockResolvedValueOnce('token'); | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Select a project'); | ||
}); | ||
|
||
it('should display environments after selecting a project', async () => { | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'org1', name: 'Org 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'project1', name: 'Project 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'env1', name: 'Environment 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(browserAuth as jest.Mock).mockResolvedValueOnce('verifier'); | ||
(authCallbackServer as jest.Mock).mockResolvedValueOnce('token'); | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Select an environment'); | ||
}); | ||
|
||
it('should display success message after selecting an environment', async () => { | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'org1', name: 'Org 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'project1', name: 'Project 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: [{ id: 'env1', name: 'Environment 1' }], | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(apiCall as jest.Mock).mockResolvedValueOnce({ | ||
response: { secret: 'secret' }, | ||
headers: { getSetCookie: jest.fn().mockReturnValue(['cookie']) }, | ||
}); | ||
(browserAuth as jest.Mock).mockResolvedValueOnce('verifier'); | ||
(authCallbackServer as jest.Mock).mockResolvedValueOnce('token'); | ||
const { lastFrame } = render(<Login options={{ key: undefined, workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Logged in as Org 1 with selected environment as Environment 1'); | ||
}); | ||
|
||
it('should display error message for invalid API key', async () => { | ||
const { lastFrame } = render(<Login options={{ key: 'invalid_key', workspace: undefined }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Invalid API Key'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import React from 'react'; | ||
import { render, act } from 'ink-testing-library'; | ||
import Logout from './logout'; | ||
import { cleanAuthToken } from '../lib/auth'; | ||
|
||
jest.mock('../lib/auth'); | ||
|
||
describe('Logout Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should display cleaning session message initially', () => { | ||
const { lastFrame } = render(<Logout />); | ||
expect(lastFrame()).toContain('Cleaning session...'); | ||
}); | ||
|
||
it('should call cleanAuthToken and display logged out message', async () => { | ||
(cleanAuthToken as jest.Mock).mockResolvedValueOnce(undefined); | ||
const { lastFrame } = render(<Logout />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(cleanAuthToken).toHaveBeenCalled(); | ||
expect(lastFrame()).toContain('Logged Out'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react'; | ||
import { render, act } from 'ink-testing-library'; | ||
import Policy from './policy'; | ||
import { loadAuthToken } from '../../lib/auth'; | ||
|
||
jest.mock('../../lib/auth'); | ||
|
||
describe('Policy Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should display loading spinner initially', () => { | ||
const { lastFrame } = render(<Policy options={{ serverUrl: 'http://localhost:8181', keyAccount: 'testAccount' }} />); | ||
expect(lastFrame()).toContain('Listing Policies on Opa Server=http://localhost:8181'); | ||
}); | ||
|
||
it('should display policies after loading', async () => { | ||
(loadAuthToken as jest.Mock).mockResolvedValueOnce('testApiKey'); | ||
const mockFetch = jest.fn().mockResolvedValueOnce({ | ||
json: jest.fn().mockResolvedValueOnce({ result: [{ id: 'policy1' }, { id: 'policy2' }] }), | ||
status: 200, | ||
}); | ||
global.fetch = mockFetch as any; | ||
const { lastFrame } = render(<Policy options={{ serverUrl: 'http://localhost:8181', keyAccount: 'testAccount' }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Showing 2 of 2 policies:'); | ||
}); | ||
|
||
it('should display error message on request failure', async () => { | ||
(loadAuthToken as jest.Mock).mockResolvedValueOnce('testApiKey'); | ||
const mockFetch = jest.fn().mockRejectedValueOnce(new Error('Request failed')); | ||
global.fetch = mockFetch as any; | ||
const { lastFrame } = render(<Policy options={{ serverUrl: 'http://localhost:8181', keyAccount: 'testAccount' }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Request failed:'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import React from 'react'; | ||
import { render, act } from 'ink-testing-library'; | ||
import Check from './check'; | ||
import { keytar } from 'keytar'; | ||
|
||
jest.mock('keytar'); | ||
|
||
describe('Check Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should display checking message initially', () => { | ||
const { lastFrame } = render(<Check options={{ user: 'testUser', action: 'create', resource: 'task', keyAccount: 'testAccount' }} />); | ||
expect(lastFrame()).toContain('Checking user="testUser" action=create resource=task at tenant=default'); | ||
}); | ||
|
||
it('should display allowed message when access is allowed', async () => { | ||
(keytar.getPassword as jest.Mock).mockResolvedValueOnce('testApiKey'); | ||
const mockFetch = jest.fn().mockResolvedValueOnce({ | ||
json: jest.fn().mockResolvedValueOnce({ allow: true }), | ||
status: 200, | ||
}); | ||
global.fetch = mockFetch as any; | ||
const { lastFrame } = render(<Check options={{ user: 'testUser', action: 'create', resource: 'task', keyAccount: 'testAccount' }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('ALLOWED'); | ||
}); | ||
|
||
it('should display denied message when access is denied', async () => { | ||
(keytar.getPassword as jest.Mock).mockResolvedValueOnce('testApiKey'); | ||
const mockFetch = jest.fn().mockResolvedValueOnce({ | ||
json: jest.fn().mockResolvedValueOnce({ allow: false }), | ||
status: 200, | ||
}); | ||
global.fetch = mockFetch as any; | ||
const { lastFrame } = render(<Check options={{ user: 'testUser', action: 'create', resource: 'task', keyAccount: 'testAccount' }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('DENIED'); | ||
}); | ||
|
||
it('should display error message on request failure', async () => { | ||
(keytar.getPassword as jest.Mock).mockResolvedValueOnce('testApiKey'); | ||
const mockFetch = jest.fn().mockRejectedValueOnce(new Error('Request failed')); | ||
global.fetch = mockFetch as any; | ||
const { lastFrame } = render(<Check options={{ user: 'testUser', action: 'create', resource: 'task', keyAccount: 'testAccount' }} />); | ||
await act(async () => { | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
}); | ||
expect(lastFrame()).toContain('Request failed:'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import React from 'react'; | ||
import { render } from 'ink-testing-library'; | ||
import Run from './run'; | ||
import { AuthProvider } from '../../components/AuthProvider'; | ||
import PDPCommand from '../../components/PDPCommand'; | ||
|
||
jest.mock('../../components/AuthProvider', () => ({ | ||
AuthProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>, | ||
})); | ||
|
||
jest.mock('../../components/PDPCommand', () => ({ | ||
__esModule: true, | ||
default: ({ opa }: { opa?: number }) => <div>PDPCommand {opa}</div>, | ||
})); | ||
|
||
describe('Run Component', () => { | ||
it('should render AuthProvider and PDPCommand components', () => { | ||
const { lastFrame } = render(<Run options={{ opa: 8181 }} />); | ||
expect(lastFrame()).toContain('PDPCommand 8181'); | ||
}); | ||
|
||
it('should render PDPCommand without opa prop', () => { | ||
const { lastFrame } = render(<Run options={{}} />); | ||
expect(lastFrame()).toContain('PDPCommand'); | ||
}); | ||
}); |
Oops, something went wrong.