from urllib.parse import parse_qs, urlparse from fastapi.testclient import TestClient from services.auth_service.app import app from services.shared.security import decode_app_token def test_login_success(): client = TestClient(app) response = client.post('/auth/login', json={'username': 'admin', 'password': 'admin123'}) assert response.status_code == 200 body = response.json() assert body['access_token'] assert body['role'] == 'admin' assert body['auth_source'] == 'local' claims = decode_app_token(body['access_token']) assert claims['username'] == 'admin' assert claims['role'] == 'admin' assert claims['auth_source'] == 'local' def test_create_user_as_admin(): client = TestClient(app) response = client.post( '/users', json={ 'username': 'newoperator', 'password': 'password1', 'full_name': 'New Operator', 'role': 'operator', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert response.status_code == 200 assert response.json()['username'] == 'newoperator' def test_operator_cannot_create_user(): client = TestClient(app) response = client.post( '/users', json={ 'username': 'blockeduser', 'password': 'password1', 'full_name': 'Blocked User', 'role': 'operator', }, headers={'X-User': 'operator', 'X-Role': 'operator'}, ) assert response.status_code == 403 def test_admin_can_update_user(): client = TestClient(app) created = client.post( '/users', json={ 'username': 'updatableuser', 'password': 'password1', 'full_name': 'Updatable User', 'role': 'operator', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert created.status_code == 200 updated = client.patch( f"/users/{created.json()['user_id']}", json={ 'full_name': 'Updated User', 'role': 'analyst', 'password': 'password2', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert updated.status_code == 200 assert updated.json()['full_name'] == 'Updated User' assert updated.json()['role'] == 'analyst' def test_supervisor_cannot_update_user(): client = TestClient(app) created = client.post( '/users', json={ 'username': 'guardeduser', 'password': 'password1', 'full_name': 'Guarded User', 'role': 'operator', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert created.status_code == 200 denied = client.patch( f"/users/{created.json()['user_id']}", json={'full_name': 'Should Not Update'}, headers={'X-User': 'supervisor', 'X-Role': 'supervisor'}, ) assert denied.status_code == 403 def test_admin_can_delete_user(): client = TestClient(app) created = client.post( '/users', json={ 'username': 'deletecandidate', 'password': 'password1', 'full_name': 'Delete Candidate', 'role': 'operator', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert created.status_code == 200 deleted = client.delete( f"/users/{created.json()['user_id']}", headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert deleted.status_code == 200 assert deleted.json()['deleted'] is True lookup = client.get( f"/users/{created.json()['user_id']}", headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert lookup.status_code == 404 def test_admin_cannot_delete_current_user(): client = TestClient(app) users = client.get('/users', headers={'X-User': 'admin', 'X-Role': 'admin'}) assert users.status_code == 200 admin_user = next(item for item in users.json() if item['username'] == 'admin') deleted = client.delete( f"/users/{admin_user['user_id']}", headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert deleted.status_code == 409 def test_supervisor_cannot_delete_user(): client = TestClient(app) created = client.post( '/users', json={ 'username': 'nodeletebysupervisor', 'password': 'password1', 'full_name': 'No Delete', 'role': 'operator', }, headers={'X-User': 'admin', 'X-Role': 'admin'}, ) assert created.status_code == 200 denied = client.delete( f"/users/{created.json()['user_id']}", headers={'X-User': 'supervisor', 'X-Role': 'supervisor'}, ) assert denied.status_code == 403 def test_oidc_config_reflects_disabled_state(monkeypatch): monkeypatch.setenv('OIDC_ENABLED', '0') client = TestClient(app) response = client.get('/auth/oidc/config') assert response.status_code == 200 assert response.json()['enabled'] is False def test_oidc_start_redirects_with_override_urls(monkeypatch): monkeypatch.setenv('OIDC_ENABLED', '1') monkeypatch.setenv('OIDC_AUTHORIZE_URL', 'http://keycloak.local/auth') monkeypatch.setenv('OIDC_TOKEN_URL', 'http://keycloak.local/token') client = TestClient(app) response = client.get('/auth/oidc/start', follow_redirects=False) assert response.status_code == 302 location = response.headers['location'] assert location.startswith('http://keycloak.local/auth?') params = parse_qs(urlparse(location).query) assert params['client_id'][0] == 'mvp-contact-center' assert params['response_type'][0] == 'code' assert 'state' in params def test_oidc_callback_accepts_mapped_mock_user(monkeypatch): monkeypatch.setenv('OIDC_ENABLED', '1') monkeypatch.setenv('OIDC_TEST_MODE', '1') monkeypatch.setenv('OIDC_AUTHORIZE_URL', 'http://keycloak.local/auth') monkeypatch.setenv('OIDC_TOKEN_URL', 'http://keycloak.local/token') client = TestClient(app) start = client.get('/auth/oidc/start', follow_redirects=False) params = parse_qs(urlparse(start.headers['location']).query) state = params['state'][0] response = client.get( f'/auth/oidc/callback?state={state}&mock_sub=sub-1&mock_username=kc.user&mock_groups=kc_operator' ) assert response.status_code == 200 assert 'oidc-login' in response.text assert 'Corporate login completed' in response.text def test_oidc_callback_rejects_unmapped_mock_user(monkeypatch): monkeypatch.setenv('OIDC_ENABLED', '1') monkeypatch.setenv('OIDC_TEST_MODE', '1') monkeypatch.setenv('OIDC_AUTHORIZE_URL', 'http://keycloak.local/auth') monkeypatch.setenv('OIDC_TOKEN_URL', 'http://keycloak.local/token') client = TestClient(app) start = client.get('/auth/oidc/start', follow_redirects=False) params = parse_qs(urlparse(start.headers['location']).query) state = params['state'][0] response = client.get( f'/auth/oidc/callback?state={state}&mock_sub=sub-2&mock_username=kc.bad&mock_groups=unknown_group' ) assert response.status_code == 403 assert 'OIDC role is not mapped' in response.text