Path: blob/master/src/python/cocalc-api/tests/test_hub.py
5600 views
"""1Tests for Hub client functionality.2"""3import time4import pytest56from cocalc_api import Hub, Project7from .conftest import assert_valid_uuid, cleanup_project, create_tracked_project, create_tracked_user, create_tracked_org8910class TestHubSystem:11"""Tests for Hub system operations."""1213def test_ping(self, hub):14"""Test basic ping connectivity with retry logic."""15# Retry with exponential backoff in case server is still starting up16max_attempts = 517delay = 2 # Start with 2 second delay1819for attempt in range(max_attempts):20try:21result = hub.system.ping()22assert result is not None23# The ping response should contain some basic server info24assert isinstance(result, dict)25print(f"✓ Server ping successful on attempt {attempt + 1}")26return # Success!27except Exception as e:28if attempt < max_attempts - 1:29print(f"Ping attempt {attempt + 1} failed, retrying in {delay}s... ({e})")30time.sleep(delay)31delay *= 2 # Exponential backoff32else:33pytest.fail(f"Server ping failed after {max_attempts} attempts: {e}")3435def test_hub_initialization(self, api_key, cocalc_host):36"""Test Hub client initialization."""37hub = Hub(api_key=api_key, host=cocalc_host)38assert hub.api_key == api_key39assert hub.host == cocalc_host40assert hub.client is not None4142def test_invalid_api_key(self, cocalc_host):43"""Test behavior with invalid API key."""44hub = Hub(api_key="invalid_key", host=cocalc_host)45with pytest.raises((ValueError, RuntimeError, Exception)): # Should raise authentication error46hub.system.ping()4748def test_multiple_pings(self, hub):49"""Test that multiple ping calls work consistently."""50for _i in range(3):51result = hub.system.ping()52assert result is not None53assert isinstance(result, dict)5455def test_user_search(self, hub, resource_tracker):56"""Test user search functionality."""57import time58timestamp = int(time.time())5960# Create a test organization and user with a unique email61org_name = f"search-test-org-{timestamp}"62test_email = f"search-test-user-{timestamp}@test.local"63test_first_name = f"SearchFirst{timestamp}"64test_last_name = f"SearchLast{timestamp}"6566# Use tracked creation67org_id = create_tracked_org(hub, resource_tracker, org_name)68print(f"\nCreated test organization: {org_name} (ID: {org_id})")6970# Create a user with unique identifiable names71user_id = create_tracked_user(hub, resource_tracker, org_name, email=test_email, firstName=test_first_name, lastName=test_last_name)72print(f"Created test user: {user_id}, email: {test_email}")7374# Give the database a moment to index the new user75time.sleep(0.5)7677# Test 1: Search by email (exact match should return only this user)78print("\n1. Testing search by email...")79results = hub.system.user_search(test_email)80assert isinstance(results, list), "user_search should return a list"81assert len(results) >= 1, f"Expected at least 1 result for email {test_email}, got {len(results)}"8283# Find our user in the results84our_user = None85for user in results:86if user.get('email_address') == test_email:87our_user = user88break8990assert our_user is not None, f"Expected to find user with email {test_email} in results"91print(f" Found user by email: {our_user['account_id']}")9293# Verify the structure of the result94assert 'account_id' in our_user95assert 'first_name' in our_user96assert 'last_name' in our_user97assert our_user['first_name'] == test_first_name98assert our_user['last_name'] == test_last_name99assert our_user['account_id'] == user_id100print(f" User data: first_name={our_user['first_name']}, last_name={our_user['last_name']}")101102# Test 2: Search by full first name (to ensure we find our user)103print("\n2. Testing search by full first name...")104# Use the full first name which is guaranteed unique with timestamp105results = hub.system.user_search(test_first_name)106assert isinstance(results, list)107print(f" Search for '{test_first_name}' returned {len(results)} results")108# Our user should be in the results109found = any(u.get('account_id') == user_id for u in results)110if not found and len(results) > 0:111print(f" Found these first names: {[u.get('first_name') for u in results]}")112assert found, f"Expected to find user {user_id} when searching for '{test_first_name}'"113print(f" Found user in {len(results)} results")114115# Test 3: Search by full last name (to ensure we find our user)116print("\n3. Testing search by full last name...")117# Use the full last name which is guaranteed unique with timestamp118results = hub.system.user_search(test_last_name)119assert isinstance(results, list)120found = any(u.get('account_id') == user_id for u in results)121assert found, f"Expected to find user {user_id} when searching for '{test_last_name}'"122print(f" Found user in {len(results)} results")123124# Test 4: Nonexistent search should return empty list125print("\n4. Testing search with unlikely query...")126unlikely_query = f"xyznonexistent{timestamp}abc"127results = hub.system.user_search(unlikely_query)128assert isinstance(results, list)129assert len(results) == 0, f"Expected 0 results for non-existent query, got {len(results)}"130print(" Search for non-existent query correctly returned 0 results")131132print("\n✅ User search test completed successfully!")133134# Note: No cleanup needed - happens automatically via cleanup_all_test_resources135136137class TestHubProjects:138"""Tests for Hub project operations."""139140def test_create_project(self, hub, resource_tracker):141"""Test creating a project via hub.projects.create_project."""142import time143timestamp = int(time.time())144title = f"test-project-{timestamp}"145description = "Test project for API testing"146147project_id = create_tracked_project(hub, resource_tracker, title=title, description=description)148149assert project_id is not None150assert_valid_uuid(project_id, "Project ID")151152# Note: No cleanup needed - happens automatically153154def test_list_projects(self, hub):155"""Test listing projects."""156projects = hub.projects.get()157assert isinstance(projects, list)158# Each project should have basic fields159for project in projects:160assert 'project_id' in project161assert isinstance(project['project_id'], str)162163def test_delete_method_exists(self, hub):164"""Test that delete method is available and callable."""165# Test that the delete method exists and is callable166assert hasattr(hub.projects, 'delete')167assert callable(hub.projects.delete)168169# Note: We don't actually delete anything in this test since170# deletion is tested in the project lifecycle via temporary_project fixture171172def test_project_state_and_status(self, hub, temporary_project, project_client):173"""Test retrieving state and status information for a project."""174project_id = temporary_project["project_id"]175176# Ensure project is responsive before checking its status177project_client.system.ping()178179state_info = hub.projects.state(project_id)180assert isinstance(state_info, dict)181state = state_info.get("state")182assert isinstance(state, str)183assert state, "Expected a non-empty state string"184185status_info = hub.projects.status(project_id)186assert isinstance(status_info, dict)187informative_keys = ("project", "start_ts", "version", "disk_MB", "memory")188assert any(key in status_info for key in informative_keys), "Status response should include resource information"189190project_status = status_info.get("project")191if isinstance(project_status, dict):192pid = project_status.get("pid")193if pid is not None:194assert isinstance(pid, int)195assert pid > 0196197if "disk_MB" in status_info:198disk_usage = status_info["disk_MB"]199assert isinstance(disk_usage, (int, float))200201memory_info = status_info.get("memory")202if memory_info is not None:203assert isinstance(memory_info, dict)204205def test_project_lifecycle(self, hub, resource_tracker):206"""Test complete project lifecycle: create, wait for ready, run command, delete, verify deletion."""207208# 1. Create a project209timestamp = int(time.time())210title = f"lifecycle-test-{timestamp}"211description = "Test project for complete lifecycle testing"212213print(f"\n1. Creating project '{title}'...")214project_id = create_tracked_project(hub, resource_tracker, title=title, description=description)215assert project_id is not None216assert_valid_uuid(project_id, "Project ID")217print(f" Created project: {project_id}")218219# Start the project220print("2. Starting project...")221hub.projects.start(project_id)222print(" Project start request sent")223224# Wait for project to become ready225print("3. Waiting for project to become ready...")226project_client = Project(project_id=project_id, api_key=hub.api_key, host=hub.host)227228ready = False229for attempt in range(12): # 60 seconds max wait time230time.sleep(5)231try:232project_client.system.ping()233ready = True234print(f" ✓ Project ready after {(attempt + 1) * 5} seconds")235break236except Exception as e:237if attempt == 11: # Last attempt238print(f" Warning: Project not ready after 60 seconds: {e}")239else:240print(f" Attempt {attempt + 1}: Project not ready yet...")241242# Check that project exists in database243print("4. Checking project exists in database...")244projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id)245assert len(projects) == 1, f"Expected 1 project, found {len(projects)}"246project = projects[0]247assert project["project_id"] == project_id248assert project["title"] == title249assert project.get("deleted") is None or project.get("deleted") is False250print(f" ✓ Project found in database: title='{project['title']}', deleted={project.get('deleted')}")251252# 2. Run a command if project is ready253if ready:254print("5. Running 'uname -a' command...")255result = project_client.system.exec("uname -a")256assert "stdout" in result257output = result["stdout"]258assert "Linux" in output, f"Expected Linux system, got: {output}"259assert result["exit_code"] == 0, f"Command failed with exit code {result['exit_code']}"260print(f" ✓ Command executed successfully: {output.strip()}")261else:262print("5. Skipping command execution - project not ready")263264# 3. Stop and delete the project265print("6. Stopping and deleting project...")266cleanup_project(hub, project_id)267268# 4. Verify project is marked as deleted in database269print("8. Verifying project is marked as deleted...")270projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id, all=True)271assert len(projects) == 1, f"Expected 1 project (still in DB), found {len(projects)}"272project = projects[0]273assert project["project_id"] == project_id274assert project.get("deleted") is True, f"Expected deleted=True, got deleted={project.get('deleted')}"275print(f" ✓ Project correctly marked as deleted in database: deleted={project.get('deleted')}")276277print("✅ Project lifecycle test completed successfully!")278279# Note: No cleanup needed - hard-delete happens automatically at session end280281def test_collaborator_management(self, hub, resource_tracker):282"""Test adding and removing collaborators from a project."""283import time284timestamp = int(time.time())285286# 1. Site admin creates two users287print("\n1. Creating two test users...")288user1_email = f"collab-user1-{timestamp}@test.local"289user2_email = f"collab-user2-{timestamp}@test.local"290291# Create a temporary organization for the users292org_name = f"collab-test-org-{timestamp}"293org_id = create_tracked_org(hub, resource_tracker, org_name)294print(f" Created organization: {org_name} (ID: {org_id})")295296user1_id = create_tracked_user(hub, resource_tracker, org_name, email=user1_email, firstName="CollabUser", lastName="One")297print(f" Created user1: {user1_id}")298299user2_id = create_tracked_user(hub, resource_tracker, org_name, email=user2_email, firstName="CollabUser", lastName="Two")300print(f" Created user2: {user2_id}")301302# 2. Create a project for the first user303print("\n2. Creating project for user1...")304project_title = f"collab-test-project-{timestamp}"305project_id = create_tracked_project(hub, resource_tracker, title=project_title)306print(f" Created project: {project_id}")307308# 3. Check initial collaborators309print("\n3. Checking initial collaborators...")310projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)311assert len(projects) == 1312initial_users = projects[0].get('users', {})313print(f" Initial collaborators: {list(initial_users.keys())}")314print(f" Number of initial collaborators: {len(initial_users)}")315316# Report on ownership structure317for user_id, perms in initial_users.items():318print(f" User {user_id}: {perms}")319320# 4. Add user1 as collaborator321print(f"\n4. Adding user1 ({user1_id}) as collaborator...")322result = hub.projects.add_collaborator(project_id=project_id, account_id=user1_id)323print(f" Add collaborator result: {result}")324325# Check collaborators after adding user1326projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)327users_after_user1 = projects[0].get('users', {})328print(f" Collaborators after adding user1: {list(users_after_user1.keys())}")329print(f" Number of collaborators: {len(users_after_user1)}")330for user_id, perms in users_after_user1.items():331print(f" User {user_id}: {perms}")332333# 5. Add user2 as collaborator334print(f"\n5. Adding user2 ({user2_id}) as collaborator...")335result = hub.projects.add_collaborator(project_id=project_id, account_id=user2_id)336print(f" Add collaborator result: {result}")337338# Check collaborators after adding user2339projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)340users_after_user2 = projects[0].get('users', {})341print(f" Collaborators after adding user2: {list(users_after_user2.keys())}")342print(f" Number of collaborators: {len(users_after_user2)}")343# Note: There will be 3 users total: the site admin (owner) + user1 + user2344for user_id, perms in users_after_user2.items():345print(f" User {user_id}: {perms}")346347# Verify user1 and user2 are present348assert user1_id in users_after_user2, f"Expected user1 ({user1_id}) to be a collaborator"349assert user2_id in users_after_user2, f"Expected user2 ({user2_id}) to be a collaborator"350351# Identify the owner (should be the site admin who created the project)352owner_id = None353for uid, perms in users_after_user2.items():354if perms.get('group') == 'owner':355owner_id = uid356break357print(f" Project owner: {owner_id}")358359# 6. Remove user1360print(f"\n6. Removing user1 ({user1_id}) from project...")361result = hub.projects.remove_collaborator(project_id=project_id, account_id=user1_id)362print(f" Remove collaborator result: {result}")363364# Check collaborators after removing user1365projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)366users_after_removal = projects[0].get('users', {})367print(f" Collaborators after removing user1: {list(users_after_removal.keys())}")368print(f" Number of collaborators: {len(users_after_removal)}")369# Should have 2 users: owner + user2370assert len(users_after_removal) == 2, f"Expected 2 collaborators (owner + user2), found {len(users_after_removal)}"371assert user2_id in users_after_removal, f"Expected user2 ({user2_id}) to still be a collaborator"372assert user1_id not in users_after_removal, f"Expected user1 ({user1_id}) to be removed"373assert owner_id in users_after_removal, f"Expected owner ({owner_id}) to remain"374for user_id, perms in users_after_removal.items():375print(f" User {user_id}: {perms}")376377print("\n✅ Collaborator management test completed successfully!")378379# Note: No cleanup needed - hard-delete happens automatically at session end380381def test_stop_project(self, hub, temporary_project):382"""Test stopping a running project."""383project_id = temporary_project["project_id"]384result = hub.projects.stop(project_id)385# Stop can return None or a dict, both are valid386assert result is None or isinstance(result, dict)387print(f"✓ Project stop request sent")388389def test_touch_project(self, hub, temporary_project):390"""Test touching a project to signal it's in use."""391project_id = temporary_project["project_id"]392result = hub.projects.touch(project_id)393# Touch can return None or a dict, both are valid394assert result is None or isinstance(result, dict)395print(f"✓ Project touched successfully")396397def test_get_names(self, hub, resource_tracker):398"""Test getting account names."""399import time400timestamp = int(time.time())401org_name = f"names-test-org-{timestamp}"402403# Create a test user first404user_id = create_tracked_user(hub, resource_tracker, org_name, email=f"names-test-{timestamp}@test.local")405406# Get the name(s) - returns a dict mapping user_id to display name407result = hub.system.get_names([user_id])408assert isinstance(result, dict)409# The result should have the user_id as a key410assert user_id in result or len(result) > 0411print(f"✓ Got names for user: {result}")412413def test_copy_path_between_projects(self, hub, temporary_project, resource_tracker, project_client):414"""Test copying paths between projects."""415import time416import uuid417timestamp = int(time.time())418419# Create a second project420project2_id = create_tracked_project(hub, resource_tracker, title=f"copy-target-{timestamp}")421project2_client = Project(project_id=project2_id, api_key=hub.api_key, host=hub.host)422423# Create a unique test string424test_string = str(uuid.uuid4())425src_filename = f"testfile-copy-{timestamp}.txt"426dst_filename = f"testfile-copied-{timestamp}.txt"427428# Create a test file in the first project429project_client.system.exec(f"echo '{test_string}' > {src_filename}")430431# Copy the file to the second project432result = hub.projects.copy_path_between_projects(src_project_id=temporary_project["project_id"],433src_path=src_filename,434target_project_id=project2_id,435target_path=dst_filename)436# copy_path_between_projects can return None or a dict437assert result is None or isinstance(result, dict)438print(f"✓ File copy request sent")439440# Verify the file was copied by reading it441verify_result = project2_client.system.exec(f"cat {dst_filename}")442assert verify_result["exit_code"] == 0443assert test_string in verify_result["stdout"]444print(f"✓ Verified copied file contains expected content")445446def test_sync_history(self, hub, temporary_project, project_client):447"""Test getting sync history of a file."""448import time449timestamp = int(time.time())450filename = f"history-test-{timestamp}.txt"451452# Create a test file453project_client.system.exec(f"echo 'initial' > {filename}")454455result = hub.sync.history(project_id=temporary_project["project_id"], path=filename)456# Result can be a list or a dict with patches and info457if isinstance(result, dict):458patches = result.get('patches', [])459assert isinstance(patches, list)460else:461assert isinstance(result, list)462print(f"✓ Got sync history")463464def test_db_query(self, hub):465"""Test database query for user info."""466result = hub.db.query({"accounts": {"first_name": None}})467assert isinstance(result, dict)468assert "accounts" in result469first_name = result["accounts"].get("first_name")470assert first_name is not None471print(f"✓ DB query successful, first_name: {first_name}")472473def test_messages_send(self, hub, resource_tracker):474"""Test sending a message."""475import time476timestamp = int(time.time())477org_name = f"msg-test-org-{timestamp}"478479# Create a test user to send message to480user_id = create_tracked_user(hub, resource_tracker, org_name, email=f"msg-test-{timestamp}@test.local")481482result = hub.messages.send(subject="Test Message", body="This is a test message", to_ids=[user_id])483assert isinstance(result, int)484assert result > 0485print(f"✓ Message sent with ID: {result}")486487def test_jupyter_kernels(self, hub, temporary_project):488"""Test getting available Jupyter kernels."""489result = hub.jupyter.kernels(project_id=temporary_project["project_id"])490assert isinstance(result, list)491# Should have at least python3492kernel_names = [k.get("name") for k in result]493assert "python3" in kernel_names or len(result) > 0494print(f"✓ Found {len(result)} Jupyter kernels: {kernel_names}")495496497