Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
integration_test_case_targets := integration-test-case1 integration-test-case2 integration-test-case3 integration-test-case5
integration_test_case_targets := integration-test-case1 integration-test-case2 integration-test-case3 integration-test-case5 integration-test-case7

.NOTPARALLEL: all
.PHONY: all
Expand Down Expand Up @@ -38,7 +38,7 @@ test:

.PHONY: $(integration_test_case_targets)
$(integration_test_case_targets): integration-test-%:
KTP_CONTROLLER_INTEGRATION_TEST_CASE='$(@:integration-test-%=%)' uv run supervisord -c supervisor/integration-test.conf
KTP_CONTROLLER_DOTENV='$(@:%=%.env)' KTP_CONTROLLER_INTEGRATION_TEST_CASE='$(@:integration-test-%=%)' uv run supervisord -c supervisor/integration-test.conf
@grep -q -x ok chain_result

.NOTPARALLEL: integration-test
Expand Down
2 changes: 1 addition & 1 deletion aux/setup-puavo-os-for-development.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if [ -e /dev/virtio-ports/com.redhat.spice.0 ]; then
sudo systemctl start spice-vdagentd
fi

sudo rsync -r --delete-after "$(git rev-parse --show-toplevel)/" /home/puavo-ers/ktp-controller/
sudo rsync -rl --delete-after "$(git rev-parse --show-toplevel)/" /home/puavo-ers/ktp-controller/
sudo chown -R puavo-ers:puavo-ers /home/puavo-ers/ktp-controller

for g in puavo; do
Expand Down
1 change: 1 addition & 0 deletions integration-test-case1.env
1 change: 1 addition & 0 deletions integration-test-case2.env
1 change: 1 addition & 0 deletions integration-test-case3.env
1 change: 1 addition & 0 deletions integration-test-case5.env
20 changes: 20 additions & 0 deletions integration-test-case7.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Customize values for your own development needs and rename this file
# to for example mydev.env and use
# KTP_CONTROLLER_DOTENV=mydev.env
# environment variable.
KTP_CONTROLLER_EXAMOMATIC_HOST=127.0.0.1:8001
KTP_CONTROLLER_EXAMOMATIC_USE_TLS=false
KTP_CONTROLLER_EXAMOMATIC_USERNAME=integration.tester1
KTP_CONTROLLER_EXAMOMATIC_PASSWORD_FILE=tests/examomatic-password.txt
KTP_CONTROLLER_EXAMOMATIC_PING_INTERVAL_SEC=30
KTP_CONTROLLER_ANSWER_TRANSFER_INTERVAL_SEC=300
KTP_CONTROLLER_REFRESH_EXAMS_INTERVAL_SEC=5
KTP_CONTROLLER_DOMAIN=integration.test
KTP_CONTROLLER_HOSTNAME=integration-test-host1
KTP_CONTROLLER_ID=1
KTP_CONTROLLER_API_HOST=127.0.0.1
KTP_CONTROLLER_API_PORT=8000
KTP_CONTROLLER_LOGGING_LEVEL=INFO
KTP_CONTROLLER_DB_PATH=tests/ktp_controller.sqlite
KTP_CONTROLLER_ABITTI2_ALLOW_STUDENTS_TO_USE_BROWSERS=true
KTP_CONTROLLER_ABITTI2_CHANGE_STUDENT_ACCESS_CODE_AUTOMATICALLY=false
1 change: 1 addition & 0 deletions ktp_controller.env
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ KTP_CONTROLLER_API_PORT=8000
KTP_CONTROLLER_LOGGING_LEVEL=INFO
KTP_CONTROLLER_DB_PATH=tests/ktp_controller.sqlite
KTP_CONTROLLER_ABITTI2_ALLOW_STUDENTS_TO_USE_BROWSERS=true
KTP_CONTROLLER_ABITTI2_CHANGE_STUDENT_ACCESS_CODE_AUTOMATICALLY=true
10 changes: 8 additions & 2 deletions ktp_controller/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,10 @@ async def __prepare_current_exam_package(
)
self.__uploaded = True

if self.__is_auto_control_enabled:
if (
self.__is_auto_control_enabled
and SETTINGS.abitti2_change_student_access_code_automatically
):
if self.__old_security_code is None:
self.__old_security_code = self.__last_received_security_code
_LOGGER.info(
Expand Down Expand Up @@ -621,7 +624,10 @@ async def __stop_current_exam_package(
self,
current_exam_package: typing.Dict[str, typing.Any],
) -> bool:
if self.__is_auto_control_enabled:
if (
self.__is_auto_control_enabled
and SETTINGS.abitti2_change_student_access_code_automatically
):
# Change the security code first to ensure students cannot enter anymore.
await ktp_controller.abitti2.client.change_student_access_code()

Expand Down
16 changes: 16 additions & 0 deletions ktp_controller/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class Settings(BaseSettings):
logging_level: str = "INFO"
db_path: str = "ktp_controller.sqlite"
abitti2_allow_students_to_use_browsers: StrictBool = False
abitti2_change_student_access_code_automatically: StrictBool = True

@field_validator("examomatic_use_tls", mode="before")
@classmethod
Expand All @@ -114,6 +115,21 @@ def _validate_abitti2_allow_students_to_use_browsers(cls, v) -> typing.Any:
raise ValueError("invalid abitti2_allow_students_to_use_browsers value", v)
return v

@field_validator("abitti2_change_student_access_code_automatically", mode="before")
@classmethod
def _validate_abitti2_change_student_access_code_automatically(
cls, v
) -> typing.Any:
if isinstance(v, str):
if v.lower().strip() in ["yes", "y", "true", "1"]:
return True
if v.lower().strip() in ["no", "n", "false", "0"]:
return False
raise ValueError(
"invalid abitti2_change_student_access_code_automatically value", v
)
return v

@classmethod
def settings_customise_sources(
cls,
Expand Down
152 changes: 152 additions & 0 deletions tests/integration_test_case7.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Standard library imports
import asyncio
import datetime
import time

# Third-party imports

# Internal imports
import ktp_controller.abitti2.client
import ktp_controller.abitti2.naksu2
import ktp_controller.api.client
import ktp_controller.examomatic.client
import ktp_controller.schemas

# Relative imports
from .utils import (
assert_abitti2_running_exams,
assert_all_answer_uploads_are_successful,
assert_clean_start,
assert_exam_scheduling_and_download,
assert_last_status_report_does_not_contain_student_names,
assert_last_status_report_has_abitti2_domain,
assert_student_access_code_is,
assert_scheduled_exam_package_gets_started,
assert_scheduled_exam_package_state_is,
)

# Test functions are and must be executed sequentially. In unit tests,
# it's not a good idea to build tests which depend on each other, but
# this is integration test scenario, and pytest is just a neat way to
# run them too. So, each test function is a sequential step in the
# testrun.


def test_clean_start(testrunstate):
testrunstate.student_access_code = assert_clean_start()


def test_student1_take_waiting_lobby_exam(student1, testrunstate):
student1.load()
student1.start_exam(
exam_uuid="390e7988-ff0e-42b4-a2e6-d13a969e7103",
exam_title="Odotusaulakoe",
access_code=testrunstate.student_access_code,
)
student1.end_exam()


def test_student2_take_waiting_lobby_exam_but_do_not_end_it(student2, testrunstate):
student2.load()
student2.start_exam(
exam_uuid="390e7988-ff0e-42b4-a2e6-d13a969e7103",
exam_title="Odotusaulakoe",
access_code=testrunstate.student_access_code,
)


def test_exam_package_is_scheduled_and_downloaded(utcnow):
assert_exam_scheduling_and_download(
exam_title="Integraatiotestikoe1",
seconds_until_start=60,
duration_seconds=90,
lock_time_duration_seconds=30,
expected_ack_count=1,
utcnow=utcnow,
)


def test_last_status_report_has_abitti2_domain():
assert_last_status_report_has_abitti2_domain()


def test_exam_package_gets_started(testrunstate):
testrunstate.scheduled_exam_package1 = assert_scheduled_exam_package_gets_started(
"Integraatiotestikoe1"
)


def test_student_access_code_is_NOT_changed_when_exam_package_is_started(
testrunstate,
):
testrunstate.student_access_code = assert_student_access_code_is("1234", "xx")


def test_student1_take_scheduled_exam(student1, testrunstate):
student1.relogin() # Exam has changed, relogin is needed.
student1.start_exam(
exam_uuid="c574f93a-ac4d-4441-8679-ca47e565fb7b",
exam_title="Integraatiotestikoe1",
access_code=testrunstate.student_access_code,
expect_exam_instructions=False, # Already seen in this browsing session, Abitti2 seems to show it only once.
)
student1.end_exam()


def test_student2_take_scheduled_exam_but_do_not_end_it(student2, testrunstate):
student2.relogin() # Exam has changed, relogin is needed.
student2.start_exam(
exam_uuid="c574f93a-ac4d-4441-8679-ca47e565fb7b",
exam_title="Integraatiotestikoe1",
access_code=testrunstate.student_access_code,
expect_exam_instructions=False, # Already seen in this browsing session, Abitti2 seems to show it only once.
)


def test_last_status_report_does_not_contain_student_names(student1, student2):
assert_last_status_report_does_not_contain_student_names(student1, student2)


def test_exam_package_does_not_get_stopped_until_student2_ends_exam(
student2, testrunstate, utcnow
):
until_scheduled_end = (
datetime.datetime.fromisoformat(
testrunstate.scheduled_exam_package1["end_time"]
)
- utcnow
).total_seconds()
time.sleep(until_scheduled_end + 10)

testrunstate.scheduled_exam_package1 = assert_scheduled_exam_package_state_is(
"running",
external_id=testrunstate.scheduled_exam_package1["external_id"],
wait=0,
)

student2.end_exam()

testrunstate.scheduled_exam_package1 = assert_scheduled_exam_package_state_is(
"stopped",
external_id=testrunstate.scheduled_exam_package1["external_id"],
wait=60,
)


def test_exam_package_gets_archived(testrunstate):
testrunstate.scheduled_exam_package1 = assert_scheduled_exam_package_state_is(
"archived",
external_id=testrunstate.scheduled_exam_package1["external_id"],
wait=90,
)
assert asyncio.run(ktp_controller.api.client.get_current_exam_package()) is None


def test_all_answer_uploads_are_successful():
assert_all_answer_uploads_are_successful()


def test_odotusaulakoe_is_running_again():
assert_abitti2_running_exams(
lambda running_exams: "Odotusaulakoe" in running_exams, wait=30
)