Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/python/cocalc-api/tests/test_org.py
5581 views
1
"""
2
Tests for Organization functionality.
3
4
Note: These tests assume the provided API key belongs to a site admin user.
5
The tests exercise actual organization functionality rather than just checking permissions.
6
"""
7
import pytest
8
import time
9
import uuid
10
11
from .conftest import assert_valid_uuid
12
13
14
class TestAdminPrivileges:
15
"""Test that the API key has admin privileges."""
16
17
def test_admin_can_get_all_orgs(self, hub):
18
"""Test that the user can call get_all() - verifies admin privileges."""
19
try:
20
result = hub.org.get_all()
21
# If we get here without an exception, the user has admin privileges
22
assert isinstance(result, list), "get_all() should return a list"
23
print(f"✓ Admin verified - found {len(result)} organizations")
24
except Exception as e:
25
pytest.fail(f"Admin verification failed. API key may not have admin privileges: {e}")
26
27
28
class TestOrganizationBasics:
29
"""Test basic organization module functionality."""
30
31
def test_org_module_import(self, hub):
32
"""Test that the org module is properly accessible from hub."""
33
assert hasattr(hub, 'org')
34
assert hub.org is not None
35
36
def test_org_methods_available(self, hub):
37
"""Test that all expected organization methods are available."""
38
org = hub.org
39
40
expected_methods = [
41
'get_all',
42
'create',
43
'get',
44
'set',
45
'add_admin',
46
'add_user',
47
'create_user',
48
'create_token',
49
'expire_token',
50
'get_users',
51
'remove_user',
52
'remove_admin',
53
'message',
54
]
55
56
for method_name in expected_methods:
57
assert hasattr(org, method_name), f"Method {method_name} not found"
58
assert callable(getattr(org, method_name)), f"Method {method_name} is not callable"
59
60
61
class TestOrganizationCRUD:
62
"""Test organization Create, Read, Update, Delete operations."""
63
64
def test_get_all_organizations(self, hub):
65
"""Test getting all organizations."""
66
orgs = hub.org.get_all()
67
assert isinstance(orgs, list), "get_all() should return a list"
68
69
# Each org should have expected fields
70
for org in orgs:
71
assert isinstance(org, dict), "Each org should be a dict"
72
assert 'name' in org, "Each org should have a 'name' field"
73
74
def test_create_and_cleanup_organization(self, hub):
75
"""Test creating an organization and basic operations."""
76
# Create unique org name
77
timestamp = int(time.time())
78
random_id = str(uuid.uuid4())[:8]
79
org_name = f"test-org-{timestamp}-{random_id}"
80
81
print(f"Creating test organization: {org_name}")
82
83
try:
84
# Create the organization
85
org_id = hub.org.create(org_name)
86
assert_valid_uuid(org_id, "Organization ID")
87
print(f"✓ Organization created with ID: {org_id}")
88
89
# Get the organization details
90
org_details = hub.org.get(org_name)
91
assert isinstance(org_details, dict), "get() should return a dict"
92
assert org_details['name'] == org_name, "Organization name should match"
93
print(f"✓ Organization retrieved: {org_details}")
94
95
# Update organization properties
96
hub.org.set(name=org_name,
97
title="Test Organization",
98
description="This is a test organization created by automated tests",
99
email_address="[email protected]",
100
link="https://example.com")
101
102
# Verify the update
103
updated_org = hub.org.get(org_name)
104
assert updated_org['title'] == "Test Organization"
105
assert updated_org['description'] == "This is a test organization created by automated tests"
106
assert updated_org['email_address'] == "[email protected]"
107
assert updated_org['link'] == "https://example.com"
108
print("✓ Organization properties updated successfully")
109
110
except Exception as e:
111
pytest.fail(f"Organization CRUD operations failed: {e}")
112
113
114
class TestOrganizationUserManagement:
115
"""Test organization user management functionality."""
116
117
@pytest.fixture(scope="class")
118
def test_organization(self, hub):
119
"""Create a test organization for user management tests."""
120
timestamp = int(time.time())
121
random_id = str(uuid.uuid4())[:8]
122
org_name = f"test-user-org-{timestamp}-{random_id}"
123
124
print(f"Creating test organization for user tests: {org_name}")
125
126
# Create the organization
127
org_id = hub.org.create(org_name)
128
129
yield {'name': org_name, 'id': org_id}
130
131
# Cleanup would go here, but since we can't delete orgs,
132
# we leave them for manual cleanup if needed
133
134
def test_get_users_empty_org(self, hub, test_organization):
135
"""Test getting users from a newly created organization."""
136
users = hub.org.get_users(test_organization['name'])
137
assert isinstance(users, list), "get_users() should return a list"
138
assert len(users) == 0, f"Newly created organization should be empty, but has {len(users)} users"
139
print("✓ Newly created organization is empty as expected")
140
141
def test_create_user_in_organization(self, hub, test_organization):
142
"""Test creating a user within an organization."""
143
# Create unique user details
144
timestamp = int(time.time())
145
test_email = f"test-user-{timestamp}@example.com"
146
147
try:
148
# Create user in the organization
149
new_user_id = hub.org.create_user(name=test_organization['name'], email=test_email, firstName="Test", lastName="User")
150
151
assert_valid_uuid(new_user_id, "User ID")
152
print(f"✓ User created with ID: {new_user_id}")
153
154
# Wait a moment for database consistency
155
import time as time_module
156
time_module.sleep(1)
157
158
# Verify user appears in org users list
159
users = hub.org.get_users(test_organization['name'])
160
user_ids = [user['account_id'] for user in users]
161
162
print(f"Debug - Organization name: '{test_organization['name']}'")
163
print(f"Debug - Created user ID: '{new_user_id}'")
164
print(f"Debug - Users in org: {len(users)}")
165
print(f"Debug - User IDs: {user_ids}")
166
167
assert new_user_id in user_ids, f"New user {new_user_id} should appear in organization users list. Found users: {user_ids}"
168
169
# Find the created user in the list
170
created_user = next((u for u in users if u['account_id'] == new_user_id), None)
171
assert created_user is not None, "Created user should be found in users list"
172
assert created_user['email_address'] == test_email, "Email should match"
173
assert created_user['first_name'] == "Test", "First name should match"
174
assert created_user['last_name'] == "User", "Last name should match"
175
176
print(f"✓ User verified in organization: {created_user}")
177
178
except Exception as e:
179
pytest.fail(f"User creation failed: {e}")
180
181
def test_admin_management(self, hub, test_organization):
182
"""Test adding and managing admins - simplified workflow."""
183
timestamp = int(time.time())
184
185
try:
186
# Create user directly in the target organization
187
user_email = f"test-admin-{timestamp}@example.com"
188
user_id = hub.org.create_user(name=test_organization['name'], email=user_email, firstName="Test", lastName="Admin")
189
assert_valid_uuid(user_id, "User ID")
190
print(f"✓ Created user in organization: {user_id}")
191
192
# Promote the user to admin
193
hub.org.add_admin(test_organization['name'], user_id)
194
print(f"✓ Promoted user to admin of {test_organization['name']}")
195
196
# Verify admin status
197
org_details = hub.org.get(test_organization['name'])
198
admin_ids = org_details.get('admin_account_ids') or []
199
assert user_id in admin_ids, "User should be in admin list"
200
print(f"✓ Admin status verified: {admin_ids}")
201
202
# Test remove_admin
203
hub.org.remove_admin(test_organization['name'], user_id)
204
print(f"✓ Admin status removed for {user_id}")
205
206
# Verify admin removal
207
updated_org = hub.org.get(test_organization['name'])
208
updated_admin_ids = updated_org.get('admin_account_ids') or []
209
assert user_id not in updated_admin_ids, "User should no longer be admin"
210
print("✓ Admin removal verified")
211
212
except Exception as e:
213
pytest.fail(f"Admin management failed: {e}")
214
215
def test_admin_workflow_documentation(self, hub):
216
"""Document the correct admin assignment workflows."""
217
timestamp = int(time.time())
218
219
try:
220
# Create target organization
221
target_org = f"target-workflow-{timestamp}"
222
hub.org.create(target_org)
223
print(f"✓ Created organization: {target_org}")
224
225
# Workflow 1: Create user in org, then promote to admin (simplest)
226
user_id = hub.org.create_user(name=target_org, email=f"workflow-simple-{timestamp}@example.com", firstName="Workflow", lastName="Simple")
227
assert_valid_uuid(user_id, "Workflow user ID")
228
print("✓ Created user in organization")
229
230
# Promote to admin - works directly since user is in same org
231
hub.org.add_admin(target_org, user_id)
232
org_details = hub.org.get(target_org)
233
admin_ids = org_details.get('admin_account_ids') or []
234
assert user_id in admin_ids
235
print("✓ Workflow 1 (Same org user → admin): SUCCESS")
236
237
# Workflow 2: Move user from org A to org B, then promote to admin
238
other_org = f"other-workflow-{timestamp}"
239
hub.org.create(other_org)
240
other_user_id = hub.org.create_user(name=other_org, email=f"workflow-cross-{timestamp}@example.com", firstName="Cross", lastName="Org")
241
print(f"✓ Created user in {other_org}")
242
243
# Step 1: Use addUser to move user from other_org to target_org (site admin only)
244
hub.org.add_user(target_org, other_user_id)
245
print(f"✓ Moved user from {other_org} to {target_org} using addUser")
246
247
# Step 2: Now promote to admin in target_org
248
hub.org.add_admin(target_org, other_user_id)
249
updated_org = hub.org.get(target_org)
250
updated_admin_ids = updated_org.get('admin_account_ids') or []
251
assert other_user_id in updated_admin_ids, "Moved user should be admin"
252
print("✓ Workflow 2 (Cross-org: addUser → addAdmin): SUCCESS")
253
print("✓ Admin workflow documentation complete")
254
255
except Exception as e:
256
pytest.fail(f"Admin workflow documentation failed: {e}")
257
258
def test_cross_org_admin_promotion_blocked(self, hub):
259
"""Test that promoting a user from org A to admin of org B is blocked."""
260
timestamp = int(time.time())
261
262
try:
263
# Create two organizations
264
org_a = f"org-a-{timestamp}"
265
org_b = f"org-b-{timestamp}"
266
hub.org.create(org_a)
267
hub.org.create(org_b)
268
print(f"✓ Created organizations: {org_a} and {org_b}")
269
270
# Create user in org A
271
user_id = hub.org.create_user(name=org_a, email=f"cross-org-user-{timestamp}@example.com", firstName="CrossOrg", lastName="User")
272
assert_valid_uuid(user_id, "Cross-org user ID")
273
print(f"✓ Created user in {org_a}")
274
275
# Try to promote user from org A to admin of org B - should fail
276
try:
277
hub.org.add_admin(org_b, user_id)
278
pytest.fail("Expected error when promoting user from different org to admin")
279
except Exception as e:
280
error_msg = str(e)
281
assert "already member of another organization" in error_msg, \
282
f"Expected 'already member of another organization' error, got: {error_msg}"
283
print(f"✓ Cross-org promotion correctly blocked: {error_msg}")
284
285
# Demonstrate correct workflow: use addUser to move, then addAdmin
286
# Note: addUser is site-admin only, so we can't test the full workflow
287
# without site admin privileges, but we document the pattern
288
print("✓ Correct workflow: Use addUser to move user between orgs first, then addAdmin")
289
print("✓ Cross-org admin promotion blocking test passed")
290
291
except Exception as e:
292
pytest.fail(f"Cross-org admin promotion test failed: {e}")
293
294
295
class TestOrganizationTokens:
296
"""Test organization token functionality."""
297
298
@pytest.fixture(scope="class")
299
def test_org_with_user(self, hub):
300
"""Create a test organization with a user for token tests."""
301
timestamp = int(time.time())
302
random_id = str(uuid.uuid4())[:8]
303
org_name = f"test-token-org-{timestamp}-{random_id}"
304
305
# Create the organization
306
org_id = hub.org.create(org_name)
307
308
# Create a user in the organization
309
test_email = f"token-user-{timestamp}@example.com"
310
user_id = hub.org.create_user(name=org_name, email=test_email, firstName="Token", lastName="User")
311
assert_valid_uuid(user_id, "Token user ID")
312
313
yield {'name': org_name, 'id': org_id, 'user_id': user_id, 'user_email': test_email}
314
315
def test_create_and_expire_token(self, hub, test_org_with_user):
316
"""Test creating and expiring access tokens."""
317
try:
318
# Create token for the user
319
token_info = hub.org.create_token(test_org_with_user['user_id'])
320
321
assert isinstance(token_info, dict), "create_token() should return a dict"
322
assert 'token' in token_info, "Token info should contain 'token' field"
323
assert 'url' in token_info, "Token info should contain 'url' field"
324
325
token = token_info['token']
326
url = token_info['url']
327
328
assert isinstance(token, str) and len(token) > 0, "Token should be a non-empty string"
329
assert isinstance(url, str) and url.startswith('http'), "URL should be a valid HTTP URL"
330
331
print(f"✓ Token created: {token[:10]}... (truncated)")
332
print(f"✓ Access URL: {url}")
333
334
# Expire the token
335
hub.org.expire_token(token)
336
print("✓ Token expired successfully")
337
338
except Exception as e:
339
pytest.fail(f"Token management failed: {e}")
340
341
342
class TestOrganizationMessaging:
343
"""Test organization messaging functionality."""
344
345
@pytest.fixture(scope="class")
346
def test_org_with_users(self, hub):
347
"""Create a test organization with multiple users for messaging tests."""
348
timestamp = int(time.time())
349
random_id = str(uuid.uuid4())[:8]
350
org_name = f"test-msg-org-{timestamp}-{random_id}"
351
352
# Create the organization
353
org_id = hub.org.create(org_name)
354
355
# Create multiple users in the organization
356
users = []
357
for i in range(2):
358
test_email = f"msg-user-{i}-{timestamp}@example.com"
359
user_id = hub.org.create_user(name=org_name, email=test_email, firstName=f"User{i}", lastName="Messaging")
360
assert_valid_uuid(user_id, f"Messaging user {i} ID")
361
362
users.append({'id': user_id, 'email': test_email})
363
364
yield {'name': org_name, 'id': org_id, 'users': users}
365
366
def test_send_message_to_organization(self, hub, test_org_with_users, cocalc_host):
367
"""Test sending a message to all organization members and verify receipt."""
368
from cocalc_api import Hub
369
370
test_subject = "Test Message from API Tests"
371
test_body = "This is a test message sent via the CoCalc API organization messaging system."
372
user_token = None
373
374
try:
375
# Step 1: Create a token for the first user to act as them
376
first_user = test_org_with_users['users'][0]
377
token_info = hub.org.create_token(first_user['id'])
378
379
assert isinstance(token_info, dict), "create_token() should return a dict"
380
assert 'token' in token_info, "Token info should contain 'token' field"
381
382
user_token = token_info['token']
383
print(f"✓ Created token for user {first_user['id']}")
384
385
# Step 2: Create Hub client using the user's token
386
user1 = Hub(api_key=user_token, host=cocalc_host)
387
print("✓ Created Hub client using user token")
388
389
# Step 3: Get user's messages before sending org message (for comparison)
390
try:
391
messages_before = user1.messages.get(limit=5, type="received")
392
print(f"✓ User has {len(messages_before)} received messages before test")
393
except Exception as e:
394
print(f"⚠ Could not get user's messages before test: {e}")
395
messages_before = []
396
397
# Step 4: Send the organization message
398
result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=test_body)
399
400
# Note: org.message() may return None, which is fine (indicates success)
401
print(f"✓ Organization message sent successfully (result: {result})")
402
403
# Step 5: Wait a moment for message delivery
404
import time
405
time.sleep(2)
406
407
# Step 6: Check if user received the message
408
try:
409
messages_after = user1.messages.get(limit=10, type="received")
410
print(f"✓ User has {len(messages_after)} received messages after test")
411
412
# Look for our test message in user's received messages
413
found_message = False
414
for msg in messages_after:
415
if isinstance(msg, dict) and msg.get('subject') == test_subject:
416
found_message = True
417
print(f"✓ VERIFIED: User received message with subject: '{msg.get('subject')}'")
418
419
# Verify message content
420
if 'body' in msg:
421
print(f"✓ Message body confirmed: {msg['body'][:50]}...")
422
break
423
424
if found_message:
425
print("🎉 SUCCESS: Organization message was successfully delivered to user!")
426
else:
427
print("⚠ Message not found in user's received messages")
428
print(f" Expected subject: '{test_subject}'")
429
if messages_after:
430
print(f" Recent subjects: {[msg.get('subject', 'No subject') for msg in messages_after[:3]]}")
431
432
except Exception as msg_check_error:
433
print(f"⚠ Could not verify message delivery: {msg_check_error}")
434
435
except Exception as e:
436
pytest.fail(f"Message sending and verification failed: {e}")
437
438
finally:
439
# Clean up: expire the token
440
if user_token:
441
try:
442
hub.org.expire_token(user_token)
443
print("✓ User token expired (cleanup)")
444
except Exception as cleanup_error:
445
print(f"⚠ Failed to expire token during cleanup: {cleanup_error}")
446
447
def test_send_markdown_message(self, hub, test_org_with_users, cocalc_host):
448
"""Test sending a message with markdown formatting and verify receipt."""
449
from cocalc_api import Hub
450
451
test_subject = "📝 Markdown Test Message"
452
markdown_body = """
453
# Test Message with Markdown
454
455
This is a **test message** with *markdown* formatting sent from the API tests.
456
457
## Features Tested
458
- Organization messaging
459
- Markdown formatting
460
- API integration
461
462
## Math Example
463
The formula $E = mc^2$ should render properly.
464
465
## Code Example
466
```python
467
print("Hello from CoCalc API!")
468
```
469
470
[CoCalc API Documentation](https://cocalc.com/api/python/)
471
472
---
473
*This message was sent automatically by the organization API tests.*
474
""".strip()
475
476
user_token = None
477
478
try:
479
# Create a token for the second user (to vary which user we test)
480
if len(test_org_with_users['users']) > 1:
481
test_user = test_org_with_users['users'][1]
482
else:
483
test_user = test_org_with_users['users'][0]
484
485
token_info = hub.org.create_token(test_user['id'])
486
user_token = token_info['token']
487
user_hub = Hub(api_key=user_token, host=cocalc_host)
488
print(f"✓ Created token and Hub client for user {test_user['id']}")
489
490
# Send the markdown message
491
result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=markdown_body)
492
493
# Note: org.message() may return None, which is fine (indicates success)
494
print(f"✓ Markdown message sent successfully (result: {result})")
495
496
# Wait for message delivery and verify
497
import time
498
time.sleep(2)
499
500
try:
501
messages = user_hub.messages.get(limit=10, type="received")
502
503
# Look for the markdown message
504
found_message = False
505
for msg in messages:
506
if isinstance(msg, dict) and msg.get('subject') == test_subject:
507
found_message = True
508
print("✓ VERIFIED: User received markdown message")
509
510
# Verify it contains markdown content
511
body = msg.get('body', '')
512
if '**test message**' in body or 'Test Message with Markdown' in body:
513
print("✓ Markdown content confirmed in received message")
514
break
515
516
if found_message:
517
print("🎉 SUCCESS: Markdown message was successfully delivered!")
518
else:
519
print("⚠ Markdown message not found in user's received messages")
520
521
except Exception as msg_check_error:
522
print(f"⚠ Could not verify markdown message delivery: {msg_check_error}")
523
524
except Exception as e:
525
pytest.fail(f"Markdown message sending and verification failed: {e}")
526
527
finally:
528
# Clean up: expire the token
529
if user_token:
530
try:
531
hub.org.expire_token(user_token)
532
print("✓ Markdown test token expired (cleanup)")
533
except Exception as cleanup_error:
534
print(f"⚠ Failed to expire markdown test token: {cleanup_error}")
535
536
537
class TestOrganizationIntegration:
538
"""Integration tests for organization functionality."""
539
540
def test_full_organization_lifecycle(self, hub):
541
"""Test a complete organization lifecycle with users and messaging."""
542
timestamp = int(time.time())
543
random_id = str(uuid.uuid4())[:8]
544
org_name = f"test-lifecycle-{timestamp}-{random_id}"
545
546
try:
547
print(f"Testing full lifecycle for organization: {org_name}")
548
549
# 1. Create organization
550
org_id = hub.org.create(org_name)
551
print(f"✓ 1. Organization created: {org_id}")
552
553
# 2. Set organization properties
554
hub.org.set(name=org_name, title="Lifecycle Test Organization", description="Testing complete organization lifecycle")
555
print("✓ 2. Organization properties set")
556
557
# 3. Create users
558
users = []
559
for i in range(2):
560
user_email = f"lifecycle-user-{i}-{timestamp}@example.com"
561
user_id = hub.org.create_user(name=org_name, email=user_email, firstName=f"User{i}", lastName="Lifecycle")
562
assert_valid_uuid(user_id, f"Lifecycle user {i} ID")
563
564
users.append({'id': user_id, 'email': user_email})
565
print(f"✓ 3. Created {len(users)} users")
566
567
# 4. Promote a user to admin (simplified workflow)
568
# Create user and promote directly to admin
569
admin_email = f"lifecycle-admin-{timestamp}@example.com"
570
admin_id = hub.org.create_user(name=org_name, email=admin_email, firstName="Admin", lastName="User")
571
assert_valid_uuid(admin_id, "Admin user ID")
572
hub.org.add_admin(org_name, admin_id)
573
print("✓ 4. Created and promoted user to admin")
574
575
# 5. Create and expire a token
576
token_info = hub.org.create_token(users[1]['id'])
577
hub.org.expire_token(token_info['token'])
578
print("✓ 5. Token created and expired")
579
580
# 6. Send message to organization
581
hub.org.message(name=org_name, subject="Lifecycle Test Complete", body="All organization lifecycle tests completed successfully!")
582
print("✓ 6. Message sent")
583
584
# 7. Verify final state
585
final_org = hub.org.get(org_name)
586
final_users = hub.org.get_users(org_name)
587
588
assert final_org['title'] == "Lifecycle Test Organization"
589
assert len(final_users) >= len(users) + 1, "All users plus admin should be in organization"
590
591
# Check admin status
592
admin_ids = final_org.get('admin_account_ids') or []
593
assert admin_id in admin_ids, "Admin should be in admin list"
594
print(f"✓ Admin assignment successful: {admin_ids}")
595
596
print(f"✓ 7. Final verification complete - org has {len(final_users)} users")
597
print(f"✓ Full lifecycle test completed successfully for {org_name}")
598
599
except Exception as e:
600
pytest.fail(f"Full lifecycle test failed: {e}")
601
602
603
def test_delete_method_still_available(hub):
604
"""Verify that projects.delete is still available after org refactoring."""
605
assert hasattr(hub.projects, 'delete')
606
assert callable(hub.projects.delete)
607
print("✓ Projects delete method still available after org refactoring")
608
609