diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 325761306..4f551ef10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,11 +116,11 @@ jobs: exit 0 - name: Run tests - timeout-minutes: 4 + timeout-minutes: 12 run: | export MOZ_HEADLESS=1 export DOJO_SSH_PORT=2222 - timeout 3m pytest -vrpP --order-dependencies --durations=0 ./test || (docker exec dojo-test dojo compose logs && false) + timeout 10m pytest -vrpP --order-dependencies --durations=0 ./test || (docker exec dojo-test dojo compose logs && false) - name: Pack docker data cache if: github.event_name == 'schedule' diff --git a/dojo_theme/static/js/dojo/challenges.js b/dojo_theme/static/js/dojo/challenges.js index 645905988..c763a4f1e 100644 --- a/dojo_theme/static/js/dojo/challenges.js +++ b/dojo_theme/static/js/dojo/challenges.js @@ -174,6 +174,13 @@ function unlockChallenge(challenge_button) { function startChallenge(event) { event.preventDefault(); + + if (!localStorage.getItem('groundRulesAccepted')) { + window.pendingChallengeEvent = event; + $('#groundRulesModal').modal('show'); + return; + } + const item = $(event.currentTarget).closest(".accordion-item"); const module = item.find("#module").val() const challenge = item.find("#challenge").val() diff --git a/dojo_theme/static/js/dojo/groundrules.dev.js b/dojo_theme/static/js/dojo/groundrules.dev.js new file mode 120000 index 000000000..cf8e19399 --- /dev/null +++ b/dojo_theme/static/js/dojo/groundrules.dev.js @@ -0,0 +1 @@ +groundrules.js \ No newline at end of file diff --git a/dojo_theme/static/js/dojo/groundrules.js b/dojo_theme/static/js/dojo/groundrules.js new file mode 100644 index 000000000..22e46f4cf --- /dev/null +++ b/dojo_theme/static/js/dojo/groundrules.js @@ -0,0 +1,56 @@ +$(document).ready(function() { + const requiredText = "I have read the ground rules and commit to not publish writeups on the internet."; + + function normalizeText(text) { + return text.toLowerCase().replace(/[^a-z]/g, ''); + } + + const normalizedRequired = normalizeText(requiredText); + + $('#groundRulesInput').on('input', function() { + const inputValue = $(this).val().trim(); + const normalizedInput = normalizeText(inputValue); + const isValid = normalizedInput === normalizedRequired; + + if (isValid) { + $(this).removeClass('is-invalid').addClass('is-valid'); + $('#acceptGroundRules').prop('disabled', false); + } else { + $(this).removeClass('is-valid'); + if (inputValue.length > 0) { + $(this).addClass('is-invalid'); + } + $('#acceptGroundRules').prop('disabled', true); + } + }); + + $('#acceptGroundRules').click(function() { + const inputValue = $('#groundRulesInput').val().trim(); + const normalizedInput = normalizeText(inputValue); + if (normalizedInput === normalizedRequired) { + localStorage.setItem('groundRulesAccepted', 'true'); + + $('#groundRulesModal').modal('hide'); + + $('#groundRulesInput').val('').removeClass('is-valid is-invalid'); + $('#acceptGroundRules').prop('disabled', true); + + if (window.pendingChallengeEvent && typeof startChallenge === 'function') { + const event = window.pendingChallengeEvent; + delete window.pendingChallengeEvent; + startChallenge(event); + } + + if (window.pendingNavbarChallengeEvent && typeof DropdownStartChallenge === 'function') { + const event = window.pendingNavbarChallengeEvent; + delete window.pendingNavbarChallengeEvent; + setTimeout(() => DropdownStartChallenge(event), 100); + } + } + }); + + $('#groundRulesModal').on('hidden.bs.modal', function() { + $('#groundRulesInput').val('').removeClass('is-valid is-invalid'); + $('#acceptGroundRules').prop('disabled', true); + }); +}); diff --git a/dojo_theme/static/js/dojo/groundrules.min.js b/dojo_theme/static/js/dojo/groundrules.min.js new file mode 120000 index 000000000..cf8e19399 --- /dev/null +++ b/dojo_theme/static/js/dojo/groundrules.min.js @@ -0,0 +1 @@ +groundrules.js \ No newline at end of file diff --git a/dojo_theme/static/js/dojo/navbar.js b/dojo_theme/static/js/dojo/navbar.js index ba12fd3a4..f40381576 100644 --- a/dojo_theme/static/js/dojo/navbar.js +++ b/dojo_theme/static/js/dojo/navbar.js @@ -73,6 +73,13 @@ async function updateNavbarDropdown() { function DropdownStartChallenge(event) { event.preventDefault(); + + if (!localStorage.getItem('groundRulesAccepted')) { + window.pendingNavbarChallengeEvent = event; + $('#groundRulesModal').modal('show'); + return; + } + const item = $(event.currentTarget).closest(".overflow-hidden"); const module = item.find("#module").val() const challenge = item.find("#challenge").val() diff --git a/dojo_theme/templates/base.html b/dojo_theme/templates/base.html index 44aa7dd83..8547c5795 100644 --- a/dojo_theme/templates/base.html +++ b/dojo_theme/templates/base.html @@ -83,6 +83,40 @@ + +
+ @@ -94,6 +128,7 @@ + {% block scripts %} {% endblock %} diff --git a/test/test_welcome.py b/test/test_welcome.py index de771224a..d866497e1 100644 --- a/test/test_welcome.py +++ b/test/test_welcome.py @@ -23,6 +23,9 @@ def random_user_browser(random_user): browser.find_element("id", "name").send_keys(random_id) browser.find_element("id", "password").send_keys(random_id) browser.find_element("id", "_submit").click() + + browser.execute_script("localStorage.setItem('groundRulesAccepted', 'true');") + return random_id, random_session, browser @@ -150,3 +153,78 @@ def test_welcome_practice(random_user_browser, welcome_dojo): flag = workspace_run("tail -n1 /tmp/out", user=random_id).stdout.split()[-1] challenge_submit(browser, idx, flag) browser.close() + + +def test_ground_rules_modal(random_user, welcome_dojo): + """Test that ground rules modal appears on first challenge start but not on second""" + random_id, random_session = random_user + + options = FirefoxOptions() + options.add_argument("--headless") + browser = Firefox(options=options) + + browser.get(f"{DOJO_URL}/login") + browser.find_element("id", "name").send_keys(random_id) + browser.find_element("id", "password").send_keys(random_id) + browser.find_element("id", "_submit").click() + + browser.get(f"{DOJO_URL}/welcome/welcome") + time.sleep(2) + browser.execute_script("localStorage.removeItem('groundRulesAccepted');") + + idx = challenge_idx(browser, "The Flag File") + challenge_expand(browser, idx) + body = browser.find_element("id", f"challenges-body-{idx}") + body.find_element("id", "challenge-start").click() + + time.sleep(2) + modal_visible = False + challenge_started = False + + try: + modal = browser.find_element("id", "groundRulesModal") + modal_visible = "show" in modal.get_attribute("class") + except: + pass + + try: + message = body.find_element("id", "result-message").text + challenge_started = "started" in message.lower() + except: + pass + + assert modal_visible or not challenge_started, "Ground rules modal should appear on first challenge start" + + if modal_visible: + browser.execute_script(""" + localStorage.setItem('groundRulesAccepted', 'true'); + $('#groundRulesModal').modal('hide'); + if (window.pendingChallengeEvent) { + window.startChallenge(window.pendingChallengeEvent); + } + """) + + wait = WebDriverWait(browser, 10) + wait.until(lambda d: "started" in body.find_element("id", "result-message").text.lower()) + else: + browser.execute_script("localStorage.setItem('groundRulesAccepted', 'true');") + + browser.get(f"{DOJO_URL}/welcome/welcome") + time.sleep(1) + idx2 = challenge_idx(browser, "Challenge Programs") + challenge_expand(browser, idx2) + body2 = browser.find_element("id", f"challenges-body-{idx2}") + body2.find_element("id", "challenge-start").click() + + time.sleep(2) + + try: + modal = browser.find_element("id", "groundRulesModal") + assert "show" not in modal.get_attribute("class"), "Ground rules modal should NOT be displayed on second challenge start" + except: + pass + + wait = WebDriverWait(browser, 10) + wait.until(lambda d: "started" in body2.find_element("id", "result-message").text.lower()) + + browser.close()