feat(properties): prepare property commands before execution
Build and Push Docker Container / build-and-push (push) Successful in 4m28s
Build and Push Docker Container / build-and-push (push) Successful in 4m28s
- Create missing note files before running property:set. - Default date-only datetime values to midnight. - Keep property names out of path authorization checks. - Split policy, vault, daily note, and command target helpers. - Split n8n node args, description, output, and API response helpers. - Split API tests by auth, command, daily note, and property behavior. - Bump API and n8n node versions.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
os.environ.setdefault("OBSIDIAN_JWT_SECRET", "test-jwt-secret")
|
||||
os.environ.setdefault("OBSIDIAN_CLI_BIN", "python")
|
||||
os.environ.setdefault("OBSIDIAN_VAULT_PATH", ".")
|
||||
|
||||
import app.auth as authmod # noqa: E402
|
||||
from app.main import app # noqa: E402
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def make_jwt(claims:dict, secret:bytes = b"test-jwt-secret") -> str:
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
|
||||
def enc(value:dict) -> str:
|
||||
raw = json.dumps(value, separators=(",", ":")).encode()
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
signing_input = f"{enc(header)}.{enc(claims)}"
|
||||
signature = hmac.new(secret, signing_input.encode(), hashlib.sha256).digest()
|
||||
return f"{signing_input}.{base64.urlsafe_b64encode(signature).decode().rstrip('=')}"
|
||||
|
||||
def jwt(claims:dict|None = None) -> str:
|
||||
claims = claims or {}
|
||||
claims.setdefault("sub", "tests")
|
||||
claims.setdefault("vaults", ["*"])
|
||||
claims.setdefault("paths", ["*"])
|
||||
claims.setdefault("commands", ["*"])
|
||||
claims.setdefault("exp", int(time.time()) + 3600)
|
||||
return make_jwt(claims)
|
||||
|
||||
def use_jwt_auth(monkeypatch, claims:dict) -> str:
|
||||
monkeypatch.setattr(authmod, "JWT_SECRET", "test-jwt-secret")
|
||||
return jwt(claims)
|
||||
|
||||
def use_work_vault(monkeypatch, vault) -> None:
|
||||
monkeypatch.setattr("app.vaults.REGISTERED_VAULTS", {"work": str(vault)})
|
||||
monkeypatch.setattr("app.vaults.DEFAULT_VAULT_NAME", "work")
|
||||
monkeypatch.setattr("app.policy.DEFAULT_VAULT_NAME", "work")
|
||||
@@ -1,263 +0,0 @@
|
||||
import hashlib, base64, json, hmac
|
||||
import time, os
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
os.environ.setdefault("OBSIDIAN_JWT_SECRET", "test-jwt-secret")
|
||||
os.environ.setdefault("OBSIDIAN_CLI_BIN", "python")
|
||||
os.environ.setdefault("OBSIDIAN_VAULT_PATH", ".")
|
||||
|
||||
import app.auth as authmod # noqa: E402
|
||||
from app.main import app # noqa: E402
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def make_jwt(claims:dict, secret:bytes = b"test-jwt-secret") -> str:
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
|
||||
def enc(value:dict) -> str:
|
||||
raw = json.dumps(value, separators=(",", ":")).encode()
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
signing_input = f"{enc(header)}.{enc(claims)}"
|
||||
signature = hmac.new(secret, signing_input.encode(), hashlib.sha256).digest()
|
||||
return f"{signing_input}.{base64.urlsafe_b64encode(signature).decode().rstrip('=')}"
|
||||
|
||||
def jwt(claims:dict|None = None) -> str:
|
||||
claims = claims or {}
|
||||
claims.setdefault("sub", "tests")
|
||||
claims.setdefault("vaults", ["*"])
|
||||
claims.setdefault("paths", ["*"])
|
||||
claims.setdefault("commands", ["*"])
|
||||
claims.setdefault("exp", int(time.time()) + 3600)
|
||||
return make_jwt(claims)
|
||||
|
||||
def use_jwt_auth(monkeypatch, claims:dict) -> str:
|
||||
monkeypatch.setattr(authmod, "JWT_SECRET", "test-jwt-secret")
|
||||
return jwt(claims)
|
||||
|
||||
def test_health():
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["ok"] is True
|
||||
assert "vault" in body
|
||||
assert body["cli"] == "python"
|
||||
assert body["jwt"] is True
|
||||
|
||||
def test_command_requires_auth():
|
||||
response = client.post("/commands", json={"args": ["--version"]})
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "missing Authorization header; expected 'Bearer <jwt>'"
|
||||
|
||||
def test_invalid_token_reports_reason(monkeypatch):
|
||||
monkeypatch.setattr("app.auth.JWT_SECRET", "secret")
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": "Bearer not-a-jwt"},
|
||||
json={"args": ["--version"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"].startswith("invalid token:")
|
||||
|
||||
def test_command_runs_configured_cli_only():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"args": ["--version"], "timeout": 5},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["exit_code"] == 0
|
||||
assert body["command"] == ["python", "--version"]
|
||||
assert "Python" in body["stdout"]
|
||||
assert body["timed_out"] is False
|
||||
|
||||
def test_plain_command_string_is_rejected():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"command": "--version"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "command strings are not allowed; use args as a list of strings"
|
||||
|
||||
def test_shell_mode_is_rejected():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"mode": "shell", "args": ["echo nope"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "mode is not allowed; only the configured Obsidian CLI can be executed"
|
||||
|
||||
def test_command_timeout():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"args": ["-c", "import time; time.sleep(2)"], "timeout": 0.1},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["exit_code"] == 124
|
||||
assert body["timed_out"] is True
|
||||
|
||||
def test_jwt_claims_become_policy():
|
||||
token = jwt({"sub": "file", "vaults": ["work"], "paths": ["Inbox/"], "commands": ["read"]})
|
||||
claims = authmod.decode_hs256_jwt(token, b"test-jwt-secret")
|
||||
policy = authmod.policy_from_jwt_claims(token, claims)
|
||||
|
||||
assert policy.token == token
|
||||
assert policy.name == "file"
|
||||
assert policy.vaults == ("work",)
|
||||
assert policy.paths == ("Inbox/",)
|
||||
assert policy.commands == ("read",)
|
||||
|
||||
def test_token_policy_restricts_command(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "limited", "commands": ["read"], "vaults": ["*"], "paths": ["*"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["create", "path=Inbox/Nope.md", "content=Nope"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to run command: create")
|
||||
|
||||
def test_token_policy_restricts_vault(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "work", "commands": ["*"], "vaults": ["work"], "paths": ["*"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=personal", "read", "path=Inbox/Test.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access vault: personal")
|
||||
|
||||
def test_token_policy_restricts_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["read", "path=Private/Secret.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Private/Secret.md")
|
||||
|
||||
def test_property_set_name_is_not_treated_as_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["property:set"], "vaults": ["*"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"]
|
||||
|
||||
def test_rename_name_is_treated_as_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["rename"], "vaults": ["*"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["rename", "path=00. Journal/Old.md", "name=Private/New.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Private/New.md")
|
||||
|
||||
def test_token_policy_allows_allowed_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["-c", "print('ok')"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["read", "path=Inbox/Allowed.md"]},
|
||||
)
|
||||
|
||||
# The authorization layer allows this. The fake Python CLI then fails because
|
||||
# "read" is not a Python flag, which is fine for this policy test.
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "read", "path=Inbox/Allowed.md"]
|
||||
|
||||
def test_path_restricted_token_allows_daily_command_from_obsidian_config(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
config = vault / ".obsidian"
|
||||
config.mkdir(parents=True)
|
||||
(config / "daily-notes.json").write_text(json.dumps({"folder": "00. Journal", "format": "YYYY-MM-DD"}))
|
||||
|
||||
monkeypatch.setattr("app.policy.REGISTERED_VAULTS", {"work": str(vault)})
|
||||
monkeypatch.setattr("app.policy.DEFAULT_VAULT_NAME", "work")
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "daily:append", "content=ok"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault=work", "daily:append", "content=ok"]
|
||||
|
||||
def test_path_restricted_token_allows_vault_info_command(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append", "vault", "vaults"], "vaults": ["*"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault"]
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vaults"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vaults"]
|
||||
|
||||
def test_path_restricted_token_denies_daily_command_outside_obsidian_config(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
config = vault / ".obsidian"
|
||||
config.mkdir(parents=True)
|
||||
(config / "daily-notes.json").write_text(json.dumps({"folder": "Journal", "format": "YYYY-MM-DD"}))
|
||||
|
||||
monkeypatch.setattr("app.policy.REGISTERED_VAULTS", {"work": str(vault)})
|
||||
monkeypatch.setattr("app.policy.DEFAULT_VAULT_NAME", "work")
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "daily:append", "content=ok"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Journal/")
|
||||
@@ -0,0 +1,87 @@
|
||||
import app.auth as authmod
|
||||
from conftest import client, jwt, use_jwt_auth
|
||||
|
||||
def test_command_requires_auth():
|
||||
response = client.post("/commands", json={"args": ["--version"]})
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "missing Authorization header; expected 'Bearer <jwt>'"
|
||||
|
||||
def test_invalid_token_reports_reason(monkeypatch):
|
||||
monkeypatch.setattr("app.auth.JWT_SECRET", "secret")
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": "Bearer not-a-jwt"},
|
||||
json={"args": ["--version"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"].startswith("invalid token:")
|
||||
|
||||
def test_jwt_claims_become_policy():
|
||||
token = jwt({"sub": "file", "vaults": ["work"], "paths": ["Inbox/"], "commands": ["read"]})
|
||||
claims = authmod.decode_hs256_jwt(token, b"test-jwt-secret")
|
||||
policy = authmod.policy_from_jwt_claims(token, claims)
|
||||
|
||||
assert policy.token == token
|
||||
assert policy.name == "file"
|
||||
assert policy.vaults == ("work",)
|
||||
assert policy.paths == ("Inbox/",)
|
||||
assert policy.commands == ("read",)
|
||||
|
||||
def test_token_policy_restricts_command(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "limited", "commands": ["read"], "vaults": ["*"], "paths": ["*"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["create", "path=Inbox/Nope.md", "content=Nope"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to run command: create")
|
||||
|
||||
def test_token_policy_restricts_vault(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "work", "commands": ["*"], "vaults": ["work"], "paths": ["*"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=personal", "read", "path=Inbox/Test.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access vault: personal")
|
||||
|
||||
def test_token_policy_restricts_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["read", "path=Private/Secret.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Private/Secret.md")
|
||||
|
||||
def test_token_policy_allows_allowed_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "inbox", "commands": ["read"], "vaults": ["*"], "paths": ["Inbox/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["-c", "print('ok')"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["read", "path=Inbox/Allowed.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "read", "path=Inbox/Allowed.md"]
|
||||
@@ -0,0 +1,57 @@
|
||||
from conftest import client, jwt
|
||||
|
||||
def test_health():
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["ok"] is True
|
||||
assert "vault" in body
|
||||
assert body["cli"] == "python"
|
||||
assert body["jwt"] is True
|
||||
|
||||
def test_command_runs_configured_cli_only():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"args": ["--version"], "timeout": 5},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["exit_code"] == 0
|
||||
assert body["command"] == ["python", "--version"]
|
||||
assert "Python" in body["stdout"]
|
||||
assert body["timed_out"] is False
|
||||
|
||||
def test_plain_command_string_is_rejected():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"command": "--version"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "command strings are not allowed; use args as a list of strings"
|
||||
|
||||
def test_shell_mode_is_rejected():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"mode": "shell", "args": ["echo nope"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "mode is not allowed; only the configured Obsidian CLI can be executed"
|
||||
|
||||
def test_command_timeout():
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {jwt()}"},
|
||||
json={"args": ["-c", "import time; time.sleep(2)"], "timeout": 0.1},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["exit_code"] == 124
|
||||
assert body["timed_out"] is True
|
||||
@@ -0,0 +1,60 @@
|
||||
import json
|
||||
|
||||
from conftest import client, use_jwt_auth, use_work_vault
|
||||
|
||||
def test_path_restricted_token_allows_daily_command_from_obsidian_config(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
config = vault / ".obsidian"
|
||||
config.mkdir(parents=True)
|
||||
(config / "daily-notes.json").write_text(json.dumps({"folder": "00. Journal", "format": "YYYY-MM-DD"}))
|
||||
|
||||
use_work_vault(monkeypatch, vault)
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "daily:append", "content=ok"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault=work", "daily:append", "content=ok"]
|
||||
|
||||
def test_path_restricted_token_allows_vault_info_command(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append", "vault", "vaults"], "vaults": ["*"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault"]
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vaults"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vaults"]
|
||||
|
||||
def test_path_restricted_token_denies_daily_command_outside_obsidian_config(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
config = vault / ".obsidian"
|
||||
config.mkdir(parents=True)
|
||||
(config / "daily-notes.json").write_text(json.dumps({"folder": "Journal", "format": "YYYY-MM-DD"}))
|
||||
|
||||
use_work_vault(monkeypatch, vault)
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "daily", "commands": ["daily:append"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "daily:append", "content=ok"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Journal/")
|
||||
@@ -0,0 +1,58 @@
|
||||
from conftest import client, use_jwt_auth, use_work_vault
|
||||
|
||||
def test_property_set_name_is_not_treated_as_path(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
use_work_vault(monkeypatch, vault)
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["property:set"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault=work", "property:set", "path=00. Journal/Note.md", "name=from", "value=Daniel", "type=text"]
|
||||
|
||||
def test_property_set_creates_missing_file(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
use_work_vault(monkeypatch, vault)
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["property:set"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "property:set", "path=00. Journal/New.md", "name=from", "value=Daniel", "type=text"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (vault / "00. Journal" / "New.md").exists()
|
||||
|
||||
def test_property_set_datetime_defaults_missing_time(monkeypatch, tmp_path):
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
use_work_vault(monkeypatch, vault)
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["property:set"], "vaults": ["work"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["vault=work", "property:set", "path=00. Journal/New.md", "name=from", "value=2026-06-24T", "type=datetime"], "cwd": str(vault)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["command"] == ["python", "vault=work", "property:set", "path=00. Journal/New.md", "name=from", "value=2026-06-24T00:00:00", "type=datetime"]
|
||||
|
||||
def test_rename_name_is_treated_as_path(monkeypatch):
|
||||
token = use_jwt_auth(monkeypatch, {"sub": "journal", "commands": ["rename"], "vaults": ["*"], "paths": ["00. Journal/"]})
|
||||
|
||||
response = client.post(
|
||||
"/commands",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"args": ["rename", "path=00. Journal/Old.md", "name=Private/New.md"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"].startswith("token is not allowed to access path: Private/New.md")
|
||||
Reference in New Issue
Block a user