9845867gave it its own plugin because Tech Tools is browser-only and this needs storage. That was the right reading of the code and the wrong answer for the person using it: Tech Tools is where you go for a utility, and a tool nobody can find is a tool nobody uses. So Tech Tools now has a blueprint, a table and a migration chain where it had none. Its docstring and manifest say why, because "every tool here runs in the browser" was load-bearing documentation and is no longer true. WHAT MOVED /api/securesend -> /api/tools/secrets securesend.create -> tools.secrets securesend0001baseline -> tools0001securesend PLUGIN_TABLE_OWNERS, EXPECTED_HEAD_REVISION, EXEMPT_ENDPOINTS, the universal profile and the api-inventory surface all follow it. THE OLD "NO BACKEND" TEST IS NARROWED, NOT DELETED. It asserted get_blueprint() is None, which is the sort of guard that gets removed when it becomes inconvenient. It now asserts the blueprint serves EXACTLY the three secret routes, with their methods - so a future tool that quietly grows an endpoint or a table has to edit this test, and therefore make the decision on purpose rather than by drift. Asserting methods rather than paths also pins the detail this design turns on: redeeming is 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 opened the link. Previously that was a comment; now it fails a test. UNINSTALLING THE OLD PLUGIN COULD NOT BE DONE WITH THE PLUGIN TOOLING. `flask plugin uninstall securesend` loads a plugin before uninstalling it, so a plugin whose directory has been deleted cannot be removed by the command that manages plugins - the registry entry had to be edited by hand. Worth fixing separately; it is the same registry-versus-code divergence the device work hit from the other side. MIGRATING A CHAIN THAT HAS ALREADY RUN needs the old table and its version row dropped first, because the relocated chain has no stamp and tries to create a table that exists. Only relevant to anyone who installed securesend during the hour it existed - it did reach the publication repo in4242028- and the fix is DROP TABLE securesendsecrets plus DROP TABLE alembic_version_securesend before running plugin upgrade-all. 1829 tests green. Still backend only: there are no pages yet, so nothing here is reachable by a person.
84 lines
3.2 KiB
Python
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
|