(Refactor) : Converted Selenium test for CanUseRoleManager and Others to playwright (#6996)

* refactor: resovled merge conflict

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* (Refactor): Removed Selenium Test for CanUseRoleManager

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* Refactor : removed spacing and extra alert message

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* (Test):Converted/Added Playwright Test for CanSigninWithLoginCode

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* (Refactor): Removed Selenium Test for CanSigninWithLoginCode

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* fix: updated UIServerController.Roles.cs to handle storeID

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* refactor : updated some minor nits

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* Fix: Preserve store context when deleting server-wide roles from store page

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

* refactor: fix auth mismatch in role Edit/Remove links for store context

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>

---------

Signed-off-by: Abhijay007 <Abhijay007j@gmail.com>
This commit is contained in:
Abhijay Jain
2025-11-27 15:17:32 +05:30
committed by GitHub
parent ab6aa1e920
commit 65dc0a761d
4 changed files with 235 additions and 201 deletions

View File

@@ -2581,6 +2581,230 @@ namespace BTCPayServer.Tests
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error, "The user is the last owner. Their role cannot be changed.");
}
[Fact]
[Trait("Playwright", "Playwright")]
public async Task CanUseRoleManager()
{
await using var s = CreatePlaywrightTester(newDb: true);
await s.StartAsync();
await s.RegisterNewUser(true);
await s.GoToHome();
await s.GoToServer(ServerNavPages.Roles);
var existingServerRoles = await s.Page.Locator("table tr").AllAsync();
Assert.Equal(5, existingServerRoles.Count);
ILocator ownerRow = null;
ILocator managerRow = null;
ILocator employeeRow = null;
ILocator guestRow = null;
foreach (var roleItem in existingServerRoles)
{
var text = await roleItem.TextContentAsync();
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (text.Contains("manager", StringComparison.InvariantCultureIgnoreCase))
{
managerRow = roleItem;
}
else if (text.Contains("employee", StringComparison.InvariantCultureIgnoreCase))
{
employeeRow = roleItem;
}
else if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
Assert.NotNull(ownerRow);
Assert.NotNull(managerRow);
Assert.NotNull(employeeRow);
Assert.NotNull(guestRow);
var ownerBadges = await ownerRow.Locator(".badge").AllAsync();
var ownerBadgeTexts = await Task.WhenAll(ownerBadges.Select(async element => await element.TextContentAsync()));
Assert.Contains(ownerBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(ownerBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var managerBadges = await managerRow.Locator(".badge").AllAsync();
var managerBadgeTexts = await Task.WhenAll(managerBadges.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(managerBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(managerBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var employeeBadges = await employeeRow.Locator(".badge").AllAsync();
var employeeBadgeTexts = await Task.WhenAll(employeeBadges.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(employeeBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(employeeBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var guestBadges = await guestRow.Locator(".badge").AllAsync();
var guestBadgeTexts = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(guestBadgeTexts, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(guestBadgeTexts, text => text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
await guestRow.Locator("#SetDefault").ClickAsync();
await s.FindAlertMessage(partialText: "Role set default");
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
foreach (var roleItem in existingServerRoles)
{
var text = await roleItem.TextContentAsync();
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
guestBadges = await guestRow.Locator(".badge").AllAsync();
var guestBadgeTexts2 = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
Assert.Contains(guestBadgeTexts2, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
ownerBadges = await ownerRow.Locator(".badge").AllAsync();
var ownerBadgeTexts2 = await Task.WhenAll(ownerBadges.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(ownerBadgeTexts2, text => text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
await ownerRow.Locator("#SetDefault").ClickAsync();
await s.FindAlertMessage(partialText: "Role set default");
await s.CreateNewStore();
await s.GoToStore(StoreNavPages.Roles);
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
Assert.Equal(5, existingServerRoles.Count);
var serverRoleTexts = await Task.WhenAll(existingServerRoles.Select(async element => await element.TextContentAsync()));
Assert.Equal(4, serverRoleTexts.Count(text => text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
foreach (var roleItem in existingServerRoles)
{
var text = await roleItem.TextContentAsync();
if (text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
break;
}
}
await ownerRow.Locator("text=Remove").ClickAsync();
await s.Page.WaitForLoadStateAsync();
Assert.DoesNotContain("ConfirmContinue", await s.Page.ContentAsync());
await s.Page.GoBackAsync();
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
foreach (var roleItem in existingServerRoles)
{
var text = await roleItem.TextContentAsync();
if (text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
await guestRow.Locator("text=Remove").ClickAsync();
await s.Page.ClickAsync("#ConfirmContinue");
await s.FindAlertMessage();
await s.GoToStore(StoreNavPages.Roles);
await s.ClickPagePrimary();
Assert.Contains("Create role", await s.Page.ContentAsync());
await s.ClickPagePrimary();
await s.Page.Locator("#Role").FillAsync("store role");
await s.ClickPagePrimary();
await s.FindAlertMessage();
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
foreach (var roleItem in existingServerRoles)
{
var text = await roleItem.TextContentAsync();
if (text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
guestBadges = await guestRow.Locator(".badge").AllAsync();
var guestBadgeTexts3 = await Task.WhenAll(guestBadges.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(guestBadgeTexts3, text => text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
await s.GoToStore(StoreNavPages.Users);
var options = await s.Page.Locator("#Role option").AllAsync();
Assert.Equal(4, options.Count);
var optionTexts = await Task.WhenAll(options.Select(async element => await element.TextContentAsync()));
Assert.Contains(optionTexts, text => text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
await s.CreateNewStore();
await s.GoToStore(StoreNavPages.Roles);
existingServerRoles = await s.Page.Locator("table tr").AllAsync();
Assert.Equal(4, existingServerRoles.Count);
var serverRoleTexts2 = await Task.WhenAll(existingServerRoles.Select(async element => await element.TextContentAsync()));
Assert.Equal(3, serverRoleTexts2.Count(text => text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(0, serverRoleTexts2.Count(text => text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
await s.GoToStore(StoreNavPages.Users);
options = await s.Page.Locator("#Role option").AllAsync();
Assert.Equal(3, options.Count);
var optionTexts2 = await Task.WhenAll(options.Select(async element => await element.TextContentAsync()));
Assert.DoesNotContain(optionTexts2, text => text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
await s.Page.Locator("#Email").FillAsync(s.AsTestAccount().Email);
await s.Page.Locator("#Role").SelectOptionAsync("Owner");
await s.Page.ClickAsync("#AddUser");
Assert.Contains("The user already has the role Owner.", await s.Page.Locator(".validation-summary-errors").TextContentAsync());
await s.Page.Locator("#Role").SelectOptionAsync("Manager");
await s.Page.ClickAsync("#AddUser");
Assert.Contains("The user is the last owner. Their role cannot be changed.", await s.Page.Locator(".validation-summary-errors").TextContentAsync());
await s.GoToStore(StoreNavPages.Roles);
await s.ClickPagePrimary();
await s.Page.Locator("#Role").FillAsync("Malice");
await s.Page.EvaluateAsync($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
await s.ClickPagePrimary();
await s.FindAlertMessage();
Assert.Contains("Malice", await s.Page.ContentAsync());
Assert.DoesNotContain(Policies.CanModifyServerSettings, await s.Page.ContentAsync());
}
[Fact]
[Trait("Playwright", "Playwright")]
public async Task CanSigninWithLoginCode()
{
await using var s = CreatePlaywrightTester();
await s.StartAsync();
var user = await s.RegisterNewUser();
await s.GoToHome();
await s.GoToProfile(ManageNavPages.LoginCodes);
string code = null;
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
string prevCode = code;
await s.Page.ReloadAsync();
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
Assert.NotEqual(prevCode, code);
await s.Page.WaitForSelectorAsync("#LoginCode .qr-code");
code = await s.Page.Locator("#LoginCode .qr-code").GetAttributeAsync("alt");
await s.Logout();
await s.GoToLogin();
await s.Page.EvaluateAsync("document.getElementById('LoginCode').value = 'bad code'");
await s.Page.EvaluateAsync("document.getElementById('logincode-form').submit()");
await s.Page.WaitForLoadStateAsync();
await s.GoToLogin();
await s.Page.EvaluateAsync($"document.getElementById('LoginCode').value = '{code}'");
await s.Page.EvaluateAsync("document.getElementById('logincode-form').submit()");
await s.Page.WaitForLoadStateAsync();
await s.Page.WaitForLoadStateAsync();
await s.CreateNewStore();
await s.GoToHome();
await s.Page.WaitForLoadStateAsync();
await s.Page.WaitForLoadStateAsync();
var content = await s.Page.ContentAsync();
Assert.Contains(user, content);
}
}
}

View File

@@ -1353,34 +1353,6 @@ namespace BTCPayServer.Tests
Assert.Contains(lnUsername, source);
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanSigninWithLoginCode()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
var user = s.RegisterNewUser();
s.GoToHome();
s.GoToProfile(ManageNavPages.LoginCodes);
string code = null;
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
string prevCode = code;
await s.Driver.Navigate().RefreshAsync();
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
Assert.NotEqual(prevCode, code);
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
s.Logout();
s.GoToLogin();
s.Driver.SetAttribute("LoginCode", "value", "bad code");
s.Driver.InvokeJSFunction("logincode-form", "submit");
s.Driver.SetAttribute("LoginCode", "value", code);
s.Driver.InvokeJSFunction("logincode-form", "submit");
s.GoToHome();
Assert.Contains(user, s.Driver.PageSource);
}
// For god know why, selenium have problems clicking on the save button, resulting in ultimate hacks
// to make it works.
private void SudoForceSaveLightningSettingsRightNowAndFast(SeleniumTester s, string cryptoCode)
@@ -1399,175 +1371,6 @@ retry:
}
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUseRoleManager()
{
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
s.RegisterNewUser(true);
s.GoToHome();
s.GoToServer(ServerNavPages.Roles);
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(5, existingServerRoles.Count);
IWebElement ownerRow = null;
IWebElement managerRow = null;
IWebElement employeeRow = null;
IWebElement guestRow = null;
foreach (var roleItem in existingServerRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (roleItem.Text.Contains("manager", StringComparison.InvariantCultureIgnoreCase))
{
managerRow = roleItem;
}
else if (roleItem.Text.Contains("employee", StringComparison.InvariantCultureIgnoreCase))
{
employeeRow = roleItem;
}
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
Assert.NotNull(ownerRow);
Assert.NotNull(managerRow);
Assert.NotNull(employeeRow);
Assert.NotNull(guestRow);
var ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
Assert.Contains(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(ownerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var managerBadges = managerRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(managerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(managerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var employeeBadges = employeeRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(employeeBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(employeeBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(guestBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
guestRow.FindElement(By.Id("SetDefault")).Click();
Assert.Contains("Role set default", s.FindAlertMessage().Text);
existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingServerRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.Contains(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
ownerRow.FindElement(By.Id("SetDefault")).Click();
s.FindAlertMessage();
Assert.Contains("Role set default", s.FindAlertMessage().Text);
s.CreateNewStore();
s.GoToStore(StoreNavPages.Roles);
var existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(5, existingStoreRoles.Count);
Assert.Equal(4, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
break;
}
}
ownerRow.FindElement(By.LinkText("Remove")).Click();
Assert.DoesNotContain("ConfirmContinue", s.Driver.PageSource);
s.Driver.Navigate().Back();
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
guestRow.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
s.GoToStore(StoreNavPages.Roles);
s.ClickPagePrimary();
Assert.Contains("Create role", s.Driver.PageSource);
s.ClickPagePrimary();
s.Driver.FindElement(By.Id("Role")).SendKeys("store role");
s.ClickPagePrimary();
s.FindAlertMessage();
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
s.GoToStore(StoreNavPages.Users);
var options = s.Driver.FindElements(By.CssSelector("#Role option"));
Assert.Equal(4, options.Count);
Assert.Contains(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.CreateNewStore();
s.GoToStore(StoreNavPages.Roles);
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(4, existingStoreRoles.Count);
Assert.Equal(3, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(0, existingStoreRoles.Count(element => element.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
s.GoToStore(StoreNavPages.Users);
options = s.Driver.FindElements(By.CssSelector("#Role option"));
Assert.Equal(3, options.Count);
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.Driver.FindElement(By.Id("Email")).SendKeys(s.AsTestAccount().Email);
s.Driver.FindElement(By.Id("Role")).SendKeys("owner");
s.Driver.FindElement(By.Id("AddUser")).Click();
Assert.Contains("The user already has the role Owner.", s.Driver.FindElement(By.CssSelector(".validation-summary-errors")).Text);
s.Driver.FindElement(By.Id("Role")).SendKeys("manager");
s.Driver.FindElement(By.Id("AddUser")).Click();
Assert.Contains("The user is the last owner. Their role cannot be changed.", s.Driver.FindElement(By.CssSelector(".validation-summary-errors")).Text);
s.GoToStore(StoreNavPages.Roles);
s.ClickPagePrimary();
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
s.Driver.ExecuteJavaScript($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
s.ClickPagePrimary();
s.FindAlertMessage();
Assert.Contains("Malice",s.Driver.PageSource);
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
}
private static string AssertUrlHasPairingCode(SeleniumTester s)
{
var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)");

View File

@@ -122,7 +122,11 @@ public partial class UIStoresController
[FromServices] StoreRepository storeRepository,
string role)
{
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);
var roleId = await storeRepository.ResolveStoreRoleId(storeId, role);
if (roleId == null)
return NotFound();
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();
@@ -142,7 +146,10 @@ public partial class UIStoresController
[FromServices] StoreRepository storeRepository,
string role)
{
var roleId = new StoreRoleId(storeId, role);
var roleId = await storeRepository.ResolveStoreRoleId(storeId, role);
if (roleId == null)
return NotFound();
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();

View File

@@ -118,8 +118,8 @@
{
<a permission="@Policies.CanModifyServerSettings" asp-action="SetDefaultRole" asp-route-role="@role.Role" asp-controller="UIServer" id="SetDefault" text-translate="true">Set as default</a>
}
<a permission="@(role.IsServerRole ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="CreateOrEditRole" asp-route-storeId="@storeId" asp-route-role="@role.Role" asp-controller="@(role.IsServerRole ? "UIServer" : "UIStores")" text-translate="true">Edit</a>
<a permission="@(role.IsServerRole ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="DeleteRole" asp-route-storeId="@storeId" asp-route-role="@role.Role" asp-controller="@(role.IsServerRole ? "UIServer" : "UIStores")" text-translate="true">Remove</a>
<a permission="@(role.IsServerRole && string.IsNullOrEmpty(storeId) ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="CreateOrEditRole" asp-route-storeId="@storeId" asp-route-role="@role.Role" asp-controller="@(role.IsServerRole && string.IsNullOrEmpty(storeId) ? "UIServer" : "UIStores")" text-translate="true">Edit</a>
<a permission="@(role.IsServerRole && string.IsNullOrEmpty(storeId) ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)" asp-action="DeleteRole" asp-route-storeId="@storeId" asp-route-role="@role.Role" asp-controller="@(role.IsServerRole && string.IsNullOrEmpty(storeId) ? "UIServer" : "UIStores")" text-translate="true">Remove</a>
</div>
</td>
</tr>