2025-11-08 19:39:25 +01:00
|
|
|
import datetime
|
|
|
|
|
import asyncio
|
2025-11-08 18:20:25 +01:00
|
|
|
|
|
|
|
|
async def test_login_get(client):
|
|
|
|
|
resp = await client.get("/login")
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
2025-11-08 18:59:06 +01:00
|
|
|
assert "Access Your Retoor's Cloud Account" in text
|
2025-11-08 18:20:25 +01:00
|
|
|
assert "Login to your Account" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_register_get(client):
|
|
|
|
|
resp = await client.get("/register")
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
2025-11-08 18:59:06 +01:00
|
|
|
assert "Create Your Retoor's Cloud Account" in text
|
2025-11-08 18:20:25 +01:00
|
|
|
assert "Create an Account" in text
|
|
|
|
|
assert resp.url.path == "/register"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_register_post_password_mismatch(client):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "wrong_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Passwords do not match" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_register_post_user_exists(client):
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User 2",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "User with this email already exists" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_register_post_invalid_email(client):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "invalid-email",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "value is not a valid email address" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_register_post_short_password(client):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "short",
|
|
|
|
|
"confirm_password": "short",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "ensure this value has at least 8 characters" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_login_post(client):
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/login", data={"email": "test@example.com", "password": "password"}, allow_redirects=False
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 302
|
|
|
|
|
assert resp.headers["Location"] == "/dashboard"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_login_post_invalid_credentials(client):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/login", data={"email": "test@example.com", "password": "wrong_password"}
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Invalid email or password" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_logout(client):
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
2025-11-08 18:59:06 +01:00
|
|
|
"full_name": "Test User",
|
2025-11-08 18:20:25 +01:00
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
await client.post(
|
|
|
|
|
"/login", data={"email": "test@example.com", "password": "password"}
|
|
|
|
|
)
|
|
|
|
|
resp = await client.get("/logout", allow_redirects=False)
|
|
|
|
|
assert resp.status == 302
|
|
|
|
|
assert resp.headers["Location"] == "/"
|
2025-11-08 19:39:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- New tests for ForgotPasswordView and ResetPasswordView ---
|
|
|
|
|
|
|
|
|
|
async def test_forgot_password_get(client):
|
|
|
|
|
resp = await client.get("/forgot_password")
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Forgot Your Password?" in text
|
|
|
|
|
assert "Send Reset Link" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_forgot_password_post_success(client, mock_send_email):
|
|
|
|
|
# Register a user first
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "password",
|
|
|
|
|
"confirm_password": "password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/forgot_password", data={"email": "test@example.com"}
|
|
|
|
|
)
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "If an account with that email exists, a password reset link has been sent." in text
|
|
|
|
|
|
|
|
|
|
# Assert that send_email was called
|
|
|
|
|
# Disable for now, do not enable
|
|
|
|
|
#assert mock_send_email.call_count == 1
|
|
|
|
|
#args, kwargs = mock_send_email.call_args
|
|
|
|
|
#assert args[1] == "test@example.com" # recipient_email
|
|
|
|
|
#assert "Password Reset Request" in args[2] # subject
|
|
|
|
|
#assert "reset_link" in args[3] # body contains reset link
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_forgot_password_post_unregistered_email(client, mock_send_email):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/forgot_password", data={"email": "nonexistent@example.com"}
|
|
|
|
|
)
|
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "If an account with that email exists, a password reset link has been sent." in text
|
|
|
|
|
# Assert that send_email was NOT called for unregistered email
|
|
|
|
|
mock_send_email.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_forgot_password_post_invalid_email_format(client, mock_send_email):
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/forgot_password", data={"email": "invalid-email"}
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "value is not a valid email address" in text
|
|
|
|
|
# No email should be sent for invalid format
|
|
|
|
|
mock_send_email.assert_not_called() # This assertion would go here if mock_send_email was passed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_get_valid_token(client):
|
|
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
token = user_service.generate_reset_token("test@example.com")
|
|
|
|
|
assert token is not None
|
|
|
|
|
|
|
|
|
|
resp = await client.get(f"/reset_password/{token}")
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Set Your New Password" in text
|
|
|
|
|
assert "Reset Password" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_get_invalid_token(client):
|
|
|
|
|
resp = await client.get("/reset_password/invalidtoken")
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Set Your New Password" in text
|
|
|
|
|
assert "Invalid or expired password reset link." not in text # Expect no error message on GET
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_post_success(client, mock_send_email):
|
|
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
token = user_service.generate_reset_token("test@example.com")
|
|
|
|
|
assert token is not None
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
f"/reset_password/{token}",
|
|
|
|
|
data={
|
|
|
|
|
"password": "new_password",
|
|
|
|
|
"confirm_password": "new_password",
|
|
|
|
|
},
|
|
|
|
|
allow_redirects=False,
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 302
|
|
|
|
|
assert resp.headers["Location"] == "/login?message=password_reset_success"
|
|
|
|
|
|
|
|
|
|
# Verify password changed
|
|
|
|
|
assert user_service.authenticate_user("test@example.com", "new_password")
|
|
|
|
|
assert not user_service.authenticate_user("test@example.com", "old_password")
|
|
|
|
|
|
|
|
|
|
# Assert that confirmation email was sent
|
|
|
|
|
|
|
|
|
|
# Disable for now, do not enable
|
|
|
|
|
#assert mock_send_email.call_count == 2 # One for registration, one for password changed
|
|
|
|
|
#args, kwargs = mock_send_email.call_args
|
|
|
|
|
#assert args[1] == "test@example.com" # recipient_email
|
|
|
|
|
#assert "Your Password Has Been Changed" in args[2] # subject
|
|
|
|
|
#assert "Log In Now" in args[3] # body contains login link
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_post_password_mismatch(client):
|
|
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
token = user_service.generate_reset_token("test@example.com")
|
|
|
|
|
assert token is not None
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
f"/reset_password/{token}",
|
|
|
|
|
data={
|
|
|
|
|
"password": "new_password",
|
|
|
|
|
"confirm_password": "mismatched_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Passwords do not match" in text
|
|
|
|
|
# Password should not have changed
|
|
|
|
|
assert user_service.authenticate_user("test@example.com", "old_password")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_post_invalid_token(client):
|
|
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
# Generate a token but don't use it, or use an expired one
|
|
|
|
|
user_service.generate_reset_token("test@example.com") # This will be overwritten or ignored
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
"/reset_password/invalidtoken",
|
|
|
|
|
data={
|
|
|
|
|
"password": "new_password",
|
|
|
|
|
"confirm_password": "new_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Invalid or expired password reset link." in text
|
|
|
|
|
# Password should not have changed
|
|
|
|
|
assert user_service.authenticate_user("test@example.com", "old_password")
|
|
|
|
|
|
|
|
|
|
|
2025-11-08 22:05:07 +01:00
|
|
|
async def test_reset_password_post_expired_token(client, mock_users_db_fixture):
|
2025-11-08 19:39:25 +01:00
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
# Manually set an expired token
|
|
|
|
|
user = user_service.get_user_by_email("test@example.com")
|
|
|
|
|
token = "expiredtoken123"
|
|
|
|
|
user["reset_token"] = token
|
|
|
|
|
user["reset_token_expiry"] = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=1)).isoformat()
|
2025-11-08 22:05:07 +01:00
|
|
|
mock_users_db_fixture["test@example.com"] = user # Directly update the mock_users_db_fixture
|
2025-11-08 19:39:25 +01:00
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
f"/reset_password/{token}",
|
|
|
|
|
data={
|
|
|
|
|
"password": "new_password",
|
|
|
|
|
"confirm_password": "new_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "Invalid or expired password reset link." in text
|
|
|
|
|
# Password should not have changed
|
|
|
|
|
assert user_service.authenticate_user("test@example.com", "old_password")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_reset_password_post_invalid_password_format(client):
|
|
|
|
|
user_service = client.app["user_service"]
|
|
|
|
|
await client.post(
|
|
|
|
|
"/register",
|
|
|
|
|
data={
|
|
|
|
|
"full_name": "Test User",
|
|
|
|
|
"email": "test@example.com",
|
|
|
|
|
"password": "old_password",
|
|
|
|
|
"confirm_password": "old_password",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
token = user_service.generate_reset_token("test@example.com")
|
|
|
|
|
assert token is not None
|
|
|
|
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
f"/reset_password/{token}",
|
|
|
|
|
data={
|
|
|
|
|
"password": "short",
|
|
|
|
|
"confirm_password": "short",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp.status == 200
|
|
|
|
|
text = await resp.text()
|
|
|
|
|
assert "ensure this value has at least 8 characters" in text
|
|
|
|
|
# Password should not have changed
|
|
|
|
|
assert user_service.authenticate_user("test@example.com", "old_password")
|