Files
shopdb-flask/tests/test_plugins/test_tools.py

84 lines
3.2 KiB
Python

"""Tests for the Tech Tools plugin.
The plugin owns no API and no tables, so the whole backend contract is: it
loads, it advertises one nav entry, and it claims no database. That last part
matters - a plugin that accidentally returns models would pull the tools
section into the migration and prune-schema machinery it has no business in.
"""
import json
import os
from plugins.tools.plugin import ToolsPlugin
PLUGIN_DIR = os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))), 'plugins', 'tools')
def test_meta_matches_manifest():
"""The manifest is the single source of truth per ADR-002."""
with open(os.path.join(PLUGIN_DIR, 'manifest.json'), encoding='utf-8') as handle:
manifest = json.load(handle)
meta = ToolsPlugin().meta
assert meta.name == manifest['name']
assert meta.version == manifest['version']
assert meta.core_version == manifest['core_version']
def test_the_only_backend_surface_is_secure_send():
"""Tech Tools used to have NO backend, and mostly still should.
This asserted `get_blueprint() is None` until secure send arrived: a link
that expires and dies on first read cannot be browser-only, because
something has to hold the ciphertext until it is claimed. The guard is kept
rather than deleted, narrowed to the one exception - a future tool that
quietly grows an endpoint or a table has to change this test, and therefore
make the decision on purpose rather than by accident.
"""
from flask import Flask
from plugins.tools.models import SecureSecret
plugin = ToolsPlugin()
assert plugin.get_models() == [SecureSecret]
probe = Flask(__name__)
probe.register_blueprint(plugin.get_blueprint(), url_prefix='/api/tools')
served = sorted(
(str(rule), sorted(rule.methods - {'HEAD', 'OPTIONS'}))
for rule in probe.url_map.iter_rules()
if not str(rule).startswith('/static'))
# Methods are asserted, not just paths: REDEEMING MUST BE POST-ONLY. Chat
# and mail clients prefetch GET urls to build previews, and a preview that
# could redeem would consume a one-view secret before its recipient ever
# opened the link. The GET on the same path reports availability only.
assert served == [
('/api/tools/secrets', ['POST']),
('/api/tools/secrets/<token>', ['GET']),
('/api/tools/secrets/<token>', ['POST']),
], served
def test_navigation_item():
items = ToolsPlugin().get_navigation_items()
assert len(items) == 1
item = items[0]
assert item['name'] == 'Tech Tools'
assert item['route'] == '/tools'
# The icon has to exist in AppLayout's iconMap or the nav entry renders
# with no glyph.
assert item['icon'] == 'wrench'
def test_nav_icon_is_mapped_in_the_frontend():
"""Guard the one cross-file coupling: nav icon name -> AppLayout iconMap."""
layout = os.path.join(os.path.dirname(PLUGIN_DIR), '..', 'frontend', 'src',
'views', 'AppLayout.vue')
with open(os.path.normpath(layout), encoding='utf-8') as handle:
source = handle.read()
for item in ToolsPlugin().get_navigation_items():
assert f"'{item['icon']}':" in source