188 lines
8.5 KiB
Python
188 lines
8.5 KiB
Python
"""Run real wrapper entry points in a relocated checkout with an isolated HOME."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import pytest
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
|
|
@pytest.fixture
|
|
def runtime(tmp_path):
|
|
root=tmp_path/'relocated checkout'
|
|
(root/'bin').mkdir(parents=True)
|
|
shutil.copy(ROOT/'redmine_cli_runtime.py',root)
|
|
for name in ('rcontacts-live','rtickets-live','rtl','rtd'):
|
|
shutil.copy(ROOT/'bin'/name,root/'bin'/name)
|
|
fake='import json,os,sys; print(json.dumps({"argv":sys.argv[1:],"env":{k:v for k,v in os.environ.items() if k.startswith("REDMINE_") or k.startswith("NMI_")}}))\n'
|
|
for name in ('redmine_tickets.py','redmine_contacts.py'):
|
|
(root/name).write_text(fake)
|
|
home=tmp_path/'home'; home.mkdir()
|
|
env={'HOME':str(home),'PATH':'/usr/bin:/bin','TMPDIR':os.environ['TMPDIR']}
|
|
def run(*args,command='rtl',extra=None):
|
|
return subprocess.run([str(root/'bin'/command),*args],cwd=tmp_path,env={**env,**(extra or {})},text=True,capture_output=True,timeout=10)
|
|
return root,home,run
|
|
|
|
|
|
def test_config_file_and_export_precedence(runtime):
|
|
root,home,run=runtime
|
|
(home/'.redmine.live').write_text('export REDMINE_API_KEY="fixture-secret"\nREDMINE_BASE_URL=https://file.example.test\nREDMINE_PROJECT=abc\nREDMINE_BASE_CACHE="data with spaces"\n')
|
|
p=run('--show-config',extra={'REDMINE_BASE_URL':'https://env.example.test'})
|
|
assert p.returncode==0,p.stderr
|
|
d=json.loads(p.stdout)
|
|
assert d['base_url']=='https://env.example.test'
|
|
assert d['project']=='abc'
|
|
assert d['base_cache']==str(home/'data with spaces')
|
|
assert d['api_key_present']
|
|
assert 'fixture-secret' not in p.stdout+p.stderr
|
|
assert 'environment' in d['sources']['base_url']
|
|
p=run('--base-url','https://cli.example.test','--show-config',extra={'REDMINE_BASE_URL':'https://env.example.test'})
|
|
assert json.loads(p.stdout)['base_url']=='https://cli.example.test'
|
|
|
|
|
|
def test_real_dispatch_preserves_stdin_arguments_and_key(runtime):
|
|
root,home,run=runtime
|
|
(home/'.redmine.live').write_text('REDMINE_API_KEY=fixture-secret\n')
|
|
p=run('--base-cache','my cache','search','--query','two words',command='rcontacts-live')
|
|
assert p.returncode==0,p.stderr
|
|
d=json.loads(p.stdout)
|
|
assert d['env']['REDMINE_API_KEY']=='fixture-secret'
|
|
assert d['argv'][-3:]==['search','--query','two words']
|
|
cache=d['argv'][d['argv'].index('--cache')+1]
|
|
assert cache.endswith('my cache/redmine_contacts/rm.ldrprep.com_customer-service.json')
|
|
assert not Path(cache).is_dir()
|
|
|
|
|
|
def test_doctor_missing_key_nonzero_with_search_paths(runtime):
|
|
root,home,run=runtime
|
|
p=run('--doctor')
|
|
assert p.returncode==2
|
|
d=json.loads(p.stdout)
|
|
assert not d['ready']
|
|
assert 'REDMINE_API_KEY' in d['problems'][0]
|
|
assert str(home/'.redmine.live') in d['searched_env_paths']
|
|
assert not (root/'cache').exists()
|
|
p=run('sync')
|
|
assert p.returncode==2 and 'REDMINE_API_KEY' in p.stderr
|
|
|
|
|
|
def test_explicit_missing_or_malformed_file_blocks(runtime):
|
|
root,home,run=runtime
|
|
assert run('--env-file','absent','--doctor').returncode==2
|
|
file=home/'bad.env'; file.write_text('not an assignment\n')
|
|
p=run('--env-file',str(file),'--show-config')
|
|
assert p.returncode==2 and 'Malformed' in p.stderr
|
|
|
|
|
|
def test_help_works_without_config(runtime):
|
|
_,_,run=runtime
|
|
p=run('--env-file','missing','--help')
|
|
assert p.returncode==0
|
|
assert 'REDMINE_API_KEY' in p.stdout
|
|
|
|
|
|
def test_aliases_match_and_dev_never_loads_live(runtime):
|
|
root,home,run=runtime
|
|
(home/'.redmine.live').write_text('REDMINE_API_KEY=fixture-live\n')
|
|
a=json.loads(run('--show-config',command='rtl').stdout)
|
|
b=json.loads(run('--show-config',command='rtickets-live').stdout)
|
|
assert a==b
|
|
d=run('--show-config',command='rtd')
|
|
data=json.loads(d.stdout)
|
|
assert not data['api_key_present']
|
|
assert data['nmi_mode']=='dev'
|
|
assert 'DEVELOPMENT' in d.stderr
|
|
assert data['ticket_cache']==str(root/'cache-dev/redmine_tickets')
|
|
|
|
|
|
def test_cache_overrides_and_scope_isolation(runtime):
|
|
root,_,run=runtime
|
|
default=json.loads(run('--show-config').stdout)
|
|
assert default['ticket_cache']==str(root/'cache/redmine_tickets')
|
|
assert default['contacts_cache'].endswith('/cache/redmine_contacts/rm.ldrprep.com_customer-service.json')
|
|
assert default['contacts_cache_layout']=='namespaced-file'
|
|
other=json.loads(run('--project','meetings','--show-config').stdout)
|
|
assert other['ticket_cache']!=default['ticket_cache']
|
|
host=json.loads(run('--base-url','https://other.example.test','--show-config').stdout)
|
|
assert host['ticket_cache']!=default['ticket_cache']
|
|
exact=json.loads(run('--cache-dir','specific path','--show-config').stdout)
|
|
assert exact['ticket_cache'].endswith('/specific path')
|
|
|
|
|
|
def test_json_config_and_key_file_propagation(runtime):
|
|
root,home,run=runtime
|
|
config=home/'live.json'; key=home/'key'; key.write_text('fixture-key-file\n')
|
|
config.write_text(json.dumps({'project':'meetings','cache-dir':'mirror','api-key-file':'key'}))
|
|
p=run('--config',str(config),'search',extra={'REDMINE_PROJECT':'overridden'})
|
|
assert p.returncode==0,p.stderr
|
|
d=json.loads(p.stdout)
|
|
assert d['env']['REDMINE_API_KEY']=='fixture-key-file'
|
|
assert d['env']['REDMINE_PROJECT']=='overridden'
|
|
assert '--config' not in d['argv']
|
|
assert str(home/'mirror') in d['argv']
|
|
|
|
|
|
def test_subcommand_config_is_not_global(runtime):
|
|
_,_,run=runtime
|
|
p=run('materialize-queues','--config','queues.json')
|
|
assert p.returncode==0,p.stderr
|
|
assert json.loads(p.stdout)['argv'][-3:]==['materialize-queues','--config','queues.json']
|
|
|
|
|
|
def test_directory_contacts_cache_rejected(runtime):
|
|
root,_,run=runtime
|
|
p=run('--cache',str(root),'--show-config',command='rcontacts-live')
|
|
assert p.returncode==2 and 'JSON file' in p.stderr
|
|
|
|
|
|
def test_legacy_contacts_snapshot_is_preserved(runtime):
|
|
root,_,run=runtime
|
|
legacy=root/'cache/redmine_contacts'
|
|
legacy.parent.mkdir(parents=True)
|
|
legacy.write_text(json.dumps({'base_url':'https://rm.ldrprep.com','project':'customer-service','contacts':[]}))
|
|
d=json.loads(run('--show-config').stdout)
|
|
assert d['contacts_cache']==str(legacy)
|
|
assert d['contacts_cache_layout']=='legacy-single-file'
|
|
|
|
|
|
def test_contacts_handoff_preserves_resolved_context(tmp_path,monkeypatch):
|
|
import argparse
|
|
import shlex
|
|
from redmine_cli_runtime import contacts_handoff
|
|
monkeypatch.setenv('REDMINE_ENV_FILE',str(tmp_path/'profile with spaces.env'))
|
|
monkeypatch.setenv('REDMINE_CONTACTS_CACHE',str(tmp_path/'cache with spaces/contacts.json'))
|
|
args=argparse.Namespace(base_url='https://rm.example.test',project='meetings',cache_dir=tmp_path/'tickets',api_key_file=tmp_path/'key file')
|
|
cmd=shlex.split(contacts_handoff(args,42))
|
|
assert cmd[cmd.index('--cache')+1]==str(tmp_path/'cache with spaces/contacts.json')
|
|
assert cmd[cmd.index('--env-file')+1]==str(tmp_path/'profile with spaces.env')
|
|
assert cmd[cmd.index('--api-key-file')+1]==str(tmp_path/'key file')
|
|
assert cmd[-3:]==['show','--contact-id','42']
|
|
|
|
|
|
def test_actual_wrapper_invalid_ticket_query_is_clean_error(tmp_path):
|
|
env={'HOME':str(tmp_path),'PATH':'/usr/bin:/bin','TMPDIR':os.environ['TMPDIR'],
|
|
'REDMINE_API_KEY':'fixture-only','REDMINE_BASE_CACHE':str(tmp_path/'cache')}
|
|
p=subprocess.run([str(ROOT/'bin/rtl'),'search','--where','id == -1'],env=env,cwd=tmp_path,
|
|
text=True,capture_output=True,timeout=10)
|
|
assert p.returncode==2
|
|
assert 'Invalid --where expression' in p.stderr
|
|
assert 'Traceback' not in p.stderr
|
|
|
|
|
|
def test_actual_wrapper_to_actual_nmi_doctor(tmp_path):
|
|
cli=ROOT.parent/'mrscClient-php/bin/nmi'
|
|
if not cli.exists(): pytest.skip('Sibling NMI checkout unavailable')
|
|
profile=tmp_path/'nmi.env'
|
|
profile.write_text('NMI_ENDPOINT=https://fixture.example.test\nNMI_MRSC_ACCESS_CODE=fixture-access\nNMI_SECRETKEY=fixture-secret\n')
|
|
redmine=tmp_path/'redmine.env'
|
|
redmine.write_text(f'NMI_CLI={cli}\nNMI_ENV_FILE={profile}\nREDMINE_BASE_CACHE={tmp_path}/cache\n')
|
|
p=subprocess.run([str(ROOT/'bin/rtickets-live'),'--env-file',str(redmine),'--doctor-nmi'],
|
|
env={'HOME':str(tmp_path),'PATH':'/usr/bin:/bin'},cwd=tmp_path,text=True,capture_output=True,timeout=10)
|
|
assert p.returncode==0,p.stdout+p.stderr
|
|
d=json.loads(p.stdout)
|
|
assert d['ready'] and d['mode']=='production'
|
|
assert d['env_file']==str(profile)
|
|
assert not d['network_checked']
|
|
assert 'fixture-secret' not in p.stdout+p.stderr
|