205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from codexpi.project_group_service import project_group_context, publish_project_group_update
|
|||
|
|
|
|||
|
|
|
|||
|
|
def register_project(logged_in, name: str) -> dict:
|
|||
|
|
client, _, projects, _, headers = logged_in
|
|||
|
|
root = projects / name.lower().replace(" ", "-")
|
|||
|
|
root.mkdir()
|
|||
|
|
response = client.post("/api/projects", headers=headers, json={"name": name, "path": str(root)})
|
|||
|
|
assert response.status_code == 201
|
|||
|
|
return response.json()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_group_creation_selects_existing_projects_and_requires_csrf(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
nasflix = register_project(logged_in, "NASflix")
|
|||
|
|
ginoflex = register_project(logged_in, "Ginoflex")
|
|||
|
|
tvrework = register_project(logged_in, "TvRework")
|
|||
|
|
payload = {
|
|||
|
|
"name": "TV",
|
|||
|
|
"description": "Applications qui pilotent ou affichent du contenu sur la télévision.",
|
|||
|
|
"project_ids": [nasflix["id"], ginoflex["id"], tvrework["id"]],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
assert client.post("/api/project-groups", json=payload).status_code == 403
|
|||
|
|
created = client.post("/api/project-groups", headers=headers, json=payload)
|
|||
|
|
|
|||
|
|
assert created.status_code == 201
|
|||
|
|
group = created.json()
|
|||
|
|
assert group["name"] == "TV"
|
|||
|
|
assert group["key"] == "tv"
|
|||
|
|
assert {item["name"] for item in group["members"]} == {"NASflix", "Ginoflex", "TvRework"}
|
|||
|
|
assert {
|
|||
|
|
row["execution_group"]
|
|||
|
|
for row in app.state.db.all(
|
|||
|
|
"SELECT execution_group FROM projects WHERE id IN (?,?,?)",
|
|||
|
|
(nasflix["id"], ginoflex["id"], tvrework["id"]),
|
|||
|
|
)
|
|||
|
|
} == {"tv"}
|
|||
|
|
assert client.get("/api/project-groups").json()[0]["description"].startswith("Applications")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_group_update_moves_members_and_delete_releases_them(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
first = register_project(logged_in, "Premier")
|
|||
|
|
second = register_project(logged_in, "Deuxième")
|
|||
|
|
third = register_project(logged_in, "Troisième")
|
|||
|
|
created = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "Télévision", "project_ids": [first["id"], second["id"]]},
|
|||
|
|
).json()
|
|||
|
|
|
|||
|
|
updated = client.put(
|
|||
|
|
f"/api/project-groups/{created['id']}",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "TV salon", "description": "Contrats HDMI et télécommande", "project_ids": [first["id"], third["id"]]},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert updated.status_code == 200
|
|||
|
|
assert updated.json()["key"] == "tv-salon"
|
|||
|
|
assert {item["id"] for item in updated.json()["members"]} == {first["id"], third["id"]}
|
|||
|
|
assert app.state.db.one("SELECT execution_group FROM projects WHERE id=?", (second["id"],))["execution_group"] == ""
|
|||
|
|
|
|||
|
|
deleted = client.delete(f"/api/project-groups/{created['id']}", headers=headers)
|
|||
|
|
assert deleted.status_code == 204
|
|||
|
|
assert client.get("/api/project-groups").json() == []
|
|||
|
|
assert app.state.db.one("SELECT execution_group FROM projects WHERE id=?", (first["id"],))["execution_group"] == ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_project_can_belong_to_multiple_groups(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
tv = register_project(logged_in, "TV commune")
|
|||
|
|
player = register_project(logged_in, "Lecteur")
|
|||
|
|
media = register_project(logged_in, "Médiathèque")
|
|||
|
|
first = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "Télévision", "project_ids": [tv["id"], player["id"]]},
|
|||
|
|
)
|
|||
|
|
second = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "Médias", "project_ids": [player["id"], media["id"]]},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert first.status_code == 201
|
|||
|
|
assert second.status_code == 201
|
|||
|
|
groups = client.get("/api/project-groups").json()
|
|||
|
|
assert len(groups) == 2
|
|||
|
|
memberships = app.state.db.all(
|
|||
|
|
"SELECT group_id FROM project_group_members WHERE project_id=?",
|
|||
|
|
(player["id"],),
|
|||
|
|
)
|
|||
|
|
assert len(memberships) == 2
|
|||
|
|
context = project_group_context(app.state.db.path, player["id"])
|
|||
|
|
assert "[GROUPE DE PROJETS : Télévision]" in context
|
|||
|
|
assert "[GROUPE DE PROJETS : Médias]" in context
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_group_rejects_remote_projects(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
local = register_project(logged_in, "Local")
|
|||
|
|
remote = register_project(logged_in, "Distant")
|
|||
|
|
app.state.db.execute("UPDATE projects SET kind='remote' WHERE id=?", (remote["id"],))
|
|||
|
|
|
|||
|
|
response = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "Mixte", "project_ids": [local["id"], remote["id"]]},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert response.status_code == 400
|
|||
|
|
assert "distants" in response.json()["detail"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_group_keeps_members_when_full_access_is_enabled(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
first = register_project(logged_in, "Premier projet")
|
|||
|
|
second = register_project(logged_in, "Deuxième projet")
|
|||
|
|
enabled = client.put(
|
|||
|
|
f"/api/projects/{second['id']}/full-access",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"enabled": True, "acknowledged": True},
|
|||
|
|
)
|
|||
|
|
assert enabled.status_code == 200
|
|||
|
|
|
|||
|
|
created = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "Coordonné", "project_ids": [first["id"], second["id"]]},
|
|||
|
|
)
|
|||
|
|
assert created.status_code == 201
|
|||
|
|
enabled_first = client.put(
|
|||
|
|
f"/api/projects/{first['id']}/full-access",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"enabled": True, "acknowledged": True},
|
|||
|
|
)
|
|||
|
|
assert enabled_first.status_code == 200
|
|||
|
|
assert app.state.db.one(
|
|||
|
|
"SELECT COUNT(*) AS count FROM project_group_members WHERE group_id=?",
|
|||
|
|
(created.json()["id"],),
|
|||
|
|
)["count"] == 2
|
|||
|
|
|
|||
|
|
context = project_group_context(
|
|||
|
|
app.state.db.path,
|
|||
|
|
first["id"],
|
|||
|
|
engine="claude",
|
|||
|
|
full_access=True,
|
|||
|
|
)
|
|||
|
|
assert f"Deuxième projet : {Path(second['path'])}" in context
|
|||
|
|
assert "aucune frontière technique" in context
|
|||
|
|
assert "demande explicitement" in context
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_group_context_exposes_other_code_read_only_and_shared_updates(logged_in):
|
|||
|
|
client, app, _, _, headers = logged_in
|
|||
|
|
sender = register_project(logged_in, "NASflix")
|
|||
|
|
receiver = register_project(logged_in, "TvReWork")
|
|||
|
|
created = client.post(
|
|||
|
|
"/api/project-groups",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"name": "TV", "description": "Applications connectées à la TV", "project_ids": [sender["id"], receiver["id"]]},
|
|||
|
|
)
|
|||
|
|
assert created.status_code == 201
|
|||
|
|
conversation = client.post(
|
|||
|
|
f"/api/projects/{sender['id']}/conversations",
|
|||
|
|
headers=headers,
|
|||
|
|
json={"title": "Contrat partagé"},
|
|||
|
|
).json()
|
|||
|
|
publish_project_group_update(
|
|||
|
|
app.state.db.path,
|
|||
|
|
sender["id"],
|
|||
|
|
conversation["id"],
|
|||
|
|
"Le contrat HDMI accepte désormais la commande play.",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
context = project_group_context(app.state.db.path, receiver["id"])
|
|||
|
|
|
|||
|
|
assert "[GROUPE DE PROJETS : TV]" in context
|
|||
|
|
assert f"NASflix : {Path(sender['path'])}" in context
|
|||
|
|
assert "lecture seule" in context
|
|||
|
|
assert "N’écris jamais" in context
|
|||
|
|
assert "Le contrat HDMI accepte désormais la commande play" in context
|
|||
|
|
assert receiver["path"] not in context
|
|||
|
|
|
|||
|
|
untrusted_context = project_group_context(
|
|||
|
|
app.state.db.path,
|
|||
|
|
receiver["id"],
|
|||
|
|
engine="claude",
|
|||
|
|
)
|
|||
|
|
assert str(Path(sender["path"])) not in untrusted_context
|
|||
|
|
assert "uniquement leurs comptes rendus" in untrusted_context
|
|||
|
|
assert "Le contrat HDMI accepte désormais la commande play" in untrusted_context
|
|||
|
|
|
|||
|
|
full_access_context = project_group_context(
|
|||
|
|
app.state.db.path,
|
|||
|
|
receiver["id"],
|
|||
|
|
engine="claude",
|
|||
|
|
full_access=True,
|
|||
|
|
)
|
|||
|
|
assert f"NASflix : {Path(sender['path'])}" in full_access_context
|
|||
|
|
assert "aucune frontière technique" in full_access_context
|