Skip to content

Commit 63ab6ec

Browse files
author
Nils Bars
committed
Add E2E tests for SSH key types (RSA, Ed25519, ECDSA)
Tests verify that students can connect via SSH using different key types. Covers basic connection and file operations for each key type.
1 parent 22e18a0 commit 63ab6ec

1 file changed

Lines changed: 361 additions & 0 deletions

File tree

tests/e2e/test_ssh_key_types.py

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
"""
2+
E2E Test: SSH Key Type Support
3+
4+
Tests SSH authentication with different key types (RSA, ed25519, ECDSA).
5+
6+
This test module verifies that users can register with different SSH key types
7+
and successfully connect to exercise containers via SSH.
8+
"""
9+
10+
import uuid
11+
from pathlib import Path
12+
from typing import Callable, Optional
13+
14+
import pytest
15+
16+
from helpers.exercise_factory import create_sample_exercise
17+
from helpers.ssh_client import REFSSHClient
18+
from helpers.web_client import REFWebClient
19+
20+
SSHClientFactory = Callable[[str, str], REFSSHClient]
21+
22+
23+
class KeyTypeTestState:
24+
"""Shared state for key type tests."""
25+
26+
exercise_name: Optional[str] = None
27+
exercise_id: Optional[int] = None
28+
# RSA student
29+
rsa_mat_num: Optional[str] = None
30+
rsa_private_key: Optional[str] = None
31+
# ed25519 student
32+
ed25519_mat_num: Optional[str] = None
33+
ed25519_private_key: Optional[str] = None
34+
# ECDSA student
35+
ecdsa_mat_num: Optional[str] = None
36+
ecdsa_private_key: Optional[str] = None
37+
38+
student_password: str = "TestPassword123!"
39+
40+
41+
@pytest.fixture(scope="module")
42+
def key_type_state() -> KeyTypeTestState:
43+
"""Shared state fixture for key type tests."""
44+
return KeyTypeTestState()
45+
46+
47+
@pytest.fixture(scope="module")
48+
def kt_exercise_name() -> str:
49+
"""Generate a unique exercise name for key type tests."""
50+
return f"keytype_test_{uuid.uuid4().hex[:6]}"
51+
52+
53+
def _generate_ed25519_key_pair() -> tuple[str, str]:
54+
"""Generate an ed25519 key pair."""
55+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
56+
from cryptography.hazmat.primitives.serialization import (
57+
Encoding,
58+
NoEncryption,
59+
PrivateFormat,
60+
PublicFormat,
61+
)
62+
63+
private_key = Ed25519PrivateKey.generate()
64+
public_key = private_key.public_key()
65+
66+
private_pem = private_key.private_bytes(
67+
Encoding.PEM, PrivateFormat.OpenSSH, NoEncryption()
68+
).decode()
69+
public_openssh = public_key.public_bytes(
70+
Encoding.OpenSSH, PublicFormat.OpenSSH
71+
).decode()
72+
73+
return private_pem, public_openssh
74+
75+
76+
def _generate_ecdsa_key_pair() -> tuple[str, str]:
77+
"""Generate an ECDSA key pair."""
78+
from cryptography.hazmat.primitives.asymmetric import ec
79+
from cryptography.hazmat.primitives.serialization import (
80+
Encoding,
81+
NoEncryption,
82+
PrivateFormat,
83+
PublicFormat,
84+
)
85+
86+
private_key = ec.generate_private_key(ec.SECP256R1())
87+
public_key = private_key.public_key()
88+
89+
private_pem = private_key.private_bytes(
90+
Encoding.PEM, PrivateFormat.OpenSSH, NoEncryption()
91+
).decode()
92+
public_openssh = public_key.public_bytes(
93+
Encoding.OpenSSH, PublicFormat.OpenSSH
94+
).decode()
95+
96+
return private_pem, public_openssh
97+
98+
99+
@pytest.mark.e2e
100+
class TestKeyTypeSetup:
101+
"""
102+
Setup tests for key type testing.
103+
104+
Creates exercise and registers students with different key types.
105+
"""
106+
107+
def test_01_admin_login(
108+
self,
109+
web_client: REFWebClient,
110+
admin_password: str,
111+
):
112+
"""Verify admin can login."""
113+
web_client.logout()
114+
success = web_client.login("0", admin_password)
115+
assert success, "Admin login failed"
116+
117+
def test_02_create_exercise(
118+
self,
119+
exercises_path: Path,
120+
kt_exercise_name: str,
121+
key_type_state: KeyTypeTestState,
122+
):
123+
"""Create a test exercise for key type tests."""
124+
key_type_state.exercise_name = kt_exercise_name
125+
exercise_dir = exercises_path / kt_exercise_name
126+
127+
if exercise_dir.exists():
128+
import shutil
129+
130+
shutil.rmtree(exercise_dir)
131+
132+
create_sample_exercise(
133+
exercise_dir,
134+
short_name=kt_exercise_name,
135+
version=1,
136+
category="Key Type Tests",
137+
)
138+
139+
assert exercise_dir.exists(), "Exercise directory not created"
140+
141+
def test_03_import_and_build_exercise(
142+
self,
143+
admin_client: REFWebClient,
144+
exercises_path: Path,
145+
key_type_state: KeyTypeTestState,
146+
):
147+
"""Import and build the exercise."""
148+
assert key_type_state.exercise_name is not None
149+
150+
exercise_path = str(exercises_path / key_type_state.exercise_name)
151+
success = admin_client.import_exercise(exercise_path)
152+
assert success, "Failed to import exercise"
153+
154+
exercise = admin_client.get_exercise_by_name(key_type_state.exercise_name)
155+
assert exercise is not None
156+
exercise_id = exercise.get("id")
157+
assert exercise_id is not None, "Exercise ID not found"
158+
key_type_state.exercise_id = exercise_id
159+
160+
success = admin_client.build_exercise(exercise_id)
161+
assert success, "Failed to start exercise build"
162+
163+
build_success = admin_client.wait_for_build(exercise_id, timeout=300.0)
164+
assert build_success, "Exercise build did not complete"
165+
166+
def test_04_enable_exercise(
167+
self,
168+
admin_client: REFWebClient,
169+
key_type_state: KeyTypeTestState,
170+
):
171+
"""Enable the exercise."""
172+
assert key_type_state.exercise_id is not None
173+
success = admin_client.toggle_exercise_default(key_type_state.exercise_id)
174+
assert success, "Failed to enable exercise"
175+
176+
def test_05_register_rsa_student(
177+
self,
178+
web_client: REFWebClient,
179+
admin_password: str,
180+
key_type_state: KeyTypeTestState,
181+
):
182+
"""Register a test student with auto-generated RSA key."""
183+
web_client.logout()
184+
mat_num = str(uuid.uuid4().int)[:8]
185+
key_type_state.rsa_mat_num = mat_num
186+
187+
success, private_key, _ = web_client.register_student(
188+
mat_num=mat_num,
189+
firstname="RSA",
190+
surname="Tester",
191+
password=key_type_state.student_password,
192+
)
193+
194+
assert success, "Failed to register RSA student"
195+
assert private_key is not None
196+
key_type_state.rsa_private_key = private_key
197+
198+
# Re-login as admin
199+
web_client.login("0", admin_password)
200+
201+
def test_06_register_ed25519_student(
202+
self,
203+
web_client: REFWebClient,
204+
admin_password: str,
205+
key_type_state: KeyTypeTestState,
206+
):
207+
"""Register a test student with ed25519 key."""
208+
web_client.logout()
209+
mat_num = str(uuid.uuid4().int)[:8]
210+
key_type_state.ed25519_mat_num = mat_num
211+
212+
private_pem, public_openssh = _generate_ed25519_key_pair()
213+
214+
success, _, _ = web_client.register_student(
215+
mat_num=mat_num,
216+
firstname="Ed25519",
217+
surname="Tester",
218+
password=key_type_state.student_password,
219+
pubkey=public_openssh,
220+
)
221+
222+
assert success, "Failed to register ed25519 student"
223+
key_type_state.ed25519_private_key = private_pem
224+
225+
# Re-login as admin
226+
web_client.login("0", admin_password)
227+
228+
def test_07_register_ecdsa_student(
229+
self,
230+
web_client: REFWebClient,
231+
admin_password: str,
232+
key_type_state: KeyTypeTestState,
233+
):
234+
"""Register a test student with ECDSA key."""
235+
web_client.logout()
236+
mat_num = str(uuid.uuid4().int)[:8]
237+
key_type_state.ecdsa_mat_num = mat_num
238+
239+
private_pem, public_openssh = _generate_ecdsa_key_pair()
240+
241+
success, _, _ = web_client.register_student(
242+
mat_num=mat_num,
243+
firstname="ECDSA",
244+
surname="Tester",
245+
password=key_type_state.student_password,
246+
pubkey=public_openssh,
247+
)
248+
249+
assert success, "Failed to register ECDSA student"
250+
key_type_state.ecdsa_private_key = private_pem
251+
252+
# Re-login as admin
253+
web_client.login("0", admin_password)
254+
255+
256+
@pytest.mark.e2e
257+
class TestRSASSHConnection:
258+
"""Test SSH connection with RSA key."""
259+
260+
def test_ssh_connect_with_rsa(
261+
self,
262+
ssh_client_factory: SSHClientFactory,
263+
key_type_state: KeyTypeTestState,
264+
):
265+
"""Verify SSH connection works with RSA key."""
266+
assert key_type_state.rsa_private_key is not None
267+
assert key_type_state.exercise_name is not None
268+
269+
client = ssh_client_factory(
270+
key_type_state.rsa_private_key,
271+
key_type_state.exercise_name,
272+
)
273+
274+
assert client.is_connected(), "RSA SSH connection failed"
275+
276+
# Execute a simple command to verify the connection works
277+
exit_code, stdout, stderr = client.execute("echo 'RSA test'")
278+
assert exit_code == 0, f"Command failed with stderr: {stderr}"
279+
assert "RSA test" in stdout
280+
281+
client.close()
282+
283+
284+
@pytest.mark.e2e
285+
class TestEd25519SSHConnection:
286+
"""Test SSH connection with ed25519 key."""
287+
288+
def test_ssh_connect_with_ed25519(
289+
self,
290+
ssh_client_factory: SSHClientFactory,
291+
key_type_state: KeyTypeTestState,
292+
):
293+
"""Verify SSH connection works with ed25519 key."""
294+
assert key_type_state.ed25519_private_key is not None
295+
assert key_type_state.exercise_name is not None
296+
297+
client = ssh_client_factory(
298+
key_type_state.ed25519_private_key,
299+
key_type_state.exercise_name,
300+
)
301+
302+
assert client.is_connected(), "ed25519 SSH connection failed"
303+
304+
# Execute a simple command to verify the connection works
305+
exit_code, stdout, stderr = client.execute("echo 'ed25519 test'")
306+
assert exit_code == 0, f"Command failed with stderr: {stderr}"
307+
assert "ed25519 test" in stdout
308+
309+
client.close()
310+
311+
def test_file_operations_with_ed25519(
312+
self,
313+
ssh_client_factory: SSHClientFactory,
314+
key_type_state: KeyTypeTestState,
315+
):
316+
"""Verify file operations work over SSH with ed25519 key."""
317+
assert key_type_state.ed25519_private_key is not None
318+
assert key_type_state.exercise_name is not None
319+
320+
client = ssh_client_factory(
321+
key_type_state.ed25519_private_key,
322+
key_type_state.exercise_name,
323+
)
324+
325+
# Write a file
326+
test_content = "Test file content from ed25519 connection"
327+
client.write_file("/tmp/ed25519_test.txt", test_content)
328+
329+
# Read it back
330+
read_content = client.read_file("/tmp/ed25519_test.txt")
331+
assert read_content == test_content
332+
333+
client.close()
334+
335+
336+
@pytest.mark.e2e
337+
class TestECDSASSHConnection:
338+
"""Test SSH connection with ECDSA key."""
339+
340+
def test_ssh_connect_with_ecdsa(
341+
self,
342+
ssh_client_factory: SSHClientFactory,
343+
key_type_state: KeyTypeTestState,
344+
):
345+
"""Verify SSH connection works with ECDSA key."""
346+
assert key_type_state.ecdsa_private_key is not None
347+
assert key_type_state.exercise_name is not None
348+
349+
client = ssh_client_factory(
350+
key_type_state.ecdsa_private_key,
351+
key_type_state.exercise_name,
352+
)
353+
354+
assert client.is_connected(), "ECDSA SSH connection failed"
355+
356+
# Execute a simple command to verify the connection works
357+
exit_code, stdout, stderr = client.execute("echo 'ECDSA test'")
358+
assert exit_code == 0, f"Command failed with stderr: {stderr}"
359+
assert "ECDSA test" in stdout
360+
361+
client.close()

0 commit comments

Comments
 (0)