Files
cve-dashboard/frontend/src/__tests__/UserProfilePanel.property.test.js

154 lines
5.5 KiB
JavaScript
Raw Normal View History

/**
* Property-Based Test: Profile panel displays all required fields
*
* Feature: user-profile, Property 1: Profile panel displays all required fields
* **Validates: Requirements 1.2**
*
* For any valid profile object with arbitrary username, email, group, created_at,
* and last_login values, rendering UserProfilePanel displays all five values
* in the output.
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import fc from 'fast-check';
import UserProfilePanel from '../components/UserProfilePanel';
// Replicate the component's formatting logic so we know what to expect in the DOM
function formatGroupName(group) {
if (!group) return '';
return group.replace(/_/g, ' ');
}
function formatDate(dateStr) {
if (!dateStr) return 'Never';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return 'Unknown';
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}) + ' at ' + date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
} catch {
return 'Unknown';
}
}
// Generate ISO date strings from integer timestamps to avoid invalid Date issues
const MIN_TS = new Date('2020-01-01T00:00:00Z').getTime();
const MAX_TS = new Date('2030-12-31T23:59:59Z').getTime();
const isoDateArbitrary = fc
.integer({ min: MIN_TS, max: MAX_TS })
.map(ts => new Date(ts).toISOString());
// Arbitrary that generates valid profile objects.
// Use minLength >= 3 for username to avoid single-character strings that
// match substrings in other UI text (e.g., "d" appearing in "Password").
// Use a custom email generator with a longer local part for the same reason.
const profileArbitrary = fc.record({
id: fc.integer({ min: 1, max: 100000 }),
username: fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/),
email: fc.tuple(
fc.stringMatching(/^[a-z]{4,10}$/),
fc.stringMatching(/^[a-z]{3,8}$/),
fc.constantFrom('com', 'org', 'net', 'io')
).map(([local, domain, tld]) => `${local}@${domain}.${tld}`),
group: fc.constantFrom('Admin', 'Standard_User', 'Leadership', 'Read_Only'),
created_at: isoDateArbitrary,
last_login: isoDateArbitrary,
});
/**
* Helper: find all fieldValue spans in the rendered component.
* The component renders each profile field in a fieldRow div containing
* a fieldLabel span and a fieldValue span. We query by the known label
* text to locate the corresponding value span.
*/
function getFieldValueByLabel(container, labelText) {
// Each field row has structure:
// <div style={fieldRow}>
// <svg ... />
// <div style={fieldContent}>
// <span style={fieldLabel}>LABEL</span>
// <span style={fieldValue}>VALUE</span>
// </div>
// </div>
const labels = container.querySelectorAll('span');
for (const label of labels) {
if (label.textContent.trim().toLowerCase() === labelText.toLowerCase()) {
// The value span is the next sibling of the label span
const valueSibling = label.nextElementSibling;
if (valueSibling) {
return valueSibling.textContent;
}
}
}
return null;
}
beforeEach(() => {
process.env.REACT_APP_API_BASE = 'http://localhost:3001/api';
});
afterEach(() => {
jest.restoreAllMocks();
});
it('Property 1: Profile panel displays all required fields for any valid profile', async () => {
await fc.assert(
fc.asyncProperty(profileArbitrary, async (profile) => {
// Mock fetch to return the generated profile
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({ ...profile }),
});
const onClose = jest.fn();
const { container, unmount } = render(
<UserProfilePanel isOpen={true} onClose={onClose} />
);
try {
// Wait for the profile data to be fetched and rendered.
// Check for the username label to confirm the profile section loaded.
await waitFor(() => {
expect(getFieldValueByLabel(container, 'Username')).not.toBeNull();
}, { timeout: 3000 });
// Assert all five field values appear in their respective field rows
// 1. Username — rendered directly in the Username field value span
const usernameValue = getFieldValueByLabel(container, 'Username');
expect(usernameValue).toBe(profile.username);
// 2. Email — rendered directly in the Email field value span
const emailValue = getFieldValueByLabel(container, 'Email');
expect(emailValue).toBe(profile.email);
// 3. Group — rendered through formatGroupName in the Group field value span
const groupValue = getFieldValueByLabel(container, 'Group');
const expectedGroup = formatGroupName(profile.group);
expect(groupValue).toContain(expectedGroup);
// 4. Created at — rendered through formatDate in the Account Created field value span
const createdAtValue = getFieldValueByLabel(container, 'Account Created');
const expectedCreatedAt = formatDate(profile.created_at);
expect(createdAtValue).toBe(expectedCreatedAt);
// 5. Last login — rendered through formatDate in the Last Login field value span
const lastLoginValue = getFieldValueByLabel(container, 'Last Login');
const expectedLastLogin = formatDate(profile.last_login);
expect(lastLoginValue).toBe(expectedLastLogin);
} finally {
// Clean up to avoid leaking state between iterations
unmount();
}
}),
{ numRuns: 100 }
);
}, 120000);