north-scape/relayboard

Relayboard: accessible team boards with audited actions, dependency maps, and shared web/MCP use cases.

A remix template · 0 remixes · Remix north-scape/relayboard

tests/test_template.py

"""The lightweight local validator must at least reject malformed YAML."""
import importlib.util
from pathlib import Path
import pytest
import yaml
import shutil

spec = importlib.util.spec_from_file_location('check_template', Path(__file__).resolve().parents[1] / 'scripts/check_template.py')
template = importlib.util.module_from_spec(spec)
spec.loader.exec_module(template)

def test_manifest_description_with_colon_must_be_quoted():
    with pytest.raises(yaml.YAMLError):
        template.parse_manifest('version: 1\ndescription: Relayboard: accessible boards\n')
    assert template.parse_manifest('version: 1\ndescription: "Relayboard: accessible boards"\n')['version'] == 1

def test_manifest_root_must_be_mapping():
    with pytest.raises(ValueError, match='mapping'):
        template.parse_manifest('- not\n- a mapping\n')

@pytest.fixture
def fresh_template(tmp_path):
    root = Path(__file__).resolve().parents[1]
    for name in ('.dodil', '.github'):
        shutil.copytree(root / name, tmp_path / name)
    for name in ('schema.sql', 'LICENSE'):
        shutil.copy(root / name, tmp_path / name)
    return tmp_path

def test_current_fresh_install_contract(fresh_template):
    template.validate_template(fresh_template)

@pytest.mark.parametrize('before,after,message', [
    ('CREATE TABLE IF NOT EXISTS boards', 'CREATE TABLE IF NOT EXISTS lost_boards', 'missing table boards'),
    (" checklist TEXT NOT NULL DEFAULT '[]',\n", '', 'missing columns'),
    (' revision INTEGER NOT NULL DEFAULT 0,', ' revision INTEGER NOT NULL,', 'default to 0'),
    ("checklist TEXT NOT NULL DEFAULT '[]'", "checklist TEXT NOT NULL DEFAULT '{}'", 'empty JSON list'),
    (' id TEXT PRIMARY KEY,', ' id TEXT,', 'primary key'),
])
def test_incomplete_fresh_schema_is_rejected(fresh_template, before, after, message):
    path = fresh_template / 'schema.sql'
    original = path.read_text()
    assert before in original
    path.write_text(original.replace(before, after))
    with pytest.raises(ValueError, match=message):
        template.validate_template(fresh_template)

def test_empty_schema_cannot_pass_vacuously(fresh_template):
    (fresh_template / 'schema.sql').write_text('-- no tables\n')
    with pytest.raises(ValueError, match='missing table boards'):
        template.validate_template(fresh_template)

@pytest.mark.parametrize('before,after,message', [
    ('create_if_missing: true', 'create_if_missing: false', 'create the app'),
    ('public_invoke: true', 'public_invoke: false', 'OAuth discovery'),
    ('user_pool: "${{ dodil.repo }}-users"', 'user_pool: old-users', 'provisioned pool'),
    ('BUCKET: "${{ dodil.repo }}-data"', 'BUCKET: old-data', 'provisioned bucket'),
    ('"${{ dodil.org }}/${{ dodil.repo }}-users"', '"original-org/original-users"', 'OAuth issuer'),
    ('"${{ secrets.DODIL_SERVICE_ACCOUNT_ID }}"', '"hardcoded-client"', 'generated CI secret'),
    ('port: 8080', 'port: 8000', 'port/health'),
])
def test_broken_remix_deploy_binding_is_rejected(fresh_template, before, after, message):
    path = fresh_template / '.dodil/deploy.yaml'
    original = path.read_text()
    assert before in original
    path.write_text(original.replace(before, after))
    with pytest.raises(ValueError, match=message):
        template.validate_template(fresh_template)

@pytest.mark.parametrize('policy', ['suffix', 'reuse'])
def test_resource_conflicts_cannot_silently_rebind(fresh_template, policy):
    path = fresh_template / '.dodil/remix.yaml'
    path.write_text(path.read_text().replace('      schema: schema.sql', f'      on_conflict: {policy}\n      schema: schema.sql'))
    with pytest.raises(ValueError, match='conflict policy'):
        template.validate_template(fresh_template)

def test_missing_models_grant_is_rejected(fresh_template):
    path = fresh_template / '.dodil/remix.yaml'
    path.write_text(path.read_text().replace('role: ignite.model-user', 'role: ignite.viewer'))
    with pytest.raises(ValueError, match='Models access'):
        template.validate_template(fresh_template)

@pytest.mark.parametrize('before,after', [
    (' revision INTEGER NOT NULL,', ' revision TEXT NOT NULL,'),
    (' due_date DATE,', ' due_date TEXT,'),
    (' updated_at TIMESTAMP', ' updated_at TEXT'),
])
def test_adapter_column_types_are_checked(fresh_template, before, after):
    path = fresh_template / 'schema.sql'
    original = path.read_text()
    assert before in original
    path.write_text(original.replace(before, after))
    with pytest.raises(ValueError, match='expected type'):
        template.validate_template(fresh_template)

def test_additional_tables_still_require_primary_keys(fresh_template):
    path = fresh_template / 'schema.sql'
    path.write_text(path.read_text() + '\nCREATE TABLE extra (id TEXT);\n')
    with pytest.raises(ValueError, match='extra: table needs a primary key'):
        template.validate_template(fresh_template)

def test_not_null_before_primary_key_is_valid(fresh_template):
    path = fresh_template / 'schema.sql'
    path.write_text(path.read_text().replace('id TEXT PRIMARY KEY', 'id TEXT NOT NULL PRIMARY KEY'))
    template.validate_template(fresh_template)

def test_remix_readme_selects_and_verifies_org_before_pool_changes():
    readme = (Path(__file__).resolve().parents[1] / 'README.md').read_text()
    after = readme.split('### After the remix: sign-in step', 1)[1]
    switch = after.index('dodil auth use-org <your-org> &&')
    identity = after.index('dodil auth status &&')
    pool = after.index('dodil appid pool get <repo>-users -o json')
    connection = after.index('dodil appid connection list')
    change = after.index('dodil appid connection add')
    assert switch < identity < pool < connection < change
    assert 'https://appid.dodil.io/<your-org>/<repo>-users' in after[:connection]
    assert 'Stop if switching fails' in after[:connection]
    assert 'including any suffix' in after[:connection]