CLI: explicit 'paskia migrate' subcommand for legacy conversion
Serve never converts databases: with no paskia.kantadb it points at 'paskia init', or at 'paskia migrate' when legacy *.paskiadb candidates exist. migrate converts a lone candidate, or the one named by --rp-id when several exist; the rest stay in place. devserver fails fast with the same hint. gitignore covers paskia.kantadb and *.converted-bak.
This commit is contained in:
@@ -6,6 +6,8 @@ dist/
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
paskia.sqlite
|
paskia.sqlite
|
||||||
*.paskiadb
|
*.paskiadb
|
||||||
|
*.converted-bak
|
||||||
|
*.kantadb
|
||||||
*.data
|
*.data
|
||||||
/paskia/frontend-build
|
/paskia/frontend-build
|
||||||
/paskia/_version.py
|
/paskia/_version.py
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ uv tool install paskia
|
|||||||
Bootstrapping is done once with `paskia init`; after that, `paskia` serves all configured realms from the database `paskia.kantadb` in the current directory. Realm configuration (rp-name, auth host, origins) is managed via the admin web interface, including adding further realms (rp-ids).
|
Bootstrapping is done once with `paskia init`; after that, `paskia` serves all configured realms from the database `paskia.kantadb` in the current directory. Realm configuration (rp-name, auth host, origins) is managed via the admin web interface, including adding further realms (rp-ids).
|
||||||
|
|
||||||
```text
|
```text
|
||||||
paskia init [options] # one-time bootstrap
|
paskia init [options] # one-time bootstrap
|
||||||
paskia [-l endpoint] # serve
|
paskia migrate [--rp-id] # convert a legacy {rp-id}.paskiadb database
|
||||||
|
paskia [-l endpoint] # serve
|
||||||
```
|
```
|
||||||
|
|
||||||
| init option | Description | Default |
|
| init option | Description | Default |
|
||||||
@@ -67,7 +68,7 @@ paskia [-l endpoint] # serve
|
|||||||
| --origin *url* | Only sites listed can login on the default realm (repeatable) | rp-id and all subdomains |
|
| --origin *url* | Only sites listed can login on the default realm (repeatable) | rp-id and all subdomains |
|
||||||
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
|
| --auth-host *url* | Dedicated authentication site, e.g. **auth.example.com** | Use **/auth/** path on each site |
|
||||||
|
|
||||||
The `paskia` serve command accepts only `--listen` (overriding the stored value). An existing legacy `{rp-id}.paskiadb` database is converted to `paskia.kantadb` automatically on first serve.
|
The `paskia` serve command accepts only `--listen` (overriding the stored value) and never converts databases: with no `paskia.kantadb` it tells you to run `paskia init`, or `paskia migrate` when a legacy `{rp-id}.paskiadb` database is present. `paskia migrate` converts the legacy database; with several candidates, `--rp-id` selects one by name and the rest are left in place.
|
||||||
|
|
||||||
## Tutorial: From Local Testing to Production
|
## Tutorial: From Local Testing to Production
|
||||||
|
|
||||||
|
|||||||
+23
-19
@@ -188,15 +188,19 @@ instance:
|
|||||||
- `--listen`: stored into `Config.listen` (process-global).
|
- `--listen`: stored into `Config.listen` (process-global).
|
||||||
- Seeds the admin user + registration reset link (link URL from the
|
- Seeds the admin user + registration reset link (link URL from the
|
||||||
default realm) and prints the link. Refuses to run if
|
default realm) and prints the link. Refuses to run if
|
||||||
`paskia.kantadb` already exists, or if an un-adopted legacy
|
`paskia.kantadb` already exists, or if an unconverted legacy
|
||||||
`*.paskiadb` is present (serve adopts it first).
|
`*.paskiadb` is present (`paskia migrate` converts it first).
|
||||||
|
- **`paskia migrate`** — converts a legacy `<rp-id>.paskiadb` database
|
||||||
|
(§10) to `paskia.kantadb`. With several legacy candidates, `--rp-id`
|
||||||
|
selects `<rp-id>.paskiadb` by name; the others are left in place.
|
||||||
- **`paskia`** — serve. Takes **no realm options**; only `--listen`
|
- **`paskia`** — serve. Takes **no realm options**; only `--listen`
|
||||||
(per-run override of stored `Config.listen`, never persisted). Startup:
|
(per-run override of stored `Config.listen`, never persisted). Startup:
|
||||||
legacy-adoption pre-flight → open `paskia.kantadb` → validate the
|
open `paskia.kantadb` → validate the stored realm set cross-realm
|
||||||
stored realm set cross-realm (rp-ids distinct; auth hosts distinct from
|
(rp-ids distinct; auth hosts distinct from each other and from every
|
||||||
each other and from every rp-id; related origins capped and
|
rp-id; related origins capped and collision-free) → build the realm
|
||||||
collision-free) → build the realm registry → serve. Missing database →
|
registry → serve. The serve command never converts databases: with no
|
||||||
startup error pointing at `paskia init`.
|
`paskia.kantadb`, the startup error points at `paskia init`, or at
|
||||||
|
`paskia migrate` when legacy `*.paskiadb` candidates are present.
|
||||||
|
|
||||||
Nested rp-ids are allowed (longest-suffix dispatch determinism). Adding a
|
Nested rp-ids are allowed (longest-suffix dispatch determinism). Adding a
|
||||||
child rp-id moves **no data** — users are global; only new ceremonies
|
child rp-id moves **no data** — users are global; only new ceremonies
|
||||||
@@ -397,17 +401,17 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
|
|||||||
deployment.
|
deployment.
|
||||||
- **User files** (avatars) live in the fixed sibling directory
|
- **User files** (avatars) live in the fixed sibling directory
|
||||||
**`paskia.data/users/`**.
|
**`paskia.data/users/`**.
|
||||||
- **Legacy adoption**: if `paskia.kantadb` is absent and exactly one
|
- **Legacy conversion**: `paskia migrate` converts a legacy
|
||||||
`*.paskiadb` candidate exists in CWD — a directory containing
|
`*.paskiadb` database — a directory containing `main.db`, or a legacy
|
||||||
`main.db`, or a legacy single-file database — it is adopted: `main.db`
|
single-file database — into `paskia.kantadb`: `main.db` (or the single
|
||||||
(or the single file) becomes `paskia.kantadb`, `users/` becomes
|
file) becomes `paskia.kantadb`, `users/` becomes `paskia.data/users/`,
|
||||||
`paskia.data/users/`, and the old directory is renamed aside to
|
and the old directory is renamed aside to `<name>.converted-bak`. A
|
||||||
`<name>.converted-bak`. Multiple candidates → startup error listing
|
lone candidate converts without options; with several candidates
|
||||||
them, asking the operator to remove or rename strays (e.g. a
|
`--rp-id <rp-id>` selects `<rp-id>.paskiadb` by name and the rest are
|
||||||
`*.bak.paskiadb` backup); empty directories are ignored. Adoption runs
|
left in place (e.g. a `*.bak.paskiadb` backup does not block
|
||||||
as an explicit pre-flight step in the serve command, before the
|
conversion). Empty directories are ignored. Conversion is an explicit
|
||||||
read-only startup open — read-only opens never trigger adoption or
|
operator action, never a serve side effect — read-only opens never
|
||||||
writes.
|
trigger conversion or writes.
|
||||||
- The legacy database's structs live in a separate module
|
- The legacy database's structs live in a separate module
|
||||||
(`paskia/db/legacy.py`). There is no multi-database merging.
|
(`paskia/db/legacy.py`). There is no multi-database merging.
|
||||||
- The startup box prints per-realm lines.
|
- The startup box prints per-realm lines.
|
||||||
@@ -417,7 +421,7 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
|
|||||||
- One `Kanta` for `paskia.kantadb`, opened once in the lifespan; one
|
- One `Kanta` for `paskia.kantadb`, opened once in the lifespan; one
|
||||||
background cleanup task (DB is global).
|
background cleanup task (DB is global).
|
||||||
- The kanta bootstrap hook only ever fires for a database created by
|
- The kanta bootstrap hook only ever fires for a database created by
|
||||||
`paskia init`; the serve command never bootstraps.
|
`paskia init` or `paskia migrate`; the serve command never bootstraps.
|
||||||
- The registry is built from the stored `Config` after open; per-realm
|
- The registry is built from the stored `Config` after open; per-realm
|
||||||
`Passkey` instances constructed (each realm's origins validated at
|
`Passkey` instances constructed (each realm's origins validated at
|
||||||
startup — fail-fast, including related-origin cap checks).
|
startup — fail-fast, including related-origin cap checks).
|
||||||
|
|||||||
+28
-6
@@ -28,6 +28,7 @@ from paskia.util.runtime import ServeConfig
|
|||||||
EPILOG = """\
|
EPILOG = """\
|
||||||
Examples:
|
Examples:
|
||||||
paskia init --rp-id example.com --rp-name "Example Corporation" --auth-host auth.example.com
|
paskia init --rp-id example.com --rp-name "Example Corporation" --auth-host auth.example.com
|
||||||
|
paskia migrate --rp-id example.com
|
||||||
paskia
|
paskia
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -87,8 +88,8 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
if found := legacy.find_legacy_databases():
|
if found := legacy.find_legacy_databases():
|
||||||
names = ", ".join(str(p) for p in found)
|
names = ", ".join(str(p) for p in found)
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"Legacy database(s) found ({names}) — run 'paskia' to adopt "
|
f"Legacy database(s) found ({names}) — run 'paskia migrate' to "
|
||||||
"and convert, not 'paskia init'."
|
"convert, not 'paskia init'."
|
||||||
)
|
)
|
||||||
|
|
||||||
rp_ids = _split_multi(args.rp_id) or ["localhost"]
|
rp_ids = _split_multi(args.rp_id) or ["localhost"]
|
||||||
@@ -148,14 +149,22 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_migrate(args: argparse.Namespace) -> None:
|
||||||
|
"""Convert a legacy <rp-id>.paskiadb database to paskia.kantadb."""
|
||||||
|
rp_id = legacy.migrate_legacy_database(args.rp_id)
|
||||||
|
print(f"✅ Converted legacy database to {db_file_path()} (realm: {rp_id})")
|
||||||
|
|
||||||
|
|
||||||
def cmd_serve(args: argparse.Namespace) -> None:
|
def cmd_serve(args: argparse.Namespace) -> None:
|
||||||
"""Open the combined database and serve all configured realms."""
|
"""Open the combined database and serve all configured realms."""
|
||||||
db_path = db_file_path()
|
db_path = db_file_path()
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
adopted = legacy.adopt_legacy_if_present()
|
if found := legacy.find_legacy_databases():
|
||||||
if adopted:
|
names = ", ".join(str(p) for p in found)
|
||||||
print(f"✅ Converted legacy database to {db_path} (realm: {adopted})")
|
raise SystemExit(
|
||||||
if not db_path.exists():
|
f"Database {db_path} not found, but legacy database(s) exist "
|
||||||
|
f"({names}) — run 'paskia migrate' to convert."
|
||||||
|
)
|
||||||
raise SystemExit(f"Database {db_path} not found — run 'paskia init' first.")
|
raise SystemExit(f"Database {db_path} not found — run 'paskia init' first.")
|
||||||
|
|
||||||
config = _load_stored_config(db_path)
|
config = _load_stored_config(db_path)
|
||||||
@@ -233,9 +242,22 @@ def main():
|
|||||||
)
|
)
|
||||||
_add_listen_option(init_parser, help_extra=" (stored in the database)")
|
_add_listen_option(init_parser, help_extra=" (stored in the database)")
|
||||||
|
|
||||||
|
migrate_parser = argparse.ArgumentParser(
|
||||||
|
prog="paskia migrate",
|
||||||
|
description="Convert a legacy <rp-id>.paskiadb database to paskia.kantadb",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
migrate_parser.add_argument(
|
||||||
|
"--rp-id",
|
||||||
|
help="rp-id of the legacy database to convert, selecting "
|
||||||
|
"<rp-id>.paskiadb when several legacy candidates exist.",
|
||||||
|
)
|
||||||
|
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
if argv and argv[0] == "init":
|
if argv and argv[0] == "init":
|
||||||
cmd_init(init_parser.parse_args(argv[1:]))
|
cmd_init(init_parser.parse_args(argv[1:]))
|
||||||
|
elif argv and argv[0] == "migrate":
|
||||||
|
cmd_migrate(migrate_parser.parse_args(argv[1:]))
|
||||||
else:
|
else:
|
||||||
cmd_serve(parser.parse_args(argv))
|
cmd_serve(parser.parse_args(argv))
|
||||||
|
|
||||||
|
|||||||
+27
-17
@@ -8,7 +8,7 @@ current schema are redefined here; unchanged structs are imported from
|
|||||||
|
|
||||||
Assumes the on-disk records are in the latest legacy format (schema
|
Assumes the on-disk records are in the latest legacy format (schema
|
||||||
migrations were discarded together with the old format). This module will
|
migrations were discarded together with the old format). This module will
|
||||||
be deleted once legacy adoption is no longer supported.
|
be deleted once legacy conversion is no longer supported.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -204,31 +204,41 @@ def find_legacy_databases(cwd: Path | None = None) -> list[Path]:
|
|||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
def adopt_legacy_if_present() -> str | None:
|
def migrate_legacy_database(rp_id: str | None = None) -> str:
|
||||||
"""Convert a lone legacy database to ``paskia.kantadb`` if present.
|
"""Convert a legacy database to ``paskia.kantadb``.
|
||||||
|
|
||||||
Returns the adopted realm's rp-id, or None when ``paskia.kantadb``
|
With ``rp_id``, selects the ``<rp-id>.paskiadb`` candidate by name;
|
||||||
already exists or no legacy database is present. The converted legacy
|
without it, exactly one candidate must exist. Returns the migrated
|
||||||
directory/file is renamed aside to ``<name>.converted-bak`` rather than
|
realm's rp-id. The converted legacy directory/file is renamed aside
|
||||||
deleted.
|
to ``<name>.converted-bak`` rather than deleted.
|
||||||
|
|
||||||
Raises SystemExit when multiple legacy databases are found — automatic
|
Raises SystemExit when ``paskia.kantadb`` already exists, when no
|
||||||
merging is not supported.
|
candidate matches, or when several candidates exist and no ``rp_id``
|
||||||
|
was given to select one.
|
||||||
"""
|
"""
|
||||||
target = db_file_path()
|
target = db_file_path()
|
||||||
if target.exists():
|
if target.exists():
|
||||||
return None
|
raise SystemExit(f"Database {target} already exists — nothing to migrate.")
|
||||||
candidates = find_legacy_databases()
|
candidates = find_legacy_databases()
|
||||||
if not candidates:
|
if rp_id is not None:
|
||||||
return None
|
name = f"{rp_id}.paskiadb"
|
||||||
if len(candidates) > 1:
|
matches = [c for c in candidates if c.name == name]
|
||||||
|
if not matches:
|
||||||
|
found = ", ".join(str(c) for c in candidates) or "none"
|
||||||
|
raise SystemExit(
|
||||||
|
f"No legacy database {name} in this directory (candidates: {found})."
|
||||||
|
)
|
||||||
|
src = matches[0]
|
||||||
|
elif not candidates:
|
||||||
|
raise SystemExit("No legacy *.paskiadb database found — nothing to migrate.")
|
||||||
|
elif len(candidates) > 1:
|
||||||
names = ", ".join(str(c) for c in candidates)
|
names = ", ".join(str(c) for c in candidates)
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"Multiple legacy databases found ({names}). Automatic merging is "
|
f"Multiple legacy databases found ({names}) — select one with "
|
||||||
"not supported — remove or rename all but the one to adopt."
|
"'paskia migrate --rp-id <rp-id>'."
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
src = candidates[0]
|
src = candidates[0]
|
||||||
legacy_file = src / "main.db" if src.is_dir() else src
|
legacy_file = src / "main.db" if src.is_dir() else src
|
||||||
config = convert_legacy_database(legacy_file, target)
|
config = convert_legacy_database(legacy_file, target)
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,16 @@ def ensure_database(rp_ids: list[str], args: argparse.Namespace, listen: str) ->
|
|||||||
"""Bootstrap paskia.kantadb via 'paskia init' when no database exists.
|
"""Bootstrap paskia.kantadb via 'paskia init' when no database exists.
|
||||||
|
|
||||||
Realm options are init-only; 'paskia' (serve) reads all configuration
|
Realm options are init-only; 'paskia' (serve) reads all configuration
|
||||||
from the database. A legacy *.paskiadb database is adopted by serve,
|
from the database. A legacy *.paskiadb database must be converted with
|
||||||
so no init is run in that case either.
|
'paskia migrate' first.
|
||||||
"""
|
"""
|
||||||
if db_file_path().exists() or find_legacy_databases():
|
if db_file_path().exists():
|
||||||
return
|
return
|
||||||
|
if find_legacy_databases():
|
||||||
|
raise SystemExit(
|
||||||
|
"Legacy *.paskiadb database found — run 'paskia migrate' to "
|
||||||
|
"convert it before starting the dev server."
|
||||||
|
)
|
||||||
|
|
||||||
cmd = [sys.executable, "-m", "paskia", "init", f"--listen={listen}"]
|
cmd = [sys.executable, "-m", "paskia", "init", f"--listen={listen}"]
|
||||||
for rp_id in rp_ids:
|
for rp_id in rp_ids:
|
||||||
|
|||||||
+56
-8
@@ -1,8 +1,9 @@
|
|||||||
"""Tests for the CLI entry point in paskia/__main__.py.
|
"""Tests for the CLI entry point in paskia/__main__.py.
|
||||||
|
|
||||||
The CLI is split into ``paskia init`` (create the combined paskia.kantadb
|
The CLI is split into ``paskia init`` (create the combined paskia.kantadb
|
||||||
with the initial realm(s)) and bare ``paskia`` (serve the stored realms,
|
with the initial realm(s)), ``paskia migrate`` (convert a legacy
|
||||||
adopting a lone legacy ``<rp-id>.paskiadb`` database if present).
|
``<rp-id>.paskiadb`` database), and bare ``paskia`` (serve the stored
|
||||||
|
realms; never migrates).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -165,7 +166,13 @@ def test_serve_listen_override_not_persisted(run_cli, tmp_path):
|
|||||||
assert stored_config(tmp_path).listen == ["4402"]
|
assert stored_config(tmp_path).listen == ["4402"]
|
||||||
|
|
||||||
|
|
||||||
def test_serve_adopts_legacy_database(run_cli, tmp_path):
|
def test_serve_suggests_migrate_when_legacy_present(run_cli, tmp_path):
|
||||||
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="example.com"))
|
||||||
|
with pytest.raises(SystemExit, match="paskia migrate"):
|
||||||
|
run_cli()
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_converts_legacy_database(run_cli, tmp_path):
|
||||||
src_dir = write_legacy_db(
|
src_dir = write_legacy_db(
|
||||||
tmp_path, legacy.LegacyConfig(rp_id="example.com", rp_name="Legacy Name")
|
tmp_path, legacy.LegacyConfig(rp_id="example.com", rp_name="Legacy Name")
|
||||||
)
|
)
|
||||||
@@ -174,12 +181,12 @@ def test_serve_adopts_legacy_database(run_cli, tmp_path):
|
|||||||
avatar.mkdir(parents=True)
|
avatar.mkdir(parents=True)
|
||||||
(avatar / "profile.webp").write_bytes(b"RIFF1234WEBP")
|
(avatar / "profile.webp").write_bytes(b"RIFF1234WEBP")
|
||||||
|
|
||||||
run_cli()
|
run_cli("migrate")
|
||||||
|
|
||||||
config = stored_config(tmp_path)
|
config = stored_config(tmp_path)
|
||||||
assert [r.rp_id for r in config.realms] == ["example.com"]
|
assert [r.rp_id for r in config.realms] == ["example.com"]
|
||||||
assert config.realms[0].rp_name == "Legacy Name"
|
assert config.realms[0].rp_name == "Legacy Name"
|
||||||
# Legacy directory renamed aside, user files adopted
|
# Legacy directory renamed aside, user files moved over
|
||||||
assert not src_dir.exists()
|
assert not src_dir.exists()
|
||||||
assert (tmp_path / "example.com.paskiadb.converted-bak").is_dir()
|
assert (tmp_path / "example.com.paskiadb.converted-bak").is_dir()
|
||||||
assert (
|
assert (
|
||||||
@@ -191,11 +198,41 @@ def test_serve_adopts_legacy_database(run_cli, tmp_path):
|
|||||||
).read_bytes() == b"RIFF1234WEBP"
|
).read_bytes() == b"RIFF1234WEBP"
|
||||||
|
|
||||||
|
|
||||||
def test_serve_multiple_legacy_databases_abort(run_cli, tmp_path):
|
def test_migrate_multiple_legacy_databases_require_rp_id(run_cli, tmp_path):
|
||||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||||
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
||||||
with pytest.raises(SystemExit, match="Multiple legacy"):
|
with pytest.raises(SystemExit, match="--rp-id"):
|
||||||
run_cli()
|
run_cli("migrate")
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_explicit_rp_id_selects_candidate(run_cli, tmp_path):
|
||||||
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||||
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="two.com"))
|
||||||
|
|
||||||
|
run_cli("migrate", "--rp-id", "two.com")
|
||||||
|
|
||||||
|
config = stored_config(tmp_path)
|
||||||
|
assert [r.rp_id for r in config.realms] == ["two.com"]
|
||||||
|
# The other candidate is left in place
|
||||||
|
assert (tmp_path / "one.com.paskiadb").is_dir()
|
||||||
|
assert (tmp_path / "two.com.paskiadb.converted-bak").is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_unknown_rp_id(run_cli, tmp_path):
|
||||||
|
write_legacy_db(tmp_path, legacy.LegacyConfig(rp_id="one.com"))
|
||||||
|
with pytest.raises(SystemExit, match="nope.com.paskiadb"):
|
||||||
|
run_cli("migrate", "--rp-id", "nope.com")
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_refuses_existing_database(run_cli):
|
||||||
|
run_cli("init")
|
||||||
|
with pytest.raises(SystemExit, match="already exists"):
|
||||||
|
run_cli("migrate")
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_without_legacy_database(run_cli):
|
||||||
|
with pytest.raises(SystemExit, match="No legacy"):
|
||||||
|
run_cli("migrate")
|
||||||
|
|
||||||
|
|
||||||
def test_cli_help():
|
def test_cli_help():
|
||||||
@@ -218,3 +255,14 @@ def test_cli_init_help():
|
|||||||
)
|
)
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
assert "Bootstrap" in result.stdout
|
assert "Bootstrap" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_migrate_help():
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "paskia", "migrate", "--help"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "Convert" in result.stdout
|
||||||
|
|||||||
Reference in New Issue
Block a user