Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""
|
|
Unit Tests for Check Identity Endpoint (US-013)
|
|
|
|
Tests the dual login support: email + username verification
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
|
|
|
|
# ============================================================================
|
|
# TEST CHECK IDENTITY REQUEST MODEL
|
|
# ============================================================================
|
|
|
|
class TestCheckIdentityRequestModel:
|
|
"""Tests for CheckIdentityRequest model validation."""
|
|
|
|
def test_valid_email_normalized_to_lowercase(self):
|
|
"""Email inputs should be normalized to lowercase."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="User@Example.COM")
|
|
assert request.identity == "user@example.com"
|
|
|
|
def test_valid_username_normalized_to_uppercase(self):
|
|
"""Username inputs (without @) should be normalized to uppercase."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="marius")
|
|
assert request.identity == "MARIUS"
|
|
|
|
def test_username_with_spaces_normalized(self):
|
|
"""Username with spaces should be preserved but uppercased."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="marius m")
|
|
assert request.identity == "MARIUS M"
|
|
|
|
def test_whitespace_trimmed(self):
|
|
"""Leading/trailing whitespace should be trimmed."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity=" user@test.com ")
|
|
assert request.identity == "user@test.com"
|
|
|
|
def test_empty_identity_raises_error(self):
|
|
"""Empty identity should raise validation error."""
|
|
from pydantic import ValidationError
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
with pytest.raises(ValidationError):
|
|
CheckIdentityRequest(identity="")
|
|
|
|
def test_too_short_identity_raises_error(self):
|
|
"""Identity shorter than 2 chars should raise validation error."""
|
|
from pydantic import ValidationError
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
with pytest.raises(ValidationError):
|
|
CheckIdentityRequest(identity="a")
|
|
|
|
|
|
# ============================================================================
|
|
# TEST CHECK IDENTITY RESPONSE MODEL
|
|
# ============================================================================
|
|
|
|
class TestCheckIdentityResponseModel:
|
|
"""Tests for CheckIdentityResponse model."""
|
|
|
|
def test_response_with_email_type(self):
|
|
"""Response should include identity_type field."""
|
|
from shared.auth.models import CheckIdentityResponse, ServerInfo
|
|
|
|
response = CheckIdentityResponse(
|
|
exists=True,
|
|
servers=[ServerInfo(id="server1", name="Server 1")],
|
|
identity_type="email"
|
|
)
|
|
assert response.exists is True
|
|
assert response.identity_type == "email"
|
|
assert len(response.servers) == 1
|
|
|
|
def test_response_with_username_type(self):
|
|
"""Response should support username identity type."""
|
|
from shared.auth.models import CheckIdentityResponse
|
|
|
|
response = CheckIdentityResponse(
|
|
exists=True,
|
|
servers=[],
|
|
identity_type="username"
|
|
)
|
|
assert response.identity_type == "username"
|
|
|
|
def test_response_default_identity_type(self):
|
|
"""Default identity_type should be 'unknown'."""
|
|
from shared.auth.models import CheckIdentityResponse
|
|
|
|
response = CheckIdentityResponse(exists=False, servers=[])
|
|
assert response.identity_type == "unknown"
|
|
|
|
|
|
# ============================================================================
|
|
# TEST IDENTITY TYPE DETECTION
|
|
# ============================================================================
|
|
|
|
class TestIdentityTypeDetection:
|
|
"""Tests for email vs username detection logic."""
|
|
|
|
def test_email_detected_by_at_sign(self):
|
|
"""Identity with @ should be treated as email."""
|
|
# This is tested via the model validator
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="test@example.com")
|
|
# Email should be lowercase
|
|
assert request.identity == "test@example.com"
|
|
assert "@" in request.identity
|
|
|
|
def test_username_detected_without_at_sign(self):
|
|
"""Identity without @ should be treated as username."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="MARIUS")
|
|
# Username should be uppercase
|
|
assert request.identity == "MARIUS"
|
|
assert "@" not in request.identity
|
|
|
|
|
|
# ============================================================================
|
|
# TEST EMAIL SERVER CACHE USERNAME LOOKUP
|
|
# ============================================================================
|
|
|
|
class TestEmailServerCacheUsernameLookup:
|
|
"""Tests for username lookup in EmailServerCache."""
|
|
|
|
@pytest.fixture
|
|
def reset_cache(self):
|
|
"""Reset the cache singleton before each test."""
|
|
from shared.auth.email_server_cache import EmailServerCache
|
|
|
|
# Reset singleton
|
|
EmailServerCache._instance = None
|
|
yield
|
|
EmailServerCache._instance = None
|
|
|
|
def test_get_servers_for_username_method_exists(self, reset_cache):
|
|
"""EmailServerCache should have get_servers_for_username method."""
|
|
from shared.auth.email_server_cache import EmailServerCache
|
|
|
|
cache = EmailServerCache()
|
|
assert hasattr(cache, 'get_servers_for_username')
|
|
assert callable(cache.get_servers_for_username)
|
|
|
|
def test_empty_username_returns_empty_list(self, reset_cache):
|
|
"""Empty username should return empty list."""
|
|
import asyncio
|
|
from shared.auth.email_server_cache import EmailServerCache
|
|
|
|
cache = EmailServerCache()
|
|
|
|
async def test():
|
|
# Mock settings to return empty servers
|
|
with patch('backend.config.settings') as mock_settings:
|
|
mock_settings.get_oracle_servers.return_value = []
|
|
result = await cache.get_servers_for_username("")
|
|
return result
|
|
|
|
result = asyncio.get_event_loop().run_until_complete(test())
|
|
assert result == []
|
|
|
|
|
|
# ============================================================================
|
|
# TEST BACKWARD COMPATIBILITY
|
|
# ============================================================================
|
|
|
|
class TestBackwardCompatibility:
|
|
"""Tests for backward compatibility with check-email endpoint."""
|
|
|
|
def test_check_email_request_still_works(self):
|
|
"""CheckEmailRequest should still work for backward compatibility."""
|
|
from shared.auth.models import CheckEmailRequest
|
|
|
|
request = CheckEmailRequest(email="user@example.com")
|
|
assert request.email == "user@example.com"
|
|
|
|
def test_check_email_response_still_works(self):
|
|
"""CheckEmailResponse should still work for backward compatibility."""
|
|
from shared.auth.models import CheckEmailResponse, ServerInfo
|
|
|
|
response = CheckEmailResponse(
|
|
exists=True,
|
|
servers=[ServerInfo(id="s1", name="Server 1")]
|
|
)
|
|
assert response.exists is True
|
|
assert len(response.servers) == 1
|
|
|
|
|
|
# ============================================================================
|
|
# TEST ACCEPTANCE CRITERIA (US-013)
|
|
# ============================================================================
|
|
|
|
class TestAcceptanceCriteria:
|
|
"""Tests verifying US-013 acceptance criteria."""
|
|
|
|
def test_ac1_check_identity_request_model_exists(self):
|
|
"""AC1: CheckIdentityRequest model exists."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
assert CheckIdentityRequest is not None
|
|
|
|
def test_ac2_email_detection_with_at_sign(self):
|
|
"""AC2: Input with @ is treated as email."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="test@domain.com")
|
|
# Email normalized to lowercase
|
|
assert "@" in request.identity
|
|
assert request.identity.islower()
|
|
|
|
def test_ac3_username_detection_without_at_sign(self):
|
|
"""AC3: Input without @ is treated as username."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="admin")
|
|
# Username normalized to uppercase
|
|
assert "@" not in request.identity
|
|
assert request.identity == "ADMIN"
|
|
|
|
def test_ac4_check_email_backward_compatible(self):
|
|
"""AC4: Old check-email models still work."""
|
|
from shared.auth.models import CheckEmailRequest, CheckEmailResponse
|
|
|
|
# Both models should be importable and usable
|
|
req = CheckEmailRequest(email="test@test.com")
|
|
resp = CheckEmailResponse(exists=False, servers=[])
|
|
|
|
assert req.email == "test@test.com"
|
|
assert resp.exists is False
|
|
|
|
def test_ac5_response_includes_identity_type(self):
|
|
"""AC5: Response includes identity_type field."""
|
|
from shared.auth.models import CheckIdentityResponse
|
|
|
|
response = CheckIdentityResponse(
|
|
exists=True,
|
|
servers=[],
|
|
identity_type="email"
|
|
)
|
|
assert hasattr(response, 'identity_type')
|
|
assert response.identity_type in ["email", "username", "unknown"]
|
|
|
|
|
|
# ============================================================================
|
|
# TEST UI REQUIREMENTS
|
|
# ============================================================================
|
|
|
|
class TestUIRequirements:
|
|
"""Tests verifying UI-related requirements."""
|
|
|
|
def test_placeholder_label_correct(self):
|
|
"""UI should use 'Email sau utilizator' as label."""
|
|
# This is a documentation test - the actual UI change is in Vue
|
|
# We verify the backend accepts both formats
|
|
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
# Should accept email format
|
|
email_req = CheckIdentityRequest(identity="user@example.com")
|
|
assert email_req.identity == "user@example.com"
|
|
|
|
# Should accept username format
|
|
username_req = CheckIdentityRequest(identity="UTILIZATOR")
|
|
assert username_req.identity == "UTILIZATOR"
|
|
|
|
def test_username_with_romanian_chars_handled(self):
|
|
"""Username with spaces (like 'MARIUS M') should be handled."""
|
|
from shared.auth.models import CheckIdentityRequest
|
|
|
|
request = CheckIdentityRequest(identity="marius m")
|
|
assert request.identity == "MARIUS M"
|