mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-23 09:03:15 -04:00
Merge pull request #20 from lanedirt/17-add-e2e-test-for-alias-edit-page
Add E2E test for AddEdit page
This commit is contained in:
@@ -2,12 +2,18 @@
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<label for="@_inputId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="@_inputId" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
<input type="text" id="@Id" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Id for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Label for the input field.
|
||||
/// </summary>
|
||||
@@ -26,8 +32,6 @@
|
||||
[Parameter]
|
||||
public EventCallback<string?> ValueChanged { get; set; }
|
||||
|
||||
private string _inputId = Guid.NewGuid().ToString();
|
||||
|
||||
private async Task OnInputChanged(ChangeEventArgs e)
|
||||
{
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
|
||||
@@ -51,10 +51,10 @@ else
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Service Name" Value="@(Obj.Service.Name)" ValueChanged="@(val => Obj.Service.Name = val)"></EditFormRow>
|
||||
<EditFormRow Id="service-name" Label="Service Name" Value="@(Obj.Service.Name)" ValueChanged="@(val => Obj.Service.Name = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Service URL" Value="@(Obj.Service.Url)" ValueChanged="@(val => Obj.Service.Url = val)"></EditFormRow>
|
||||
<EditFormRow Id="service-url" Label="Service URL" Value="@(Obj.Service.Url)" ValueChanged="@(val => Obj.Service.Url = val)"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,14 +72,14 @@ else
|
||||
</div>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Email" Value="@(Obj.Identity.EmailPrefix)" ValueChanged="@(val => Obj.Identity.EmailPrefix = val)"></EditFormRow>
|
||||
<EditFormRow Id="email" Label="Email" Value="@(Obj.Identity.EmailPrefix)" ValueChanged="@(val => Obj.Identity.EmailPrefix = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Username" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
<EditFormRow Id="username" Label="Username" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Label="Password" Value="@(Obj.Password.Value)" ValueChanged="@(val => Obj.Password.Value = val)"></EditFormRow>
|
||||
<EditFormRow Id="password" Label="Password" Value="@(Obj.Password.Value)" ValueChanged="@(val => Obj.Password.Value = val)"></EditFormRow>
|
||||
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,43 +94,43 @@ else
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="First Name" Value="@(Obj.Identity.FirstName)" ValueChanged="@(val => Obj.Identity.FirstName = val)"></EditFormRow>
|
||||
<EditFormRow Id="first-name" Label="First Name" Value="@(Obj.Identity.FirstName)" ValueChanged="@(val => Obj.Identity.FirstName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Last Name" Value="@(Obj.Identity.LastName)" ValueChanged="@(val => Obj.Identity.LastName = val)"></EditFormRow>
|
||||
<EditFormRow Id="last-name" Label="Last Name" Value="@(Obj.Identity.LastName)" ValueChanged="@(val => Obj.Identity.LastName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Gender" Value="@(Obj.Identity.Gender)" ValueChanged="@(val => Obj.Identity.Gender = val)"></EditFormRow>
|
||||
<EditFormRow Id="gender" Label="Gender" Value="@(Obj.Identity.Gender)" ValueChanged="@(val => Obj.Identity.Gender = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Nick Name" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
<EditFormRow Id="nickname" Label="Nick Name" Value="@(Obj.Identity.NickName)" ValueChanged="@(val => Obj.Identity.NickName = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Birth Date" Value="@(Obj.Identity.BirthDate)" ValueChanged="@(val => Obj.Identity.BirthDate = val)"></EditFormRow>
|
||||
<EditFormRow Id="birthdate" Label="Birth Date" Value="@(Obj.Identity.BirthDate)" ValueChanged="@(val => Obj.Identity.BirthDate = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Address Street" Value="@(Obj.Identity.AddressStreet)" ValueChanged="@(val => Obj.Identity.AddressStreet = val)"></EditFormRow>
|
||||
<EditFormRow Id="street" Label="Address Street" Value="@(Obj.Identity.AddressStreet)" ValueChanged="@(val => Obj.Identity.AddressStreet = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Address City" Value="@(Obj.Identity.AddressCity)" ValueChanged="@(val => Obj.Identity.AddressCity = val)"></EditFormRow>
|
||||
<EditFormRow Id="city" Label="Address City" Value="@(Obj.Identity.AddressCity)" ValueChanged="@(val => Obj.Identity.AddressCity = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Address State" Value="@(Obj.Identity.AddressState)" ValueChanged="@(val => Obj.Identity.AddressState = val)"></EditFormRow>
|
||||
<EditFormRow Id="state" Label="Address State" Value="@(Obj.Identity.AddressState)" ValueChanged="@(val => Obj.Identity.AddressState = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Address Zip Code" Value="@(Obj.Identity.AddressZipCode)" ValueChanged="@(val => Obj.Identity.AddressZipCode = val)"></EditFormRow>
|
||||
<EditFormRow Id="zipcode" Label="Address Zip Code" Value="@(Obj.Identity.AddressZipCode)" ValueChanged="@(val => Obj.Identity.AddressZipCode = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Address Country" Value="@(Obj.Identity.AddressCountry)" ValueChanged="@(val => Obj.Identity.AddressCountry = val)"></EditFormRow>
|
||||
<EditFormRow Id="country" Label="Address Country" Value="@(Obj.Identity.AddressCountry)" ValueChanged="@(val => Obj.Identity.AddressCountry = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Hobbies" Value="@(Obj.Identity.Hobbies)" ValueChanged="@(val => Obj.Identity.Hobbies = val)"></EditFormRow>
|
||||
<EditFormRow Id="hobbies" Label="Hobbies" Value="@(Obj.Identity.Hobbies)" ValueChanged="@(val => Obj.Identity.Hobbies = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Phone Mobile" Value="@(Obj.Identity.PhoneMobile)" ValueChanged="@(val => Obj.Identity.PhoneMobile = val)"></EditFormRow>
|
||||
<EditFormRow Id="phone-mobile" Label="Phone Mobile" Value="@(Obj.Identity.PhoneMobile)" ValueChanged="@(val => Obj.Identity.PhoneMobile = val)"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Label="Bank Account IBAN" Value="@(Obj.Identity.BankAccountIBAN)" ValueChanged="@(val => Obj.Identity.BankAccountIBAN = val)"></EditFormRow>
|
||||
<EditFormRow Id="iban" Label="Bank Account IBAN" Value="@(Obj.Identity.BankAccountIBAN)" ValueChanged="@(val => Obj.Identity.BankAccountIBAN = val)"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,50 +16,15 @@ public class AliasTests : PlaywrightTest
|
||||
{
|
||||
private static readonly Random Random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to fill all input fields on a page with random data.
|
||||
/// </summary>
|
||||
/// <param name="page">IPage instance where to fill the input fields for.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
public static async Task FillAllInputFields(IPage page)
|
||||
{
|
||||
// Locate all input fields
|
||||
var inputFields = page.Locator("input");
|
||||
|
||||
// Get the count of input fields
|
||||
var count = await inputFields.CountAsync();
|
||||
|
||||
// Iterate through each input field and fill with random data
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var input = inputFields.Nth(i);
|
||||
var inputType = await input.GetAttributeAsync("type");
|
||||
|
||||
// Generate appropriate random data based on input type
|
||||
string randomData = inputType switch
|
||||
{
|
||||
"email" => GenerateRandomEmail(),
|
||||
"number" => GenerateRandomNumber(),
|
||||
"password" => GenerateRandomPassword(),
|
||||
_ => GenerateRandomString(), // Default for all other types
|
||||
};
|
||||
|
||||
await input.FillAsync(randomData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the alias listing index page works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task AliasListingCorrect()
|
||||
public async Task AliasListingTest()
|
||||
{
|
||||
await Page.GotoAsync(AppBaseUrl + "aliases");
|
||||
await WaitForURLAsync("**/aliases");
|
||||
|
||||
// Wait for the content to load.
|
||||
await Page.WaitForSelectorAsync("text=AliasVault");
|
||||
await WaitForURLAsync("**/aliases", "AliasVault");
|
||||
|
||||
// Check if the expected content is present.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
@@ -71,34 +36,115 @@ public class AliasTests : PlaywrightTest
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task CreateAlias()
|
||||
public async Task CreateAliasTest()
|
||||
{
|
||||
await Page.GotoAsync(AppBaseUrl + "add-alias");
|
||||
await WaitForURLAsync("**/add-alias");
|
||||
// Create a new alias with service name = "Test Service".
|
||||
var serviceName = "Test Service";
|
||||
await CreateAlias(new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
});
|
||||
|
||||
// Wait for the content to load.
|
||||
await Page.WaitForSelectorAsync("text=AliasVault");
|
||||
// Check that the service name is present in the content.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain(serviceName), "Created alias service name does not appear on alias page.");
|
||||
}
|
||||
|
||||
// Check if a button with text "Generate Random Identity" appears
|
||||
var generateButton = Page.Locator("text=Generate Random Identity");
|
||||
Assert.That(generateButton, Is.Not.Null, "Generate button not found.");
|
||||
/// <summary>
|
||||
/// Test if editing a created alias works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task EditAliasTest()
|
||||
{
|
||||
// Create a new alias with service name = "Alias service before".
|
||||
var serviceNameBefore = "Alias service before";
|
||||
await CreateAlias(new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceNameBefore },
|
||||
});
|
||||
|
||||
// Fill all input fields with random data
|
||||
await FillAllInputFields(Page);
|
||||
// Check that the service name is present in the content.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain(serviceNameBefore), "Created alias service name does not appear on alias page.");
|
||||
|
||||
// Click the edit button.
|
||||
var editButton = Page.Locator("text=Edit alias").First;
|
||||
await editButton.ClickAsync();
|
||||
await WaitForURLAsync("**/edit", "Save Alias");
|
||||
|
||||
// Replace the service name with "Alias service after".
|
||||
var serviceNameAfter = "Alias service after";
|
||||
await FillInputFields(
|
||||
page: Page,
|
||||
fieldValues: new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceNameAfter },
|
||||
});
|
||||
|
||||
// Press submit button with text "Create Alias"
|
||||
var submitButton = Page.Locator("text=Save Alias").First;
|
||||
await submitButton.ClickAsync();
|
||||
await WaitForURLAsync("**/alias/**");
|
||||
await WaitForURLAsync("**/alias/**", "View alias");
|
||||
|
||||
// Wait for the content to load.
|
||||
await Page.WaitForSelectorAsync("text=Login credentials");
|
||||
// Check if the alias was correctly updated.
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain(serviceNameAfter), "Alias not updated correctly.");
|
||||
}
|
||||
|
||||
// Check if the alias was created
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("Login credentials"), "Alias not created.");
|
||||
/// <summary>
|
||||
/// Helper method to fill specified input fields on a page with given values.
|
||||
/// </summary>
|
||||
/// <param name="page">IPage instance where to fill the input fields for.</param>
|
||||
/// <param name="fieldValues">Dictionary with html element ids and values to input as field value.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
private static async Task FillInputFields(IPage page, Dictionary<string, string>? fieldValues = null)
|
||||
{
|
||||
var inputFields = page.Locator("input");
|
||||
var count = await inputFields.CountAsync();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var input = inputFields.Nth(i);
|
||||
var inputId = await input.GetAttributeAsync("id");
|
||||
|
||||
// TODO: Implement proper data input and verification if what was created is correct.
|
||||
// If fieldValues dictionary is provided and the inputId is found in it, fill the input with the value.
|
||||
if (inputId is not null && fieldValues is not null && fieldValues.TryGetValue(inputId, out var fieldValue))
|
||||
{
|
||||
await input.FillAsync(fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to fill all empty input fields on a page with random data if not provided.
|
||||
/// </summary>
|
||||
/// <param name="page">IPage instance where to fill the input fields for.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
private static async Task FillEmptyInputFieldsWithRandom(IPage page)
|
||||
{
|
||||
var inputFields = page.Locator("input");
|
||||
var count = await inputFields.CountAsync();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var input = inputFields.Nth(i);
|
||||
var inputType = await input.GetAttributeAsync("type");
|
||||
|
||||
// If is not empty, skip.
|
||||
if (!string.IsNullOrEmpty(await input.InputValueAsync()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate appropriate random data based on input type.
|
||||
string randomData = inputType switch
|
||||
{
|
||||
"email" => GenerateRandomEmail(),
|
||||
"number" => GenerateRandomNumber(),
|
||||
"password" => GenerateRandomPassword(),
|
||||
_ => GenerateRandomString(), // Default for all other types.
|
||||
};
|
||||
|
||||
await input.FillAsync(randomData);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateRandomString(int length = 10)
|
||||
@@ -124,4 +170,31 @@ public class AliasTests : PlaywrightTest
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[Random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new alias.
|
||||
/// </summary>
|
||||
/// <param name="formValues">Dictionary with html element ids and values to input as field value.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
private async Task CreateAlias(Dictionary<string, string>? formValues = null)
|
||||
{
|
||||
await Page.GotoAsync(AppBaseUrl + "add-alias");
|
||||
await WaitForURLAsync("**/add-alias", "Add alias");
|
||||
|
||||
// Check if a button with text "Generate Random Identity" appears
|
||||
var generateButton = Page.Locator("text=Generate Random Identity");
|
||||
Assert.That(generateButton, Is.Not.Null, "Generate button not found.");
|
||||
|
||||
// Fill all input fields with specified values and remaining empty fields with random data.
|
||||
await FillInputFields(Page, formValues);
|
||||
await FillEmptyInputFieldsWithRandom(Page);
|
||||
|
||||
var submitButton = Page.Locator("text=Save Alias").First;
|
||||
await submitButton.ClickAsync();
|
||||
await WaitForURLAsync("**/alias/**", "Login credentials");
|
||||
|
||||
// Check if the alias was created
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("Login credentials"), "Alias not created.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace AliasVault.E2ETests;
|
||||
|
||||
/// <summary>
|
||||
@@ -17,7 +15,7 @@ namespace AliasVault.E2ETests;
|
||||
public class AuthTests : PlaywrightTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test if registering a new account works.
|
||||
/// Test if logging out and logging in works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
@@ -25,13 +23,10 @@ public class AuthTests : PlaywrightTest
|
||||
{
|
||||
// Logout.
|
||||
await Page.GotoAsync(AppBaseUrl + "user/logout");
|
||||
await Page.WaitForURLAsync("**/user/logout", new PageWaitForURLOptions() { Timeout = 2000 });
|
||||
|
||||
// Wait for the content to load.
|
||||
await Page.WaitForSelectorAsync("text=AliasVault");
|
||||
await WaitForURLAsync("**/user/logout", "AliasVault");
|
||||
|
||||
// Wait and check if we get redirected to /user/login.
|
||||
await Page.WaitForURLAsync("**/user/login", new PageWaitForURLOptions() { Timeout = 2000 });
|
||||
await WaitForURLAsync("**/user/login");
|
||||
|
||||
await Login();
|
||||
}
|
||||
@@ -40,7 +35,7 @@ public class AuthTests : PlaywrightTest
|
||||
/// Test if logging in works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
public async Task Login()
|
||||
private async Task Login()
|
||||
{
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
|
||||
|
||||
@@ -138,6 +138,21 @@ public class PlaywrightTest
|
||||
await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = timeoutInMs });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the specified URL to be loaded with a custom timeout.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to wait for. This may also contains wildcard such as "**/user/login".</param>
|
||||
/// <param name="waitForText">Wait until a certain text appears on the page.
|
||||
/// This can be useful for content that is loaded via AJAX after navigation.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
protected async Task WaitForURLAsync(string url, string waitForText)
|
||||
{
|
||||
await Page.WaitForURLAsync(url, new PageWaitForURLOptions() { Timeout = TestDefaults.DefaultTimeout });
|
||||
|
||||
// Wait for actual content to load (web API calls, etc.)
|
||||
await Page.WaitForSelectorAsync("text=" + waitForText, new PageWaitForSelectorOptions() { Timeout = TestDefaults.DefaultTimeout });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new random account.
|
||||
/// </summary>
|
||||
|
||||
@@ -113,7 +113,6 @@ public class BlazorWasmAppManager
|
||||
}
|
||||
|
||||
Console.WriteLine(e.Message);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user