mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-29 16:17:25 +09:00
The fetch -> TTL -> last-good -> bundled pipeline that four services re-implemented (and that produced the bundled-index and proxy-rewrite incidents) now lives once in utils/remote_index. The extension registry and theme registry become thin normalizers over it; the registry gains the failure-retry throttling the theme copy had already hardened to (last-good under full TTL, bundled held only error_ttl). Deliberate non-adopters, documented: template_service (keyed per-repo cache with targeted invalidation) and security_feed (DB-durable last-good for a daily job). Engine unit tests pin the semantics; registry/theme/signing suites stay green (test seeds updated to the shared cache seam). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
526 lines
21 KiB
Python
526 lines
21 KiB
Python
"""Registry fetch/cache, checksum-verified install, version gates, update flow."""
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import zipfile
|
|
|
|
import pytest
|
|
|
|
from app import db
|
|
from app.models.plugin import InstalledPlugin
|
|
from app.services import plugin_service, registry_service
|
|
from app.utils import version as version_util
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_registry_cache():
|
|
registry_service._cache.update({'ts': 0.0, 'entries': None, 'source': None})
|
|
yield
|
|
registry_service._cache.update({'ts': 0.0, 'entries': None, 'source': None})
|
|
|
|
|
|
@pytest.fixture
|
|
def plugin_dirs(tmp_path, monkeypatch):
|
|
backend = tmp_path / 'b'
|
|
frontend = tmp_path / 'f'
|
|
for d in (backend, frontend):
|
|
d.mkdir()
|
|
monkeypatch.setattr(plugin_service, 'BACKEND_PLUGINS_DIR', str(backend))
|
|
monkeypatch.setattr(plugin_service, 'FRONTEND_PLUGINS_DIR', str(frontend))
|
|
return {'backend': backend, 'frontend': frontend}
|
|
|
|
|
|
def _make_plugin_zip(slug='regext', version='2.0.0'):
|
|
"""A minimal, valid backend-less plugin zip (frontend-only)."""
|
|
manifest = {
|
|
'name': slug, 'display_name': 'Registry Ext', 'version': version,
|
|
'category': 'utility',
|
|
'contributions': {'nav': [{'id': slug, 'label': 'Reg', 'route': '/reg'}]},
|
|
}
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, 'w') as zf:
|
|
zf.writestr('plugin.json', json.dumps(manifest))
|
|
zf.writestr('frontend/index.jsx', 'export function P(){return null;}\n')
|
|
return buf.getvalue()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Registry fetch / cache (offline-tolerant → bundled fallback)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_bundled_registry_lists_serverkit_gui(app):
|
|
# No SERVERKIT_REGISTRY_URL in tests → bundled index is used.
|
|
entries = registry_service.list_extensions()
|
|
slugs = {e['slug'] for e in entries}
|
|
assert 'serverkit-gui' in slugs
|
|
assert registry_service.registry_source_label() == 'bundled'
|
|
|
|
|
|
def test_registry_catalog_carries_install_state(app):
|
|
catalog = registry_service.list_catalog()
|
|
gui = next(e for e in catalog if e['slug'] == 'serverkit-gui')
|
|
assert gui['installed'] is False
|
|
assert gui['status'] == 'not_installed'
|
|
assert gui['source_kind'] == 'registry'
|
|
|
|
|
|
def test_registry_endpoint(app, client, auth_headers):
|
|
resp = client.get('/api/v1/marketplace/registry', headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert any(e['slug'] == 'serverkit-gui' for e in data['extensions'])
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Version helpers + compat gates
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_version_helpers():
|
|
assert version_util.compare_versions('2.0.0', '1.9.9') == 1
|
|
assert version_util.compare_versions('1.0.0', '1.0.0') == 0
|
|
assert version_util.version_satisfies('1.7.5', min_version='1.7.0') is True
|
|
assert version_util.version_satisfies('1.6.0', min_version='1.7.0') is False
|
|
assert version_util.version_satisfies('2.5.0', max_version='2.0.0') is False
|
|
|
|
|
|
def test_registry_install_blocked_by_min_panel_version(app, plugin_dirs, monkeypatch):
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'future-ext', 'display_name': 'Future', 'version': '1.0.0',
|
|
'source': 'https://example.com/x.zip', 'min_panel_version': '999.0.0',
|
|
})],
|
|
})
|
|
with pytest.raises(ValueError, match='needs panel'):
|
|
plugin_service.install_registry_extension('future-ext')
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Checksum-verified install
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_checksum_mismatch_rejected(app, plugin_dirs, monkeypatch):
|
|
zip_bytes = _make_plugin_zip()
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(zip_bytes))
|
|
|
|
with pytest.raises(ValueError, match='Checksum mismatch'):
|
|
plugin_service.install_from_url('https://x/y.zip', expected_sha256='deadbeef')
|
|
|
|
# Nothing was installed.
|
|
assert InstalledPlugin.query.filter_by(slug='regext').first() is None
|
|
|
|
|
|
def test_checksum_match_installs(app, plugin_dirs, monkeypatch):
|
|
zip_bytes = _make_plugin_zip()
|
|
digest = hashlib.sha256(zip_bytes).hexdigest()
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(zip_bytes))
|
|
|
|
plugin = plugin_service.install_from_url('https://x/y.zip', expected_sha256=digest)
|
|
assert plugin.status == InstalledPlugin.STATUS_ACTIVE
|
|
assert plugin.version == '2.0.0'
|
|
# A plain URL install keeps the historical stamp.
|
|
assert plugin.source_type == 'url'
|
|
|
|
|
|
def test_registry_install_stamps_registry_source_type(app, plugin_dirs, monkeypatch):
|
|
"""install_registry_extension records where the plugin really came from."""
|
|
zip_bytes = _make_plugin_zip()
|
|
digest = hashlib.sha256(zip_bytes).hexdigest()
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(zip_bytes))
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'regext', 'display_name': 'Registry Ext', 'version': '2.0.0',
|
|
'source': 'https://x/regext.zip', 'sha256': digest,
|
|
'min_panel_version': '0.0.1',
|
|
})],
|
|
})
|
|
|
|
plugin = plugin_service.install_registry_extension('regext')
|
|
assert plugin.status == InstalledPlugin.STATUS_ACTIVE
|
|
assert plugin.source_type == 'registry'
|
|
assert plugin.source_url == 'https://x/regext.zip'
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Update flow
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_update_flow(app, plugin_dirs, monkeypatch):
|
|
# Install v1.0.0 first.
|
|
v1 = _make_plugin_zip(slug='regext', version='1.0.0')
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v1))
|
|
plugin = plugin_service.install_from_url('https://x/regext.zip')
|
|
assert plugin.version == '1.0.0'
|
|
assert plugin.source_type == 'url'
|
|
|
|
# Registry advertises v2.0.0 for the same slug.
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'regext', 'display_name': 'Registry Ext', 'version': '2.0.0',
|
|
'source': 'https://x/regext.zip', 'min_panel_version': '0.0.1',
|
|
})],
|
|
})
|
|
|
|
updates = {u['slug']: u for u in plugin_service.check_for_updates()}
|
|
assert updates['regext']['update_available'] is True
|
|
assert updates['regext']['available_version'] == '2.0.0'
|
|
assert updates['regext']['compatible'] is True
|
|
|
|
# Serve v2 bytes and run the update (reinstall over active → force).
|
|
v2 = _make_plugin_zip(slug='regext', version='2.0.0')
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v2))
|
|
updated = plugin_service.update_plugin(plugin.id)
|
|
assert updated.version == '2.0.0'
|
|
assert InstalledPlugin.query.filter_by(slug='regext').count() == 1
|
|
# The update itself came from the registry — the row now says so.
|
|
assert updated.source_type == 'registry'
|
|
|
|
|
|
def test_update_keeps_registry_source_type(app, plugin_dirs, monkeypatch):
|
|
"""A registry-installed plugin stays source_type='registry' after update."""
|
|
v1 = _make_plugin_zip(slug='regext', version='1.0.0')
|
|
digest_v1 = hashlib.sha256(v1).hexdigest()
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v1))
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'regext', 'display_name': 'Registry Ext', 'version': '1.0.0',
|
|
'source': 'https://x/regext.zip', 'sha256': digest_v1,
|
|
'min_panel_version': '0.0.1',
|
|
})],
|
|
})
|
|
plugin = plugin_service.install_registry_extension('regext')
|
|
assert plugin.source_type == 'registry'
|
|
|
|
# Registry moves to v2; run the update.
|
|
v2 = _make_plugin_zip(slug='regext', version='2.0.0')
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v2))
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'regext', 'display_name': 'Registry Ext', 'version': '2.0.0',
|
|
'source': 'https://x/regext.zip',
|
|
'sha256': hashlib.sha256(v2).hexdigest(),
|
|
'min_panel_version': '0.0.1',
|
|
})],
|
|
})
|
|
updated = plugin_service.update_plugin(plugin.id)
|
|
assert updated.version == '2.0.0'
|
|
assert updated.source_type == 'registry'
|
|
|
|
|
|
def test_update_preserves_builtin_source_type(app, plugin_dirs, monkeypatch):
|
|
"""A registry-driven update must not clobber a builtin/local/upload stamp."""
|
|
v1 = _make_plugin_zip(slug='regext', version='1.0.0')
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v1))
|
|
plugin = plugin_service.install_from_url('https://x/regext.zip')
|
|
plugin.source_type = 'builtin'
|
|
db.session.commit()
|
|
|
|
v2 = _make_plugin_zip(slug='regext', version='2.0.0')
|
|
monkeypatch.setattr(plugin_service, '_download_zip', lambda url: io.BytesIO(v2))
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [registry_service._normalize({
|
|
'slug': 'regext', 'display_name': 'Registry Ext', 'version': '2.0.0',
|
|
'source': 'https://x/regext.zip',
|
|
'sha256': hashlib.sha256(v2).hexdigest(),
|
|
'min_panel_version': '0.0.1',
|
|
})],
|
|
})
|
|
updated = plugin_service.update_plugin(plugin.id)
|
|
assert updated.version == '2.0.0'
|
|
assert updated.source_type == 'builtin'
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Registry URL resolution (live default vs explicit opt-out)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_registry_url_defaults_to_public_index(monkeypatch):
|
|
"""Unset env → the curated index via serverkit.ai (the go-live default;
|
|
it proxies the raw serverkit-extensions index with caching + logo URL
|
|
rewriting). conftest pins it EMPTY suite-wide, so simulate unset here."""
|
|
monkeypatch.delenv('SERVERKIT_REGISTRY_URL', raising=False)
|
|
assert registry_service._index.url() == registry_service.DEFAULT_REGISTRY_URL
|
|
assert registry_service.DEFAULT_REGISTRY_URL == 'https://serverkit.ai/ext/index.json'
|
|
|
|
|
|
def test_registry_url_empty_disables_remote(monkeypatch):
|
|
"""Explicitly-empty env disables the remote entirely (bundled only) —
|
|
the hermetic-test/air-gapped escape hatch."""
|
|
monkeypatch.setenv('SERVERKIT_REGISTRY_URL', '')
|
|
assert registry_service._index.url() == ''
|
|
assert registry_service._index._fetch_remote() is None
|
|
|
|
|
|
def test_registry_url_env_override_wins(monkeypatch):
|
|
monkeypatch.setenv('SERVERKIT_REGISTRY_URL', 'https://example.test/index.json')
|
|
assert registry_service._index.url() == 'https://example.test/index.json'
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Index v2 fields — logo / repo / bundled
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_v2_entry_normalizes_logo_and_repo():
|
|
"""A v2 entry keeps logo/repo; an absolute logo passes through unchanged."""
|
|
e = registry_service._normalize({
|
|
'slug': 'gui', 'display_name': 'GUI', 'version': '1.0.0',
|
|
'source': 'https://x/gui.zip',
|
|
'repo': 'https://github.com/acme/gui',
|
|
'logo': 'https://cdn.example/gui.svg',
|
|
})
|
|
assert e['repo'] == 'https://github.com/acme/gui'
|
|
assert e['logo'] == 'https://cdn.example/gui.svg'
|
|
assert e['bundled'] is False
|
|
|
|
|
|
def test_relative_logo_resolved_against_index_base():
|
|
"""A repo-relative logo becomes absolute against the index URL it came
|
|
from (serverkit.ai index → /ext/assets URL; raw-GitHub index → raw asset
|
|
URL when used as a manual fallback)."""
|
|
entry = {
|
|
'slug': 'gui', 'display_name': 'GUI', 'version': '1.0.0',
|
|
'source': 'https://x/gui.zip',
|
|
'logo': 'assets/gui/logo.svg',
|
|
}
|
|
e = registry_service._normalize(
|
|
entry, base_url=registry_service.DEFAULT_REGISTRY_URL)
|
|
assert e['logo'] == 'https://serverkit.ai/ext/assets/gui/logo.svg'
|
|
|
|
e = registry_service._normalize(
|
|
entry,
|
|
base_url='https://raw.githubusercontent.com/jhd3197/serverkit-extensions/main/index.json')
|
|
assert e['logo'] == (
|
|
'https://raw.githubusercontent.com/jhd3197/serverkit-extensions/'
|
|
'main/assets/gui/logo.svg'
|
|
)
|
|
|
|
|
|
def test_v1_entry_unaffected_by_v2_defaults():
|
|
"""An entry with no v2 fields normalizes with safe defaults."""
|
|
e = registry_service._normalize({
|
|
'slug': 'old', 'display_name': 'Old', 'version': '1.0.0',
|
|
'source': 'https://x/old.zip',
|
|
})
|
|
assert e['bundled'] is False
|
|
assert e['repo'] == ''
|
|
assert e['logo'] is None
|
|
|
|
|
|
def test_bundled_excluded_from_catalog_by_default(app, monkeypatch):
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [
|
|
registry_service._normalize({
|
|
'slug': 'community-ext', 'display_name': 'Community', 'version': '1.0.0',
|
|
'source': 'https://x/c.zip',
|
|
}),
|
|
registry_service._normalize({
|
|
'slug': 'serverkit-wordpress', 'display_name': 'WordPress',
|
|
'version': '1.0.0', 'bundled': True,
|
|
}),
|
|
],
|
|
})
|
|
default_slugs = {e['slug'] for e in registry_service.list_catalog()}
|
|
assert 'community-ext' in default_slugs
|
|
assert 'serverkit-wordpress' not in default_slugs # bundled hidden from Browse
|
|
|
|
full_slugs = {e['slug'] for e in registry_service.list_catalog(include_bundled=True)}
|
|
assert 'serverkit-wordpress' in full_slugs
|
|
assert 'community-ext' in full_slugs
|
|
|
|
|
|
def test_registry_endpoint_include_bundled_flag(app, client, auth_headers, monkeypatch):
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test',
|
|
'entries': [
|
|
registry_service._normalize({
|
|
'slug': 'serverkit-wordpress', 'display_name': 'WordPress',
|
|
'version': '1.0.0', 'bundled': True,
|
|
}),
|
|
],
|
|
})
|
|
resp = client.get('/api/v1/marketplace/registry', headers=auth_headers)
|
|
assert not any(e['slug'] == 'serverkit-wordpress' for e in resp.get_json()['extensions'])
|
|
|
|
resp2 = client.get('/api/v1/marketplace/registry?include_bundled=true', headers=auth_headers)
|
|
assert any(e['slug'] == 'serverkit-wordpress' for e in resp2.get_json()['extensions'])
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Review stamps → trust derivation + the install acknowledgment gate
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
_SHA_A = 'a' * 64
|
|
_SHA_B = 'b' * 64
|
|
|
|
|
|
def _entry(**overrides):
|
|
base = {
|
|
'slug': 'community-ext', 'display_name': 'Community', 'version': '1.0.0',
|
|
'source': 'https://x/c.zip', 'sha256': _SHA_A,
|
|
}
|
|
base.update(overrides)
|
|
return registry_service._normalize(base)
|
|
|
|
|
|
def test_trust_first_party_wins():
|
|
e = _entry(first_party=True, review={'reviewer': 'jhd3197', 'sha256': _SHA_B})
|
|
assert e['trust'] == 'first_party'
|
|
|
|
|
|
def test_trust_reviewed_when_hashes_match():
|
|
e = _entry(review={'reviewer': 'jhd3197', 'date': '2026-07-23', 'sha256': _SHA_A})
|
|
assert e['trust'] == 'reviewed'
|
|
assert e['review']['reviewer'] == 'jhd3197'
|
|
|
|
|
|
def test_trust_unreviewed_without_review():
|
|
assert _entry()['trust'] == 'unreviewed'
|
|
assert _entry()['review'] is None
|
|
|
|
|
|
def test_trust_unreviewed_when_review_hash_is_stale():
|
|
# The artifact changed → entry sha256 moved on → the stamp no longer counts.
|
|
e = _entry(review={'reviewer': 'jhd3197', 'sha256': _SHA_B})
|
|
assert e['trust'] == 'unreviewed'
|
|
|
|
|
|
@pytest.mark.parametrize('review', [
|
|
'not-a-dict',
|
|
{'reviewer': 'jhd3197'}, # no sha256
|
|
{'sha256': 'ABCDEF' + '0' * 58}, # uppercase
|
|
{'sha256': 'a' * 63}, # too short
|
|
{'sha256': 'g' * 64}, # non-hex
|
|
])
|
|
def test_trust_unreviewed_when_review_malformed(review):
|
|
e = _entry(review=review)
|
|
assert e['review'] is None
|
|
assert e['trust'] == 'unreviewed'
|
|
|
|
|
|
class _FakePlugin:
|
|
id = 1
|
|
name = 'community-ext'
|
|
slug = 'community-ext'
|
|
version = '1.0.0'
|
|
|
|
def to_dict(self):
|
|
return {'name': self.name, 'version': self.version}
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_install(monkeypatch):
|
|
"""Stub the heavy install; the gate tests only exercise the route logic."""
|
|
monkeypatch.setattr(
|
|
plugin_service, 'install_registry_extension', lambda slug, user_id=None: _FakePlugin())
|
|
|
|
|
|
def _seed_cache(monkeypatch, entry):
|
|
registry_service._cache.update({
|
|
'ts': 9e18, 'source': 'test', 'entries': [entry],
|
|
})
|
|
|
|
|
|
def test_install_gate_blocks_unreviewed_without_ack(app, client, auth_headers, fake_install, monkeypatch):
|
|
_seed_cache(monkeypatch, _entry())
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers)
|
|
assert resp.status_code == 409
|
|
data = resp.get_json()
|
|
assert data['requires_acknowledgment'] is True
|
|
assert data['trust'] == 'unreviewed'
|
|
assert data['reason'] == 'unreviewed'
|
|
assert 'unreviewed' in data['error']
|
|
|
|
|
|
def test_install_gate_blocks_first_party_without_sha256(app, client, auth_headers, fake_install, monkeypatch):
|
|
# trusted publisher, but no pinned checksum to verify the artifact against.
|
|
e = _entry(first_party=True, sha256=None)
|
|
assert e['trust'] == 'first_party'
|
|
_seed_cache(monkeypatch, e)
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers)
|
|
assert resp.status_code == 409
|
|
data = resp.get_json()
|
|
assert data['requires_acknowledgment'] is True
|
|
assert data['reason'] == 'unverified'
|
|
assert 'checksum' in data['error']
|
|
|
|
|
|
def test_install_gate_passes_with_acknowledge_risk(app, client, auth_headers, fake_install, monkeypatch):
|
|
_seed_cache(monkeypatch, _entry())
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers, json={'acknowledge_risk': True})
|
|
assert resp.status_code == 201
|
|
|
|
|
|
def test_install_gate_unsigned_first_party_requires_ack(app, client, auth_headers, fake_install, monkeypatch):
|
|
# Audit M3: a first-party entry without a verifiable signature is a
|
|
# possible downgrade — consent-gated like untrusted_key.
|
|
_seed_cache(monkeypatch, _entry(first_party=True))
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers)
|
|
assert resp.status_code == 409
|
|
data = resp.get_json()
|
|
assert data['requires_acknowledgment'] is True
|
|
assert data['reason'] == 'unsigned'
|
|
assert 'first-party' in data['error']
|
|
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers, json={'acknowledge_risk': True})
|
|
assert resp.status_code == 201
|
|
|
|
|
|
def test_install_gate_unaffected_for_reviewed(app, client, auth_headers, fake_install, monkeypatch):
|
|
_seed_cache(monkeypatch, _entry(review={'reviewer': 'jhd3197', 'sha256': _SHA_A}))
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers)
|
|
assert resp.status_code == 201
|
|
|
|
|
|
def test_catalog_hides_unreviewed_outside_dev_mode(app, client, auth_headers, monkeypatch):
|
|
# Production panel with dev_mode off: unreviewed entries never list.
|
|
monkeypatch.setattr(registry_service, '_show_unreviewed', lambda: False)
|
|
_seed_cache(monkeypatch, _entry())
|
|
resp = client.get('/api/v1/marketplace/registry', headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()['extensions'] == []
|
|
|
|
|
|
def test_catalog_lists_unreviewed_in_dev_mode(app, client, auth_headers, monkeypatch):
|
|
monkeypatch.setattr(registry_service, '_show_unreviewed', lambda: True)
|
|
_seed_cache(monkeypatch, _entry())
|
|
resp = client.get('/api/v1/marketplace/registry', headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
slugs = [e['slug'] for e in resp.get_json()['extensions']]
|
|
assert 'community-ext' in slugs
|
|
|
|
|
|
def test_catalog_keeps_reviewed_visible_outside_dev_mode(app, client, auth_headers, monkeypatch):
|
|
monkeypatch.setattr(registry_service, '_show_unreviewed', lambda: False)
|
|
_seed_cache(monkeypatch, _entry(review={'reviewer': 'jhd3197', 'sha256': _SHA_A}))
|
|
resp = client.get('/api/v1/marketplace/registry', headers=auth_headers)
|
|
assert resp.status_code == 200
|
|
slugs = [e['slug'] for e in resp.get_json()['extensions']]
|
|
assert 'community-ext' in slugs
|
|
|
|
|
|
def test_install_hidden_unreviewed_returns_404(app, client, auth_headers, fake_install, monkeypatch):
|
|
# Hidden means not installable, even with acknowledge_risk.
|
|
monkeypatch.setattr(registry_service, '_show_unreviewed', lambda: False)
|
|
_seed_cache(monkeypatch, _entry())
|
|
resp = client.post('/api/v1/marketplace/registry/community-ext/install',
|
|
headers=auth_headers, json={'acknowledge_risk': True})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_show_unreviewed_defaults_on_under_testing_config(app):
|
|
# The test suite runs with DEBUG/TESTING on, i.e. a development context.
|
|
with app.test_request_context():
|
|
assert registry_service._show_unreviewed() is True
|