Skip to content

Commit 29dc582

Browse files
committed
cocalc-api: fix tests, creating accounts started many project servers – which is actually something a mere API call via the org management should not do
1 parent 200810a commit 29dc582

File tree

6 files changed

+65
-81
lines changed

6 files changed

+65
-81
lines changed

src/packages/server/accounts/account-creation-actions.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ export default async function accountCreationActions({
1616
account_id,
1717
tags,
1818
noFirstProject,
19+
dontStartProject,
1920
}: {
2021
email_address?: string;
2122
account_id: string;
2223
tags?: string[];
2324
// if set, don't do any initial project actions (i.e., creating or starting projects)
2425
noFirstProject?: boolean;
26+
// if set, create the first project but do not start it. Only applies if noFirstProject is false.
27+
dontStartProject?: boolean;
2528
}): Promise<void> {
2629
log.debug({ account_id, email_address, tags });
2730

@@ -56,7 +59,7 @@ export default async function accountCreationActions({
5659
const projects = await getProjects({ account_id, limit: 1 });
5760
if (projects.length == 0) {
5861
// you really have no projects at all.
59-
await firstProject({ account_id, tags });
62+
await firstProject({ account_id, tags, dontStartProject });
6063
}
6164
} catch (err) {
6265
// non-fatal; they can make their own project
@@ -65,19 +68,22 @@ export default async function accountCreationActions({
6568
})();
6669
} else if (numProjects > 0) {
6770
// Make sure project is running so they have a good first experience.
68-
(async () => {
69-
try {
70-
const { project_id } = await getOneProject(account_id);
71-
const project = getProject(project_id);
72-
await project.start();
73-
} catch (err) {
74-
log.error(
75-
"failed to start newest project invited to",
76-
err,
77-
account_id,
78-
);
79-
}
80-
})();
71+
// Only start if dontStartProject is not set
72+
if (!dontStartProject) {
73+
(async () => {
74+
try {
75+
const { project_id } = await getOneProject(account_id);
76+
const project = getProject(project_id);
77+
await project.start();
78+
} catch (err) {
79+
log.error(
80+
"failed to start newest project invited to",
81+
err,
82+
account_id,
83+
);
84+
}
85+
})();
86+
}
8187
}
8288
}
8389
}

src/packages/server/accounts/create-account.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ interface Params {
2727
// I added this to avoid leaks with unit testing, but it may be useful in other contexts, e.g.,
2828
// avoiding confusion with self-hosted installs.
2929
noFirstProject?: boolean;
30+
// if set, create the first project but do not start it. Only applies if noFirstProject is false.
31+
dontStartProject?: boolean;
3032
}
3133

3234
export default async function createAccount({
@@ -39,6 +41,7 @@ export default async function createAccount({
3941
signupReason,
4042
owner_id,
4143
noFirstProject,
44+
dontStartProject,
4245
}: Params): Promise<void> {
4346
try {
4447
log.debug(
@@ -78,11 +81,11 @@ export default async function createAccount({
7881
account_id,
7982
tags,
8083
noFirstProject,
84+
dontStartProject,
8185
});
8286
await creationActionsDone(account_id);
8387
} catch (error) {
8488
log.error("Error creating account", error);
8589
throw error; // re-throw to bubble up to higher layers if needed
8690
}
8791
}
88-

src/packages/server/accounts/first-project.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ const log = getLogger("server:accounts:first-project");
2222
export default async function firstProject({
2323
account_id,
2424
tags,
25+
dontStartProject,
2526
}: {
2627
account_id: string;
2728
tags?: string[];
29+
dontStartProject?: boolean;
2830
}): Promise<string> {
2931
log.debug(account_id, tags);
3032
if (!isValidUUID(account_id)) {
@@ -35,8 +37,10 @@ export default async function firstProject({
3537
title: "My First Project",
3638
});
3739
log.debug("created new project", project_id);
38-
const project = getProject(project_id);
39-
await project.start();
40+
if (!dontStartProject) {
41+
const project = getProject(project_id);
42+
await project.start();
43+
}
4044
if (!WELCOME_FILES || tags == null || tags.length == 0) {
4145
return project_id;
4246
}
@@ -57,7 +61,7 @@ export default async function firstProject({
5761
account_id,
5862
project_id,
5963
language,
60-
welcome + jupyterExtra
64+
welcome + jupyterExtra,
6165
);
6266
}
6367
}
@@ -78,7 +82,7 @@ async function createJupyterNotebookIfAvailable(
7882
account_id: string,
7983
project_id: string,
8084
language: string,
81-
welcome: string
85+
welcome: string,
8286
): Promise<string> {
8387
// find the highest priority kernel with the given language
8488
let kernelspec: any = null;
@@ -129,7 +133,7 @@ async function createWelcome(
129133
account_id: string,
130134
project_id: string,
131135
ext: string,
132-
welcome: string
136+
welcome: string,
133137
): Promise<string> {
134138
const path = `welcome/welcome.${ext}`;
135139
const { torun } = TAGS_MAP[ext] ?? {};

src/packages/server/conat/api/org.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ export async function createUser({
285285
account_id: new_account_id,
286286
owner_id: account_id,
287287
password,
288+
dontStartProject: true, // Don't auto-start projects for API-created users. A "first project" will be created, though.
288289
});
289290
// add account to org
290291
const pool = getPool();

src/python/cocalc-api/tests/conftest.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Pytest configuration and fixtures for cocalc-api tests.
33
"""
44
import os
5+
import time
56
import uuid
67
import pytest
78

@@ -25,6 +26,25 @@ def assert_valid_uuid(value, description="value"):
2526
pytest.fail(f"{description} should be a valid UUID, got: {value}")
2627

2728

29+
def cleanup_project(hub, project_id):
30+
"""
31+
Clean up a test project by stopping it and deleting it.
32+
33+
Args:
34+
hub: Hub client instance
35+
project_id: Project ID to cleanup
36+
"""
37+
try:
38+
hub.projects.stop(project_id)
39+
except Exception as e:
40+
print(f"Warning: Failed to stop project {project_id}: {e}")
41+
42+
try:
43+
hub.projects.delete(project_id)
44+
except Exception as e:
45+
print(f"Warning: Failed to delete project {project_id}: {e}")
46+
47+
2848
@pytest.fixture(scope="session")
2949
def api_key():
3050
"""Get API key from environment variable."""
@@ -59,18 +79,11 @@ def temporary_project(hub, request):
5979
title = f"CoCalc API Test {timestamp}"
6080
description = "Temporary project created by cocalc-api tests"
6181

62-
print("\n" + "="*70)
63-
print("=== Creating temporary project for entire test session ===")
64-
print("=== THIS SHOULD ONLY PRINT ONCE ===")
65-
print("="*70)
6682
project_id = hub.projects.create_project(title=title, description=description)
67-
print(f"Created project {project_id}")
68-
print("="*70)
6983

7084
# Start the project so it can respond to API calls
7185
try:
7286
hub.projects.start(project_id)
73-
print(f"Starting project {project_id}, waiting for it to become ready...")
7487

7588
# Wait for project to be ready (can take 10-15 seconds)
7689
from cocalc_api import Project
@@ -81,39 +94,19 @@ def temporary_project(hub, request):
8194
# Try to ping the project to see if it's ready
8295
test_project = Project(project_id=project_id, api_key=hub.api_key, host=hub.host)
8396
test_project.system.ping() # If this succeeds, project is ready
84-
print(f"✓ Project {project_id} is ready after {(attempt + 1) * 5} seconds")
8597
break
8698
except Exception:
8799
if attempt == 9: # Last attempt
88-
print(f"Warning: Project {project_id} did not become ready within 50 seconds")
100+
print(f"Warning: Project {project_id} did not become ready within 50 seconds")
89101

90102
except Exception as e:
91-
print(f"Warning: Failed to start project {project_id}: {e}")
103+
print(f"Warning: Failed to start project {project_id}: {e}")
92104

93105
project_info = {'project_id': project_id, 'title': title, 'description': description}
94106

95-
# Register cleanup using finalizer (more reliable than yield teardown)
107+
# Register cleanup using finalizer
96108
def cleanup():
97-
print(f"\n=== Cleaning up test project '{title}' (ID: {project_id}) ===")
98-
99-
try:
100-
# Stop the project first
101-
print(f"Stopping project {project_id}...")
102-
hub.projects.stop(project_id)
103-
print("✓ Project stop command sent")
104-
# Wait for the project process to actually terminate
105-
time.sleep(3)
106-
print(f"✓ Waited for project {project_id} to stop")
107-
except Exception as e:
108-
print(f"⚠ Failed to stop project {project_id}: {e}")
109-
110-
try:
111-
# Delete the project
112-
print(f"Deleting project {project_id}...")
113-
hub.projects.delete(project_id)
114-
print(f"✓ Project {project_id} deleted")
115-
except Exception as e:
116-
print(f"⚠ Failed to delete project {project_id}: {e}")
109+
cleanup_project(hub, project_id)
117110

118111
request.addfinalizer(cleanup)
119112

src/python/cocalc-api/tests/test_hub.py

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from cocalc_api import Hub, Project
8-
from .conftest import assert_valid_uuid
8+
from .conftest import assert_valid_uuid, cleanup_project
99

1010

1111
class TestHubSystem:
@@ -69,19 +69,9 @@ def test_create_project(self, hub):
6969
try:
7070
assert project_id is not None
7171
assert_valid_uuid(project_id, "Project ID")
72-
print(f"✓ Created project: {project_id}")
7372
finally:
7473
# Cleanup: stop then delete the project
75-
try:
76-
print(f"Cleaning up test project {project_id}...")
77-
hub.projects.stop(project_id)
78-
print("✓ Project stop command sent")
79-
time.sleep(3) # Wait for process to terminate
80-
print(f"✓ Waited for project {project_id} to stop")
81-
hub.projects.delete(project_id)
82-
print(f"✓ Project {project_id} deleted")
83-
except Exception as e:
84-
print(f"⚠ Failed to cleanup project {project_id}: {e}")
74+
cleanup_project(hub, project_id)
8575

8676
def test_list_projects(self, hub):
8777
"""Test listing projects."""
@@ -162,15 +152,8 @@ def test_project_lifecycle(self, hub):
162152
print("5. Skipping command execution - project not ready")
163153

164154
# 3. Stop and delete the project
165-
print("6. Stopping project...")
166-
hub.projects.stop(project_id)
167-
print(" ✓ Project stop command sent")
168-
time.sleep(3) # Wait for process to terminate
169-
print(" ✓ Waited for project to stop")
170-
171-
print("7. Deleting project...")
172-
delete_result = hub.projects.delete(project_id)
173-
print(f" ✓ Delete result: {delete_result}")
155+
print("6. Stopping and deleting project...")
156+
cleanup_project(hub, project_id)
174157

175158
# 4. Verify project is marked as deleted in database
176159
print("8. Verifying project is marked as deleted...")
@@ -185,15 +168,9 @@ def test_project_lifecycle(self, hub):
185168

186169
except Exception as e:
187170
# Cleanup: attempt to stop and delete project if test fails
188-
print(f"\n❌ Test failed: {e}")
171+
print(f"\nTest failed: {e}")
189172
try:
190-
print("Attempting cleanup: stopping then deleting project...")
191-
hub.projects.stop(project_id)
192-
print("✓ Project stop command sent")
193-
time.sleep(3) # Wait for process to terminate
194-
print("✓ Waited for project to stop")
195-
hub.projects.delete(project_id)
196-
print("✓ Project deleted")
173+
cleanup_project(hub, project_id)
197174
except Exception as cleanup_error:
198-
print(f"Cleanup failed: {cleanup_error}")
175+
print(f"Cleanup failed: {cleanup_error}")
199176
raise e

0 commit comments

Comments
 (0)