from contextlib import contextmanager from unittest.mock import MagicMock import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.workflows import customer_activation as api, integration, worker @pytest.fixture def client(monkeypatch): state={'exists':True,'schema':True,'override':None,'history':[],'rights':{'customers.view','customers.edit','workflows.activate'}} class Cursor: def execute(self,sql,params=None): self.sql=sql if sql.startswith('INSERT INTO bmc_workflow_customer_activation('): state['override']={'customer_id':params[0],'enabled':params[1],'updated_by':params[2],'updated_at':None} elif sql.startswith('INSERT INTO bmc_workflow_customer_activation_history'): state['history'].append(params) def fetchone(self): if 'FROM customers' in self.sql: return {'id':24} if state['exists'] else None if 'to_regclass' in self.sql: return {'name':'bmc_workflow_customer_activation' if state['schema'] else None} return state['override'] @contextmanager def transaction(): yield Cursor() monkeypatch.setattr(api,'transaction',transaction) monkeypatch.setattr(api.AuthService,'user_has_permission',lambda actor,permission:permission in state['rights']) monkeypatch.setattr(api.settings,'WORKFLOWS_ENABLED',False) monkeypatch.setattr(api.settings,'WORKFLOWS_MODE','active') monkeypatch.setattr(api.settings,'WORKFLOWS_CUSTOMER_IDS',[24]) app=FastAPI() app.include_router(api.router,prefix='/api/v1') app.dependency_overrides[api.get_current_user]=lambda:{'id':4,'username':'tester','is_superadmin':False} return TestClient(app),state def test_inherited_env_preference_does_not_enable_global_master(client): browser,state=client response=browser.get('/api/v1/workflows/customers/24/activation') assert response.status_code==200 body=response.json() assert body['enabled'] is True and body['inherited'] is True assert body['explicit_override'] is None and body['effective_enabled'] is False assert state['history']==[] def test_false_override_wins_and_records_reason(client): browser,state=client response=browser.put('/api/v1/workflows/customers/24/activation',json={'enabled':False,'reason':'Stop automatik'}) assert response.status_code==200 body=response.json() assert body['enabled'] is False and body['inherited'] is False and body['explicit_override'] is False assert state['history']==[(24,None,False,True,4,'Stop automatik')] def test_saved_true_preference_remains_inactive_in_shadow(client,monkeypatch): browser,state=client monkeypatch.setattr(api.settings,'WORKFLOWS_ENABLED',True) monkeypatch.setattr(api.settings,'WORKFLOWS_MODE','shadow') result=browser.put('/api/v1/workflows/customers/24/activation',json={'enabled':True,'reason':'Kunde har godkendt'}) assert result.status_code==200 assert result.json()['enabled'] is True and result.json()['effective_enabled'] is False @pytest.mark.parametrize('missing',['customers.edit','workflows.activate']) def test_write_requires_both_permissions(client,missing): browser,state=client state['rights'].remove(missing) response=browser.put('/api/v1/workflows/customers/24/activation',json={'enabled':True,'reason':'Test årsag'}) assert response.status_code==403 and state['history']==[] def test_read_accepts_workflow_view_without_customer_view(client): browser,state=client state['rights']={'workflows.view'} response=browser.get('/api/v1/workflows/customers/24/activation') assert response.status_code==200 and response.json()['can_manage'] is False def test_nonexistent_customer_and_short_reason_rejected(client): browser,state=client state['exists']=False assert browser.get('/api/v1/workflows/customers/24/activation').status_code==404 state['exists']=True assert browser.put('/api/v1/workflows/customers/24/activation',json={'enabled':True,'reason':' '}).status_code==422 assert state['history']==[] def test_missing_schema_reads_env_but_refuses_write(client): browser,state=client state['schema']=False assert browser.get('/api/v1/workflows/customers/24/activation').json()['enabled'] is True assert browser.put('/api/v1/workflows/customers/24/activation',json={'enabled':True,'reason':'Test årsag'}).status_code==503 def test_helper_false_database_override_wins_even_explicit_fallback(monkeypatch): cursor=MagicMock() cursor.fetchone.side_effect=[{'name':'activation'},{'enabled':False}] assert integration.customer_enabled(24,cursor,fallback_ids=[24]) is False assert 'FOR SHARE' in cursor.execute.call_args.args[0] def test_effective_ids_merge_true_and_false_overrides(monkeypatch): monkeypatch.setattr(integration.settings,'WORKFLOWS_CUSTOMER_IDS',[24,25]) cursor=MagicMock() cursor.fetchone.return_value={'name':'activation'} cursor.fetchall.return_value=[{'customer_id':24,'enabled':False},{'customer_id':26,'enabled':True}] assert integration.effective_customer_ids(cursor)==[25,26] def test_active_global_empty_env_allows_saved_customer(monkeypatch): monkeypatch.setattr(integration.settings,'WORKFLOWS_ENABLED',True) monkeypatch.setattr(integration.settings,'WORKFLOWS_MODE','active') monkeypatch.setattr(integration.settings,'WORKFLOWS_CUSTOMER_IDS',[]) monkeypatch.setattr(integration,'execute_query_single',lambda sql,params=None:{'name':'activation'} if 'to_regclass' in sql else {'enabled':True}) assert integration.active_for_customer(24) is True def test_active_empty_environment_does_not_prevent_customer_card_activation(monkeypatch): monkeypatch.setattr(integration.settings,'WORKFLOWS_ENABLED',True) monkeypatch.setattr(integration.settings,'WORKFLOWS_MODE','active') monkeypatch.setattr(integration.settings,'WORKFLOWS_CUSTOMER_IDS',[]) monkeypatch.setattr(integration.settings,'WORKFLOWS_INTERNAL_ENABLED',False) monkeypatch.setattr(integration,'execute_query_single',lambda sql:{'name':'runtime'}) writes=MagicMock() monkeypatch.setattr(integration,'execute_update',writes) integration.configure_runtime() writes.assert_called_once() assert 'bmc_workflow_customer_activation' not in writes.call_args.args[0] def test_real_database_error_does_not_fall_back_to_env_enablement(monkeypatch): cursor=MagicMock() cursor.execute.side_effect=RuntimeError('Database unavailable') with pytest.raises(RuntimeError,match='unavailable'): integration.customer_enabled(24,cursor,fallback_ids=[24])