Skip to content

Commit 00a1a46

Browse files
committed
Added tests for problems.py endpoints. Removed unused console.log/print statements. User passwords must now be 7 chcaracters long at least.
1 parent 8e9585e commit 00a1a46

7 files changed

Lines changed: 243 additions & 34 deletions

File tree

backend/tests/routes/test_adventures.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,6 @@ def test_get_user_attempts_success(self, mock_service_class, adventure_client):
536536
mock_service.get_user_attempts.return_value = mock_attempts
537537

538538
response = adventure_client.get("/adventures/attempts")
539-
print(f"Status: {response.status_code}")
540-
print(f"Response: {response.json()}")
541539

542540
assert response.status_code == 200
543541
data = response.json()
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""This file contains all tests for the endpoints within routes/problems.py"""
2+
3+
4+
import pytest
5+
import json
6+
from unittest.mock import Mock, patch, AsyncMock
7+
from fastapi import status, FastAPI
8+
from fastapi.testclient import TestClient
9+
from sqlalchemy.orm import Session
10+
from datetime import datetime, timedelta
11+
import uuid
12+
from models.user import User as UserModel
13+
from schemas.problem import ProblemBase, ProblemCreate
14+
from services.code_execution_service import CodeExecutionService
15+
from services.problem_service import ProblemService, ProblemCreate, ProblemUpdate
16+
from exceptions import NotFoundError, ValidationError, AuthorisationError
17+
from routes.problems import router as problem_router
18+
19+
@pytest.fixture
20+
21+
def problem_app():
22+
"""Create a FastApi instance with the problem routes included"""
23+
app = FastAPI()
24+
app.include_router(problem_router)
25+
return app
26+
27+
28+
@pytest.fixture
29+
def problem_client(problem_app, db_session, test_user):
30+
"""This creates a test client with database and authentication overides"""
31+
32+
def get_test_db():
33+
"""This overides the database dependency for testing"""
34+
yield db_session
35+
36+
def get_current_user():
37+
"""This overides the authentication dependency for testsing"""
38+
yield test_user
39+
40+
from database import get_db
41+
from dependencies import get_current_user as get_current_user_dep
42+
43+
problem_app.dependency_overrides[get_db] = get_test_db
44+
problem_app.dependency_overrides[get_current_user_dep] = get_current_user
45+
46+
with TestClient(problem_app) as client:
47+
yield client
48+
49+
problem_app.dependency_overrides.clear()
50+
51+
52+
def test_create_problem_success(problem_client, test_user):
53+
"""Test successful problem creation"""
54+
form_data = {
55+
"title": "Test Problem",
56+
"description": "Test description",
57+
"code_snippet": "print('Hello')",
58+
"expected_output": "Hello",
59+
"language": "python",
60+
"is_public": True
61+
}
62+
63+
response = problem_client.post("/problems", data=form_data)
64+
data = response.json()
65+
66+
assert response.status_code == status.HTTP_200_OK
67+
assert data["message"] == "Problem created successfully"
68+
assert "access_code" in data
69+
assert "problem_id" in data
70+
71+
def test_create_problem_missing_fields(problem_client):
72+
"""Test creation with missing required fields"""
73+
form_data = {
74+
"title": "Incomplete Problem",
75+
"code_snippet": "print('Hello')",
76+
77+
}
78+
79+
response = problem_client.post("/problems", data=form_data)
80+
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
81+
82+
def test_get_user_problems_success(problem_client, test_user, db_session):
83+
"""Test retrieving user's problems"""
84+
85+
problem_service = ProblemService(db_session)
86+
for _ in range(3):
87+
problem_service.create_problem(
88+
ProblemCreate(
89+
title="Test",
90+
description="Test",
91+
code_snippet="code",
92+
expected_output="output",
93+
language="python",
94+
is_public=True
95+
),
96+
test_user
97+
)
98+
99+
response = problem_client.get("/problems")
100+
data = response.json()
101+
102+
assert response.status_code == status.HTTP_200_OK
103+
assert len(data) == 3
104+
for problem in data:
105+
assert problem["creator_id"] == test_user.id
106+
107+
def test_get_problem_by_access_code_success(problem_client, test_user, db_session):
108+
"""Test retrieving problem by valid access code"""
109+
problem_service = ProblemService(db_session)
110+
problem = problem_service.create_problem(
111+
ProblemCreate(
112+
title="Access Test",
113+
description="Test",
114+
code_snippet="code",
115+
expected_output="output",
116+
language="python",
117+
is_public=False
118+
),
119+
test_user
120+
)
121+
122+
response = problem_client.get(f"/problems/access/{problem.access_code}")
123+
data = response.json()
124+
125+
assert response.status_code == status.HTTP_200_OK
126+
assert data["id"] == problem.id
127+
assert data["access_code"] == problem.access_code
128+
129+
def test_get_problem_by_access_code_not_found(problem_client):
130+
"""Test invalid access code returns 404"""
131+
response = problem_client.get("/problems/access/invalid_code")
132+
assert response.status_code == status.HTTP_404_NOT_FOUND
133+
134+
def test_delete_problem_success(problem_client, test_user, db_session):
135+
"""Test successful problem deletion"""
136+
problem_service = ProblemService(db_session)
137+
problem = problem_service.create_problem(
138+
ProblemCreate(
139+
title="Delete Test",
140+
description="Test",
141+
code_snippet="code",
142+
expected_output="output",
143+
language="python",
144+
is_public=True
145+
),
146+
test_user
147+
)
148+
149+
response = problem_client.delete(f"/problems/{problem.id}")
150+
data = response.json()
151+
152+
assert response.status_code == status.HTTP_200_OK
153+
assert data["message"] == "Problem deleted successfully"
154+
155+
response = problem_client.get(f"/problems/access/{problem.access_code}")
156+
assert response.status_code == status.HTTP_404_NOT_FOUND
157+
158+
def test_delete_problem_not_found(problem_client):
159+
"""Test deleting non-existent problem"""
160+
response = problem_client.delete("/problems/9999")
161+
assert response.status_code == status.HTTP_404_NOT_FOUND
162+
163+
def test_delete_problem_unauthorized(problem_client, db_session, test_user):
164+
"""Test deleting another user's problem"""
165+
166+
other_user = UserModel(
167+
email="other@test.com",
168+
password_hash="password",
169+
username="otheruser"
170+
)
171+
db_session.add(other_user)
172+
db_session.commit()
173+
db_session.refresh(other_user)
174+
175+
176+
problem_service = ProblemService(db_session)
177+
problem = problem_service.create_problem(
178+
ProblemCreate(
179+
title="Unauthorized Delete",
180+
description="Test",
181+
code_snippet="code",
182+
expected_output="output",
183+
language="python",
184+
is_public=True
185+
),
186+
other_user
187+
)
188+
189+
response = problem_client.delete(f"/problems/{problem.id}")
190+
assert response.status_code == status.HTTP_403_FORBIDDEN
191+
192+
193+
def test_unauthenticated_access(problem_client, problem_app):
194+
"""Test endpoints without authentication"""
195+
196+
from dependencies import get_current_user as get_current_user_dep
197+
problem_app.dependency_overrides.pop(get_current_user_dep)
198+
199+
response = problem_client.post("/problems", data={})
200+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
201+
202+
response = problem_client.get("/problems")
203+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
204+
205+
response = problem_client.delete("/problems/1")
206+
assert response.status_code == status.HTTP_401_UNAUTHORIZED

backend/tests/routes/test_submissions.py

Whitespace-only changes.

frontend/src/api/adventure.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,31 +82,34 @@ export const getAdventureAttempt = async (adventureId: number, token?: string):
8282
}
8383
}
8484

85-
export const getAdventureByAccessCode = async (accessCode: string, headers: Record<string, string>): Promise<DetailedAdventure> => {
85+
export const getAdventureByAccessCode = async (
86+
accessCode: string,
87+
headers: Record<string, string>
88+
): Promise<DetailedAdventure> => {
8689
try {
87-
88-
const res = await axios.get<DetailedAdventure>(
89-
`${FASTAPI_BACKEND_URL}/api/adventures/access/${accessCode}`,
90-
{ headers }
91-
);
92-
93-
return res.data
94-
} catch(err) {
95-
const error = err as AxiosError;
96-
const status = error.response?.status;
97-
const data = error.response?.data;
98-
if (status === 401) {
99-
throw new Error("Unauthorized: Please log in again.");
100-
} else if (status === 422 && isValidationErrorResponse(data)) {
101-
const messages = data.detail.map((d) => d.msg).join(", ");
102-
throw new Error(`Validation error: ${messages}`);
103-
} else if (typeof data === "string") {
104-
throw new Error(data);
105-
} else {
106-
throw new Error("Something went wrong. Try again later.");
107-
}
90+
const res = await axios.get<DetailedAdventure>(
91+
`${FASTAPI_BACKEND_URL}/api/adventures/access/${accessCode}`,
92+
{ headers }
93+
);
94+
return res.data;
95+
} catch (err) {
96+
const error = err as AxiosError;
97+
const status = error.response?.status;
98+
const data = error.response?.data;
99+
100+
if (status === 401) {
101+
throw new Error("Unauthorized: Please log in again.");
102+
} else if (status === 422 && isValidationErrorResponse(data)) {
103+
const messages = data.detail.map((d) => d.msg).join(", ");
104+
throw new Error(`Validation error: ${messages}`);
105+
}
106+
if (status === 404) {
107+
throw error;
108108
}
109+
110+
throw new Error("Something went wrong. Try again later.");
109111
}
112+
}
110113

111114
export const submitGuestCode = async (form: URLSearchParams): Promise<AdventureSubmissionResponse> => {
112115
try {

frontend/src/hooks/useAdventureGraph.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const useAdventureGraph = () => {
5959

6060
const condition = edge.data?.condition;
6161
if (condition !== "correct" && condition !== "incorrect") {
62-
return `Invalid edge condition "${condition}". Only "correct" and "incorrect" edges are allowed.`;
62+
return `Invalid edge condition "${condition}". Only "correct" and "incorrect" edges are allowed. See the instructions page for examples of valid/invalid graphs`;
6363
}
6464

6565
// edge maps
@@ -76,12 +76,12 @@ export const useAdventureGraph = () => {
7676
);
7777

7878
if (startingNodes.length === 0) {
79-
return "Adventure must have a starting problem (with no incoming connections)";
79+
return "Adventure must have a starting problem (with no incoming connections). See the instructions page for examples of valid/invalid graphs";
8080
}
8181

8282
if (startingNodes.length > 1) {
8383
return "Adventure can only have one starting problem. Currently has: " +
84-
startingNodes.map(n => n.data.title).join(", ");
84+
startingNodes.map(n => n.data.title).join(", ") + " (Starting nodes are nodes with no incoming edges)";
8585
}
8686

8787
const endingNodes = nodes.filter(node =>
@@ -93,7 +93,7 @@ export const useAdventureGraph = () => {
9393
}
9494

9595
if (endingNodes.length > 1) {
96-
return `Adventure can only have one ending problem. Currently has ${endingNodes.length}: ` + endingNodes.map(n => n.data.title).join(", ");
96+
return `Adventure can only have one ending problem. Currently has ${endingNodes.length}: ` + endingNodes.map(n => n.data.title).join(", ") + " (ending nodes are nodes with no outgoing edges)";
9797
}
9898

9999
// Each problem node must be reachable

frontend/src/hooks/useAttemptAdventure.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ export const useAttemptAdventure = (accessCode: string | undefined): UseAttemptA
109109
);
110110
})
111111
.catch((err) => {
112-
console.error("Failed to fetch adventure:", err);
113112
setAdventure(null);
114113
setLoading(false);
115-
114+
116115
if (err.response?.status === 404) {
117116
setError("Adventure not found. Please check the access code and try again.");
118117
} else if (err.response?.status === 401 || err.response?.status === 403) {
@@ -121,6 +120,7 @@ export const useAttemptAdventure = (accessCode: string | undefined): UseAttemptA
121120
setError("Failed to load adventure. Please try again later.");
122121
}
123122
});
123+
124124
}, [accessCode, navigate]);
125125

126126

@@ -169,24 +169,22 @@ export const useAttemptAdventure = (accessCode: string | undefined): UseAttemptA
169169
const currentNodeId = attempt.current_node_id;
170170

171171
if (currentNodeId && (previousNodeId.current !== currentNodeId || previousNodeId.current === null)) {
172-
console.log('Loading code for node:', currentNodeId);
173172

174173
const nodeEntries = attempt.path_taken
175174
?.filter(entry => entry.node_id === currentNodeId && entry.code) || [];
176175

177176
if (nodeEntries.length > 0) {
178177

179178
const codeToLoad = nodeEntries[nodeEntries.length - 1].code || "";
180-
console.log('Loading previous submission:', codeToLoad);
181179
setCode(codeToLoad);
182180
} else {
183181

184182
const currentNode = nodes.find(n => n.id === currentNodeId);
185183
if (currentNode && currentNode.data.code_snippet) {
186-
console.log('Loading fresh code snippet:', currentNode.data.code_snippet);
184+
187185
setCode(currentNode.data.code_snippet);
188186
} else {
189-
console.log('No code snippet found, clearing code');
187+
190188
setCode("");
191189
}
192190
}

frontend/src/pages/Signup.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export default function Signup() {
2222

2323
const handleSubmit = async (e: React.FormEvent) => {
2424
e.preventDefault();
25+
if (formData.password.length < 7) {
26+
setMessage("Password must be at least 7 characters long.");
27+
return;
28+
}
2529
try {
2630
const res = await axios.post(
2731
`${FASTAPI_BACKEND_URL}/api/signup`,

0 commit comments

Comments
 (0)