Compare commits
1 Commits
main
...
feat/inges
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
834eac1235 |
@@ -1,20 +1,102 @@
|
|||||||
# Ingestion E2E Scenarios (Updated)
|
# Ingestion E2E Scenarios
|
||||||
|
|
||||||
## 1. View Ingestion Sources
|
## 1. View Ingestion Settings Page
|
||||||
|
|
||||||
- **Precondition:** User is admin
|
- **Precondition:** User is logged in and has admin/editor role
|
||||||
- **Steps:**
|
- **Steps:**
|
||||||
1. Navigate to the Integrations page
|
1. Navigate to Settings page
|
||||||
- **Expected:** List of available data sources/integrations is displayed
|
2. Click on "Ingestion" tab in the settings sidebar
|
||||||
|
- **Expected:**
|
||||||
|
- Page displays "Ingestion Keys" heading
|
||||||
|
- Shows subtitle "Create and manage ingestion keys for the SigNoz Cloud"
|
||||||
|
- Displays ingestion URL and region information (if available)
|
||||||
|
- Shows search box for ingestion keys
|
||||||
|
- Shows "New Ingestion key" button
|
||||||
|
|
||||||
## 2. Configure Ingestion Sources
|
## 2. Search Ingestion Keys
|
||||||
|
|
||||||
- **Precondition:** User is admin
|
- **Precondition:** User is on the Ingestion Settings page
|
||||||
- **Steps:**
|
- **Steps:**
|
||||||
1. Click 'Configure' for a data source/integration
|
1. Enter text in the search box
|
||||||
2. Complete the configuration flow (modal or page, as available)
|
2. Wait for search results to update
|
||||||
- **Expected:** Source is configured (UI feedback/confirmation should be checked)
|
- **Expected:** Table filters to show only matching ingestion keys
|
||||||
|
|
||||||
## 3. Disable/Enable Ingestion
|
## 3. Create New Ingestion Key
|
||||||
|
|
||||||
- **Note:** No visible enable/disable toggle for ingestion sources in the current UI. Ingestion is managed via the Integrations configuration flows.
|
- **Precondition:** User is on the Ingestion Settings page
|
||||||
|
- **Steps:**
|
||||||
|
1. Click "New Ingestion key" button
|
||||||
|
2. Fill in the name field (minimum 6 characters, alphanumeric with underscores/hyphens)
|
||||||
|
3. Set expiration date
|
||||||
|
4. Add optional tags
|
||||||
|
5. Click "Create new Ingestion key" button
|
||||||
|
- **Expected:**
|
||||||
|
- New ingestion key is created
|
||||||
|
- Success notification is shown
|
||||||
|
- New key appears in the table
|
||||||
|
|
||||||
|
## 4. Edit Ingestion Key
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with existing ingestion keys
|
||||||
|
- **Steps:**
|
||||||
|
1. Click the edit (pen) icon for an existing ingestion key
|
||||||
|
2. Modify tags or expiration date
|
||||||
|
3. Click "Update Ingestion Key" button
|
||||||
|
- **Expected:**
|
||||||
|
- Ingestion key is updated
|
||||||
|
- Success notification is shown
|
||||||
|
- Changes are reflected in the table
|
||||||
|
|
||||||
|
## 5. Delete Ingestion Key
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with existing ingestion keys
|
||||||
|
- **Steps:**
|
||||||
|
1. Click the delete (trash) icon for an existing ingestion key
|
||||||
|
2. Confirm deletion in the modal
|
||||||
|
3. Click "Delete Ingestion Key" button
|
||||||
|
- **Expected:**
|
||||||
|
- Ingestion key is deleted
|
||||||
|
- Success notification is shown
|
||||||
|
- Key is removed from the table
|
||||||
|
|
||||||
|
## 6. Copy Ingestion Key Value
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with existing ingestion keys
|
||||||
|
- **Steps:**
|
||||||
|
1. Click the copy icon next to an ingestion key value
|
||||||
|
- **Expected:**
|
||||||
|
- Key value is copied to clipboard
|
||||||
|
- Success notification is shown
|
||||||
|
|
||||||
|
## 7. Copy Ingestion URL and Region
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with deployment data available
|
||||||
|
- **Steps:**
|
||||||
|
1. Click on the ingestion URL to copy it
|
||||||
|
2. Click on the region name to copy it
|
||||||
|
- **Expected:**
|
||||||
|
- Respective values are copied to clipboard
|
||||||
|
- Success notification is shown
|
||||||
|
|
||||||
|
## 8. Manage Ingestion Key Limits - Pending
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with existing ingestion keys
|
||||||
|
- **Steps:**
|
||||||
|
1. Expand an ingestion key to view its details
|
||||||
|
2. For each signal (logs, traces, metrics):
|
||||||
|
- Click "Limits" button to add limits
|
||||||
|
- Or click edit/delete icons to modify existing limits
|
||||||
|
- Configure daily and per-second limits
|
||||||
|
- Save or cancel changes
|
||||||
|
- **Expected:**
|
||||||
|
- Limits are properly configured for each signal
|
||||||
|
- Success notifications are shown for successful operations
|
||||||
|
|
||||||
|
## 9. Pagination
|
||||||
|
|
||||||
|
- **Precondition:** User is on the Ingestion Settings page with multiple ingestion keys
|
||||||
|
- **Steps:**
|
||||||
|
1. Navigate through pagination controls
|
||||||
|
- **Expected:**
|
||||||
|
- Different pages of ingestion keys are displayed
|
||||||
|
- Pagination information shows correct totals
|
||||||
|
|||||||
@@ -1,48 +1,326 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { ensureLoggedIn } from '../../../utils/login.util';
|
import { ensureLoggedIn } from '../../../utils/login.util';
|
||||||
|
|
||||||
test('Ingestion Settings - View and Interact', async ({ page }) => {
|
test.describe('Ingestion Settings', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
await ensureLoggedIn(page);
|
await ensureLoggedIn(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('View Ingestion Settings Page', async ({ page }) => {
|
||||||
// 1. Open the sidebar settings menu using data-testid
|
// 1. Open the sidebar settings menu using data-testid
|
||||||
await page.getByTestId('settings-nav-item').click();
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
|
||||||
// 2. Click Account Settings in the dropdown (by role/name or data-testid if available)
|
// 2. Click Account Settings in the dropdown
|
||||||
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
|
||||||
// Assert the main tabpanel/heading (confirmed by DOM)
|
// Assert the main tabpanel/heading
|
||||||
await expect(page.getByTestId('settings-page-title')).toBeVisible();
|
await expect(page.getByTestId('settings-page-title')).toBeVisible();
|
||||||
|
|
||||||
// Focus on the settings page sidenav
|
// Focus on the settings page sidenav
|
||||||
await page.getByTestId('settings-page-sidenav').focus();
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
|
||||||
// Click Ingestion tab in the settings sidebar (by data-testid)
|
// Click Ingestion tab in the settings sidebar
|
||||||
await page.getByTestId('ingestion').click();
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
// Assert heading and subheading (Integrations page)
|
// Assert heading and subheading
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Integrations' }),
|
page.getByRole('heading', { name: 'Ingestion Keys' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Manage Integrations for this workspace'),
|
page.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Assert presence of search box
|
// Assert presence of search box
|
||||||
await expect(
|
await expect(
|
||||||
page.getByPlaceholder('Search for an integration...'),
|
page.getByPlaceholder('Search for ingestion key...'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Assert at least one data source with Configure button
|
// Assert presence of New Ingestion key button
|
||||||
const configureBtn = page.getByRole('button', { name: 'Configure' }).first();
|
const newBtn = page.getByRole('button', { name: 'New Ingestion key' });
|
||||||
await expect(configureBtn).toBeVisible();
|
await expect(newBtn).toBeVisible();
|
||||||
|
|
||||||
// Assert Request more integrations section
|
// Assert Learn more link
|
||||||
|
await expect(page.getByRole('link', { name: /Learn more/ })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Search Ingestion Keys', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Get the search input
|
||||||
|
const searchInput = page.getByPlaceholder('Search for ingestion key...');
|
||||||
|
await expect(searchInput).toBeVisible();
|
||||||
|
|
||||||
|
// Enter search text
|
||||||
|
await searchInput.fill('test-key');
|
||||||
|
|
||||||
|
// Wait for search to complete (debounced)
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create New Ingestion Key', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Click New Ingestion key button
|
||||||
|
await page.getByRole('button', { name: 'New Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Assert modal is visible
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Create new ingestion key' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Fill in the form
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Enter Ingestion Key name')
|
||||||
|
.fill('test-ingestion-key');
|
||||||
|
|
||||||
|
// Set expiration date (future date)
|
||||||
|
await page.locator('.ant-picker-input').click();
|
||||||
|
|
||||||
|
// enter tomorrow date in yyyy-mm-dd format
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: '* Expiration' })
|
||||||
|
.fill(formattedDate, {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// press enter
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// Click Create button
|
||||||
|
await page.getByRole('button', { name: 'Create new Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Assert success (modal should close and new key should appear)
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Create new ingestion key' }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit Ingestion Key', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Wait for ingestion keys to load
|
||||||
|
await page.waitForSelector('.ingestion-key-container', { timeout: 10000 });
|
||||||
|
|
||||||
|
// if there are no ingestion keys, create a new one
|
||||||
|
if (await page.locator('.ant-empty-description').isVisible()) {
|
||||||
|
// Click New Ingestion key button
|
||||||
|
await page.getByRole('button', { name: 'New Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Assert modal is visible
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Create new ingestion key' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Fill in the form
|
||||||
|
await page
|
||||||
|
.getByPlaceholder('Enter Ingestion Key name')
|
||||||
|
.fill('test-ingestion-key');
|
||||||
|
|
||||||
|
// Set expiration date (future date)
|
||||||
|
await page.locator('.ant-picker-input').click();
|
||||||
|
|
||||||
|
// enter tomorrow date in yyyy-mm-dd format
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: '* Expiration' })
|
||||||
|
.fill(formattedDate, {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// press enter
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// Click Create button
|
||||||
|
await page.getByRole('button', { name: 'Create new Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Assert success (modal should close and new key should appear)
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Create new ingestion key' }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click edit button for the first ingestion key
|
||||||
|
const editButton = page.locator('.action-btn button').first();
|
||||||
|
await editButton.click();
|
||||||
|
|
||||||
|
// Assert edit modal is visible
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Edit Ingestion Key' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Add a new tag
|
||||||
|
await page.getByRole('button', { name: 'plus New Tag' }).click();
|
||||||
|
await page.getByRole('textbox').nth(2).fill('test');
|
||||||
|
await page.getByRole('textbox').nth(2).press('Enter');
|
||||||
|
|
||||||
|
// Update expiration date
|
||||||
|
|
||||||
|
// Set expiration date (future date)
|
||||||
|
await page.locator('.ant-picker-input').click();
|
||||||
|
|
||||||
|
// enter tomorrow date in yyyy-mm-dd format
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const formattedDate = tomorrow.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: '* Expiration' })
|
||||||
|
.fill(formattedDate, {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// press enter
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
// Click Update button
|
||||||
|
await page.getByRole('button', { name: 'Update Ingestion Key' }).click();
|
||||||
|
|
||||||
|
// Assert modal closes
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Edit Ingestion Key' }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Copy Ingestion URL and Region', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Wait for ingestion setup details to load
|
||||||
|
await page.waitForSelector('.ingestion-setup-details-links', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click copy button for ingestion URL
|
||||||
|
const urlCopyButton = page.locator('.ingestion-key-url-value');
|
||||||
|
await urlCopyButton.click();
|
||||||
|
|
||||||
|
// Assert copy success
|
||||||
|
await expect(page.getByText('Copied to clipboard')).toBeVisible();
|
||||||
|
|
||||||
|
// wait for 1 second
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Click copy button for region
|
||||||
|
const regionCopyButton = page.locator('.ingestion-data-region-value');
|
||||||
|
await regionCopyButton.click();
|
||||||
|
|
||||||
|
// Assert copy success
|
||||||
|
await expect(page.getByText('Copied to clipboard')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pagination', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Wait for ingestion keys to load
|
||||||
|
await page.waitForSelector('.ingestion-key-container', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Check if pagination is present
|
||||||
|
const pagination = page.locator('.ant-pagination');
|
||||||
|
if (await pagination.isVisible()) {
|
||||||
|
// Click next page
|
||||||
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
|
|
||||||
|
// Assert page changed
|
||||||
|
await expect(page.getByText('2-')).toBeVisible();
|
||||||
|
|
||||||
|
// Click previous page
|
||||||
|
await page.getByRole('button', { name: 'Previous' }).click();
|
||||||
|
|
||||||
|
// Assert back to first page
|
||||||
|
await expect(page.getByText('1-')).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Form Validation for Create Ingestion Key', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Click New Ingestion key button
|
||||||
|
await page.getByRole('button', { name: 'New Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Try to submit without filling required fields
|
||||||
|
await page.getByRole('button', { name: 'Create new Ingestion key' }).click();
|
||||||
|
|
||||||
|
// Assert validation errors
|
||||||
|
await expect(page.getByText('Please enter Name')).toBeVisible();
|
||||||
|
await expect(page.getByText('Please enter Expiration')).toBeVisible();
|
||||||
|
|
||||||
|
// Test invalid name (too short)
|
||||||
|
await page.getByPlaceholder('Enter Ingestion Key name').fill('abc');
|
||||||
|
await page.getByPlaceholder('Enter Ingestion Key name').blur();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Name must be at least 6 characters'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Test invalid name (special characters)
|
||||||
|
await page.getByPlaceholder('Enter Ingestion Key name').fill('test@key');
|
||||||
|
await page.getByPlaceholder('Enter Ingestion Key name').blur();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
page.getByText(
|
||||||
"Can't find what you’re looking for? Request more integrations",
|
'Ingestion key name should only contain letters, numbers, underscores, and hyphens',
|
||||||
),
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.getByPlaceholder('Enter integration name...')).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
|
// Close modal
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete Ingestion Key', async ({ page }) => {
|
||||||
|
// Navigate to ingestion settings
|
||||||
|
await page.getByTestId('settings-nav-item').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Account Settings' }).click();
|
||||||
|
await page.getByTestId('settings-page-sidenav').focus();
|
||||||
|
await page.getByTestId('ingestion').click();
|
||||||
|
|
||||||
|
// Wait for ingestion keys to load
|
||||||
|
await page.waitForSelector('.ingestion-key-container', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Click delete button for the first ingestion key (second button in action area)
|
||||||
|
const deleteButton = page.locator('.action-btn button').nth(1);
|
||||||
|
await deleteButton.click();
|
||||||
|
|
||||||
|
// Assert delete confirmation modal is visible
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Delete Ingestion Key' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Confirm deletion
|
||||||
|
await page.getByRole('button', { name: 'Delete Ingestion Key' }).click();
|
||||||
|
|
||||||
|
// Assert modal closes
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Delete Ingestion Key' }),
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default defineConfig({
|
|||||||
colorScheme: 'dark',
|
colorScheme: 'dark',
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
@@ -59,7 +60,6 @@ export default defineConfig({
|
|||||||
name: 'firefox',
|
name: 'firefox',
|
||||||
use: { ...devices['Desktop Firefox'] },
|
use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'webkit',
|
name: 'webkit',
|
||||||
use: { ...devices['Desktop Safari'] },
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
|||||||
Reference in New Issue
Block a user