@@ -107,3 +107,59 @@ def test_http_ingress_proxy(app_server):
|
||||
httpd.shutdown()
|
||||
get_table("instances").delete(uid=uid)
|
||||
refresh_snapshot()
|
||||
|
||||
|
||||
def test_http_ingress_proxy_injects_a_base_into_the_root_document_only(app_server):
|
||||
import http.server
|
||||
import socket
|
||||
import socketserver
|
||||
import threading
|
||||
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
b"<!doctype html><html><head><title>t</title></head>"
|
||||
b"<body><script src='app.js'></script></body></html>"
|
||||
)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
httpd = socketserver.TCPServer(("127.0.0.1", port), Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
slug = f"base{port}"
|
||||
uid = f"basetest-{port}"
|
||||
get_table("instances").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"name": "ingress-base",
|
||||
"project_uid": "ingtest",
|
||||
"status": "running",
|
||||
"ingress_slug": slug,
|
||||
"ingress_port": 8000,
|
||||
"ports_json": f'[{{"host": {port}, "container": 8000, "proto": "tcp"}}]',
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
)
|
||||
refresh_snapshot()
|
||||
try:
|
||||
root = requests.get(f"{BASE_URL}/p/{slug}")
|
||||
assert root.status_code == 200, root.text
|
||||
assert f'<base href="/p/{slug}/">' in root.text
|
||||
nested = requests.get(f"{BASE_URL}/p/{slug}/static/webview/pre/index.html")
|
||||
assert nested.status_code == 200, nested.text
|
||||
assert "<base" not in nested.text
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
get_table("instances").delete(uid=uid)
|
||||
refresh_snapshot()
|
||||
|
||||
@@ -1082,3 +1082,76 @@ def test_republishing_a_live_tunnel_keeps_its_certificate():
|
||||
assert again["uid"] == row["uid"]
|
||||
assert again["status"] == tunnels.STATUS_ACTIVE
|
||||
assert again["last_error"] == ""
|
||||
|
||||
|
||||
def _editor_listener():
|
||||
import socket
|
||||
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.listen(8)
|
||||
return sock
|
||||
|
||||
|
||||
def _workspace_json(slug: str, key: str) -> dict:
|
||||
import requests
|
||||
|
||||
return requests.get(
|
||||
f"{BASE_URL}/projects/{slug}/workspace",
|
||||
headers={"Accept": "application/json", "X-API-KEY": key},
|
||||
timeout=HTTP_TIMEOUT,
|
||||
).json()
|
||||
|
||||
|
||||
def test_workspace_json_reports_the_phase_from_a_live_editor_probe(app_server, seeded_db):
|
||||
owner = _seeded_user("bob_test")
|
||||
project = _http_project(owner["uid"], "WS Phase Http")
|
||||
sock = _editor_listener()
|
||||
port = sock.getsockname()[1]
|
||||
try:
|
||||
_instance(
|
||||
project_uid=project["uid"],
|
||||
workspace_owner_uid=owner["uid"],
|
||||
editor_port=editor.EDITOR_DEFAULT_PORT,
|
||||
ports_json=(
|
||||
f'[{{"host": {port}, "container": {editor.EDITOR_DEFAULT_PORT}, '
|
||||
'"proto": "tcp"}]'
|
||||
),
|
||||
)
|
||||
body = _workspace_json(project["slug"], owner["api_key"])
|
||||
assert body["workspace"]["phase"] == provision.PHASE_READY
|
||||
assert body["workspace"]["phase_label"] == "Ready"
|
||||
assert body["workspace"]["editor_ready"] is True
|
||||
assert body["workspace"]["owner_uid"] == owner["uid"]
|
||||
assert body["editor_url"].endswith("/code/")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
body = _workspace_json(project["slug"], owner["api_key"])
|
||||
assert body["workspace"]["phase"] == provision.PHASE_STARTING
|
||||
assert body["workspace"]["phase_label"] == "Starting"
|
||||
assert body["workspace"]["editor_ready"] is False
|
||||
assert body["workspace"]["status"] == "running"
|
||||
|
||||
|
||||
def test_editor_proxy_refuses_a_stopped_workspace_as_plain_text(app_server, seeded_db):
|
||||
import requests
|
||||
|
||||
owner = _seeded_user("bob_test")
|
||||
project = _http_project(owner["uid"], "WS Proxy Stopped")
|
||||
instance = _instance(
|
||||
project_uid=project["uid"],
|
||||
workspace_owner_uid=owner["uid"],
|
||||
status="stopped",
|
||||
desired_state="stopped",
|
||||
)
|
||||
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
|
||||
headers={"X-API-KEY": owner["api_key"]},
|
||||
timeout=HTTP_TIMEOUT,
|
||||
)
|
||||
|
||||
assert response.status_code == 409, response.text[:400]
|
||||
assert response.headers["content-type"].startswith("text/plain")
|
||||
assert "not running" in response.text
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -14,6 +15,9 @@ from devplacepy.services.containers.workspace import flags, naming
|
||||
from devplacepy.utils import make_combined_slug
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
EDITOR_CONTAINER_PORT = 8443
|
||||
TRANSITION_TIMEOUT_MS = 8000
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _workspaces_on():
|
||||
@@ -62,7 +66,20 @@ def _project_for(owner_uid: str, title: str = "WS Project") -> dict:
|
||||
return row
|
||||
|
||||
|
||||
def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
|
||||
@pytest.fixture(scope="module")
|
||||
def editor_listener():
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.listen(16)
|
||||
try:
|
||||
yield sock.getsockname()[1]
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def _workspace_for(
|
||||
project: dict, owner_uid: str, editor_port: int = 0, **overrides
|
||||
) -> dict:
|
||||
payload = {
|
||||
"project_uid": project["uid"],
|
||||
"name": "ws-e2e",
|
||||
@@ -73,10 +90,20 @@ def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
|
||||
"tunnel_name": naming.generate(),
|
||||
"ports_json": '[{"host": 20777, "container": 8080, "proto": "tcp"}]',
|
||||
}
|
||||
if editor_port:
|
||||
payload["editor_port"] = EDITOR_CONTAINER_PORT
|
||||
payload["ports_json"] = (
|
||||
f'[{{"host": {editor_port}, "container": {EDITOR_CONTAINER_PORT}, '
|
||||
'"proto": "tcp"}]'
|
||||
)
|
||||
payload.update(overrides)
|
||||
return store.create_instance(payload)
|
||||
|
||||
|
||||
def _phase_view(page, phase: str):
|
||||
return page.locator(f".workspace-phase-view[data-phase-view~='{phase}']")
|
||||
|
||||
|
||||
def test_project_page_offers_the_workspace_entry_point_to_the_owner(alice):
|
||||
page, user = alice
|
||||
project = _project_for(_row_for(user)["uid"], "WS Entry")
|
||||
@@ -104,11 +131,13 @@ def test_project_page_hides_the_workspace_entry_point_from_a_non_owner(bob):
|
||||
assert not page.locator(".context-menu-item:has-text('Workspace')").count()
|
||||
|
||||
|
||||
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(alice):
|
||||
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(
|
||||
alice, editor_listener
|
||||
):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Direct")
|
||||
instance = _workspace_for(project, row["uid"])
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
@@ -383,11 +412,11 @@ def test_resetting_editor_preferences_restores_the_site_default(alice):
|
||||
get_table("workspace_editor_prefs").delete(owner_id=row["uid"])
|
||||
|
||||
|
||||
def test_the_editor_launch_control_carries_the_window_profile(alice):
|
||||
def test_the_editor_launch_control_carries_the_window_profile(alice, editor_listener):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Editor Launch")
|
||||
instance = _workspace_for(project, row["uid"])
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
@@ -415,3 +444,88 @@ def test_the_workspace_help_states_that_every_folder_is_trusted(alice):
|
||||
help_card.wait_for(state="visible")
|
||||
expect(help_card).to_contain_text("Restricted Mode")
|
||||
expect(help_card).to_contain_text("dpc")
|
||||
|
||||
|
||||
def test_direct_vscode_button_is_absent_while_the_editor_is_unreachable(alice):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Booting")
|
||||
_workspace_for(project, row["uid"])
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
page.locator(".project-detail-actions").wait_for(state="visible")
|
||||
assert not page.locator(".project-detail-actions a[data-editor-open]").count()
|
||||
|
||||
|
||||
def test_a_running_workspace_whose_editor_is_not_listening_reads_starting(alice):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Not Listening")
|
||||
_workspace_for(project, row["uid"])
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
|
||||
expect(_phase_view(page, "starting")).to_be_visible()
|
||||
expect(_phase_view(page, "starting").locator(".btn.is-loading")).to_be_disabled()
|
||||
expect(_phase_view(page, "ready")).to_be_hidden()
|
||||
assert not page.locator(".workspace-actions a[data-editor-open]").is_visible()
|
||||
|
||||
|
||||
def test_start_shows_progress_and_flips_to_open_editor_when_the_editor_answers(
|
||||
alice, editor_listener
|
||||
):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Start Flow")
|
||||
instance = _workspace_for(
|
||||
project,
|
||||
row["uid"],
|
||||
editor_port=editor_listener,
|
||||
status="stopped",
|
||||
desired_state="stopped",
|
||||
)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
|
||||
start = _phase_view(page, "stopped").locator("button[type='submit']")
|
||||
expect(start).to_be_visible()
|
||||
start.click()
|
||||
expect(_phase_view(page, "starting")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(_phase_view(page, "stopped")).to_be_hidden()
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
|
||||
assert store.get_instance(instance["uid"])["desired_state"] == "running"
|
||||
|
||||
store.update_instance(instance["uid"], {"status": "running"})
|
||||
launch = page.locator(".workspace-actions a[data-editor-open]")
|
||||
launch.wait_for(state="visible", timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
|
||||
expect(_phase_view(page, "starting")).to_be_hidden()
|
||||
expect(launch).to_have_attribute(
|
||||
"href",
|
||||
f"/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
|
||||
)
|
||||
|
||||
|
||||
def test_stop_shows_progress_until_the_container_is_down(alice, editor_listener):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Stop Flow")
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
|
||||
_phase_view(page, "ready").locator("form[data-workspace-action] button").click()
|
||||
expect(_phase_view(page, "stopping")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopping")
|
||||
assert store.get_instance(instance["uid"])["desired_state"] == "stopped"
|
||||
|
||||
store.update_instance(instance["uid"], {"status": "stopped"})
|
||||
expect(_phase_view(page, "stopped")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.containers import forward
|
||||
|
||||
PREFIX = "/projects/demo/containers/instances/abc/code"
|
||||
|
||||
|
||||
def test_the_root_document_is_the_empty_or_slash_path():
|
||||
assert forward.is_root_document("") is True
|
||||
assert forward.is_root_document("/") is True
|
||||
assert forward.is_root_document("//") is True
|
||||
|
||||
|
||||
def test_a_nested_path_is_never_the_root_document():
|
||||
for path in (
|
||||
"index.html",
|
||||
"login",
|
||||
"stable-abc/static/out/vs/workbench/contrib/webview/browser/pre/index.html",
|
||||
"nested/page.html",
|
||||
"/nested/",
|
||||
):
|
||||
assert forward.is_root_document(path) is False, path
|
||||
|
||||
|
||||
def test_inject_base_adds_a_base_after_head():
|
||||
body = b"<!doctype html><html><head><meta charset=utf-8></head><body>x</body></html>"
|
||||
injected = forward.inject_base(body, PREFIX)
|
||||
assert injected.startswith(b"<!doctype html><html><head>" + f'<base href="{PREFIX}/">'.encode())
|
||||
|
||||
|
||||
def test_inject_base_leaves_an_existing_base_alone():
|
||||
body = b'<html><head><base href="./"></head><body></body></html>'
|
||||
assert forward.inject_base(body, PREFIX) == body
|
||||
|
||||
|
||||
def test_inject_base_prepends_when_there_is_no_head_or_html():
|
||||
body = b"<p>fragment</p>"
|
||||
assert forward.inject_base(body, PREFIX) == f'<base href="{PREFIX}/">'.encode() + body
|
||||
|
||||
|
||||
def test_rewrite_location_prefixes_only_absolute_paths():
|
||||
assert forward.rewrite_location("/login", PREFIX) == f"{PREFIX}/login"
|
||||
assert forward.rewrite_location("./login", PREFIX) == "./login"
|
||||
assert forward.rewrite_location("//cdn.example/x", PREFIX) == "//cdn.example/x"
|
||||
assert forward.rewrite_location("/login", "") == "/login"
|
||||
@@ -0,0 +1,178 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import itertools
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from devplacepy.database import get_table, init_db
|
||||
from devplacepy.services.containers import store
|
||||
from devplacepy.services.containers.workspace import provision
|
||||
|
||||
OWNER = "provision-owner"
|
||||
STATUSES = (
|
||||
store.ST_CREATED,
|
||||
store.ST_STARTING,
|
||||
store.ST_RUNNING,
|
||||
store.ST_STOPPED,
|
||||
store.ST_PAUSED,
|
||||
store.ST_CRASHED,
|
||||
store.ST_RESTARTING,
|
||||
store.ST_REMOVING,
|
||||
store.ST_REMOVED,
|
||||
"",
|
||||
)
|
||||
DESIRED = (store.DESIRED_RUNNING, store.DESIRED_STOPPED, store.DESIRED_PAUSED, "")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _provision_db():
|
||||
init_db()
|
||||
yield
|
||||
get_table("instances").delete(workspace_owner_uid=OWNER)
|
||||
|
||||
|
||||
def _instance(**overrides) -> dict:
|
||||
row = {
|
||||
"uid": "ws-phase",
|
||||
"status": store.ST_RUNNING,
|
||||
"desired_state": store.DESIRED_RUNNING,
|
||||
"suspended_at": "",
|
||||
"editor_port": 8443,
|
||||
"ports_json": "[]",
|
||||
"container_ip": "",
|
||||
"workspace_owner_uid": OWNER,
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def listener():
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.listen(4)
|
||||
try:
|
||||
yield sock
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status, desired, ready, expected",
|
||||
[
|
||||
(store.ST_RUNNING, store.DESIRED_RUNNING, True, provision.PHASE_READY),
|
||||
(store.ST_RUNNING, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
||||
(store.ST_CREATED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
||||
(store.ST_STOPPED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
||||
(store.ST_CRASHED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
||||
(store.ST_RUNNING, store.DESIRED_STOPPED, True, provision.PHASE_STOPPING),
|
||||
(store.ST_PAUSED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
|
||||
(store.ST_RESTARTING, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
|
||||
(store.ST_CRASHED, store.DESIRED_STOPPED, False, provision.PHASE_CRASHED),
|
||||
(store.ST_STOPPED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
|
||||
(store.ST_CREATED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
|
||||
("", "", False, provision.PHASE_STOPPED),
|
||||
],
|
||||
)
|
||||
def test_phase_follows_desired_state_status_and_the_editor_probe(
|
||||
status, desired, ready, expected
|
||||
):
|
||||
row = _instance(status=status, desired_state=desired)
|
||||
assert provision.phase(row, ready) == expected
|
||||
|
||||
|
||||
def test_a_suspended_workspace_is_suspended_whatever_else_it_says():
|
||||
for status, desired, ready in itertools.product(STATUSES, DESIRED, (True, False)):
|
||||
row = _instance(
|
||||
status=status, desired_state=desired, suspended_at="2026-01-01T00:00:00"
|
||||
)
|
||||
assert provision.phase(row, ready) == provision.PHASE_SUSPENDED
|
||||
|
||||
|
||||
def test_ready_needs_a_running_container_that_wants_to_run():
|
||||
for status, desired in itertools.product(STATUSES, DESIRED):
|
||||
row = _instance(status=status, desired_state=desired)
|
||||
is_ready = provision.phase(row, True) == provision.PHASE_READY
|
||||
assert is_ready == (
|
||||
status == store.ST_RUNNING and desired == store.DESIRED_RUNNING
|
||||
)
|
||||
|
||||
|
||||
def test_every_phase_over_the_whole_input_domain_has_a_label():
|
||||
for status, desired, ready, suspended in itertools.product(
|
||||
STATUSES, DESIRED, (True, False), ("", "2026-01-01T00:00:00")
|
||||
):
|
||||
row = _instance(status=status, desired_state=desired, suspended_at=suspended)
|
||||
assert provision.phase(row, ready) in provision.PHASE_LABELS
|
||||
|
||||
|
||||
def test_transitional_phases_are_exactly_starting_and_stopping():
|
||||
assert set(provision.TRANSITIONAL_PHASES) == {
|
||||
provision.PHASE_STARTING,
|
||||
provision.PHASE_STOPPING,
|
||||
}
|
||||
|
||||
|
||||
def test_editor_ready_when_the_editor_port_accepts_connections(listener):
|
||||
port = listener.getsockname()[1]
|
||||
row = _instance(container_ip="127.0.0.1", editor_port=port)
|
||||
assert provision.editor_ready(row) is True
|
||||
|
||||
|
||||
def test_editor_ready_is_false_when_nothing_listens():
|
||||
probe = socket.socket()
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
port = probe.getsockname()[1]
|
||||
probe.close()
|
||||
row = _instance(container_ip="127.0.0.1", editor_port=port)
|
||||
assert provision.editor_ready(row) is False
|
||||
|
||||
|
||||
def test_editor_ready_is_false_without_a_reachable_target(listener):
|
||||
row = _instance(container_ip="", ports_json="[]")
|
||||
assert provision.editor_ready(row) is False
|
||||
|
||||
|
||||
def test_editor_ready_is_false_unless_the_container_runs(listener):
|
||||
port = listener.getsockname()[1]
|
||||
for status in STATUSES:
|
||||
if status == store.ST_RUNNING:
|
||||
continue
|
||||
row = _instance(container_ip="127.0.0.1", editor_port=port, status=status)
|
||||
assert provision.editor_ready(row) is False
|
||||
|
||||
|
||||
def test_editor_ready_is_false_while_suspended(listener):
|
||||
port = listener.getsockname()[1]
|
||||
row = _instance(
|
||||
container_ip="127.0.0.1", editor_port=port, suspended_at="2026-01-01T00:00:00"
|
||||
)
|
||||
assert provision.editor_ready(row) is False
|
||||
|
||||
|
||||
def test_view_carries_the_phase_its_label_readiness_and_the_owner(listener):
|
||||
port = listener.getsockname()[1]
|
||||
instance = store.create_instance(
|
||||
{
|
||||
"project_uid": "provision-project",
|
||||
"name": "ws-view",
|
||||
"status": store.ST_RUNNING,
|
||||
"desired_state": store.DESIRED_RUNNING,
|
||||
"is_workspace": 1,
|
||||
"workspace_owner_uid": OWNER,
|
||||
"container_ip": "127.0.0.1",
|
||||
"editor_port": port,
|
||||
"ports_json": "[]",
|
||||
}
|
||||
)
|
||||
view = provision.view(instance)
|
||||
assert view["phase"] == provision.PHASE_READY
|
||||
assert view["phase_label"] == "Ready"
|
||||
assert view["editor_ready"] is True
|
||||
assert view["owner_uid"] == OWNER
|
||||
|
||||
store.update_instance(instance["uid"], {"desired_state": store.DESIRED_STOPPED})
|
||||
view = provision.view(store.get_instance(instance["uid"]))
|
||||
assert view["phase"] == provision.PHASE_STOPPING
|
||||
assert view["editor_ready"] is True
|
||||
@@ -0,0 +1,92 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import pytest
|
||||
|
||||
from devplacepy.database import get_table, init_db
|
||||
from devplacepy.services import live_view_relay
|
||||
from devplacepy.services.containers import store
|
||||
from devplacepy.services.containers.workspace import provision
|
||||
from tests.conftest import run_async
|
||||
|
||||
PROJECT_UID = "relay-ws-project"
|
||||
OWNER = "relay-ws-owner"
|
||||
OTHER = "relay-ws-other"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _relay_db():
|
||||
init_db()
|
||||
get_table("projects").insert(
|
||||
{
|
||||
"uid": PROJECT_UID,
|
||||
"user_uid": OWNER,
|
||||
"title": "Relay",
|
||||
"description": "",
|
||||
"slug": "relay-ws",
|
||||
"project_type": "software",
|
||||
"status": "In Development",
|
||||
"platforms": "",
|
||||
"is_private": 0,
|
||||
"read_only": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-01-01T00:00:00+00:00",
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
)
|
||||
yield
|
||||
get_table("instances").delete(project_uid=PROJECT_UID)
|
||||
get_table("projects").delete(uid=PROJECT_UID)
|
||||
|
||||
|
||||
def _handler(topic: str):
|
||||
for pattern, compute, interval in live_view_relay.VIEWS:
|
||||
match = pattern.match(topic)
|
||||
if match is not None:
|
||||
return compute, match, interval
|
||||
raise AssertionError(f"no live view for {topic}")
|
||||
|
||||
|
||||
def _workspace(**overrides) -> dict:
|
||||
row = {
|
||||
"project_uid": PROJECT_UID,
|
||||
"name": "ws-relay",
|
||||
"status": store.ST_RUNNING,
|
||||
"desired_state": store.DESIRED_RUNNING,
|
||||
"is_workspace": 1,
|
||||
"workspace_owner_uid": OWNER,
|
||||
"ports_json": "[]",
|
||||
}
|
||||
row.update(overrides)
|
||||
return store.create_instance(row)
|
||||
|
||||
|
||||
def test_the_workspace_topic_lives_in_the_owner_namespace():
|
||||
instance = _workspace()
|
||||
compute, match, interval = _handler(f"user.{OWNER}.workspace.{instance['uid']}")
|
||||
assert compute is live_view_relay._workspace_detail
|
||||
assert interval == 3.0
|
||||
payload = run_async(compute(match))
|
||||
assert payload["workspace"]["uid"] == instance["uid"]
|
||||
assert payload["workspace"]["phase"] == provision.PHASE_STARTING
|
||||
assert payload["workspace"]["editor_ready"] is False
|
||||
assert payload["editor_url"] == (
|
||||
f"/projects/relay-ws/containers/instances/{instance['uid']}/code/"
|
||||
)
|
||||
|
||||
|
||||
def test_the_workspace_topic_never_answers_another_owner():
|
||||
instance = _workspace()
|
||||
compute, match, _interval = _handler(f"user.{OTHER}.workspace.{instance['uid']}")
|
||||
assert run_async(compute(match)) is None
|
||||
|
||||
|
||||
def test_the_workspace_topic_ignores_a_plain_container():
|
||||
instance = _workspace(is_workspace=0)
|
||||
compute, match, _interval = _handler(f"user.{OWNER}.workspace.{instance['uid']}")
|
||||
assert run_async(compute(match)) is None
|
||||
|
||||
|
||||
def test_the_workspace_topic_ignores_an_unknown_instance():
|
||||
compute, match, _interval = _handler(f"user.{OWNER}.workspace.does-not-exist")
|
||||
assert run_async(compute(match)) is None
|
||||
Reference in New Issue
Block a user