Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e74c2f3
Add files via upload
sebastian-zak Apr 21, 2025
2b54dcf
update plugin and added unit test
MilenaChowaniec Apr 25, 2025
43809a1
deleted dist file
MilenaChowaniec Apr 29, 2025
65a6d0b
updated readme file
MilenaChowaniec Apr 29, 2025
d4b161a
updated files
MilenaChowaniec May 8, 2025
08b98c5
update readme
MilenaChowaniec May 8, 2025
52d3671
changed Makefile name
MilenaChowaniec May 9, 2025
6356db6
Added yml files, updated plugin
MilenaChowaniec May 15, 2025
22cb5ac
files updated
MilenaChowaniec May 16, 2025
5898dd0
update
MilenaChowaniec May 16, 2025
225a775
added e2e tests
MilenaChowaniec May 20, 2025
877692c
changed plugin and e2e test
MilenaChowaniec Jul 11, 2025
5f13d49
run black to format code
MilenaChowaniec Jul 11, 2025
bae6b88
deleted unused Flask
MilenaChowaniec Jul 11, 2025
4c917ea
fix mistakes
MilenaChowaniec Jul 11, 2025
c66cf9d
deleted files
MilenaChowaniec Jul 11, 2025
7ed34e7
test
MilenaChowaniec Jul 31, 2025
7edbf27
lint fix
MilenaChowaniec Jul 31, 2025
10f6fd8
Merge branch 'main' into addedUnitTest
MilenaChowaniec Jul 31, 2025
eaa3f2d
fix issues
MilenaChowaniec Jul 31, 2025
d2a4359
lint fix
MilenaChowaniec Jul 31, 2025
8a934c9
update
MilenaChowaniec Jul 31, 2025
93e5c56
update
MilenaChowaniec Jul 31, 2025
910140a
update
MilenaChowaniec Aug 1, 2025
c061f9e
update
MilenaChowaniec Aug 1, 2025
3b4a6c7
update
MilenaChowaniec Aug 1, 2025
4a3c908
update
MilenaChowaniec Aug 1, 2025
32ab65d
update
MilenaChowaniec Aug 1, 2025
4991288
update
MilenaChowaniec Aug 7, 2025
9b5893b
update
MilenaChowaniec Sep 5, 2025
8e4efaa
tests fix
MilenaChowaniec Sep 5, 2025
56247d6
tests fix
MilenaChowaniec Sep 5, 2025
2f79ff4
e2e data dix
MilenaChowaniec Sep 5, 2025
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
28 changes: 4 additions & 24 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
- name: Run unit-tests
run: make unit-tests

# Set Node.js version to 18 for running Cypress and npm commands
- name: Set up Node.js
uses: actions/setup-node@v4
with:
Expand All @@ -44,31 +43,12 @@ jobs:
run: |
npm install cypress

# Run all Cypress tests using default config
- name: Run Cypress tests
working-directory: tests/e2e_tests/e2e
run: npx cypress run --spec "cypress/e2e/**/*"

- name: Run Flask server in background
- name: Run e2e tests
run: |
poetry run flask run &
echo $! > flask_pid.txt
sleep 3

# Run Cypress tests specifying the folder with your test specs
- name: Run Cypress tests
run: npx cypress run --spec "tests/e2e_tests/cypress/e2e/**/*"
poetry run flask --app "tests.e2e_tests.e2e_app:create_app(config_path='tests/e2e_tests/e2e_test_config.yml')" run --debug --host=localhost &
sleep 6
cd tests/e2e_tests && npx cypress run --spec "cypress/e2e/**/*"
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden e2e step: avoid reloader, wait for readiness, and ensure clean teardown

Current approach uses --debug (spawns a reloader) and a fixed sleep which can flake. Start a single process, wait until it’s responsive, and always kill it on exit.

Apply:

-        poetry run flask --app "tests.e2e_tests.e2e_app:create_app(config_path='tests/e2e_tests/e2e_test_config.yml')" run --debug --host=localhost &
-        sleep 6
-        cd tests/e2e_tests && npx cypress run --spec "cypress/e2e/**/*"
+        set -euo pipefail
+        poetry run flask --app "tests.e2e_tests.e2e_app:create_app(config_path='tests/e2e_tests/e2e_test_config.yml')" run --host=127.0.0.1 --port=5000 --no-reload > flask.log 2>&1 &
+        SERVER_PID=$!
+        trap 'kill "$SERVER_PID" || true' EXIT
+        # Wait up to 30s for server readiness
+        for i in {1..30}; do
+          curl -fsS http://127.0.0.1:5000/ >/dev/null && break
+          sleep 1
+        done
+        cd tests/e2e_tests
+        npx cypress run --spec "cypress/e2e/**/*"

Optional: upload flask.log on failure to aid debugging.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
poetry run flask --app "tests.e2e_tests.e2e_app:create_app(config_path='tests/e2e_tests/e2e_test_config.yml')" run --debug --host=localhost &
sleep 6
cd tests/e2e_tests && npx cypress run --spec "cypress/e2e/**/*"
set -euo pipefail
poetry run flask --app "tests.e2e_tests.e2e_app:create_app(config_path='tests/e2e_tests/e2e_test_config.yml')" \
run --host=127.0.0.1 --port=5000 --no-reload > flask.log 2>&1 &
SERVER_PID=$!
trap 'kill "$SERVER_PID" || true' EXIT
# Wait up to 30s for server readiness
for i in {1..30}; do
curl -fsS http://127.0.0.1:5000/ >/dev/null && break
sleep 1
done
cd tests/e2e_tests
npx cypress run --spec "cypress/e2e/**/*"
🤖 Prompt for AI Agents
.github/workflows/tests.yml lines 48-50: the e2e job currently launches Flask
with --debug (which spawns a reloader) and uses a fixed sleep, causing flakiness
and orphaned processes; change to start a single, non-reloading Flask process
(use the CLI flag to disable the reloader), run it in background capturing its
PID, install a trap/cleanup to always kill that PID on EXIT, replace the fixed
sleep with a readiness probe loop (poll the app URL with a short timeout and
bounded retries until a 200/healthy response or fail), and on failure upload
flask.log for debugging; ensure the readiness loop times out and exits non-zero
so Cypress doesn’t run against an unready server.


# Stop the Flask server using the saved process ID
- name: Stop Flask server
run: kill $(cat flask_pid.txt)


# - name: Run tests with coverage
# run: make coverage
# - name: Coveralls
# uses: coverallsapp/github-action@v1
# with:
# path-to-lcov: "./coverage.lcov"
# github-token: ${{ secrets.GITHUB_TOKEN }}

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ The B4ULeave plugin displays a modal window asking the user whether they want to
## Installation

```sh
pip install platzky-b4uLeave
pip install platzky-b4uleave
```

## Usage

```json
"plugins": [
{
"name": "b4uLeave",
"name": "b4uleave",
"config": {
"message": "Your custom message goes here",
"stay": "Staying custom message",
Expand Down
31 changes: 20 additions & 11 deletions platzky_b4uleave/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
from flask import Flask, Response
from flask import Response
from typing import Any, Dict

def process(app, plugin_config: Dict[str, Any]): # Defines the main `process` function taking a Flask app instance and plugin configuration

def process(
app, plugin_config: Dict[str, Any]
): # Defines the main `process` function taking a Flask app instance and plugin configuration
# Store plugin configuration (defaults to empty dict)
app.config['b4uleave'] = plugin_config or {}
app.config["b4uleave"] = plugin_config or {}

message = app.config['b4uleave'].get('message', 'Czy na pewno chcesz<br>opuścić naszą stronę?')
stay = app.config['b4uleave'].get('stay', 'Stay')
leave = app.config['b4uleave'].get('leave', 'Leave')
message = app.config["b4uleave"].get(
"message", "Czy na pewno chcesz<br>opuścić naszą stronę?"
)
stay = app.config["b4uleave"].get("stay", "Stay")
leave = app.config["b4uleave"].get("leave", "Leave")

Comment on lines +11 to 16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Unsanitised user-controlled HTML ⇒ reflected-XSS vector.

message, stay, and leave come straight from plugin_config and are interpolated into the DOM without escaping.
An attacker configuring the plugin (or compromising the config source) can inject arbitrary scripts.

-import ...
+import html  # at top

 ...
-message = app.config["b4uleave"].get(
-    "message", "Czy na pewno chcesz<br>opuścić naszą stronę?"
-)
-stay = app.config["b4uleave"].get("stay", "Stay")
-leave = app.config["b4uleave"].get("leave", "Leave")
+cfg      = app.config["b4uleave"]
+message  = html.escape(cfg.get("message", "Czy na pewno chcesz<br>opuścić naszą stronę?"), quote=True)
+stay     = html.escape(cfg.get("stay",   "Stay"),  quote=True)
+leave    = html.escape(cfg.get("leave",  "Leave"), quote=True)

If you intentionally allow HTML in message, use flask.Markup to whitelist only that field and still escape the others.

Also applies to: 69-73

🤖 Prompt for AI Agents
In platzky_b4uleave/entrypoint.py around lines 11 to 16, the variables message,
stay, and leave are taken directly from the config and inserted into the DOM
without escaping, creating a reflected XSS risk. To fix this, apply proper
escaping to stay and leave to prevent script injection, and if HTML is
intentionally allowed in message, wrap it with flask.Markup to safely mark it as
trusted HTML. Also apply the same escaping and marking approach to the similar
code at lines 69 to 73.

@app.after_request # Decorator that registers a function to run after each request is processed
@app.after_request # Decorator that registers a function to run after each request is processed
def add_B4ULeave(response: Response) -> Response:
if 'text/html' in response.headers.get('Content-Type', ''): # Function receives a Response object and returns a modified Response
if "text/html" in response.headers.get(
"Content-Type", ""
): # Function receives a Response object and returns a modified Response
html = f"""
<style>
#B4ULeave-ModalWindow {{
Expand Down Expand Up @@ -103,8 +110,10 @@ def add_B4ULeave(response: Response) -> Response:
}});
}})();
</script>"""
response.set_data(response.get_data(as_text=True).replace('</body>', html + '</body>'))
return response

response.set_data(
response.get_data(as_text=True).replace("</body>", html + "</body>")
)
Comment on lines +115 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

"</body>" string replace is brittle; use a real parser.

replace("</body>", html + "</body>")
• fails for <BODY> / mixed-case tags,
• injects nothing if the tag is missing,
• may break when the string appears inside a <script> or <textarea>.

Consider BeautifulSoup or lxml.html for robust insertion:

from bs4 import BeautifulSoup

soup = BeautifulSoup(response.get_data(as_text=True), "html.parser")
if soup.body:
    soup.body.append(BeautifulSoup(html, "html.parser"))
    response.set_data(str(soup))

At minimum, perform a case-insensitive search and fall back gracefully.

🤖 Prompt for AI Agents
In platzky_b4uleave/entrypoint.py around lines 115 to 116, the code uses a
brittle string replace to insert HTML before the closing </body> tag, which
fails with case variations, missing tags, or when the string appears inside
scripts. Replace this with a robust HTML parser like BeautifulSoup: parse the
response data, check for the body tag, append the new HTML safely, and then set
the modified HTML back to the response. This ensures correct insertion
regardless of tag case or content structure.

return response

return app
return app
2 changes: 1 addition & 1 deletion tests/e2e_tests/cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { defineConfig } = require("cypress");

module.exports = defineConfig({
e2e: {
baseUrl: 'http://www.goodmap.localhost:5000/',
baseUrl: 'http://localhost:5000/',
setupNodeEvents(on, config) {
return config
},
Expand Down
31 changes: 31 additions & 0 deletions tests/e2e_tests/e2e_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from flask import render_template_string
from platzky.platzky import create_app as base_create_app


def create_app(config_path: str):
# Base app
app = base_create_app(config_path=config_path)

@app.route("/")
def index():
return render_template_string(
"""
<html>
<head>
<title>E2E Test Page</title>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<h1>Welcome to E2E Test</h1>
<p>This page includes the B4ULeave plugin for testing.</p>
</body>
</html>
"""
)

return app
21 changes: 21 additions & 0 deletions tests/e2e_tests/e2e_test_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
APP_NAME: Bridges in Wroclaw
SECRET_KEY: SECRET
BLOG_PREFIX: "/blog"
DB:
TYPE: json_file
PATH: tests/e2e_tests/e2e_test_data.json

#TODO this should not be necessary argument
LANGUAGES:
en:
name: English
flag: gb
country: GB
pl:
name: polski
flag: pl
country: PL

FEATURE_FLAGS:
USE_LAZY_LOADING: True
SHOW_ACCESSIBILITY_TABLE: True
133 changes: 133 additions & 0 deletions tests/e2e_tests/e2e_test_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"map": {
"data": [
{
"name": "Grunwaldzki",
"position": [
51.1095,
17.0525
],
"accessible_by": [
"pedestrians",
"cars"
],
"type_of_place": "big bridge",
"uuid": "hidden",
"CTA": {
"type": "CTA",
"value": "https://www.example.com",
"displayValue": "Visit example.org!"
}
},
{
"name": "Zwierzyniecka",
"position": [
51.10655,
17.0555
],
"accessible_by": [
"bikes",
"pedestrians"
],
"type_of_place": "small bridge",
"uuid": "dattarro"
}
],
"location_obligatory_fields": [
["name", "str"],
["accessible_by", "list"],
["type_of_place", "str"]
],
"categories": {
"accessible_by": [
"bikes",
"cars",
"pedestrians"
],
"type_of_place": [
"big bridge",
"small bridge"
]
},
"visible_data": [
"accessible_by",
"type_of_place",
"CTA"
],
"meta_data": [
"uuid"
]
},
"site_content": {
"pages": [
{
"title": "O nas",
"slug": "o-nas",
"coverImage": {
"url": "",
"alternateText": ""
},
"date": "01-01-2024",
"author": "",
"comments": [],
"excerpt": "",
"tags": [],
"language": "pl",
"contentInMarkdown": "o nas"
},
{
"title": "About",
"slug": "about",
"coverImage": {
"url": "",
"alternateText": ""
},
"date": "01-01-2024",
"author": "",
"comments": [],
"excerpt": "",
"tags": [],
"language": "en",
"contentInMarkdown": "about"
}
],
"menu_items": {
"pl": [
{
"name": "Mapa",
"url": "/"
},
{
"name": "O nas",
"url": "/blog/page/o-nas"
}
],
"en": [
{
"name": "Map",
"url": "/"
},
{
"name": "About",
"url": "/blog/page/about"
}
]
},
"logo_url": "",
"font": {
"name": "Poppins",
"url": "https://fonts.googleapis.com/css2?family=Poppins"
},
"primary_color": "#FFFFFF",
"secondary_color": "#245466",
"left_bar_width": "300px"
},
"plugins": [
{
"name": "b4uleave",
"config": {
"message": "Your custom message goes here"
}
}
]
}
2 changes: 1 addition & 1 deletion tests/unit_tests/test_b4uleave.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_that_plugin_loads_b4uleave():

response = app_with_plugin.test_client().get("/")

assert response.status_code == 200
assert response.status_code == 404
decoded_response = response.data.decode()
assert b4uleave_function in decoded_response
assert custom_message in decoded_response
Expand Down