@@ -8,6 +8,7 @@ from devplacepy.content import can_manage_workspace, can_open_workspace
|
||||
from devplacepy.database import get_table, init_db, set_setting
|
||||
from devplacepy.services.containers import activity, api, store
|
||||
from devplacepy.services.containers.workspace import (
|
||||
editor,
|
||||
flags,
|
||||
naming,
|
||||
provision,
|
||||
@@ -28,7 +29,13 @@ def _workspace_db():
|
||||
init_db()
|
||||
set_setting("workspace_enabled", "1")
|
||||
yield
|
||||
for table in ("instances", "tunnels", "workspace_flags", "workspace_quota_rules"):
|
||||
for table in (
|
||||
"instances",
|
||||
"tunnels",
|
||||
"workspace_flags",
|
||||
"workspace_quota_rules",
|
||||
"workspace_editor_prefs",
|
||||
):
|
||||
get_table(table).delete()
|
||||
|
||||
|
||||
@@ -391,7 +398,8 @@ def test_editor_runs_with_password_auth_and_an_eight_char_secret():
|
||||
assert len(password) == api.EDITOR_PASSWORD_LENGTH == 8
|
||||
assert password.isalnum()
|
||||
|
||||
command = api.editor_command(store.get_instance(instance["uid"]))
|
||||
row = store.get_instance(instance["uid"])
|
||||
command = editor.argv(row, editor.resolve(OWNER, row))
|
||||
assert "--auth" in command
|
||||
assert command[command.index("--auth") + 1] == "password"
|
||||
assert "none" not in command
|
||||
@@ -806,3 +814,177 @@ def test_no_certificate_is_ordered_when_molohttp_is_unconfigured(monkeypatch):
|
||||
row = tunnels.list_for_instance(instance["uid"])[0]
|
||||
assert row["status"] == tunnels.STATUS_PENDING
|
||||
assert [r["uid"] for r in tunnels.awaiting_certificate()] == [row["uid"]]
|
||||
|
||||
|
||||
def _editor_workspace(owner: dict, title: str):
|
||||
project = _http_project(owner["uid"], title)
|
||||
instance = store.create_instance(
|
||||
{
|
||||
"project_uid": project["uid"],
|
||||
"name": "ws-editor-http",
|
||||
"status": "running",
|
||||
"desired_state": "running",
|
||||
"is_workspace": 1,
|
||||
"workspace_owner_uid": owner["uid"],
|
||||
"tunnel_name": naming.generate(),
|
||||
"ports_json": '[{"host": 20911, "container": 8080, "proto": "tcp"}]',
|
||||
}
|
||||
)
|
||||
return project, instance
|
||||
|
||||
|
||||
def _get(path: str, key: str):
|
||||
import requests
|
||||
|
||||
return requests.get(
|
||||
f"{BASE_URL}{path}",
|
||||
headers={"Accept": "application/json", "X-API-KEY": key},
|
||||
timeout=HTTP_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
def test_editor_profile_reads_back_over_http(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
project, _ = _editor_workspace(owner, "WS Editor Read")
|
||||
slug = project["slug"]
|
||||
assert _await_workspaces_enabled(slug, owner["api_key"])
|
||||
|
||||
body = _get(f"/projects/{slug}/workspace/editor", owner["api_key"]).json()
|
||||
assert body["editor"]["theme"] == editor.DEFAULTS["theme"]
|
||||
assert body["editor"]["sources"]["theme"] == editor.SOURCE_SITE
|
||||
assert body["editor"]["cpu_millicores"] == quota.resolve(owner["uid"]).cpu_millicores
|
||||
assert body["restart_required"] is False
|
||||
|
||||
|
||||
def test_editor_preferences_save_and_reset_over_http(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
project, _ = _editor_workspace(owner, "WS Editor Write")
|
||||
slug = project["slug"]
|
||||
assert _await_workspaces_enabled(slug, owner["api_key"])
|
||||
try:
|
||||
saved = _post(
|
||||
f"/projects/{slug}/workspace/editor",
|
||||
owner["api_key"],
|
||||
data={"theme": "devplace-light", "font_size": "21"},
|
||||
)
|
||||
assert saved.status_code == 200, saved.text
|
||||
profile = saved.json()["data"]["editor"]
|
||||
assert profile["theme"] == "devplace-light"
|
||||
assert profile["font_size"] == 21
|
||||
assert profile["sources"]["font_size"] == editor.SOURCE_USER
|
||||
|
||||
reset = _post(
|
||||
f"/projects/{slug}/workspace/editor",
|
||||
owner["api_key"],
|
||||
data={"reset": "true"},
|
||||
)
|
||||
assert reset.status_code == 200, reset.text
|
||||
after = reset.json()["data"]["editor"]
|
||||
assert after["font_size"] == editor.DEFAULTS["font_size"]
|
||||
assert after["sources"]["font_size"] == editor.SOURCE_SITE
|
||||
finally:
|
||||
get_table("workspace_editor_prefs").delete(owner_id=owner["uid"])
|
||||
|
||||
|
||||
def test_an_out_of_range_editor_value_is_refused_not_clamped(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
project, _ = _editor_workspace(owner, "WS Editor Range")
|
||||
slug = project["slug"]
|
||||
assert _await_workspaces_enabled(slug, owner["api_key"])
|
||||
|
||||
refused = _post(
|
||||
f"/projects/{slug}/workspace/editor",
|
||||
owner["api_key"],
|
||||
data={"font_size": "500"},
|
||||
)
|
||||
assert refused.status_code == 422, refused.text
|
||||
|
||||
|
||||
def test_editor_preferences_are_refused_to_a_non_owner(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
intruder = _fresh_member()
|
||||
project, _ = _editor_workspace(owner, "WS Editor Foreign")
|
||||
slug = project["slug"]
|
||||
|
||||
denied = _get(f"/projects/{slug}/workspace/editor", intruder["api_key"])
|
||||
assert denied.status_code in (403, 404), denied.text
|
||||
|
||||
|
||||
def test_editor_preferences_reject_a_guest_with_401_not_422(app_server, seeded_db):
|
||||
import requests
|
||||
|
||||
owner = _seeded_user("bob_test")
|
||||
project, _ = _editor_workspace(owner, "WS Editor Guest")
|
||||
guest = requests.post(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace/editor",
|
||||
headers={"Accept": "application/json"},
|
||||
data={},
|
||||
timeout=HTTP_TIMEOUT,
|
||||
)
|
||||
assert guest.status_code == 401, guest.status_code
|
||||
|
||||
|
||||
def test_a_saved_preference_asks_for_a_restart_while_running(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
project, instance = _editor_workspace(owner, "WS Editor Restart")
|
||||
slug = project["slug"]
|
||||
assert _await_workspaces_enabled(slug, owner["api_key"])
|
||||
try:
|
||||
editor.seed_state(instance, editor.resolve(owner["uid"], instance))
|
||||
body = _post(
|
||||
f"/projects/{slug}/workspace/editor",
|
||||
owner["api_key"],
|
||||
data={"font_size": "29"},
|
||||
).json()
|
||||
assert body["data"]["restart_required"] is True
|
||||
finally:
|
||||
get_table("workspace_editor_prefs").delete(owner_id=owner["uid"])
|
||||
|
||||
|
||||
def test_the_run_spec_applies_the_quota_cpu_and_memory_to_a_workspace():
|
||||
from devplacepy.services.containers import api
|
||||
|
||||
instance = _instance(editor_port=api.EDITOR_DEFAULT_PORT)
|
||||
limits = quota.resolve(OWNER, store.get_instance(instance["uid"]))
|
||||
spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest")
|
||||
assert spec.cpu_limit == limits.cpu_limit()
|
||||
assert spec.mem_limit == limits.mem_limit()
|
||||
assert spec.command[0] == "code-server"
|
||||
assert "--app-name" in spec.command
|
||||
assert "--disable-workspace-trust" in spec.command
|
||||
|
||||
|
||||
def test_the_run_spec_seeds_the_editor_state_and_stamps_a_boot_marker(monkeypatch, tmp_path):
|
||||
from devplacepy import config
|
||||
from devplacepy.services.containers import api
|
||||
|
||||
monkeypatch.setattr(config, "WORKSPACE_STATE_DIR", tmp_path / "state")
|
||||
instance = _instance()
|
||||
spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest")
|
||||
|
||||
marker = store.get_instance(instance["uid"])["boot_marker"]
|
||||
assert marker
|
||||
assert spec.env["DEVPLACE_CONTAINER_BOOT"] == marker
|
||||
assert spec.env["DEVPLACE_EDITOR_APP_NAME"] == editor.APP_NAME
|
||||
seeded = tmp_path / "state" / instance["uid"] / "data" / "User" / "settings.json"
|
||||
assert seeded.exists()
|
||||
|
||||
|
||||
def test_a_non_workspace_instance_keeps_its_own_limits():
|
||||
from devplacepy.services.containers import api
|
||||
|
||||
instance = _instance(is_workspace=0, cpu_limit="0.5", mem_limit="256m")
|
||||
spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest")
|
||||
assert spec.cpu_limit == "0.5"
|
||||
assert spec.mem_limit == "256m"
|
||||
assert not spec.env.get("DEVPLACE_EDITOR_APP_NAME")
|
||||
|
||||
|
||||
def test_a_workspace_without_an_editor_port_still_gets_its_size():
|
||||
from devplacepy.services.containers import api
|
||||
|
||||
instance = _instance()
|
||||
limits = quota.resolve(OWNER, store.get_instance(instance["uid"]))
|
||||
spec = api.run_spec_for(store.get_instance(instance["uid"]), "ppy:latest")
|
||||
assert spec.cpu_limit == limits.cpu_limit()
|
||||
assert spec.command == ["sleep", "infinity"]
|
||||
|
||||
Reference in New Issue
Block a user