Skip to content

Commit fba0df2

Browse files
committed
chore: add unit tests
1 parent 1e40397 commit fba0df2

94 files changed

Lines changed: 9934 additions & 201 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/app/src/app/(app)/[orgId]/auditor/(overview)/page.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb';
22
import { serverApi } from '@/lib/api-server';
3+
import { parseRolesString } from '@/lib/permissions';
34
import { Role } from '@db';
45
import type { Metadata } from 'next';
56
import { notFound, redirect } from 'next/navigation';
67
import { AuditorView } from './components/AuditorView';
78

8-
function parseRolesString(rolesStr: string | null | undefined): Role[] {
9-
if (!rolesStr) return [];
10-
return rolesStr
11-
.split(',')
12-
.map((r) => r.trim())
13-
.filter((r) => r in Role) as Role[];
14-
}
15-
169
interface PeopleMember {
1710
userId: string;
1811
role: string;
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
setMockPermissions,
5+
ADMIN_PERMISSIONS,
6+
AUDITOR_PERMISSIONS,
7+
NO_PERMISSIONS,
8+
mockHasPermission,
9+
} from '@/test-utils/mocks/permissions';
10+
11+
vi.mock('@/hooks/use-permissions', () => ({
12+
usePermissions: () => ({
13+
permissions: {},
14+
hasPermission: mockHasPermission,
15+
}),
16+
}));
17+
18+
// Mock useControls hook
19+
const mockDeleteControl = vi.fn();
20+
vi.mock('../../hooks/useControls', () => ({
21+
useControls: () => ({
22+
deleteControl: mockDeleteControl,
23+
controls: [],
24+
isLoading: false,
25+
error: null,
26+
mutate: vi.fn(),
27+
createControl: vi.fn(),
28+
}),
29+
}));
30+
31+
// Mock next/navigation
32+
vi.mock('next/navigation', () => ({
33+
useRouter: () => ({
34+
push: vi.fn(),
35+
refresh: vi.fn(),
36+
}),
37+
}));
38+
39+
// Mock sonner
40+
vi.mock('sonner', () => ({
41+
toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() },
42+
}));
43+
44+
// Mock @comp/ui components
45+
vi.mock('@comp/ui/button', () => ({
46+
Button: ({ children, disabled, ...props }: any) => (
47+
<button disabled={disabled} {...props}>
48+
{children}
49+
</button>
50+
),
51+
}));
52+
53+
vi.mock('@comp/ui/dialog', () => ({
54+
Dialog: ({ children, open }: any) =>
55+
open ? <div data-testid="dialog">{children}</div> : null,
56+
DialogContent: ({ children }: any) => (
57+
<div data-testid="dialog-content">{children}</div>
58+
),
59+
DialogDescription: ({ children }: any) => <p>{children}</p>,
60+
DialogFooter: ({ children }: any) => <div>{children}</div>,
61+
DialogHeader: ({ children }: any) => <div>{children}</div>,
62+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
63+
}));
64+
65+
vi.mock('@comp/ui/form', () => ({
66+
Form: ({ children, ...props }: any) => (
67+
<div data-testid="form-provider" {...props}>
68+
{children}
69+
</div>
70+
),
71+
}));
72+
73+
// Mock lucide-react
74+
vi.mock('lucide-react', () => ({
75+
Trash2: () => <span data-testid="trash-icon" />,
76+
}));
77+
78+
import { ControlDeleteDialog } from './ControlDeleteDialog';
79+
80+
const mockControl = {
81+
id: 'ctrl-1',
82+
name: 'Access Control',
83+
description: 'Test control',
84+
organizationId: 'org-1',
85+
createdAt: new Date('2024-01-01'),
86+
updatedAt: new Date('2024-01-01'),
87+
lastUpdatedBy: null,
88+
projectId: null,
89+
} as any;
90+
91+
const defaultProps = {
92+
isOpen: true,
93+
onClose: vi.fn(),
94+
control: mockControl,
95+
};
96+
97+
describe('ControlDeleteDialog', () => {
98+
beforeEach(() => {
99+
vi.clearAllMocks();
100+
});
101+
102+
describe('Permission gating', () => {
103+
it('enables the delete button when user has control:delete permission', () => {
104+
setMockPermissions(ADMIN_PERMISSIONS);
105+
106+
render(<ControlDeleteDialog {...defaultProps} />);
107+
108+
const deleteButton = screen.getByRole('button', { name: /delete/i });
109+
expect(deleteButton).not.toBeDisabled();
110+
});
111+
112+
it('disables the delete button when user lacks control:delete permission (auditor)', () => {
113+
setMockPermissions(AUDITOR_PERMISSIONS);
114+
115+
render(<ControlDeleteDialog {...defaultProps} />);
116+
117+
const deleteButton = screen.getByRole('button', { name: /delete/i });
118+
expect(deleteButton).toBeDisabled();
119+
});
120+
121+
it('disables the delete button when user has no permissions', () => {
122+
setMockPermissions(NO_PERMISSIONS);
123+
124+
render(<ControlDeleteDialog {...defaultProps} />);
125+
126+
const deleteButton = screen.getByRole('button', { name: /delete/i });
127+
expect(deleteButton).toBeDisabled();
128+
});
129+
130+
it('checks the correct resource and action for permission', () => {
131+
setMockPermissions(ADMIN_PERMISSIONS);
132+
133+
render(<ControlDeleteDialog {...defaultProps} />);
134+
135+
expect(mockHasPermission).toHaveBeenCalledWith('control', 'delete');
136+
});
137+
});
138+
139+
describe('Rendering', () => {
140+
it('renders dialog title and description', () => {
141+
setMockPermissions(ADMIN_PERMISSIONS);
142+
143+
render(<ControlDeleteDialog {...defaultProps} />);
144+
145+
expect(screen.getByText('Delete Control')).toBeInTheDocument();
146+
expect(
147+
screen.getByText(/are you sure you want to delete this control/i),
148+
).toBeInTheDocument();
149+
});
150+
151+
it('renders the cancel button that is always enabled', () => {
152+
setMockPermissions(AUDITOR_PERMISSIONS);
153+
154+
render(<ControlDeleteDialog {...defaultProps} />);
155+
156+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
157+
expect(cancelButton).not.toBeDisabled();
158+
});
159+
160+
it('does not render when isOpen is false', () => {
161+
setMockPermissions(ADMIN_PERMISSIONS);
162+
163+
render(
164+
<ControlDeleteDialog {...defaultProps} isOpen={false} />,
165+
);
166+
167+
expect(screen.queryByText('Delete Control')).not.toBeInTheDocument();
168+
});
169+
});
170+
});

apps/app/src/app/(app)/[orgId]/controls/[controlId]/components/ControlDeleteDialog.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useForm } from 'react-hook-form';
1919
import { toast } from 'sonner';
2020
import { z } from 'zod';
2121
import { useControls } from '../../hooks/useControls';
22+
import { usePermissions } from '@/hooks/use-permissions';
2223

2324
const formSchema = z.object({
2425
comment: z.string().optional(),
@@ -34,6 +35,7 @@ interface ControlDeleteDialogProps {
3435

3536
export function ControlDeleteDialog({ isOpen, onClose, control }: ControlDeleteDialogProps) {
3637
const { deleteControl } = useControls();
38+
const { hasPermission } = usePermissions();
3739
const router = useRouter();
3840
const [isSubmitting, setIsSubmitting] = useState(false);
3941

@@ -72,7 +74,7 @@ export function ControlDeleteDialog({ isOpen, onClose, control }: ControlDeleteD
7274
<Button type="button" variant="outline" onClick={onClose} disabled={isSubmitting}>
7375
Cancel
7476
</Button>
75-
<Button type="submit" variant="destructive" disabled={isSubmitting} className="gap-2">
77+
<Button type="submit" variant="destructive" disabled={isSubmitting || !hasPermission('control', 'delete')} className="gap-2">
7678
{isSubmitting ? (
7779
<span className="flex items-center gap-2">
7880
<span className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
setMockPermissions,
5+
ADMIN_PERMISSIONS,
6+
AUDITOR_PERMISSIONS,
7+
NO_PERMISSIONS,
8+
mockHasPermission,
9+
} from '@/test-utils/mocks/permissions';
10+
11+
vi.mock('@/hooks/use-permissions', () => ({
12+
usePermissions: () => ({
13+
permissions: {},
14+
hasPermission: mockHasPermission,
15+
}),
16+
}));
17+
18+
// Mock useFrameworks hook
19+
const mockDeleteFramework = vi.fn();
20+
vi.mock('../../hooks/useFrameworks', () => ({
21+
useFrameworks: () => ({
22+
deleteFramework: mockDeleteFramework,
23+
frameworks: [],
24+
isLoading: false,
25+
error: null,
26+
mutate: vi.fn(),
27+
addFrameworks: vi.fn(),
28+
}),
29+
}));
30+
31+
// Mock next/navigation
32+
vi.mock('next/navigation', () => ({
33+
useRouter: () => ({
34+
push: vi.fn(),
35+
refresh: vi.fn(),
36+
}),
37+
}));
38+
39+
// Mock sonner
40+
vi.mock('sonner', () => ({
41+
toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() },
42+
}));
43+
44+
// Mock @comp/ui components
45+
vi.mock('@comp/ui/button', () => ({
46+
Button: ({ children, disabled, ...props }: any) => (
47+
<button disabled={disabled} {...props}>
48+
{children}
49+
</button>
50+
),
51+
}));
52+
53+
vi.mock('@comp/ui/dialog', () => ({
54+
Dialog: ({ children, open }: any) =>
55+
open ? <div data-testid="dialog">{children}</div> : null,
56+
DialogContent: ({ children }: any) => (
57+
<div data-testid="dialog-content">{children}</div>
58+
),
59+
DialogDescription: ({ children }: any) => <p>{children}</p>,
60+
DialogFooter: ({ children }: any) => <div>{children}</div>,
61+
DialogHeader: ({ children }: any) => <div>{children}</div>,
62+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
63+
}));
64+
65+
vi.mock('@comp/ui/form', () => ({
66+
Form: ({ children, ...props }: any) => (
67+
<div data-testid="form-provider" {...props}>
68+
{children}
69+
</div>
70+
),
71+
}));
72+
73+
// Mock lucide-react
74+
vi.mock('lucide-react', () => ({
75+
Trash2: () => <span data-testid="trash-icon" />,
76+
}));
77+
78+
import { FrameworkDeleteDialog } from './FrameworkDeleteDialog';
79+
80+
const mockFrameworkInstance = {
81+
id: 'fi-1',
82+
organizationId: 'org-1',
83+
frameworkId: 'fw-1',
84+
framework: {
85+
id: 'fw-1',
86+
name: 'SOC 2',
87+
description: 'SOC 2 Type II compliance framework',
88+
},
89+
controls: [],
90+
createdAt: new Date('2024-01-01'),
91+
updatedAt: new Date('2024-01-01'),
92+
} as any;
93+
94+
const defaultProps = {
95+
isOpen: true,
96+
onClose: vi.fn(),
97+
frameworkInstance: mockFrameworkInstance,
98+
};
99+
100+
describe('FrameworkDeleteDialog', () => {
101+
beforeEach(() => {
102+
vi.clearAllMocks();
103+
});
104+
105+
describe('Permission gating', () => {
106+
it('enables the delete button when user has framework:delete permission', () => {
107+
setMockPermissions(ADMIN_PERMISSIONS);
108+
109+
render(<FrameworkDeleteDialog {...defaultProps} />);
110+
111+
const deleteButton = screen.getByRole('button', { name: /delete/i });
112+
expect(deleteButton).not.toBeDisabled();
113+
});
114+
115+
it('disables the delete button when user lacks framework:delete permission (auditor)', () => {
116+
setMockPermissions(AUDITOR_PERMISSIONS);
117+
118+
render(<FrameworkDeleteDialog {...defaultProps} />);
119+
120+
const deleteButton = screen.getByRole('button', { name: /delete/i });
121+
expect(deleteButton).toBeDisabled();
122+
});
123+
124+
it('disables the delete button when user has no permissions', () => {
125+
setMockPermissions(NO_PERMISSIONS);
126+
127+
render(<FrameworkDeleteDialog {...defaultProps} />);
128+
129+
const deleteButton = screen.getByRole('button', { name: /delete/i });
130+
expect(deleteButton).toBeDisabled();
131+
});
132+
133+
it('checks the correct resource and action for permission', () => {
134+
setMockPermissions(ADMIN_PERMISSIONS);
135+
136+
render(<FrameworkDeleteDialog {...defaultProps} />);
137+
138+
expect(mockHasPermission).toHaveBeenCalledWith('framework', 'delete');
139+
});
140+
});
141+
142+
describe('Rendering', () => {
143+
it('renders dialog title and description', () => {
144+
setMockPermissions(ADMIN_PERMISSIONS);
145+
146+
render(<FrameworkDeleteDialog {...defaultProps} />);
147+
148+
expect(screen.getByText('Delete Framework')).toBeInTheDocument();
149+
expect(
150+
screen.getByText(/are you sure you want to delete this framework/i),
151+
).toBeInTheDocument();
152+
});
153+
154+
it('renders the cancel button that is always enabled', () => {
155+
setMockPermissions(AUDITOR_PERMISSIONS);
156+
157+
render(<FrameworkDeleteDialog {...defaultProps} />);
158+
159+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
160+
expect(cancelButton).not.toBeDisabled();
161+
});
162+
163+
it('does not render when isOpen is false', () => {
164+
setMockPermissions(ADMIN_PERMISSIONS);
165+
166+
render(
167+
<FrameworkDeleteDialog {...defaultProps} isOpen={false} />,
168+
);
169+
170+
expect(screen.queryByText('Delete Framework')).not.toBeInTheDocument();
171+
});
172+
});
173+
});

0 commit comments

Comments
 (0)