Files
shopdb-flask/tests/test_installer_defaults.py
cproudlock 5321649e02 fix(installer): stop blocking the wizard on things the installer itself installs
The preflight page began refusing to continue while any check was failing, which
is right for something the operator must go and fix. HttpPlatformHandler was
marked FAIL when absent - so on a server without it the wizard stopped dead,
telling the operator the server was not ready, over a module the bundle carries
and stage 4 installs a few pages later. The only way forward was to go and
install by hand the exact thing the installer was about to install.

It is now INFO: reported, not blocking, matching how URL Rewrite is already
handled. Nothing the installer SUPPLIES may block the wizard, and
tests/test_installer_defaults.py now fails if that rule is broken again.

The site-port conflict check is downgraded from FAIL to WARN for the same class
of reason: it runs before the operator reaches the Address page, so it tests the
DEFAULT port rather than the one they intend to use, and blocking refuses an
install over a conflict the very next page lets them resolve.

Genuine blockers are unchanged - no IIS, no WebAdministration, wrong Windows
edition or architecture, no disk, and the MySQL 5.6 index flags. Those the
operator really does have to fix first.
2026-08-04 10:00:44 -04:00

85 lines
3.2 KiB
Python

"""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))