161 lines
6.5 KiB
Python
161 lines
6.5 KiB
Python
"""Portable runtime contract tests; fake child responses never contact NMI."""
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import redmine_ticket_nmi as bridge
|
|
|
|
|
|
def fake_cli(tmp_path):
|
|
p = tmp_path / 'space dir' / 'nmi'
|
|
p.parent.mkdir()
|
|
p.write_text('<?php // test fixture\n')
|
|
return p
|
|
|
|
|
|
def doctor_result(cmd, **kwargs):
|
|
return subprocess.CompletedProcess(cmd, 0, json.dumps({
|
|
'ready': True, 'mode': 'production', 'env_file': None,
|
|
'endpoint': 'https://nmi.example.test/api', 'problem': None,
|
|
}), '')
|
|
|
|
|
|
def test_environment_only_runtime_uses_actual_doctor(monkeypatch, tmp_path):
|
|
cli = fake_cli(tmp_path)
|
|
calls = []
|
|
def invoke(cmd, **kwargs):
|
|
calls.append((cmd, kwargs))
|
|
return doctor_result(cmd, **kwargs)
|
|
monkeypatch.setattr(bridge.subprocess, 'run', invoke)
|
|
profile = bridge.resolve_runtime_profile(environ={
|
|
'NMI_CLI': str(cli), 'NMI_SECRETKEY': 'fixture-secret',
|
|
'NMI_MRSC_ACCESS_CODE': 'fixture-access',
|
|
'NMI_ENDPOINT': 'https://nmi.example.test/api',
|
|
})
|
|
assert profile.ready
|
|
assert profile.mode == 'production'
|
|
assert profile.env_file is None
|
|
assert len(calls) == 1
|
|
cmd, kwargs = calls[0]
|
|
assert '--doctor' in cmd and '--json' in cmd
|
|
assert not any(arg.startswith('--env-file') for arg in cmd)
|
|
assert 'fixture-secret' not in ' '.join(cmd)
|
|
assert kwargs['env']['NMI_SECRETKEY'] == 'fixture-secret'
|
|
|
|
|
|
def test_mode_and_binary_are_resolved_at_call_time(monkeypatch, tmp_path):
|
|
cli = fake_cli(tmp_path)
|
|
monkeypatch.setenv('NMI_CLI', str(cli))
|
|
monkeypatch.setenv('NMI_MODE', 'dev')
|
|
monkeypatch.delenv('NMI_ENV_FILE', raising=False)
|
|
calls = []
|
|
def invoke(cmd, **kwargs):
|
|
calls.append(cmd)
|
|
return subprocess.CompletedProcess(cmd, 0, '{}', '')
|
|
monkeypatch.setattr(bridge.subprocess, 'run', invoke)
|
|
bridge.run_nmi(['resolve', '--item-no=200000001'])
|
|
assert str(cli) in calls[0]
|
|
assert '--mode=dev' in calls[0]
|
|
assert not any('None' in value for value in calls[0])
|
|
|
|
|
|
def test_explicit_missing_env_blocks_child(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(bridge.subprocess, 'run', lambda *a, **k: pytest.fail('must not spawn'))
|
|
p = bridge.resolve_runtime_profile(env_file=str(tmp_path / 'absent'), nmi_bin=str(fake_cli(tmp_path)), environ={})
|
|
assert not p.ready
|
|
assert 'does not exist' in p.problem
|
|
|
|
|
|
def test_doctor_nonzero_is_not_ready(monkeypatch, tmp_path):
|
|
def invoke(cmd, **kwargs):
|
|
return subprocess.CompletedProcess(cmd, 2, json.dumps({'ready': False, 'problem': 'NMI_SECRETKEY is missing'}), '')
|
|
monkeypatch.setattr(bridge.subprocess, 'run', invoke)
|
|
p = bridge.resolve_runtime_profile(nmi_bin=str(fake_cli(tmp_path)), environ={})
|
|
assert not p.ready
|
|
assert 'NMI_SECRETKEY' in p.problem
|
|
|
|
|
|
def test_doctor_timeout_actionable(monkeypatch, tmp_path):
|
|
def invoke(cmd, **kwargs):
|
|
raise subprocess.TimeoutExpired(cmd, 15)
|
|
monkeypatch.setattr(bridge.subprocess, 'run', invoke)
|
|
p = bridge.resolve_runtime_profile(nmi_bin=str(fake_cli(tmp_path)), environ={})
|
|
assert not p.ready
|
|
assert 'timed out' in p.problem.lower()
|
|
|
|
|
|
def test_brief_executes_resolved_profile_not_import_defaults(monkeypatch, tmp_path):
|
|
cli = fake_cli(tmp_path)
|
|
env_file = tmp_path / 'selected.env'
|
|
env_file.write_text('NMI_MODE=production\n')
|
|
profile = bridge.NmiRuntimeProfile('production', env_file, cli, 'https://nmi.example.test/api', True)
|
|
monkeypatch.setattr(bridge, 'resolve_runtime_profile', lambda **kwargs: profile)
|
|
calls = []
|
|
def brief(plan, **kwargs):
|
|
calls.append(kwargs)
|
|
return {'ok': True, 'text': '=== NMI CS BRIEF ===\nfixture'}
|
|
monkeypatch.setattr(bridge, 'try_cs_brief', brief)
|
|
result = bridge.brief_for_issue({'id': 1}, [{'type': 'item_no', 'value': '200000001'}], with_history=False)
|
|
assert result['ok']
|
|
assert calls[0]['nmi_bin'] == str(cli)
|
|
assert calls[0]['env_file'] == str(env_file)
|
|
assert calls[0]['endpoint'] == profile.endpoint
|
|
|
|
|
|
def test_history_uses_one_item_identifier(monkeypatch):
|
|
seen = []
|
|
monkeypatch.setattr(bridge, 'try_item_history', lambda item, **kwargs: seen.append(item) or {'ok': True})
|
|
bridge.enrich_with_history({'plan': {'item_no': ['200000001', '200000002']}})
|
|
assert seen == ['200000001']
|
|
|
|
|
|
def test_real_php_doctor_integration(tmp_path):
|
|
"""Executes sibling NMI's real config resolver, not its network API."""
|
|
import os
|
|
import shutil
|
|
cli = Path(bridge.DEFAULT_NMI_BIN)
|
|
if not cli.is_file() or not shutil.which('php'):
|
|
pytest.skip('Sibling NMI/PHP not available')
|
|
env_file = tmp_path / 'test profile.env'
|
|
env_file.write_text('NMI_MRSC_ACCESS_CODE=fixture-access\nNMI_SECRETKEY=fixture-secret\nNMI_ENDPOINT=https://fixture.example.test\n')
|
|
profile = bridge.resolve_runtime_profile(environ={
|
|
'HOME': str(tmp_path), 'PATH': '/usr/bin:/bin', 'NMI_CLI': str(cli),
|
|
'NMI_ENV_FILE': str(env_file),
|
|
})
|
|
assert profile.ready, profile.problem
|
|
assert profile.mode == 'production'
|
|
assert profile.env_file == env_file
|
|
assert profile.endpoint == 'https://fixture.example.test'
|
|
|
|
|
|
def test_real_php_child_receives_same_runtime_for_brief_and_history(tmp_path, monkeypatch):
|
|
"""Fake PHP service-facing CLI; exercises real subprocess argv forwarding."""
|
|
import shutil
|
|
if not shutil.which('php'):
|
|
pytest.skip('PHP unavailable')
|
|
cli = tmp_path / 'fake nmi.php'
|
|
log = tmp_path / 'calls.jsonl'
|
|
env_file = tmp_path / 'profile.env'
|
|
env_file.write_text('NMI_MODE=dev\n')
|
|
cli.write_text('''<?php
|
|
file_put_contents(getenv('BRIDGE_TEST_LOG'), json_encode($argv)."\\n", FILE_APPEND);
|
|
if (in_array('--doctor', $argv, true)) {
|
|
echo json_encode(['ready'=>true,'mode'=>'dev','env_file'=>getenv('NMI_ENV_FILE'),'endpoint'=>'https://fixture.example.test','problem'=>null]);
|
|
} elseif (in_array('cs-brief',$argv,true)) {
|
|
echo "=== NMI CS BRIEF === fixture only\\n";
|
|
} else { echo json_encode(['fixture'=>true]); }
|
|
''')
|
|
monkeypatch.setenv('NMI_CLI', str(cli))
|
|
monkeypatch.setenv('NMI_ENV_FILE', str(env_file))
|
|
monkeypatch.setenv('NMI_MODE', 'dev')
|
|
monkeypatch.setenv('BRIDGE_TEST_LOG', str(log))
|
|
result = bridge.brief_for_issue({'id':1}, [{'type':'item_no','value':'200000001'}])
|
|
assert result['ok'] and result['item_history']['ok']
|
|
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
|
assert len(calls) == 3
|
|
for call in calls:
|
|
assert '--mode=dev' in call
|
|
assert '--env-file='+str(env_file) in call
|
|
assert '--item-no=200000001' in calls[-1]
|