diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index 717a56931..7ebfd9c4c 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -772,6 +772,10 @@ video { margin-right: 0.25rem; } +.ml-2 { + margin-left: 0.5rem; +} + .block { display: block; } @@ -1141,6 +1145,12 @@ video { overscroll-behavior-y: auto; } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .whitespace-nowrap { white-space: nowrap; } @@ -1608,6 +1618,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + .opacity-0 { opacity: 0; } @@ -1777,6 +1792,11 @@ video { border-color: rgb(244 149 65 / var(--tw-border-opacity)); } +.focus\:border-blue-500:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -1839,6 +1859,11 @@ video { --tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity)); } +.focus\:ring-blue-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + .focus\:ring-offset-2:focus { --tw-ring-offset-width: 2px; } @@ -2041,6 +2066,11 @@ video { border-color: rgb(244 149 65 / var(--tw-border-opacity)); } +.dark\:focus\:border-blue-500:focus:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + .dark\:focus\:ring-blue-800:focus:is(.dark *) { --tw-ring-opacity: 1; --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); @@ -2091,6 +2121,16 @@ video { --tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-blue-500:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-blue-600:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:col-span-3 { grid-column: span 3 / span 3; @@ -2108,6 +2148,10 @@ video { width: auto; } + .sm\:flex-row { + flex-direction: row; + } + .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); diff --git a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs index aa70caaf8..6f86ca21f 100644 --- a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs +++ b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs @@ -190,6 +190,27 @@ public class ClientPlaywrightTest : PlaywrightTest Assert.That(pageContent, Does.Contain("Login credentials"), "Credential not created."); } + /// + /// Logout the current user and register a new account. + /// + /// Task. + protected async Task LogoutAndLoginAsNewUser() + { + // Logout. + await NavigateUsingBlazorRouter("user/logout"); + await WaitForUrlAsync("user/logout", "AliasVault"); + + // Wait and check if we get redirected to /user/login. + await WaitForUrlAsync("user/login"); + + // Reset username and password so a new random account is created. + TestUserUsername = string.Empty; + TestUserPassword = string.Empty; + + // Register a new account random account. + await Register(); + } + /// /// Register a new random account. /// diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTest.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTest.cs index 890c2eb71..c9c7ac6ea 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTest.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTest.cs @@ -88,7 +88,6 @@ public class EmailDecryptionTest : ClientPlaywrightTest Assert.That(emailReceived.Subject, Does.Not.Contain(textSubject), "Email subject stored as plain text in database. Check email encryption logic."); // Attempt to click on email refresh button to get new emails. - // Id = recent-email-refresh await Page.Locator("id=recent-email-refresh").First.ClickAsync(); // Wait for 1 sec @@ -121,6 +120,47 @@ public class EmailDecryptionTest : ClientPlaywrightTest Assert.That(claim, Is.Null, "Claim for unknown email address domain found in database. Check if claim creation domain check is working correctly."); } + /// + /// Test that a user cannot claim an email address that is already claimed by another user. + /// + /// Async task. + [Test] + public async Task EmailDuplicateClaimTest() + { + // Create credential which should automatically create claim on server during database sync. + const string serviceName = "Test Service"; + const string email = "testclaim@example.tld"; + await CreateCredentialEntry(new Dictionary + { + { "service-name", serviceName }, + { "email", email }, + }); + + // Assert that the claim was created on the server. + var claim = await ApiDbContext.UserEmailClaims.FirstOrDefaultAsync(x => x.Address == email); + + // Login as new user. + await LogoutAndLoginAsNewUser(); + + // Try to claim the same email address again. + await CreateCredentialEntry(new Dictionary + { + { "service-name", serviceName }, + { "email", email }, + }); + + // Assert that still only one claim exists for the email address. + var claimCount = await ApiDbContext.UserEmailClaims.CountAsync(x => x.Address == email); + Assert.That( + claimCount, + Is.LessThanOrEqualTo(1), + "More than one claim for email address found in database while only one should exist. Check if claim creation domain check is working correctly."); + + // Assert that error is displayed on the page. + var pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Contain("The current chosen email address is already in use"), "Error message not displayed on page when trying to claim email address already claimed by another user."); + } + /// /// Tear down logic for every test. ///