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
|