Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/python/cocalc-api/tests/test_hub.py
5600 views
1
"""
2
Tests for Hub client functionality.
3
"""
4
import time
5
import pytest
6
7
from cocalc_api import Hub, Project
8
from .conftest import assert_valid_uuid, cleanup_project, create_tracked_project, create_tracked_user, create_tracked_org
9
10
11
class TestHubSystem:
12
"""Tests for Hub system operations."""
13
14
def test_ping(self, hub):
15
"""Test basic ping connectivity with retry logic."""
16
# Retry with exponential backoff in case server is still starting up
17
max_attempts = 5
18
delay = 2 # Start with 2 second delay
19
20
for attempt in range(max_attempts):
21
try:
22
result = hub.system.ping()
23
assert result is not None
24
# The ping response should contain some basic server info
25
assert isinstance(result, dict)
26
print(f"✓ Server ping successful on attempt {attempt + 1}")
27
return # Success!
28
except Exception as e:
29
if attempt < max_attempts - 1:
30
print(f"Ping attempt {attempt + 1} failed, retrying in {delay}s... ({e})")
31
time.sleep(delay)
32
delay *= 2 # Exponential backoff
33
else:
34
pytest.fail(f"Server ping failed after {max_attempts} attempts: {e}")
35
36
def test_hub_initialization(self, api_key, cocalc_host):
37
"""Test Hub client initialization."""
38
hub = Hub(api_key=api_key, host=cocalc_host)
39
assert hub.api_key == api_key
40
assert hub.host == cocalc_host
41
assert hub.client is not None
42
43
def test_invalid_api_key(self, cocalc_host):
44
"""Test behavior with invalid API key."""
45
hub = Hub(api_key="invalid_key", host=cocalc_host)
46
with pytest.raises((ValueError, RuntimeError, Exception)): # Should raise authentication error
47
hub.system.ping()
48
49
def test_multiple_pings(self, hub):
50
"""Test that multiple ping calls work consistently."""
51
for _i in range(3):
52
result = hub.system.ping()
53
assert result is not None
54
assert isinstance(result, dict)
55
56
def test_user_search(self, hub, resource_tracker):
57
"""Test user search functionality."""
58
import time
59
timestamp = int(time.time())
60
61
# Create a test organization and user with a unique email
62
org_name = f"search-test-org-{timestamp}"
63
test_email = f"search-test-user-{timestamp}@test.local"
64
test_first_name = f"SearchFirst{timestamp}"
65
test_last_name = f"SearchLast{timestamp}"
66
67
# Use tracked creation
68
org_id = create_tracked_org(hub, resource_tracker, org_name)
69
print(f"\nCreated test organization: {org_name} (ID: {org_id})")
70
71
# Create a user with unique identifiable names
72
user_id = create_tracked_user(hub, resource_tracker, org_name, email=test_email, firstName=test_first_name, lastName=test_last_name)
73
print(f"Created test user: {user_id}, email: {test_email}")
74
75
# Give the database a moment to index the new user
76
time.sleep(0.5)
77
78
# Test 1: Search by email (exact match should return only this user)
79
print("\n1. Testing search by email...")
80
results = hub.system.user_search(test_email)
81
assert isinstance(results, list), "user_search should return a list"
82
assert len(results) >= 1, f"Expected at least 1 result for email {test_email}, got {len(results)}"
83
84
# Find our user in the results
85
our_user = None
86
for user in results:
87
if user.get('email_address') == test_email:
88
our_user = user
89
break
90
91
assert our_user is not None, f"Expected to find user with email {test_email} in results"
92
print(f" Found user by email: {our_user['account_id']}")
93
94
# Verify the structure of the result
95
assert 'account_id' in our_user
96
assert 'first_name' in our_user
97
assert 'last_name' in our_user
98
assert our_user['first_name'] == test_first_name
99
assert our_user['last_name'] == test_last_name
100
assert our_user['account_id'] == user_id
101
print(f" User data: first_name={our_user['first_name']}, last_name={our_user['last_name']}")
102
103
# Test 2: Search by full first name (to ensure we find our user)
104
print("\n2. Testing search by full first name...")
105
# Use the full first name which is guaranteed unique with timestamp
106
results = hub.system.user_search(test_first_name)
107
assert isinstance(results, list)
108
print(f" Search for '{test_first_name}' returned {len(results)} results")
109
# Our user should be in the results
110
found = any(u.get('account_id') == user_id for u in results)
111
if not found and len(results) > 0:
112
print(f" Found these first names: {[u.get('first_name') for u in results]}")
113
assert found, f"Expected to find user {user_id} when searching for '{test_first_name}'"
114
print(f" Found user in {len(results)} results")
115
116
# Test 3: Search by full last name (to ensure we find our user)
117
print("\n3. Testing search by full last name...")
118
# Use the full last name which is guaranteed unique with timestamp
119
results = hub.system.user_search(test_last_name)
120
assert isinstance(results, list)
121
found = any(u.get('account_id') == user_id for u in results)
122
assert found, f"Expected to find user {user_id} when searching for '{test_last_name}'"
123
print(f" Found user in {len(results)} results")
124
125
# Test 4: Nonexistent search should return empty list
126
print("\n4. Testing search with unlikely query...")
127
unlikely_query = f"xyznonexistent{timestamp}abc"
128
results = hub.system.user_search(unlikely_query)
129
assert isinstance(results, list)
130
assert len(results) == 0, f"Expected 0 results for non-existent query, got {len(results)}"
131
print(" Search for non-existent query correctly returned 0 results")
132
133
print("\n✅ User search test completed successfully!")
134
135
# Note: No cleanup needed - happens automatically via cleanup_all_test_resources
136
137
138
class TestHubProjects:
139
"""Tests for Hub project operations."""
140
141
def test_create_project(self, hub, resource_tracker):
142
"""Test creating a project via hub.projects.create_project."""
143
import time
144
timestamp = int(time.time())
145
title = f"test-project-{timestamp}"
146
description = "Test project for API testing"
147
148
project_id = create_tracked_project(hub, resource_tracker, title=title, description=description)
149
150
assert project_id is not None
151
assert_valid_uuid(project_id, "Project ID")
152
153
# Note: No cleanup needed - happens automatically
154
155
def test_list_projects(self, hub):
156
"""Test listing projects."""
157
projects = hub.projects.get()
158
assert isinstance(projects, list)
159
# Each project should have basic fields
160
for project in projects:
161
assert 'project_id' in project
162
assert isinstance(project['project_id'], str)
163
164
def test_delete_method_exists(self, hub):
165
"""Test that delete method is available and callable."""
166
# Test that the delete method exists and is callable
167
assert hasattr(hub.projects, 'delete')
168
assert callable(hub.projects.delete)
169
170
# Note: We don't actually delete anything in this test since
171
# deletion is tested in the project lifecycle via temporary_project fixture
172
173
def test_project_state_and_status(self, hub, temporary_project, project_client):
174
"""Test retrieving state and status information for a project."""
175
project_id = temporary_project["project_id"]
176
177
# Ensure project is responsive before checking its status
178
project_client.system.ping()
179
180
state_info = hub.projects.state(project_id)
181
assert isinstance(state_info, dict)
182
state = state_info.get("state")
183
assert isinstance(state, str)
184
assert state, "Expected a non-empty state string"
185
186
status_info = hub.projects.status(project_id)
187
assert isinstance(status_info, dict)
188
informative_keys = ("project", "start_ts", "version", "disk_MB", "memory")
189
assert any(key in status_info for key in informative_keys), "Status response should include resource information"
190
191
project_status = status_info.get("project")
192
if isinstance(project_status, dict):
193
pid = project_status.get("pid")
194
if pid is not None:
195
assert isinstance(pid, int)
196
assert pid > 0
197
198
if "disk_MB" in status_info:
199
disk_usage = status_info["disk_MB"]
200
assert isinstance(disk_usage, (int, float))
201
202
memory_info = status_info.get("memory")
203
if memory_info is not None:
204
assert isinstance(memory_info, dict)
205
206
def test_project_lifecycle(self, hub, resource_tracker):
207
"""Test complete project lifecycle: create, wait for ready, run command, delete, verify deletion."""
208
209
# 1. Create a project
210
timestamp = int(time.time())
211
title = f"lifecycle-test-{timestamp}"
212
description = "Test project for complete lifecycle testing"
213
214
print(f"\n1. Creating project '{title}'...")
215
project_id = create_tracked_project(hub, resource_tracker, title=title, description=description)
216
assert project_id is not None
217
assert_valid_uuid(project_id, "Project ID")
218
print(f" Created project: {project_id}")
219
220
# Start the project
221
print("2. Starting project...")
222
hub.projects.start(project_id)
223
print(" Project start request sent")
224
225
# Wait for project to become ready
226
print("3. Waiting for project to become ready...")
227
project_client = Project(project_id=project_id, api_key=hub.api_key, host=hub.host)
228
229
ready = False
230
for attempt in range(12): # 60 seconds max wait time
231
time.sleep(5)
232
try:
233
project_client.system.ping()
234
ready = True
235
print(f" ✓ Project ready after {(attempt + 1) * 5} seconds")
236
break
237
except Exception as e:
238
if attempt == 11: # Last attempt
239
print(f" Warning: Project not ready after 60 seconds: {e}")
240
else:
241
print(f" Attempt {attempt + 1}: Project not ready yet...")
242
243
# Check that project exists in database
244
print("4. Checking project exists in database...")
245
projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id)
246
assert len(projects) == 1, f"Expected 1 project, found {len(projects)}"
247
project = projects[0]
248
assert project["project_id"] == project_id
249
assert project["title"] == title
250
assert project.get("deleted") is None or project.get("deleted") is False
251
print(f" ✓ Project found in database: title='{project['title']}', deleted={project.get('deleted')}")
252
253
# 2. Run a command if project is ready
254
if ready:
255
print("5. Running 'uname -a' command...")
256
result = project_client.system.exec("uname -a")
257
assert "stdout" in result
258
output = result["stdout"]
259
assert "Linux" in output, f"Expected Linux system, got: {output}"
260
assert result["exit_code"] == 0, f"Command failed with exit code {result['exit_code']}"
261
print(f" ✓ Command executed successfully: {output.strip()}")
262
else:
263
print("5. Skipping command execution - project not ready")
264
265
# 3. Stop and delete the project
266
print("6. Stopping and deleting project...")
267
cleanup_project(hub, project_id)
268
269
# 4. Verify project is marked as deleted in database
270
print("8. Verifying project is marked as deleted...")
271
projects = hub.projects.get(fields=['project_id', 'title', 'deleted'], project_id=project_id, all=True)
272
assert len(projects) == 1, f"Expected 1 project (still in DB), found {len(projects)}"
273
project = projects[0]
274
assert project["project_id"] == project_id
275
assert project.get("deleted") is True, f"Expected deleted=True, got deleted={project.get('deleted')}"
276
print(f" ✓ Project correctly marked as deleted in database: deleted={project.get('deleted')}")
277
278
print("✅ Project lifecycle test completed successfully!")
279
280
# Note: No cleanup needed - hard-delete happens automatically at session end
281
282
def test_collaborator_management(self, hub, resource_tracker):
283
"""Test adding and removing collaborators from a project."""
284
import time
285
timestamp = int(time.time())
286
287
# 1. Site admin creates two users
288
print("\n1. Creating two test users...")
289
user1_email = f"collab-user1-{timestamp}@test.local"
290
user2_email = f"collab-user2-{timestamp}@test.local"
291
292
# Create a temporary organization for the users
293
org_name = f"collab-test-org-{timestamp}"
294
org_id = create_tracked_org(hub, resource_tracker, org_name)
295
print(f" Created organization: {org_name} (ID: {org_id})")
296
297
user1_id = create_tracked_user(hub, resource_tracker, org_name, email=user1_email, firstName="CollabUser", lastName="One")
298
print(f" Created user1: {user1_id}")
299
300
user2_id = create_tracked_user(hub, resource_tracker, org_name, email=user2_email, firstName="CollabUser", lastName="Two")
301
print(f" Created user2: {user2_id}")
302
303
# 2. Create a project for the first user
304
print("\n2. Creating project for user1...")
305
project_title = f"collab-test-project-{timestamp}"
306
project_id = create_tracked_project(hub, resource_tracker, title=project_title)
307
print(f" Created project: {project_id}")
308
309
# 3. Check initial collaborators
310
print("\n3. Checking initial collaborators...")
311
projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)
312
assert len(projects) == 1
313
initial_users = projects[0].get('users', {})
314
print(f" Initial collaborators: {list(initial_users.keys())}")
315
print(f" Number of initial collaborators: {len(initial_users)}")
316
317
# Report on ownership structure
318
for user_id, perms in initial_users.items():
319
print(f" User {user_id}: {perms}")
320
321
# 4. Add user1 as collaborator
322
print(f"\n4. Adding user1 ({user1_id}) as collaborator...")
323
result = hub.projects.add_collaborator(project_id=project_id, account_id=user1_id)
324
print(f" Add collaborator result: {result}")
325
326
# Check collaborators after adding user1
327
projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)
328
users_after_user1 = projects[0].get('users', {})
329
print(f" Collaborators after adding user1: {list(users_after_user1.keys())}")
330
print(f" Number of collaborators: {len(users_after_user1)}")
331
for user_id, perms in users_after_user1.items():
332
print(f" User {user_id}: {perms}")
333
334
# 5. Add user2 as collaborator
335
print(f"\n5. Adding user2 ({user2_id}) as collaborator...")
336
result = hub.projects.add_collaborator(project_id=project_id, account_id=user2_id)
337
print(f" Add collaborator result: {result}")
338
339
# Check collaborators after adding user2
340
projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)
341
users_after_user2 = projects[0].get('users', {})
342
print(f" Collaborators after adding user2: {list(users_after_user2.keys())}")
343
print(f" Number of collaborators: {len(users_after_user2)}")
344
# Note: There will be 3 users total: the site admin (owner) + user1 + user2
345
for user_id, perms in users_after_user2.items():
346
print(f" User {user_id}: {perms}")
347
348
# Verify user1 and user2 are present
349
assert user1_id in users_after_user2, f"Expected user1 ({user1_id}) to be a collaborator"
350
assert user2_id in users_after_user2, f"Expected user2 ({user2_id}) to be a collaborator"
351
352
# Identify the owner (should be the site admin who created the project)
353
owner_id = None
354
for uid, perms in users_after_user2.items():
355
if perms.get('group') == 'owner':
356
owner_id = uid
357
break
358
print(f" Project owner: {owner_id}")
359
360
# 6. Remove user1
361
print(f"\n6. Removing user1 ({user1_id}) from project...")
362
result = hub.projects.remove_collaborator(project_id=project_id, account_id=user1_id)
363
print(f" Remove collaborator result: {result}")
364
365
# Check collaborators after removing user1
366
projects = hub.projects.get(fields=['project_id', 'users'], project_id=project_id)
367
users_after_removal = projects[0].get('users', {})
368
print(f" Collaborators after removing user1: {list(users_after_removal.keys())}")
369
print(f" Number of collaborators: {len(users_after_removal)}")
370
# Should have 2 users: owner + user2
371
assert len(users_after_removal) == 2, f"Expected 2 collaborators (owner + user2), found {len(users_after_removal)}"
372
assert user2_id in users_after_removal, f"Expected user2 ({user2_id}) to still be a collaborator"
373
assert user1_id not in users_after_removal, f"Expected user1 ({user1_id}) to be removed"
374
assert owner_id in users_after_removal, f"Expected owner ({owner_id}) to remain"
375
for user_id, perms in users_after_removal.items():
376
print(f" User {user_id}: {perms}")
377
378
print("\n✅ Collaborator management test completed successfully!")
379
380
# Note: No cleanup needed - hard-delete happens automatically at session end
381
382
def test_stop_project(self, hub, temporary_project):
383
"""Test stopping a running project."""
384
project_id = temporary_project["project_id"]
385
result = hub.projects.stop(project_id)
386
# Stop can return None or a dict, both are valid
387
assert result is None or isinstance(result, dict)
388
print(f"✓ Project stop request sent")
389
390
def test_touch_project(self, hub, temporary_project):
391
"""Test touching a project to signal it's in use."""
392
project_id = temporary_project["project_id"]
393
result = hub.projects.touch(project_id)
394
# Touch can return None or a dict, both are valid
395
assert result is None or isinstance(result, dict)
396
print(f"✓ Project touched successfully")
397
398
def test_get_names(self, hub, resource_tracker):
399
"""Test getting account names."""
400
import time
401
timestamp = int(time.time())
402
org_name = f"names-test-org-{timestamp}"
403
404
# Create a test user first
405
user_id = create_tracked_user(hub, resource_tracker, org_name, email=f"names-test-{timestamp}@test.local")
406
407
# Get the name(s) - returns a dict mapping user_id to display name
408
result = hub.system.get_names([user_id])
409
assert isinstance(result, dict)
410
# The result should have the user_id as a key
411
assert user_id in result or len(result) > 0
412
print(f"✓ Got names for user: {result}")
413
414
def test_copy_path_between_projects(self, hub, temporary_project, resource_tracker, project_client):
415
"""Test copying paths between projects."""
416
import time
417
import uuid
418
timestamp = int(time.time())
419
420
# Create a second project
421
project2_id = create_tracked_project(hub, resource_tracker, title=f"copy-target-{timestamp}")
422
project2_client = Project(project_id=project2_id, api_key=hub.api_key, host=hub.host)
423
424
# Create a unique test string
425
test_string = str(uuid.uuid4())
426
src_filename = f"testfile-copy-{timestamp}.txt"
427
dst_filename = f"testfile-copied-{timestamp}.txt"
428
429
# Create a test file in the first project
430
project_client.system.exec(f"echo '{test_string}' > {src_filename}")
431
432
# Copy the file to the second project
433
result = hub.projects.copy_path_between_projects(src_project_id=temporary_project["project_id"],
434
src_path=src_filename,
435
target_project_id=project2_id,
436
target_path=dst_filename)
437
# copy_path_between_projects can return None or a dict
438
assert result is None or isinstance(result, dict)
439
print(f"✓ File copy request sent")
440
441
# Verify the file was copied by reading it
442
verify_result = project2_client.system.exec(f"cat {dst_filename}")
443
assert verify_result["exit_code"] == 0
444
assert test_string in verify_result["stdout"]
445
print(f"✓ Verified copied file contains expected content")
446
447
def test_sync_history(self, hub, temporary_project, project_client):
448
"""Test getting sync history of a file."""
449
import time
450
timestamp = int(time.time())
451
filename = f"history-test-{timestamp}.txt"
452
453
# Create a test file
454
project_client.system.exec(f"echo 'initial' > {filename}")
455
456
result = hub.sync.history(project_id=temporary_project["project_id"], path=filename)
457
# Result can be a list or a dict with patches and info
458
if isinstance(result, dict):
459
patches = result.get('patches', [])
460
assert isinstance(patches, list)
461
else:
462
assert isinstance(result, list)
463
print(f"✓ Got sync history")
464
465
def test_db_query(self, hub):
466
"""Test database query for user info."""
467
result = hub.db.query({"accounts": {"first_name": None}})
468
assert isinstance(result, dict)
469
assert "accounts" in result
470
first_name = result["accounts"].get("first_name")
471
assert first_name is not None
472
print(f"✓ DB query successful, first_name: {first_name}")
473
474
def test_messages_send(self, hub, resource_tracker):
475
"""Test sending a message."""
476
import time
477
timestamp = int(time.time())
478
org_name = f"msg-test-org-{timestamp}"
479
480
# Create a test user to send message to
481
user_id = create_tracked_user(hub, resource_tracker, org_name, email=f"msg-test-{timestamp}@test.local")
482
483
result = hub.messages.send(subject="Test Message", body="This is a test message", to_ids=[user_id])
484
assert isinstance(result, int)
485
assert result > 0
486
print(f"✓ Message sent with ID: {result}")
487
488
def test_jupyter_kernels(self, hub, temporary_project):
489
"""Test getting available Jupyter kernels."""
490
result = hub.jupyter.kernels(project_id=temporary_project["project_id"])
491
assert isinstance(result, list)
492
# Should have at least python3
493
kernel_names = [k.get("name") for k in result]
494
assert "python3" in kernel_names or len(result) > 0
495
print(f"✓ Found {len(result)} Jupyter kernels: {kernel_names}")
496
497