mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
Add entry management CLI and API
This commit is contained in:
@@ -81,6 +81,64 @@ def get_entry(entry_id: int, authorization: str | None = Header(None)) -> Any:
|
||||
return entry
|
||||
|
||||
|
||||
@app.post("/api/v1/entry")
|
||||
def create_entry(
|
||||
entry: dict,
|
||||
authorization: str | None = Header(None),
|
||||
) -> dict[str, int]:
|
||||
"""Create a new password entry."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
index = _pm.entry_manager.add_entry(
|
||||
entry.get("label"),
|
||||
int(entry.get("length", 12)),
|
||||
entry.get("username"),
|
||||
entry.get("url"),
|
||||
)
|
||||
return {"id": index}
|
||||
|
||||
|
||||
@app.put("/api/v1/entry/{entry_id}")
|
||||
def update_entry(
|
||||
entry_id: int,
|
||||
entry: dict,
|
||||
authorization: str | None = Header(None),
|
||||
) -> dict[str, str]:
|
||||
"""Update an existing entry."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
_pm.entry_manager.modify_entry(
|
||||
entry_id,
|
||||
username=entry.get("username"),
|
||||
url=entry.get("url"),
|
||||
notes=entry.get("notes"),
|
||||
label=entry.get("label"),
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/v1/entry/{entry_id}/archive")
|
||||
def archive_entry(
|
||||
entry_id: int, authorization: str | None = Header(None)
|
||||
) -> dict[str, str]:
|
||||
"""Archive an entry."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
_pm.entry_manager.archive_entry(entry_id)
|
||||
return {"status": "archived"}
|
||||
|
||||
|
||||
@app.post("/api/v1/entry/{entry_id}/unarchive")
|
||||
def unarchive_entry(
|
||||
entry_id: int, authorization: str | None = Header(None)
|
||||
) -> dict[str, str]:
|
||||
"""Restore an archived entry."""
|
||||
_check_token(authorization)
|
||||
assert _pm is not None
|
||||
_pm.entry_manager.restore_entry(entry_id)
|
||||
return {"status": "active"}
|
||||
|
||||
|
||||
@app.get("/api/v1/config/{key}")
|
||||
def get_config(key: str, authorization: str | None = Header(None)) -> Any:
|
||||
_check_token(authorization)
|
||||
|
@@ -126,6 +126,52 @@ def entry_get(ctx: typer.Context, query: str) -> None:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@entry_app.command("add")
|
||||
def entry_add(
|
||||
ctx: typer.Context,
|
||||
label: str,
|
||||
length: int = typer.Option(12, "--length"),
|
||||
username: Optional[str] = typer.Option(None, "--username"),
|
||||
url: Optional[str] = typer.Option(None, "--url"),
|
||||
) -> None:
|
||||
"""Add a new password entry and output its index."""
|
||||
pm = _get_pm(ctx)
|
||||
index = pm.entry_manager.add_entry(label, length, username, url)
|
||||
typer.echo(str(index))
|
||||
|
||||
|
||||
@entry_app.command("modify")
|
||||
def entry_modify(
|
||||
ctx: typer.Context,
|
||||
entry_id: int,
|
||||
label: Optional[str] = typer.Option(None, "--label"),
|
||||
username: Optional[str] = typer.Option(None, "--username"),
|
||||
url: Optional[str] = typer.Option(None, "--url"),
|
||||
notes: Optional[str] = typer.Option(None, "--notes"),
|
||||
) -> None:
|
||||
"""Modify an existing entry."""
|
||||
pm = _get_pm(ctx)
|
||||
pm.entry_manager.modify_entry(
|
||||
entry_id, username=username, url=url, notes=notes, label=label
|
||||
)
|
||||
|
||||
|
||||
@entry_app.command("archive")
|
||||
def entry_archive(ctx: typer.Context, entry_id: int) -> None:
|
||||
"""Archive an entry."""
|
||||
pm = _get_pm(ctx)
|
||||
pm.entry_manager.archive_entry(entry_id)
|
||||
typer.echo(str(entry_id))
|
||||
|
||||
|
||||
@entry_app.command("unarchive")
|
||||
def entry_unarchive(ctx: typer.Context, entry_id: int) -> None:
|
||||
"""Restore an archived entry."""
|
||||
pm = _get_pm(ctx)
|
||||
pm.entry_manager.restore_entry(entry_id)
|
||||
typer.echo(str(entry_id))
|
||||
|
||||
|
||||
@vault_app.command("export")
|
||||
def vault_export(
|
||||
ctx: typer.Context, file: str = typer.Option(..., help="Output file")
|
||||
|
@@ -16,6 +16,10 @@ def client(monkeypatch):
|
||||
entry_manager=SimpleNamespace(
|
||||
search_entries=lambda q: [(1, "Site", "user", "url", False)],
|
||||
retrieve_entry=lambda i: {"label": "Site"},
|
||||
add_entry=lambda *a, **k: 1,
|
||||
modify_entry=lambda *a, **k: None,
|
||||
archive_entry=lambda i: None,
|
||||
restore_entry=lambda i: None,
|
||||
),
|
||||
config_manager=SimpleNamespace(
|
||||
load_config=lambda require_pin=False: {"k": "v"}
|
||||
@@ -98,6 +102,35 @@ def test_get_nostr_pubkey(client):
|
||||
assert res.headers.get("access-control-allow-origin") == "http://example.com"
|
||||
|
||||
|
||||
def test_create_modify_archive_entry(client):
|
||||
cl, token = client
|
||||
headers = {"Authorization": f"Bearer {token}", "Origin": "http://example.com"}
|
||||
|
||||
res = cl.post(
|
||||
"/api/v1/entry",
|
||||
json={"label": "test", "length": 12},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"id": 1}
|
||||
|
||||
res = cl.put(
|
||||
"/api/v1/entry/1",
|
||||
json={"username": "bob"},
|
||||
headers=headers,
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "ok"}
|
||||
|
||||
res = cl.post("/api/v1/entry/1/archive", headers=headers)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "archived"}
|
||||
|
||||
res = cl.post("/api/v1/entry/1/unarchive", headers=headers)
|
||||
assert res.status_code == 200
|
||||
assert res.json() == {"status": "active"}
|
||||
|
||||
|
||||
def test_shutdown(client, monkeypatch):
|
||||
cl, token = client
|
||||
|
||||
@@ -130,10 +163,17 @@ def test_shutdown(client, monkeypatch):
|
||||
("get", "/api/v1/fingerprint"),
|
||||
("get", "/api/v1/nostr/pubkey"),
|
||||
("post", "/api/v1/shutdown"),
|
||||
("post", "/api/v1/entry"),
|
||||
("put", "/api/v1/entry/1"),
|
||||
("post", "/api/v1/entry/1/archive"),
|
||||
("post", "/api/v1/entry/1/unarchive"),
|
||||
],
|
||||
)
|
||||
def test_invalid_token_other_endpoints(client, method, path):
|
||||
cl, _token = client
|
||||
req = getattr(cl, method)
|
||||
res = req(path, headers={"Authorization": "Bearer bad"})
|
||||
kwargs = {"headers": {"Authorization": "Bearer bad"}}
|
||||
if method in {"post", "put"}:
|
||||
kwargs["json"] = {}
|
||||
res = req(path, **kwargs)
|
||||
assert res.status_code == 401
|
||||
|
@@ -165,3 +165,84 @@ def test_api_start_passes_fingerprint(monkeypatch):
|
||||
result = runner.invoke(app, ["--fingerprint", "abc", "api", "start"])
|
||||
assert result.exit_code == 0
|
||||
assert called.get("fp") == "abc"
|
||||
|
||||
|
||||
def test_entry_add(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def add_entry(label, length, username=None, url=None):
|
||||
called["args"] = (label, length, username, url)
|
||||
return 2
|
||||
|
||||
pm = SimpleNamespace(
|
||||
entry_manager=SimpleNamespace(add_entry=add_entry),
|
||||
select_fingerprint=lambda fp: None,
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"entry",
|
||||
"add",
|
||||
"Example",
|
||||
"--length",
|
||||
"16",
|
||||
"--username",
|
||||
"bob",
|
||||
"--url",
|
||||
"ex.com",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "2" in result.stdout
|
||||
assert called["args"] == ("Example", 16, "bob", "ex.com")
|
||||
|
||||
|
||||
def test_entry_modify(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def modify_entry(index, username=None, url=None, notes=None, label=None):
|
||||
called["args"] = (index, username, url, notes, label)
|
||||
|
||||
pm = SimpleNamespace(
|
||||
entry_manager=SimpleNamespace(modify_entry=modify_entry),
|
||||
select_fingerprint=lambda fp: None,
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
result = runner.invoke(app, ["entry", "modify", "1", "--username", "alice"])
|
||||
assert result.exit_code == 0
|
||||
assert called["args"] == (1, "alice", None, None, None)
|
||||
|
||||
|
||||
def test_entry_archive(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def archive_entry(i):
|
||||
called["id"] = i
|
||||
|
||||
pm = SimpleNamespace(
|
||||
entry_manager=SimpleNamespace(archive_entry=archive_entry),
|
||||
select_fingerprint=lambda fp: None,
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
result = runner.invoke(app, ["entry", "archive", "3"])
|
||||
assert result.exit_code == 0
|
||||
assert "3" in result.stdout
|
||||
assert called["id"] == 3
|
||||
|
||||
|
||||
def test_entry_unarchive(monkeypatch):
|
||||
called = {}
|
||||
|
||||
def restore_entry(i):
|
||||
called["id"] = i
|
||||
|
||||
pm = SimpleNamespace(
|
||||
entry_manager=SimpleNamespace(restore_entry=restore_entry),
|
||||
select_fingerprint=lambda fp: None,
|
||||
)
|
||||
monkeypatch.setattr(cli, "PasswordManager", lambda: pm)
|
||||
result = runner.invoke(app, ["entry", "unarchive", "4"])
|
||||
assert result.exit_code == 0
|
||||
assert "4" in result.stdout
|
||||
assert called["id"] == 4
|
||||
|
Reference in New Issue
Block a user