"""The installer wizard's pre-ticked feature list must match the plugin manifests. The wizard cannot read manifest.json - Inno's Pascal Script has no JSON parser - so the default set is a hardcoded list in ShopDBFlask.iss. It drifted: two plugins that ship "default_enabled": false were pre-ticked, so every site taking the defaults installed and enabled them against their own manifests. A hardcoded list is fine; a hardcoded list nobody checks is not. """ import json import re from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[1] ISS = REPO / 'deploy' / 'windows' / 'installer' / 'ShopDBFlask.iss' PLUGINS = REPO / 'plugins' pytestmark = pytest.mark.skipif(not ISS.exists(), reason='installer not in this tree') def manifests_defaulting_off(): off = set() for manifest in PLUGINS.glob('*/manifest.json'): try: data = json.loads(manifest.read_text()) except (ValueError, OSError): continue if data.get('default_enabled') is False: off.add(manifest.parent.name) return off def wizard_excludes(): """The names PluginDefault returns False for.""" body = re.search(r'function PluginDefault.*?\nend;', ISS.read_text(), re.S) assert body, 'PluginDefault not found in ShopDBFlask.iss' return set(re.findall(r"Name <> '([a-z_]+)'", body.group(0))) def test_wizard_defaults_match_the_manifests(): off = manifests_defaulting_off() assert off, 'no plugin declares default_enabled false - has the field moved?' assert wizard_excludes() == off, ( 'ShopDBFlask.iss PluginDefault disagrees with the manifests.\n' ' manifests default_enabled=false: %s\n' ' wizard leaves unticked: %s' % (sorted(off), sorted(wizard_excludes()))) def test_every_wizard_exclusion_is_a_real_plugin(): """A typo in the .iss list silently pre-ticks the plugin it meant to exclude.""" for name in wizard_excludes(): assert (PLUGINS / name / 'manifest.json').exists(), ( '%s is excluded by the wizard but has no manifest' % name) # The preflight blocks the wizard on any FAIL. That is correct for something the # operator must go and fix, and wrong for anything the installer carries in its # own bundle - there, blocking stops the wizard over something it was about to do # itself, with no way forward. BUNDLE_SUPPLIED = [ 'HttpPlatformHandler', # httpplatformhandler\*.msi, installed by stage 4 'URL Rewrite', # urlrewrite\*.msi, installed by stage 4 ] PREFLIGHT = REPO / 'deploy' / 'windows' / 'installer' / 'shopdb-preflight.ps1' def test_nothing_the_bundle_supplies_is_a_blocker(): """Regression: HttpPlatformHandler was a FAIL, so a server without it could not get past the preflight page - to install the very thing that was missing.""" text = PREFLIGHT.read_text(encoding='utf-8', errors='replace') offenders = [] for line in text.splitlines(): if "'FAIL'" not in line: continue for component in BUNDLE_SUPPLIED: if component.lower() in line.lower(): offenders.append(line.strip()) assert not offenders, ( 'the installer supplies these, so they must not block the wizard:\n %s' % '\n '.join(offenders)) def test_installer_controlled_paths_have_no_spaces(): """msiexec and bootstrapper command lines split on spaces. The installer passes these as PROPERTY=value / TargetDir=value on a command line. A value containing a space has to be quoted as PROPERTY="C:\\..." - quoting the whole token instead makes the tool reject the line and print its usage dialog, which is what 'the bundled MySQL database could not be installed' looked like from the outside. The Python target was already space-free for this exact reason; MySQL was not. """ text = PREFLIGHT.parent.joinpath('shopdb-install.ps1').read_text( encoding='utf-8', errors='replace') offenders = [] for line in text.splitlines(): match = re.match(r"\s*\[string\]\s*\$(Mysql\w*|PyTarget)\s*=\s*'([^']+)'", line) if match and ' ' in match.group(2): offenders.append('%s = %s' % (match.group(1), match.group(2))) assert not offenders, ( 'these are passed on a command line and must not contain spaces:\n %s' % '\n '.join(offenders))