feat(admin): add create-user flow with API, UI, and tests
Admins can now add users directly from the admin portal via a modal form (name, email, password). Created users are pre-approved and can log in immediately. - POST /api/admin/users endpoint (AddUserHandler, AddUserCommand) - Add user modal in AdminPage with inline error handling - 5 new integration tests covering auth, happy path, duplicate email, and immediate login; fix SQLite file-lock on test cleanup via SqliteConnection.ClearAllPools() - 4 new E2E tests covering modal open/close, happy path, and duplicate email error
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Randall.Application.Admin.AddUser;
|
||||||
using Randall.Application.Admin.ApproveUser;
|
using Randall.Application.Admin.ApproveUser;
|
||||||
using Randall.Application.Admin.DeleteUser;
|
using Randall.Application.Admin.DeleteUser;
|
||||||
using Randall.Application.Admin.GetAllUsers;
|
using Randall.Application.Admin.GetAllUsers;
|
||||||
@@ -9,10 +10,13 @@ using Randall.Application.Admin.MakeAdmin;
|
|||||||
|
|
||||||
namespace Randall.Api.Admin;
|
namespace Randall.Api.Admin;
|
||||||
|
|
||||||
|
public record AddUserRequest(string Email, string Name, string Password);
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/admin")]
|
[Route("api/admin")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class AdminController(
|
public class AdminController(
|
||||||
|
AddUserHandler addUserHandler,
|
||||||
GetAllUsersHandler getAllUsersHandler,
|
GetAllUsersHandler getAllUsersHandler,
|
||||||
GetPendingUsersHandler getPendingUsersHandler,
|
GetPendingUsersHandler getPendingUsersHandler,
|
||||||
ApproveUserHandler approveUserHandler,
|
ApproveUserHandler approveUserHandler,
|
||||||
@@ -23,6 +27,16 @@ public class AdminController(
|
|||||||
private Guid RequesterId => Guid.Parse(User.FindFirstValue(System.Security.Claims.ClaimTypes.NameIdentifier)
|
private Guid RequesterId => Guid.Parse(User.FindFirstValue(System.Security.Claims.ClaimTypes.NameIdentifier)
|
||||||
?? User.FindFirstValue("sub")!);
|
?? User.FindFirstValue("sub")!);
|
||||||
|
|
||||||
|
[HttpPost("users")]
|
||||||
|
public async Task<IActionResult> AddUser([FromBody] AddUserRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!IsAdmin) return Forbid();
|
||||||
|
var result = await addUserHandler.HandleAsync(new AddUserCommand(request.Email, request.Name, request.Password), ct);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return BadRequest(new ProblemDetails { Detail = result.Error });
|
||||||
|
return CreatedAtAction(nameof(GetAllUsers), result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("users")]
|
[HttpGet("users")]
|
||||||
public async Task<IActionResult> GetAllUsers(CancellationToken ct)
|
public async Task<IActionResult> GetAllUsers(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Randall.Application.Admin.AddUser;
|
||||||
|
|
||||||
|
public record AddUserCommand(string Email, string Name, string Password);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Randall.Application.Admin.GetAllUsers;
|
||||||
|
using Randall.Application.Common;
|
||||||
|
using Randall.Domain.Common;
|
||||||
|
using Randall.Domain.Users;
|
||||||
|
|
||||||
|
namespace Randall.Application.Admin.AddUser;
|
||||||
|
|
||||||
|
public class AddUserHandler(IUserRepository userRepository, IPasswordHasher passwordHasher)
|
||||||
|
{
|
||||||
|
public async Task<Result<AdminUserDto>> HandleAsync(AddUserCommand command, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var exists = await userRepository.ExistsByEmailAsync(command.Email, ct);
|
||||||
|
if (exists)
|
||||||
|
return Result.Failure<AdminUserDto>("An account with this email already exists.");
|
||||||
|
|
||||||
|
var hash = passwordHasher.Hash(command.Password);
|
||||||
|
|
||||||
|
var result = User.Create(command.Email, command.Name, hash);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return Result.Failure<AdminUserDto>(result.Error!);
|
||||||
|
|
||||||
|
var user = result.Value!;
|
||||||
|
user.Approve();
|
||||||
|
|
||||||
|
await userRepository.AddAsync(user, ct);
|
||||||
|
await userRepository.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return Result.Success(new AdminUserDto(user.Id, user.Name, user.Email, user.IsApproved, user.IsAdmin));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Randall.Application.Admin.AddUser;
|
||||||
using Randall.Application.Admin.ApproveUser;
|
using Randall.Application.Admin.ApproveUser;
|
||||||
using Randall.Application.Admin.DeleteUser;
|
using Randall.Application.Admin.DeleteUser;
|
||||||
using Randall.Application.Admin.GetAllUsers;
|
using Randall.Application.Admin.GetAllUsers;
|
||||||
@@ -22,6 +23,7 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
services.AddScoped<RegisterHandler>();
|
services.AddScoped<RegisterHandler>();
|
||||||
services.AddScoped<LoginHandler>();
|
services.AddScoped<LoginHandler>();
|
||||||
|
services.AddScoped<AddUserHandler>();
|
||||||
services.AddScoped<GetAllUsersHandler>();
|
services.AddScoped<GetAllUsersHandler>();
|
||||||
services.AddScoped<GetPendingUsersHandler>();
|
services.AddScoped<GetPendingUsersHandler>();
|
||||||
services.AddScoped<ApproveUserHandler>();
|
services.AddScoped<ApproveUserHandler>();
|
||||||
|
|||||||
@@ -146,4 +146,85 @@ public class AdminTests(CustomWebApplicationFactory factory) : IClassFixture<Cus
|
|||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddUser_WithoutAuth_Returns401()
|
||||||
|
{
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = "new@test.com", Name = "New User", Password = "Test@1234" });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddUser_WithNonAdmin_Returns403()
|
||||||
|
{
|
||||||
|
var email = $"{Guid.NewGuid():N}@test.com";
|
||||||
|
var token = await AuthHelper.CreateApprovedUserAndLoginAsync(factory, email);
|
||||||
|
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
AuthHelper.SetBearerToken(client, token);
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = $"{Guid.NewGuid():N}@test.com", Name = "New User", Password = "Test@1234" });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddUser_WithAdmin_Returns201WithUserDetails()
|
||||||
|
{
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||||
|
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||||
|
|
||||||
|
var email = $"{Guid.NewGuid():N}@test.com";
|
||||||
|
var response = await client.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = email, Name = "New User", Password = "Test@1234" });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||||
|
var user = await response.Content.ReadFromJsonAsync<AdminUserResponse>(AuthHelper.JsonOptions);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(email, user.Email);
|
||||||
|
Assert.Equal("New User", user.Name);
|
||||||
|
Assert.True(user.IsApproved);
|
||||||
|
Assert.False(user.IsAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddUser_DuplicateEmail_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
var adminLogin = await AuthHelper.LoginAsAdminAsync(client);
|
||||||
|
AuthHelper.SetBearerToken(client, adminLogin.Token);
|
||||||
|
|
||||||
|
var email = $"{Guid.NewGuid():N}@test.com";
|
||||||
|
await client.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = email, Name = "First User", Password = "Test@1234" });
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = email, Name = "Duplicate User", Password = "Test@1234" });
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddUser_CreatedUserCanLoginImmediately()
|
||||||
|
{
|
||||||
|
var adminClient = factory.CreateClient();
|
||||||
|
var adminLogin = await AuthHelper.LoginAsAdminAsync(adminClient);
|
||||||
|
AuthHelper.SetBearerToken(adminClient, adminLogin.Token);
|
||||||
|
|
||||||
|
var email = $"{Guid.NewGuid():N}@test.com";
|
||||||
|
const string password = "Test@1234";
|
||||||
|
await adminClient.PostAsJsonAsync("/api/admin/users",
|
||||||
|
new { Email = email, Name = "Direct User", Password = password });
|
||||||
|
|
||||||
|
var loginClient = factory.CreateClient();
|
||||||
|
var login = await AuthHelper.LoginAsync(loginClient, email, password);
|
||||||
|
|
||||||
|
Assert.NotEmpty(login.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Randall.Api.IntegrationTests;
|
namespace Randall.Api.IntegrationTests;
|
||||||
@@ -28,6 +29,9 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
if (disposing && File.Exists(_dbPath))
|
if (disposing && File.Exists(_dbPath))
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
File.Delete(_dbPath);
|
File.Delete(_dbPath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AdminUser, AuthResponse, CreateReservationRequest, PendingUser, RegisterPendingResponse, Reservation, WorkplaceScheduleItem } from './types';
|
import type { AddUserRequest, AdminUser, AuthResponse, CreateReservationRequest, PendingUser, RegisterPendingResponse, Reservation, WorkplaceScheduleItem } from './types';
|
||||||
|
|
||||||
const BASE = '/api';
|
const BASE = '/api';
|
||||||
|
|
||||||
@@ -38,6 +38,15 @@ export const api = {
|
|||||||
return handleResponse<RegisterPendingResponse>(res);
|
return handleResponse<RegisterPendingResponse>(res);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async addUser(data: AddUserRequest): Promise<AdminUser> {
|
||||||
|
const res = await fetch(`${BASE}/admin/users`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return handleResponse<AdminUser>(res);
|
||||||
|
},
|
||||||
|
|
||||||
getAdminUsers(): Promise<AdminUser[]> {
|
getAdminUsers(): Promise<AdminUser[]> {
|
||||||
return fetch(`${BASE}/admin/users`, {
|
return fetch(`${BASE}/admin/users`, {
|
||||||
headers: authHeaders(),
|
headers: authHeaders(),
|
||||||
|
|||||||
@@ -59,3 +59,9 @@ export interface AdminUser {
|
|||||||
isApproved: boolean;
|
isApproved: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddUserRequest {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export function AdminPage({ onLogout }: AdminPageProps) {
|
|||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [confirmAdminId, setConfirmAdminId] = useState<string | null>(null);
|
const [confirmAdminId, setConfirmAdminId] = useState<string | null>(null);
|
||||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||||
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
|
const [addForm, setAddForm] = useState({ name: '', email: '', password: '' });
|
||||||
|
const [addError, setAddError] = useState('');
|
||||||
|
const [addSubmitting, setAddSubmitting] = useState(false);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -81,6 +85,22 @@ export function AdminPage({ onLogout }: AdminPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAddUser(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setAddSubmitting(true);
|
||||||
|
setAddError('');
|
||||||
|
try {
|
||||||
|
const newUser = await api.addUser(addForm);
|
||||||
|
setUsers((prev) => [...prev, newUser]);
|
||||||
|
setShowAddModal(false);
|
||||||
|
setAddForm({ name: '', email: '', password: '' });
|
||||||
|
} catch (err) {
|
||||||
|
setAddError(err instanceof Error ? err.message : 'Failed to add user');
|
||||||
|
} finally {
|
||||||
|
setAddSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = users.filter((u) => {
|
const filtered = users.filter((u) => {
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
return !q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q);
|
return !q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q);
|
||||||
@@ -241,12 +261,15 @@ export function AdminPage({ onLogout }: AdminPageProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button style={{
|
<button
|
||||||
|
onClick={() => { setAddForm({ name: '', email: '', password: '' }); setAddError(''); setShowAddModal(true); }}
|
||||||
|
style={{
|
||||||
padding: '8px 16px', borderRadius: 99,
|
padding: '8px 16px', borderRadius: 99,
|
||||||
background: SAGE, border: `1px solid ${SAGE_DEEP}`,
|
background: SAGE, border: `1px solid ${SAGE_DEEP}`,
|
||||||
color: PURPLE_DEEP, fontSize: 12, fontWeight: 500,
|
color: PURPLE_DEEP, fontSize: 12, fontWeight: 500,
|
||||||
cursor: 'pointer', fontFamily: 'inherit',
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
+ Add user
|
+ Add user
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,6 +461,96 @@ export function AdminPage({ onLogout }: AdminPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Add user modal */}
|
||||||
|
{showAddModal && (
|
||||||
|
<div
|
||||||
|
onClick={() => setShowAddModal(false)}
|
||||||
|
style={{
|
||||||
|
position: 'fixed', inset: 0,
|
||||||
|
background: 'rgba(63,51,168,0.18)', backdropFilter: 'blur(2px)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
zIndex: 100,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
background: PAPER, borderRadius: 20,
|
||||||
|
border: '1px solid rgba(91,79,199,0.15)',
|
||||||
|
padding: '32px 32px 28px',
|
||||||
|
width: 380, boxShadow: '0 8px 40px rgba(63,51,168,0.14)',
|
||||||
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{
|
||||||
|
margin: '0 0 24px',
|
||||||
|
fontFamily: "'Rubik Mono One', monospace",
|
||||||
|
fontSize: 22, fontWeight: 400, letterSpacing: '-0.02em', color: PURPLE,
|
||||||
|
}}>
|
||||||
|
Add user
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleAddUser} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||||
|
{(['name', 'email', 'password'] as const).map((field) => (
|
||||||
|
<div key={field}>
|
||||||
|
<label style={{
|
||||||
|
display: 'block', fontSize: 10, letterSpacing: '0.16em',
|
||||||
|
textTransform: 'uppercase', color: PURPLE, opacity: 0.7,
|
||||||
|
fontWeight: 500, marginBottom: 6,
|
||||||
|
}}>
|
||||||
|
{field}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={field === 'password' ? 'password' : field === 'email' ? 'email' : 'text'}
|
||||||
|
value={addForm[field]}
|
||||||
|
onChange={(e) => setAddForm((prev) => ({ ...prev, [field]: e.target.value }))}
|
||||||
|
required
|
||||||
|
style={{
|
||||||
|
width: '100%', boxSizing: 'border-box',
|
||||||
|
padding: '9px 13px', borderRadius: 10,
|
||||||
|
border: '1px solid rgba(91,79,199,0.22)',
|
||||||
|
background: 'rgba(255,255,255,0.7)',
|
||||||
|
fontSize: 13, color: PURPLE_DEEP, fontFamily: 'inherit',
|
||||||
|
outline: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{addError && (
|
||||||
|
<p style={{ margin: 0, fontSize: 12, color: '#c0392b' }}>{addError}</p>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 8 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAddModal(false)}
|
||||||
|
style={{
|
||||||
|
background: 'none', border: 'none', padding: '8px 14px',
|
||||||
|
fontSize: 13, color: PURPLE, opacity: 0.65,
|
||||||
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={addSubmitting}
|
||||||
|
style={{
|
||||||
|
padding: '8px 20px', borderRadius: 99,
|
||||||
|
background: addSubmitting ? SAGE : PURPLE,
|
||||||
|
border: `1px solid ${addSubmitting ? SAGE_DEEP : PURPLE_DEEP}`,
|
||||||
|
color: addSubmitting ? PURPLE_DEEP : '#fff',
|
||||||
|
fontSize: 12, fontWeight: 500,
|
||||||
|
cursor: addSubmitting ? 'default' : 'pointer',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{addSubmitting ? '…' : 'Add user'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,4 +45,57 @@ test.describe('Admin portal', () => {
|
|||||||
await page.waitForURL('/');
|
await page.waitForURL('/');
|
||||||
await expect(page.getByRole('heading', { name: /Where to sit/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /Where to sit/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('+ Add user button opens the add-user modal', async ({ page }) => {
|
||||||
|
await page.getByRole('button', { name: 'Admin' }).click();
|
||||||
|
await page.waitForURL('/admin');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: '+ Add user' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add user' })).toBeVisible();
|
||||||
|
await expect(page.locator('form input[type="text"]')).toBeVisible();
|
||||||
|
await expect(page.locator('form input[type="email"]')).toBeVisible();
|
||||||
|
await expect(page.locator('form input[type="password"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add-user modal closes when Cancel is clicked', async ({ page }) => {
|
||||||
|
await page.getByRole('button', { name: 'Admin' }).click();
|
||||||
|
await page.waitForURL('/admin');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: '+ Add user' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add user' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('form').getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add user' })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can add a new user via the modal and see them in the table', async ({ page }) => {
|
||||||
|
await page.getByRole('button', { name: 'Admin' }).click();
|
||||||
|
await page.waitForURL('/admin');
|
||||||
|
|
||||||
|
const email = `adduser+${Date.now()}@test.com`;
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: '+ Add user' }).click();
|
||||||
|
await page.locator('form input[type="text"]').fill('New Test User');
|
||||||
|
await page.locator('form input[type="email"]').fill(email);
|
||||||
|
await page.locator('form input[type="password"]').fill('Test@1234');
|
||||||
|
await page.locator('form button[type="submit"]').click();
|
||||||
|
|
||||||
|
await expect(page.getByText(email)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add-user modal shows an error for a duplicate email', async ({ page }) => {
|
||||||
|
await page.getByRole('button', { name: 'Admin' }).click();
|
||||||
|
await page.waitForURL('/admin');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: '+ Add user' }).click();
|
||||||
|
await page.locator('form input[type="text"]').fill('Duplicate');
|
||||||
|
await page.locator('form input[type="email"]').fill('admin@randall.local');
|
||||||
|
await page.locator('form input[type="password"]').fill('Test@1234');
|
||||||
|
await page.locator('form button[type="submit"]').click();
|
||||||
|
|
||||||
|
await expect(page.getByText(/already exists/i)).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add user' })).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user