Path: blob/master/src/python/cocalc-api/tests/test_org.py
5581 views
"""1Tests for Organization functionality.23Note: These tests assume the provided API key belongs to a site admin user.4The tests exercise actual organization functionality rather than just checking permissions.5"""6import pytest7import time8import uuid910from .conftest import assert_valid_uuid111213class TestAdminPrivileges:14"""Test that the API key has admin privileges."""1516def test_admin_can_get_all_orgs(self, hub):17"""Test that the user can call get_all() - verifies admin privileges."""18try:19result = hub.org.get_all()20# If we get here without an exception, the user has admin privileges21assert isinstance(result, list), "get_all() should return a list"22print(f"✓ Admin verified - found {len(result)} organizations")23except Exception as e:24pytest.fail(f"Admin verification failed. API key may not have admin privileges: {e}")252627class TestOrganizationBasics:28"""Test basic organization module functionality."""2930def test_org_module_import(self, hub):31"""Test that the org module is properly accessible from hub."""32assert hasattr(hub, 'org')33assert hub.org is not None3435def test_org_methods_available(self, hub):36"""Test that all expected organization methods are available."""37org = hub.org3839expected_methods = [40'get_all',41'create',42'get',43'set',44'add_admin',45'add_user',46'create_user',47'create_token',48'expire_token',49'get_users',50'remove_user',51'remove_admin',52'message',53]5455for method_name in expected_methods:56assert hasattr(org, method_name), f"Method {method_name} not found"57assert callable(getattr(org, method_name)), f"Method {method_name} is not callable"585960class TestOrganizationCRUD:61"""Test organization Create, Read, Update, Delete operations."""6263def test_get_all_organizations(self, hub):64"""Test getting all organizations."""65orgs = hub.org.get_all()66assert isinstance(orgs, list), "get_all() should return a list"6768# Each org should have expected fields69for org in orgs:70assert isinstance(org, dict), "Each org should be a dict"71assert 'name' in org, "Each org should have a 'name' field"7273def test_create_and_cleanup_organization(self, hub):74"""Test creating an organization and basic operations."""75# Create unique org name76timestamp = int(time.time())77random_id = str(uuid.uuid4())[:8]78org_name = f"test-org-{timestamp}-{random_id}"7980print(f"Creating test organization: {org_name}")8182try:83# Create the organization84org_id = hub.org.create(org_name)85assert_valid_uuid(org_id, "Organization ID")86print(f"✓ Organization created with ID: {org_id}")8788# Get the organization details89org_details = hub.org.get(org_name)90assert isinstance(org_details, dict), "get() should return a dict"91assert org_details['name'] == org_name, "Organization name should match"92print(f"✓ Organization retrieved: {org_details}")9394# Update organization properties95hub.org.set(name=org_name,96title="Test Organization",97description="This is a test organization created by automated tests",98email_address="[email protected]",99link="https://example.com")100101# Verify the update102updated_org = hub.org.get(org_name)103assert updated_org['title'] == "Test Organization"104assert updated_org['description'] == "This is a test organization created by automated tests"105assert updated_org['email_address'] == "[email protected]"106assert updated_org['link'] == "https://example.com"107print("✓ Organization properties updated successfully")108109except Exception as e:110pytest.fail(f"Organization CRUD operations failed: {e}")111112113class TestOrganizationUserManagement:114"""Test organization user management functionality."""115116@pytest.fixture(scope="class")117def test_organization(self, hub):118"""Create a test organization for user management tests."""119timestamp = int(time.time())120random_id = str(uuid.uuid4())[:8]121org_name = f"test-user-org-{timestamp}-{random_id}"122123print(f"Creating test organization for user tests: {org_name}")124125# Create the organization126org_id = hub.org.create(org_name)127128yield {'name': org_name, 'id': org_id}129130# Cleanup would go here, but since we can't delete orgs,131# we leave them for manual cleanup if needed132133def test_get_users_empty_org(self, hub, test_organization):134"""Test getting users from a newly created organization."""135users = hub.org.get_users(test_organization['name'])136assert isinstance(users, list), "get_users() should return a list"137assert len(users) == 0, f"Newly created organization should be empty, but has {len(users)} users"138print("✓ Newly created organization is empty as expected")139140def test_create_user_in_organization(self, hub, test_organization):141"""Test creating a user within an organization."""142# Create unique user details143timestamp = int(time.time())144test_email = f"test-user-{timestamp}@example.com"145146try:147# Create user in the organization148new_user_id = hub.org.create_user(name=test_organization['name'], email=test_email, firstName="Test", lastName="User")149150assert_valid_uuid(new_user_id, "User ID")151print(f"✓ User created with ID: {new_user_id}")152153# Wait a moment for database consistency154import time as time_module155time_module.sleep(1)156157# Verify user appears in org users list158users = hub.org.get_users(test_organization['name'])159user_ids = [user['account_id'] for user in users]160161print(f"Debug - Organization name: '{test_organization['name']}'")162print(f"Debug - Created user ID: '{new_user_id}'")163print(f"Debug - Users in org: {len(users)}")164print(f"Debug - User IDs: {user_ids}")165166assert new_user_id in user_ids, f"New user {new_user_id} should appear in organization users list. Found users: {user_ids}"167168# Find the created user in the list169created_user = next((u for u in users if u['account_id'] == new_user_id), None)170assert created_user is not None, "Created user should be found in users list"171assert created_user['email_address'] == test_email, "Email should match"172assert created_user['first_name'] == "Test", "First name should match"173assert created_user['last_name'] == "User", "Last name should match"174175print(f"✓ User verified in organization: {created_user}")176177except Exception as e:178pytest.fail(f"User creation failed: {e}")179180def test_admin_management(self, hub, test_organization):181"""Test adding and managing admins - simplified workflow."""182timestamp = int(time.time())183184try:185# Create user directly in the target organization186user_email = f"test-admin-{timestamp}@example.com"187user_id = hub.org.create_user(name=test_organization['name'], email=user_email, firstName="Test", lastName="Admin")188assert_valid_uuid(user_id, "User ID")189print(f"✓ Created user in organization: {user_id}")190191# Promote the user to admin192hub.org.add_admin(test_organization['name'], user_id)193print(f"✓ Promoted user to admin of {test_organization['name']}")194195# Verify admin status196org_details = hub.org.get(test_organization['name'])197admin_ids = org_details.get('admin_account_ids') or []198assert user_id in admin_ids, "User should be in admin list"199print(f"✓ Admin status verified: {admin_ids}")200201# Test remove_admin202hub.org.remove_admin(test_organization['name'], user_id)203print(f"✓ Admin status removed for {user_id}")204205# Verify admin removal206updated_org = hub.org.get(test_organization['name'])207updated_admin_ids = updated_org.get('admin_account_ids') or []208assert user_id not in updated_admin_ids, "User should no longer be admin"209print("✓ Admin removal verified")210211except Exception as e:212pytest.fail(f"Admin management failed: {e}")213214def test_admin_workflow_documentation(self, hub):215"""Document the correct admin assignment workflows."""216timestamp = int(time.time())217218try:219# Create target organization220target_org = f"target-workflow-{timestamp}"221hub.org.create(target_org)222print(f"✓ Created organization: {target_org}")223224# Workflow 1: Create user in org, then promote to admin (simplest)225user_id = hub.org.create_user(name=target_org, email=f"workflow-simple-{timestamp}@example.com", firstName="Workflow", lastName="Simple")226assert_valid_uuid(user_id, "Workflow user ID")227print("✓ Created user in organization")228229# Promote to admin - works directly since user is in same org230hub.org.add_admin(target_org, user_id)231org_details = hub.org.get(target_org)232admin_ids = org_details.get('admin_account_ids') or []233assert user_id in admin_ids234print("✓ Workflow 1 (Same org user → admin): SUCCESS")235236# Workflow 2: Move user from org A to org B, then promote to admin237other_org = f"other-workflow-{timestamp}"238hub.org.create(other_org)239other_user_id = hub.org.create_user(name=other_org, email=f"workflow-cross-{timestamp}@example.com", firstName="Cross", lastName="Org")240print(f"✓ Created user in {other_org}")241242# Step 1: Use addUser to move user from other_org to target_org (site admin only)243hub.org.add_user(target_org, other_user_id)244print(f"✓ Moved user from {other_org} to {target_org} using addUser")245246# Step 2: Now promote to admin in target_org247hub.org.add_admin(target_org, other_user_id)248updated_org = hub.org.get(target_org)249updated_admin_ids = updated_org.get('admin_account_ids') or []250assert other_user_id in updated_admin_ids, "Moved user should be admin"251print("✓ Workflow 2 (Cross-org: addUser → addAdmin): SUCCESS")252print("✓ Admin workflow documentation complete")253254except Exception as e:255pytest.fail(f"Admin workflow documentation failed: {e}")256257def test_cross_org_admin_promotion_blocked(self, hub):258"""Test that promoting a user from org A to admin of org B is blocked."""259timestamp = int(time.time())260261try:262# Create two organizations263org_a = f"org-a-{timestamp}"264org_b = f"org-b-{timestamp}"265hub.org.create(org_a)266hub.org.create(org_b)267print(f"✓ Created organizations: {org_a} and {org_b}")268269# Create user in org A270user_id = hub.org.create_user(name=org_a, email=f"cross-org-user-{timestamp}@example.com", firstName="CrossOrg", lastName="User")271assert_valid_uuid(user_id, "Cross-org user ID")272print(f"✓ Created user in {org_a}")273274# Try to promote user from org A to admin of org B - should fail275try:276hub.org.add_admin(org_b, user_id)277pytest.fail("Expected error when promoting user from different org to admin")278except Exception as e:279error_msg = str(e)280assert "already member of another organization" in error_msg, \281f"Expected 'already member of another organization' error, got: {error_msg}"282print(f"✓ Cross-org promotion correctly blocked: {error_msg}")283284# Demonstrate correct workflow: use addUser to move, then addAdmin285# Note: addUser is site-admin only, so we can't test the full workflow286# without site admin privileges, but we document the pattern287print("✓ Correct workflow: Use addUser to move user between orgs first, then addAdmin")288print("✓ Cross-org admin promotion blocking test passed")289290except Exception as e:291pytest.fail(f"Cross-org admin promotion test failed: {e}")292293294class TestOrganizationTokens:295"""Test organization token functionality."""296297@pytest.fixture(scope="class")298def test_org_with_user(self, hub):299"""Create a test organization with a user for token tests."""300timestamp = int(time.time())301random_id = str(uuid.uuid4())[:8]302org_name = f"test-token-org-{timestamp}-{random_id}"303304# Create the organization305org_id = hub.org.create(org_name)306307# Create a user in the organization308test_email = f"token-user-{timestamp}@example.com"309user_id = hub.org.create_user(name=org_name, email=test_email, firstName="Token", lastName="User")310assert_valid_uuid(user_id, "Token user ID")311312yield {'name': org_name, 'id': org_id, 'user_id': user_id, 'user_email': test_email}313314def test_create_and_expire_token(self, hub, test_org_with_user):315"""Test creating and expiring access tokens."""316try:317# Create token for the user318token_info = hub.org.create_token(test_org_with_user['user_id'])319320assert isinstance(token_info, dict), "create_token() should return a dict"321assert 'token' in token_info, "Token info should contain 'token' field"322assert 'url' in token_info, "Token info should contain 'url' field"323324token = token_info['token']325url = token_info['url']326327assert isinstance(token, str) and len(token) > 0, "Token should be a non-empty string"328assert isinstance(url, str) and url.startswith('http'), "URL should be a valid HTTP URL"329330print(f"✓ Token created: {token[:10]}... (truncated)")331print(f"✓ Access URL: {url}")332333# Expire the token334hub.org.expire_token(token)335print("✓ Token expired successfully")336337except Exception as e:338pytest.fail(f"Token management failed: {e}")339340341class TestOrganizationMessaging:342"""Test organization messaging functionality."""343344@pytest.fixture(scope="class")345def test_org_with_users(self, hub):346"""Create a test organization with multiple users for messaging tests."""347timestamp = int(time.time())348random_id = str(uuid.uuid4())[:8]349org_name = f"test-msg-org-{timestamp}-{random_id}"350351# Create the organization352org_id = hub.org.create(org_name)353354# Create multiple users in the organization355users = []356for i in range(2):357test_email = f"msg-user-{i}-{timestamp}@example.com"358user_id = hub.org.create_user(name=org_name, email=test_email, firstName=f"User{i}", lastName="Messaging")359assert_valid_uuid(user_id, f"Messaging user {i} ID")360361users.append({'id': user_id, 'email': test_email})362363yield {'name': org_name, 'id': org_id, 'users': users}364365def test_send_message_to_organization(self, hub, test_org_with_users, cocalc_host):366"""Test sending a message to all organization members and verify receipt."""367from cocalc_api import Hub368369test_subject = "Test Message from API Tests"370test_body = "This is a test message sent via the CoCalc API organization messaging system."371user_token = None372373try:374# Step 1: Create a token for the first user to act as them375first_user = test_org_with_users['users'][0]376token_info = hub.org.create_token(first_user['id'])377378assert isinstance(token_info, dict), "create_token() should return a dict"379assert 'token' in token_info, "Token info should contain 'token' field"380381user_token = token_info['token']382print(f"✓ Created token for user {first_user['id']}")383384# Step 2: Create Hub client using the user's token385user1 = Hub(api_key=user_token, host=cocalc_host)386print("✓ Created Hub client using user token")387388# Step 3: Get user's messages before sending org message (for comparison)389try:390messages_before = user1.messages.get(limit=5, type="received")391print(f"✓ User has {len(messages_before)} received messages before test")392except Exception as e:393print(f"⚠ Could not get user's messages before test: {e}")394messages_before = []395396# Step 4: Send the organization message397result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=test_body)398399# Note: org.message() may return None, which is fine (indicates success)400print(f"✓ Organization message sent successfully (result: {result})")401402# Step 5: Wait a moment for message delivery403import time404time.sleep(2)405406# Step 6: Check if user received the message407try:408messages_after = user1.messages.get(limit=10, type="received")409print(f"✓ User has {len(messages_after)} received messages after test")410411# Look for our test message in user's received messages412found_message = False413for msg in messages_after:414if isinstance(msg, dict) and msg.get('subject') == test_subject:415found_message = True416print(f"✓ VERIFIED: User received message with subject: '{msg.get('subject')}'")417418# Verify message content419if 'body' in msg:420print(f"✓ Message body confirmed: {msg['body'][:50]}...")421break422423if found_message:424print("🎉 SUCCESS: Organization message was successfully delivered to user!")425else:426print("⚠ Message not found in user's received messages")427print(f" Expected subject: '{test_subject}'")428if messages_after:429print(f" Recent subjects: {[msg.get('subject', 'No subject') for msg in messages_after[:3]]}")430431except Exception as msg_check_error:432print(f"⚠ Could not verify message delivery: {msg_check_error}")433434except Exception as e:435pytest.fail(f"Message sending and verification failed: {e}")436437finally:438# Clean up: expire the token439if user_token:440try:441hub.org.expire_token(user_token)442print("✓ User token expired (cleanup)")443except Exception as cleanup_error:444print(f"⚠ Failed to expire token during cleanup: {cleanup_error}")445446def test_send_markdown_message(self, hub, test_org_with_users, cocalc_host):447"""Test sending a message with markdown formatting and verify receipt."""448from cocalc_api import Hub449450test_subject = "📝 Markdown Test Message"451markdown_body = """452# Test Message with Markdown453454This is a **test message** with *markdown* formatting sent from the API tests.455456## Features Tested457- Organization messaging458- Markdown formatting459- API integration460461## Math Example462The formula $E = mc^2$ should render properly.463464## Code Example465```python466print("Hello from CoCalc API!")467```468469[CoCalc API Documentation](https://cocalc.com/api/python/)470471---472*This message was sent automatically by the organization API tests.*473""".strip()474475user_token = None476477try:478# Create a token for the second user (to vary which user we test)479if len(test_org_with_users['users']) > 1:480test_user = test_org_with_users['users'][1]481else:482test_user = test_org_with_users['users'][0]483484token_info = hub.org.create_token(test_user['id'])485user_token = token_info['token']486user_hub = Hub(api_key=user_token, host=cocalc_host)487print(f"✓ Created token and Hub client for user {test_user['id']}")488489# Send the markdown message490result = hub.org.message(name=test_org_with_users['name'], subject=test_subject, body=markdown_body)491492# Note: org.message() may return None, which is fine (indicates success)493print(f"✓ Markdown message sent successfully (result: {result})")494495# Wait for message delivery and verify496import time497time.sleep(2)498499try:500messages = user_hub.messages.get(limit=10, type="received")501502# Look for the markdown message503found_message = False504for msg in messages:505if isinstance(msg, dict) and msg.get('subject') == test_subject:506found_message = True507print("✓ VERIFIED: User received markdown message")508509# Verify it contains markdown content510body = msg.get('body', '')511if '**test message**' in body or 'Test Message with Markdown' in body:512print("✓ Markdown content confirmed in received message")513break514515if found_message:516print("🎉 SUCCESS: Markdown message was successfully delivered!")517else:518print("⚠ Markdown message not found in user's received messages")519520except Exception as msg_check_error:521print(f"⚠ Could not verify markdown message delivery: {msg_check_error}")522523except Exception as e:524pytest.fail(f"Markdown message sending and verification failed: {e}")525526finally:527# Clean up: expire the token528if user_token:529try:530hub.org.expire_token(user_token)531print("✓ Markdown test token expired (cleanup)")532except Exception as cleanup_error:533print(f"⚠ Failed to expire markdown test token: {cleanup_error}")534535536class TestOrganizationIntegration:537"""Integration tests for organization functionality."""538539def test_full_organization_lifecycle(self, hub):540"""Test a complete organization lifecycle with users and messaging."""541timestamp = int(time.time())542random_id = str(uuid.uuid4())[:8]543org_name = f"test-lifecycle-{timestamp}-{random_id}"544545try:546print(f"Testing full lifecycle for organization: {org_name}")547548# 1. Create organization549org_id = hub.org.create(org_name)550print(f"✓ 1. Organization created: {org_id}")551552# 2. Set organization properties553hub.org.set(name=org_name, title="Lifecycle Test Organization", description="Testing complete organization lifecycle")554print("✓ 2. Organization properties set")555556# 3. Create users557users = []558for i in range(2):559user_email = f"lifecycle-user-{i}-{timestamp}@example.com"560user_id = hub.org.create_user(name=org_name, email=user_email, firstName=f"User{i}", lastName="Lifecycle")561assert_valid_uuid(user_id, f"Lifecycle user {i} ID")562563users.append({'id': user_id, 'email': user_email})564print(f"✓ 3. Created {len(users)} users")565566# 4. Promote a user to admin (simplified workflow)567# Create user and promote directly to admin568admin_email = f"lifecycle-admin-{timestamp}@example.com"569admin_id = hub.org.create_user(name=org_name, email=admin_email, firstName="Admin", lastName="User")570assert_valid_uuid(admin_id, "Admin user ID")571hub.org.add_admin(org_name, admin_id)572print("✓ 4. Created and promoted user to admin")573574# 5. Create and expire a token575token_info = hub.org.create_token(users[1]['id'])576hub.org.expire_token(token_info['token'])577print("✓ 5. Token created and expired")578579# 6. Send message to organization580hub.org.message(name=org_name, subject="Lifecycle Test Complete", body="All organization lifecycle tests completed successfully!")581print("✓ 6. Message sent")582583# 7. Verify final state584final_org = hub.org.get(org_name)585final_users = hub.org.get_users(org_name)586587assert final_org['title'] == "Lifecycle Test Organization"588assert len(final_users) >= len(users) + 1, "All users plus admin should be in organization"589590# Check admin status591admin_ids = final_org.get('admin_account_ids') or []592assert admin_id in admin_ids, "Admin should be in admin list"593print(f"✓ Admin assignment successful: {admin_ids}")594595print(f"✓ 7. Final verification complete - org has {len(final_users)} users")596print(f"✓ Full lifecycle test completed successfully for {org_name}")597598except Exception as e:599pytest.fail(f"Full lifecycle test failed: {e}")600601602def test_delete_method_still_available(hub):603"""Verify that projects.delete is still available after org refactoring."""604assert hasattr(hub.projects, 'delete')605assert callable(hub.projects.delete)606print("✓ Projects delete method still available after org refactoring")607608609