diff --git a/BTCPayServer.Tests/PlaywrightTests.cs b/BTCPayServer.Tests/PlaywrightTests.cs index 617450bd3..128a1ae5e 100644 --- a/BTCPayServer.Tests/PlaywrightTests.cs +++ b/BTCPayServer.Tests/PlaywrightTests.cs @@ -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); + } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 6da6f99da..12fd1d552 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -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=([^&]*)"); diff --git a/BTCPayServer/Controllers/UIStoresController.Roles.cs b/BTCPayServer/Controllers/UIStoresController.Roles.cs index 50069038c..01f73d7b0 100644 --- a/BTCPayServer/Controllers/UIStoresController.Roles.cs +++ b/BTCPayServer/Controllers/UIStoresController.Roles.cs @@ -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(); diff --git a/BTCPayServer/Views/Shared/ListRoles.cshtml b/BTCPayServer/Views/Shared/ListRoles.cshtml index 22926bc8c..847840022 100644 --- a/BTCPayServer/Views/Shared/ListRoles.cshtml +++ b/BTCPayServer/Views/Shared/ListRoles.cshtml @@ -118,8 +118,8 @@ { Set as default } - Edit - Remove + Edit + Remove