Files

645 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const PageManager = require('../../pages/page-manager.js');
const testLogger = require('../utils/test-logger.js');
const { isCloudEnvironment } = require('../utils/cloud-auth.js');
test.describe.configure({ mode: 'parallel' });
test.describe("Organization Management - CRUD Operations", () => {
let pageManager;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pageManager = new PageManager(page);
// Navigate to organizations page
await pageManager.createOrgPage.navigateToOrg();
await page.waitForLoadState('domcontentloaded');
testLogger.info('Organization management test setup completed');
});
test("should create a new organization and verify it appears in the list", {
tag: ['@organization', '@all', '@crud', '@create']
}, async ({ page }) => {
testLogger.info('Testing organization creation');
// Generate unique organization name
const timestamp = Date.now();
const randomSuffix = Math.floor(Math.random() * 1000);
const orgName = `testorg_${timestamp}_${randomSuffix}`;
let orgIdentifier = null;
try {
testLogger.info(`Creating organization: ${orgName}`);
// Create organization through UI
await pageManager.createOrgPage.clickAddOrg();
await pageManager.createOrgPage.fillOrgName(orgName);
// Verify save button is enabled
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
expect(isSaveEnabled).toBe(true);
// Save organization
await pageManager.createOrgPage.clickSaveOrg();
// Assert the success toast fires and the drawer closes on success.
await pageManager.createOrgPage.verifyOrgAddedSuccessfully('Organization added successfully.');
await expect(pageManager.createOrgPage.addUpdateDialog).not.toBeVisible({ timeout: 10000 });
// Search and verify organization appears in the list
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Get the organization identifier for cleanup (already searched above)
orgIdentifier = await pageManager.createOrgPage.getOrgIdentifierFromTable(orgName);
testLogger.info(`✓ Organization ${orgName} created successfully with identifier: ${orgIdentifier}`);
} finally {
// NOTE: Organization deletion is not supported in this environment
// Organizations created during tests will persist
if (orgIdentifier) {
testLogger.info(`⚠️ Organization cleanup skipped - deletion not supported in this environment`);
testLogger.info(`Organization ${orgName} (${orgIdentifier}) will remain in the system`);
// Attempt cleanup anyway to log the actual response
testLogger.info(`Attempting cleanup for diagnostic purposes...`);
const deleted = await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier);
if (!deleted) {
await pageManager.createOrgPage.deleteOrgViaUI(orgName);
}
}
}
});
test("should search organization by name and find it", {
tag: ['@organization', '@all', '@search', '@name']
}, async ({ page }) => {
testLogger.info('Testing organization search by name');
// Generate unique organization name
const timestamp = Date.now();
const randomSuffix = Math.floor(Math.random() * 1000);
const orgName = `searchtest_${timestamp}_${randomSuffix}`;
let orgIdentifier = null;
try {
// Create organization first
testLogger.info(`Creating organization for search test: ${orgName}`);
await pageManager.createOrgPage.clickAddOrg();
await pageManager.createOrgPage.fillOrgName(orgName);
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
// Get identifier for cleanup
orgIdentifier = await pageManager.createOrgPage.getOrgIdentifierFromTable(orgName);
// Search for the organization
testLogger.info(`Searching for organization: ${orgName}`);
await pageManager.createOrgPage.searchOrg(orgName);
// Verify organization is found
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Clear search to show all organizations
await pageManager.createOrgPage.clearSearchOrg();
testLogger.info(`✓ Organization ${orgName} found via name search`);
} finally {
// NOTE: Organization deletion is not supported in this environment
if (orgIdentifier) {
testLogger.info(`⚠️ Organization cleanup skipped - deletion not supported`);
testLogger.info(`Organization ${orgName} (${orgIdentifier}) will remain in the system`);
// Attempt cleanup for diagnostic purposes
const deleted = await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier);
if (!deleted) {
await pageManager.createOrgPage.deleteOrgViaUI(orgName);
}
}
}
});
test("should create organization via API and verify identifier", {
tag: ['@organization', '@all', '@api', '@identifier']
}, async ({ page }) => {
testLogger.info('Testing organization creation via API and identifier verification');
// Generate unique organization name
const timestamp = Date.now();
const randomSuffix = Math.floor(Math.random() * 1000);
const orgName = `apitest_${timestamp}_${randomSuffix}`;
let orgIdentifier = null;
try {
testLogger.info(`Creating organization via API: ${orgName}`);
// Create organization via API (this returns the identifier)
orgIdentifier = await pageManager.createOrgPage.createOrg(orgName);
testLogger.info(`✓ Organization created via API with identifier: ${orgIdentifier}`);
// Refresh the page to see the new organization
await page.reload();
await page.waitForLoadState('domcontentloaded');
await pageManager.createOrgPage.navigateToOrg();
// Search and verify organization exists in UI
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Verify it's findable by searching again
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
testLogger.info(`✓ Organization ${orgName} verified in UI with identifier ${orgIdentifier}`);
} finally {
// NOTE: Organization deletion is not supported in this environment
if (orgIdentifier) {
testLogger.info(`⚠️ Organization cleanup skipped - deletion not supported`);
testLogger.info(`Organization ${orgName} (${orgIdentifier}) will remain in the system`);
// Attempt cleanup for diagnostic purposes
await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier);
}
}
});
test("should search for non-existent organization and show no results", {
tag: ['@organization', '@all', '@search', '@negative']
}, async ({ page }) => {
testLogger.info('Testing search for non-existent organization');
const nonExistentOrgName = `nonexistent_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
testLogger.info(`Searching for non-existent organization: ${nonExistentOrgName}`);
// Search for non-existent organization
await pageManager.createOrgPage.searchOrg(nonExistentOrgName);
// Verify no results are shown
await pageManager.createOrgPage.verifyOrgNotExists();
testLogger.info('✓ No results shown for non-existent organization search');
});
test("should validate organization name input field", {
tag: ['@organization', '@all', '@validation', '@ui']
}, async ({ page }) => {
testLogger.info('Testing organization name validation');
// Click to add organization
await pageManager.createOrgPage.clickAddOrg();
// Try to save with empty name
testLogger.info('Testing save with empty organization name');
const isSaveEnabledEmpty = await pageManager.createOrgPage.checkSaveEnabled();
// Save button should be disabled or validation should prevent saving
if (isSaveEnabledEmpty) {
testLogger.info('Save button enabled - testing if validation prevents save');
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
// Organization should not be created with empty name
} else {
testLogger.info('✓ Save button correctly disabled for empty name');
}
// Test with valid name
const validOrgName = `valid_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
testLogger.info(`Testing with valid name: ${validOrgName}`);
await pageManager.createOrgPage.fillOrgName(validOrgName);
const isSaveEnabledValid = await pageManager.createOrgPage.checkSaveEnabled();
expect(isSaveEnabledValid).toBe(true);
// Cancel instead of creating
await pageManager.createOrgPage.clickCancelButton();
testLogger.info('✓ Organization name validation working correctly');
});
test("should handle empty organization name validation", {
tag: ['@organization', '@all', '@validation', '@negative']
}, async ({ page }) => {
testLogger.info('Testing empty organization name validation');
try {
// Click to add organization
await pageManager.createOrgPage.clickAddOrg();
// Dialog must be open.
await expect(pageManager.createOrgPage.addUpdateDialog).toBeVisible();
// R3 (Zod migration): Save stays ENABLED; the schema gates the submit
// and reveals the inline required error. Assert deterministically.
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
expect(isSaveEnabled).toBe(true);
testLogger.info('Attempting to save organization with empty name');
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
const validationError = pageManager.createOrgPage.getOrgNameError();
await expect(validationError).toBeVisible({ timeout: 10000 });
await expect(validationError).toContainText('Name is required');
} finally {
// Try to cancel/close the dialog
try {
await pageManager.createOrgPage.clickCancelButton();
} catch (error) {
// If cancel fails, the dialog might have closed already
testLogger.info('Cancel button not available - dialog may have closed');
}
}
testLogger.info('✓ Empty name validation test completed');
});
test("should handle whitespace-only organization name", {
tag: ['@organization', '@all', '@validation', '@negative']
}, async ({ page }) => {
testLogger.info('Testing whitespace-only organization name');
try {
await pageManager.createOrgPage.clickAddOrg();
// Fill with only spaces
const whitespaceNames = [' ', '\t\t', '\n\n', ' \t \n '];
for (const whitespaceName of whitespaceNames) {
testLogger.info(`Testing with whitespace pattern: "${whitespaceName.replace(/\s/g, '·')}"`);
await pageManager.createOrgPage.fillOrgName(whitespaceName);
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
if (isSaveEnabled) {
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
// Check for validation error
const validationError = pageManager.createOrgPage.getOrgNameError();
if (await validationError.isVisible({ timeout: 2000 })) {
testLogger.info('✓ Whitespace-only name properly rejected with validation message');
expect(await validationError.isVisible()).toBe(true);
} else {
testLogger.info('ℹ No validation message shown for whitespace-only name');
}
} else {
testLogger.info('✓ Save button disabled for whitespace-only name');
}
// Clear field for next test
await pageManager.createOrgPage.clearOrgName();
}
} finally {
try {
await pageManager.createOrgPage.clickCancelButton();
} catch (error) {
testLogger.info('Cancel not available - continuing');
}
}
testLogger.info('✓ Whitespace-only name validation completed');
});
test("should handle special characters in organization name", {
tag: ['@organization', '@all', '@validation', '@specialChars']
}, async ({ page }) => {
testLogger.info('Testing special characters in organization name');
const timestamp = Date.now();
// All entries below contain characters outside the allowed set
// /^[a-zA-Z0-9_ ]+$/ so the form must reject them.
const specialCharTests = [
{ name: `test<script>alert('xss')</script>_${timestamp}`, description: 'HTML/JS injection' },
{ name: `test"SELECT*FROM users"_${timestamp}`, description: 'SQL-like syntax' },
{ name: `test@#$%^&*()_${timestamp}`, description: 'Special symbols' },
{ name: `test café ñoño_${timestamp}`, description: 'Unicode characters' },
{ name: `test\`DROP TABLE\`_${timestamp}`, description: 'SQL injection attempt' },
{ name: `test🚀🎉📊_${timestamp}`, description: 'Emoji characters' }
];
// Open the drawer once and reuse it for every iteration. Cancel does
// not clear the ?action=add route query, so a close-then-reopen flow
// is unreliable.
await pageManager.createOrgPage.clickAddOrg();
try {
for (const testCase of specialCharTests) {
testLogger.info(`Testing ${testCase.description}: "${testCase.name}"`);
await pageManager.createOrgPage.clearOrgName();
await pageManager.createOrgPage.fillOrgName(testCase.name);
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
if (isSaveEnabled) {
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
}
// All inputs here violate /^[a-zA-Z0-9_ ]+$/ so the inline charset
// error must surface.
const orgNameError = pageManager.createOrgPage.getOrgNameError();
await expect(orgNameError, `Charset error should show for ${testCase.description}`).toBeVisible({ timeout: 10000 });
await expect(orgNameError).toContainText('Use alphanumeric characters, space and underscore only.');
// Invalid names cause either Save to stay disabled, or onSubmit() to
// early-return when clicked — in both cases the drawer must still
// be open and the org-name input still present.
const drawerStillOpen = await pageManager.createOrgPage.isDrawerOpen();
expect(drawerStillOpen, `Drawer should remain open for invalid input: ${testCase.description}`).toBe(true);
testLogger.info(`✓ Organization with ${testCase.description} properly rejected (drawer remains open)`);
}
} finally {
try {
await pageManager.createOrgPage.clickCancelButton();
} catch (error) {
testLogger.info('Cancel not available - drawer may have closed');
}
}
testLogger.info('✓ Special characters validation test completed');
});
test("should handle numeric-only organization name", {
tag: ['@organization', '@all', '@validation', '@numeric']
}, async ({ page }) => {
testLogger.info('Testing numeric-only organization name');
let orgIdentifier = null;
const numericName = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
try {
testLogger.info(`Testing numeric-only name: ${numericName}`);
await pageManager.createOrgPage.clickAddOrg();
await pageManager.createOrgPage.fillOrgName(numericName);
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
expect(isSaveEnabled).toBe(true);
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
// Verify numeric name is accepted
await pageManager.createOrgPage.searchOrg(numericName);
await pageManager.createOrgPage.verifyOrgExists(numericName);
orgIdentifier = await pageManager.createOrgPage.getOrgIdentifierFromTable(numericName);
testLogger.info('✓ Numeric-only organization name accepted');
} finally {
// NOTE: Organization deletion is not supported - org will persist
if (orgIdentifier) {
testLogger.info(`⚠️ Organization ${numericName} (${orgIdentifier}) will remain in system`);
await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier);
}
}
});
test("should show proper validation message for invalid characters", {
tag: ['@organization', '@all', '@validation', '@errorMessage']
}, async ({ page }) => {
testLogger.info('Testing validation error message for invalid characters');
// Every entry below must violate /^[a-zA-Z0-9_ ]+$/ — names that only
// use letters, digits, spaces, and underscores are valid and would
// cause the org to be created (which is not what this test is about).
const invalidCharacterTests = [
'!@#$%',
'test<script>',
'name!with#special',
'test&validation',
'café🚀'
];
// Open the drawer once. Reusing it across iterations avoids the
// close/reopen reliability issue (the parent component's watcher
// does not re-fire if ?action=add is already in the URL).
await pageManager.createOrgPage.clickAddOrg();
try {
for (const invalidName of invalidCharacterTests) {
testLogger.info(`Testing validation message for: "${invalidName}"`);
await pageManager.createOrgPage.clearOrgName();
await pageManager.createOrgPage.fillOrgName(invalidName);
const isSaveEnabled = await pageManager.createOrgPage.checkSaveEnabled();
if (isSaveEnabled) {
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
}
// The inline charset error must surface with its exact message.
const orgNameError = pageManager.createOrgPage.getOrgNameError();
await expect(orgNameError, `Charset error should show for "${invalidName}"`).toBeVisible({ timeout: 10000 });
await expect(orgNameError).toContainText('Use alphanumeric characters, space and underscore only.');
// Invalid names cause either Save to stay disabled, or onSubmit() to
// return early when clicked. In both cases the drawer must stay
// open. The input keeps the rejected value, which is the strongest
// observable signal that submission was blocked.
const drawerStillOpen = await pageManager.createOrgPage.isDrawerOpen();
expect(drawerStillOpen, `Drawer should remain open for invalid input: "${invalidName}"`).toBe(true);
testLogger.info(`✓ Invalid name "${invalidName}" rejected — drawer remained open`);
}
} finally {
try {
await pageManager.createOrgPage.clickCancelButton();
} catch (error) {
testLogger.info('Cancel not available - drawer may have closed');
}
}
testLogger.info('✓ Validation message testing completed');
});
test("should clear the org name error when corrected to a valid value", {
tag: ['@organization', '@all', '@validation', '@errorMessage']
}, async ({ page }) => {
testLogger.info('Testing org name error clears on correction');
try {
await pageManager.createOrgPage.clickAddOrg();
// Fill invalid → submit reveals the charset error (R3 submit-then-change).
await pageManager.createOrgPage.fillOrgName('bad@name!');
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
const orgNameError = pageManager.createOrgPage.getOrgNameError();
await expect(orgNameError).toBeVisible({ timeout: 10000 });
await expect(orgNameError).toContainText('Use alphanumeric characters, space and underscore only.');
// After the first submit the form revalidates live on change, so
// correcting to a valid value clears the error and keeps Save enabled.
await pageManager.createOrgPage.clearOrgName();
await pageManager.createOrgPage.fillOrgName('valid_name');
await expect(orgNameError).not.toBeVisible();
expect(await pageManager.createOrgPage.checkSaveEnabled()).toBe(true);
testLogger.info('✓ Org name error correctly cleared on fix');
} finally {
try {
await pageManager.createOrgPage.clickCancelButton();
} catch (error) {
testLogger.info('Cancel not available - drawer may have closed');
}
}
});
test("should not add organization when cancel is clicked", {
tag: ['@organization', '@all', '@validation', '@cancel']
}, async ({ page }) => {
testLogger.info('Testing organization is not added when cancel is clicked');
const timestamp = Date.now();
const orgName = `cancelorg_${timestamp}_${Math.floor(Math.random() * 1000)}`;
await pageManager.createOrgPage.clickAddOrg();
await pageManager.createOrgPage.fillOrgName(orgName);
// Cancel closes the dialog without creating the org.
await pageManager.createOrgPage.clickCancelButton();
await expect(pageManager.createOrgPage.addUpdateDialog).not.toBeVisible({ timeout: 10000 });
// The organization must not exist in the list.
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgNotExists();
testLogger.info('✓ Organization correctly not added after cancel');
});
test("should test organization management workflow end-to-end", {
tag: ['@organization', '@all', '@workflow', '@e2e']
}, async ({ page }) => {
testLogger.info('Testing complete organization management workflow');
// Generate unique organization names
const timestamp = Date.now();
const randomSuffix = Math.floor(Math.random() * 1000);
const orgName = `workflow_${timestamp}_${randomSuffix}`;
const apiOrgName = `api_${timestamp}_${randomSuffix}`;
let orgIdentifier1 = null;
let orgIdentifier2 = null;
try {
// Step 1: Create organization via UI
testLogger.info(`Step 1: Creating organization ${orgName}`);
await pageManager.createOrgPage.clickAddOrg();
await pageManager.createOrgPage.fillOrgName(orgName);
await pageManager.createOrgPage.clickSaveOrgAndWaitForResult();
// Step 2: Verify it appears in the list (search first to find it in the long list)
testLogger.info('Step 2: Verifying organization appears in list');
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Get identifier for cleanup after verification
orgIdentifier1 = await pageManager.createOrgPage.getOrgIdentifierFromTable(orgName);
// Step 3: Search for the organization
testLogger.info('Step 3: Searching for organization');
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Step 4: Clear search and verify it's still there by searching again
testLogger.info('Step 4: Clearing search and verifying persistence');
await pageManager.createOrgPage.clearSearchOrg();
// Search again to verify it persists
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
// Step 5: Create another organization via API and verify both exist
testLogger.info(`Step 5: Creating second organization via API: ${apiOrgName}`);
orgIdentifier2 = await pageManager.createOrgPage.createOrg(apiOrgName);
// Refresh to see API-created organization
await page.reload();
await page.waitForLoadState('domcontentloaded');
await pageManager.createOrgPage.navigateToOrg();
// Verify both organizations exist by searching for them individually
testLogger.info('Verifying first organization exists');
await pageManager.createOrgPage.searchOrg(orgName);
await pageManager.createOrgPage.verifyOrgExists(orgName);
testLogger.info('Verifying second organization exists');
await pageManager.createOrgPage.searchOrg(apiOrgName);
await pageManager.createOrgPage.verifyOrgExists(apiOrgName);
testLogger.info(`✓ Workflow completed successfully. API org identifier: ${orgIdentifier2}`);
} finally {
// NOTE: Organization deletion is not supported - orgs will persist
if (orgIdentifier1) {
testLogger.info(`⚠️ Organization ${orgName} (${orgIdentifier1}) will remain in system`);
await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier1);
}
if (orgIdentifier2) {
testLogger.info(`⚠️ Organization ${apiOrgName} (${orgIdentifier2}) will remain in system`);
await pageManager.createOrgPage.deleteOrgViaAPI(orgIdentifier2);
}
testLogger.info('Workflow test cleanup completed');
}
});
test("should fetch all organizations via GET /api/_meta/organizations?page_size=1000000", {
tag: ['@organization', '@all', '@api', '@adminOrg']
}, async ({ page }) => {
test.skip(!isCloudEnvironment(), 'Admin org listing via _meta is only available in cloud enterprise');
testLogger.info('Testing GET /api/_meta/organizations?page_size=1000000 endpoint');
testLogger.info('Calling admin org listing API for _meta org');
const { status, data } = await pageManager.createOrgPage.getAdminOrgs();
testLogger.info(`Response status: ${status}`);
testLogger.info(`Response data: ${JSON.stringify(data)}`);
expect(status).toBe(200);
expect(data).toBeDefined();
expect(data).toHaveProperty('list');
expect(Array.isArray(data.list)).toBe(true);
expect(data.list.length).toBeGreaterThan(0);
testLogger.info(`✓ _meta admin org listing API returned ${data.list.length} organization(s)`);
});
test("should return 401 unauthorized when fetching organizations with non-_meta org", {
tag: ['@organization', '@all', '@api', '@adminOrg', '@negative']
}, async ({ page }) => {
test.skip(!isCloudEnvironment(), 'Admin org 401 check is only applicable in cloud enterprise');
testLogger.info('Testing GET /api/{orgId}/organizations?page_size=1000000 with non-_meta org');
const orgId = process.env.ORGNAME || 'default';
testLogger.info(`Calling admin org listing API with non-_meta orgId: ${orgId}`);
const { status, data } = await pageManager.createOrgPage.getAdminOrgs(orgId);
testLogger.info(`Response status: ${status}`);
testLogger.info(`Response data: ${JSON.stringify(data)}`);
expect(status).toBe(401);
expect(data).toBeDefined();
expect(data).toHaveProperty('code', 401);
expect(data).toHaveProperty('message', 'Unauthorized Access');
testLogger.info(`✓ Non-_meta org correctly returned 401 Unauthorized Access`);
});
test.afterEach(async () => {
testLogger.info('Organization test cleanup completed');
});
});