feat(policies): UI fuer ABAC-Richtlinien mit Conditions-Builder — Modul 10/16 des UI-Backlogs
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Policies settings page tests — ABAC entity policy management UI
|
||||
* (UI-Backlog module 10/16).
|
||||
*
|
||||
* Covers: rendering, entity type tabs, permission gating, policy cards,
|
||||
* create form with conditions builder, toggle enable/disable, edit flow,
|
||||
* delete flow.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { SettingsPoliciesPage } from '@/pages/SettingsPolicies';
|
||||
import type { Policy } from '@/api/policies';
|
||||
|
||||
const createMut = vi.fn().mockResolvedValue({});
|
||||
const updateMut = vi.fn().mockResolvedValue({});
|
||||
const deleteMut = vi.fn().mockResolvedValue({});
|
||||
|
||||
const makePolicy = (overrides: Partial<Policy> = {}): Policy => ({
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
name: 'VIP-Kunden erlauben',
|
||||
entity_type: 'contact',
|
||||
principal_type: 'user',
|
||||
principal_id: '22222222-2222-2222-2222-222222222222',
|
||||
effect: 'allow',
|
||||
conditions: {
|
||||
operator: 'AND',
|
||||
rules: [{ field: 'status', op: 'eq', value: 'active' }],
|
||||
},
|
||||
priority: 10,
|
||||
tenant_id: 't-1',
|
||||
enabled: true,
|
||||
created_at: '2026-09-01T10:00:00Z',
|
||||
updated_at: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
let mockItems: Policy[] = [];
|
||||
let mockCanRead = true;
|
||||
let mockCanWrite = true;
|
||||
|
||||
vi.mock('@/api/policies', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/api/policies')>();
|
||||
return {
|
||||
...actual,
|
||||
usePolicies: () => ({
|
||||
data: { items: mockItems, total: mockItems.length },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}),
|
||||
useCreatePolicy: () => ({ mutate: createMut, isPending: false }),
|
||||
useUpdatePolicy: () => ({ mutate: updateMut, isPending: false }),
|
||||
useDeletePolicy: () => ({ mutate: deleteMut, isPending: false }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/api/users', () => ({
|
||||
useUsers: () => ({
|
||||
data: {
|
||||
items: [
|
||||
{ id: '22222222-2222-2222-2222-222222222222', name: 'Admin User', email: 'admin@test.de' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/groups', () => ({
|
||||
useGroups: () => ({
|
||||
data: {
|
||||
items: [{ id: '33333333-3333-3333-3333-333333333333', name: 'Sales' }],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/roles', () => ({
|
||||
useRoles: () => ({
|
||||
data: {
|
||||
items: [{ id: '44444444-4444-4444-4444-444444444444', name: 'manager' }],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/usePermission', () => ({
|
||||
usePermission: () => ({
|
||||
hasPermission: (perm: string) =>
|
||||
(mockCanRead || perm !== 'policies:read') &&
|
||||
(mockCanWrite || perm !== 'policies:write'),
|
||||
}),
|
||||
}));
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<SettingsPoliciesPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockItems = [];
|
||||
mockCanRead = true;
|
||||
mockCanWrite = true;
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('SettingsPoliciesPage', () => {
|
||||
it('renders page title and entity type tabs', () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId('policies-page')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('policy-tab-contact')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('policy-tab-file')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('policy-tab-task')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('policy-tab-workflow')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no policies exist', () => {
|
||||
renderPage();
|
||||
expect(screen.getByTestId('policies-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no-permission card when policies:read is missing', () => {
|
||||
mockCanRead = false;
|
||||
renderPage();
|
||||
expect(screen.getByTestId('policies-no-permission')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('policies-empty')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders policy cards with effect badge and conditions summary', () => {
|
||||
mockItems = [makePolicy()];
|
||||
renderPage();
|
||||
const card = screen.getByTestId('policy-card-11111111-1111-1111-1111-111111111111');
|
||||
expect(card).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('policy-effect-11111111-1111-1111-1111-111111111111'),
|
||||
).toHaveTextContent('Erlauben');
|
||||
expect(
|
||||
screen.getByTestId('policy-conditions-11111111-1111-1111-1111-111111111111'),
|
||||
).toHaveTextContent('status eq active');
|
||||
});
|
||||
|
||||
it('shows deny badge and disabled badge for deny policies', () => {
|
||||
mockItems = [makePolicy({ effect: 'deny', enabled: false })];
|
||||
renderPage();
|
||||
expect(
|
||||
screen.getByTestId('policy-effect-11111111-1111-1111-1111-111111111111'),
|
||||
).toHaveTextContent('Verweigern');
|
||||
expect(screen.getByText('Deaktiviert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides action buttons without policies:write', () => {
|
||||
mockItems = [makePolicy()];
|
||||
mockCanWrite = false;
|
||||
renderPage();
|
||||
expect(
|
||||
screen.queryByTestId('policy-edit-11111111-1111-1111-1111-111111111111'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('policy-create-btn')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the create form, adds a rule, and submits the payload', async () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('policy-create-btn'));
|
||||
|
||||
const nameInput = screen.getByTestId('policy-form-name');
|
||||
fireEvent.change(nameInput, { target: { value: 'Neue Richtlinie' } });
|
||||
fireEvent.change(screen.getByTestId('policy-form-principal'), {
|
||||
target: { value: '22222222-2222-2222-2222-222222222222' },
|
||||
});
|
||||
|
||||
// Add one condition rule (whitelisted field from ABAC_ALLOWED_FIELDS.contact)
|
||||
fireEvent.click(screen.getByTestId('policy-add-rule'));
|
||||
const ruleRow = screen.getByTestId('policy-rule-0');
|
||||
const fieldSelect = ruleRow.querySelector('select');
|
||||
if (fieldSelect) {
|
||||
fireEvent.change(fieldSelect, { target: { value: 'status' } });
|
||||
}
|
||||
const valueInput = ruleRow.querySelector('input');
|
||||
if (valueInput) {
|
||||
fireEvent.change(valueInput, { target: { value: 'active' } });
|
||||
}
|
||||
|
||||
fireEvent.click(screen.getByTestId('policy-form-submit'));
|
||||
|
||||
await waitFor(() => expect(createMut).toHaveBeenCalled());
|
||||
const call = createMut.mock.calls[0][0];
|
||||
expect(call.name).toBe('Neue Richtlinie');
|
||||
expect(call.entity_type).toBe('contact');
|
||||
expect(call.principal_id).toBe('22222222-2222-2222-2222-222222222222');
|
||||
expect(call.conditions).toEqual({
|
||||
operator: 'AND',
|
||||
rules: [{ field: 'status', op: 'eq', value: 'active' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('submits null conditions when no rules are added', async () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('policy-create-btn'));
|
||||
fireEvent.change(screen.getByTestId('policy-form-name'), {
|
||||
target: { value: 'Alles erlauben' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('policy-form-principal'), {
|
||||
target: { value: '33333333-3333-3333-3333-333333333333' },
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('policy-form-submit'));
|
||||
|
||||
await waitFor(() => expect(createMut).toHaveBeenCalled());
|
||||
expect(createMut.mock.calls[0][0].conditions).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles a policy enabled state via update mutation', async () => {
|
||||
mockItems = [makePolicy()];
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('policy-toggle-11111111-1111-1111-1111-111111111111'));
|
||||
await waitFor(() =>
|
||||
expect(updateMut).toHaveBeenCalledWith({
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
data: { enabled: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the edit form pre-filled with policy values', async () => {
|
||||
mockItems = [makePolicy()];
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('policy-edit-11111111-1111-1111-1111-111111111111'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('policy-form-name')).toHaveValue('VIP-Kunden erlauben');
|
||||
});
|
||||
// number inputs report numeric values
|
||||
expect(screen.getByTestId('policy-form-priority')).toHaveValue(10);
|
||||
});
|
||||
|
||||
it('deletes a policy after confirm', async () => {
|
||||
mockItems = [makePolicy()];
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('policy-delete-11111111-1111-1111-1111-111111111111'));
|
||||
await waitFor(() =>
|
||||
expect(deleteMut).toHaveBeenCalledWith({
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
entityType: 'contact',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('switches entity type tab and shows the tab as selected', () => {
|
||||
renderPage();
|
||||
const tab = screen.getByTestId('policy-tab-file');
|
||||
fireEvent.click(tab);
|
||||
expect(tab).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user