feat: add E2E test for profile update and delete account
Some checks are pending
CI/CD - Build & Test / Backend Tests (push) Waiting to run
CI/CD - Build & Test / Frontend Tests (push) Waiting to run
CI/CD - Build & Test / Security Scans (push) Waiting to run
CI/CD - Build & Test / Docker Build Test (push) Blocked by required conditions
CI/CD - Build & Test / Terraform Validate (push) Waiting to run
Deploy to Production / Build & Test (push) Waiting to run
Deploy to Production / Security Scan (push) Blocked by required conditions
Deploy to Production / Build Docker Images (push) Blocked by required conditions
Deploy to Production / Deploy to Staging (push) Blocked by required conditions
Deploy to Production / E2E Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Production (push) Blocked by required conditions
E2E Tests / Run E2E Tests (push) Waiting to run
E2E Tests / Visual Regression Tests (push) Blocked by required conditions
E2E Tests / Smoke Tests (push) Waiting to run
Some checks are pending
CI/CD - Build & Test / Backend Tests (push) Waiting to run
CI/CD - Build & Test / Frontend Tests (push) Waiting to run
CI/CD - Build & Test / Security Scans (push) Waiting to run
CI/CD - Build & Test / Docker Build Test (push) Blocked by required conditions
CI/CD - Build & Test / Terraform Validate (push) Waiting to run
Deploy to Production / Build & Test (push) Waiting to run
Deploy to Production / Security Scan (push) Blocked by required conditions
Deploy to Production / Build Docker Images (push) Blocked by required conditions
Deploy to Production / Deploy to Staging (push) Blocked by required conditions
Deploy to Production / E2E Tests (push) Blocked by required conditions
Deploy to Production / Deploy to Production (push) Blocked by required conditions
E2E Tests / Run E2E Tests (push) Waiting to run
E2E Tests / Visual Regression Tests (push) Blocked by required conditions
E2E Tests / Smoke Tests (push) Waiting to run
This commit is contained in:
45
frontend/tests/e2e/profile-update.spec.ts
Normal file
45
frontend/tests/e2e/profile-update.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E tests for user profile management:
|
||||
* - Update name/email via PUT /auth/me
|
||||
* - Delete (deactivate) account via DELETE /auth/me
|
||||
*/
|
||||
|
||||
test.describe('User Profile Management', () => {
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
const password = 'TestPass123!';
|
||||
|
||||
test.beforeAll(async ({ page }) => {
|
||||
// Register a new user first
|
||||
await page.goto('/register');
|
||||
await page.fill('input[placeholder="Full name"]', 'John Doe');
|
||||
await page.fill('input[placeholder="name@example.com"]', email);
|
||||
await page.fill('input[placeholder="Password"]', password);
|
||||
await page.click('button:has-text("Sign up")');
|
||||
await expect(page).toHaveURL('/')
|
||||
});
|
||||
|
||||
test('update profile information', async ({ page }) => {
|
||||
// Open settings profile page
|
||||
await page.goto('/settings/profile');
|
||||
await page.fill('input[name="first_name"]', 'Jane');
|
||||
await page.fill('input[name="last_name"]', 'Smith');
|
||||
await page.fill('input[name="email"]', `jane.${Date.now()}@example.com`);
|
||||
await page.click('button:has-text("Save Changes")');
|
||||
await expect(page.locator('p')).toContainText('Jane Smith');
|
||||
});
|
||||
|
||||
test('delete account (soft‑delete)', async ({ page }) => {
|
||||
await page.goto('/settings/account');
|
||||
await page.click('button:has-text("Delete Account")');
|
||||
// Confirm modal (Playwright handles native confirm)
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
await expect(page).toHaveURL('/login');
|
||||
// Verify login fails after deletion
|
||||
await page.fill('input[placeholder="name@example.com"]', email);
|
||||
await page.fill('input[placeholder="Password"]', password);
|
||||
await page.click('button:has-text("Sign in")');
|
||||
await expect(page.locator('div[role="alert"]')).toContainText('Invalid email or password');
|
||||
});
|
||||
});
|
||||
@@ -246,6 +246,74 @@ async def get_me(
|
||||
"""
|
||||
return current_user
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Update profile (PUT /auth/me)
|
||||
# ---------------------------------------------------
|
||||
@router.put(
|
||||
"/me",
|
||||
response_model=UserResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def update_me(
|
||||
update_data: UserUpdate,
|
||||
current_user: Annotated[UserResponse, Depends(get_current_user)],
|
||||
session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update current user profile (first_name, last_name, email)."""
|
||||
# fetch full user record
|
||||
from uuid import UUID
|
||||
user = await get_user_by_id(session, UUID(current_user.id))
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
if update_data.first_name is not None:
|
||||
user.first_name = update_data.first_name
|
||||
if update_data.last_name is not None:
|
||||
user.last_name = update_data.last_name
|
||||
if update_data.email is not None:
|
||||
# ensure email is unique
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(User).where(User.email == update_data.email, User.id != user.id))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already in use")
|
||||
user.email = update_data.email
|
||||
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return UserResponse.model_validate(user)
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Delete account (DELETE /auth/me)
|
||||
# ---------------------------------------------------
|
||||
@router.delete(
|
||||
"/me",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def delete_me(
|
||||
current_user: Annotated[UserResponse, Depends(get_current_user)],
|
||||
session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Soft‑delete (deactivate) the current user account and revoke API keys."""
|
||||
from uuid import UUID
|
||||
user = await get_user_by_id(session, UUID(current_user.id))
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# deactivate user
|
||||
user.is_active = False
|
||||
session.add(user)
|
||||
# revoke all API keys (if any)
|
||||
try:
|
||||
from src.models.api_key import APIKey
|
||||
from sqlalchemy import update
|
||||
await session.execute(update(APIKey).where(APIKey.user_id == user.id).values(active=False))
|
||||
except Exception:
|
||||
pass # ignore if APIKey model not present
|
||||
await session.commit()
|
||||
return {"message": "Account deactivated successfully"}
|
||||
|
||||
|
||||
|
||||
@router.post(
|
||||
"/change-password",
|
||||
|
||||
@@ -16,7 +16,8 @@ class User(Base, TimestampMixin):
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email = Column(String(255), nullable=False, unique=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
full_name = Column(String(255), nullable=True)
|
||||
first_name = Column(String(255), nullable=True)
|
||||
last_name = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_superuser = Column(Boolean, default=False, nullable=False)
|
||||
last_login = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
@@ -10,7 +10,8 @@ class UserBase(BaseModel):
|
||||
"""Base user schema."""
|
||||
|
||||
email: EmailStr
|
||||
full_name: Optional[str] = Field(None, max_length=255)
|
||||
first_name: Optional[str] = Field(None, max_length=255)
|
||||
last_name: Optional[str] = Field(None, max_length=255)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
@@ -22,7 +23,8 @@ class UserCreate(UserBase):
|
||||
class UserUpdate(BaseModel):
|
||||
"""Schema for updating a user."""
|
||||
|
||||
full_name: Optional[str] = Field(None, max_length=255)
|
||||
first_name: Optional[str] = Field(None, max_length=255)
|
||||
last_name: Optional[str] = Field(None, max_length=255)
|
||||
email: Optional[EmailStr] = Field(None, max_length=255)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user