mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-06 17:46:06 +00:00
Compare commits
28 Commits
ui-preview
...
multiple-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e99d41be | ||
|
|
cdf07983db | ||
|
|
27401b3202 | ||
|
|
4baa9489c3 | ||
|
|
503e67f33e | ||
|
|
f290d0e5fe | ||
|
|
933d4ce886 | ||
|
|
50c7e1bf8c | ||
|
|
73eb00a3e9 | ||
|
|
e75561b2f5 | ||
|
|
005ed20741 | ||
|
|
2b0f851cbc | ||
|
|
56aeefc53e | ||
|
|
f7c7a3dbb8 | ||
|
|
e5775dd68e | ||
|
|
97e4ae1194 | ||
|
|
de955a54bd | ||
|
|
5037e66ec1 | ||
|
|
db48cc64f5 | ||
|
|
bce02f9c82 | ||
|
|
76ffc3e891 | ||
|
|
c6ee6687b5 | ||
|
|
de48892243 | ||
|
|
6aded50aca | ||
|
|
b8e279a025 | ||
|
|
8041d00e75 | ||
|
|
6a0e14cfce | ||
|
|
be91c5425c |
227
.github/workflows/test-only.yml
vendored
227
.github/workflows/test-only.yml
vendored
@@ -4,17 +4,10 @@ name: ChangeDetection.io App Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-application:
|
lint-code:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Mainly just for link/flake8
|
|
||||||
- name: Set up Python 3.11
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
pip3 install flake8
|
pip3 install flake8
|
||||||
@@ -23,202 +16,24 @@ jobs:
|
|||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
- name: Spin up ancillary testable services
|
test-application-3-10:
|
||||||
run: |
|
needs: lint-code
|
||||||
|
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
||||||
docker network create changedet-network
|
|
||||||
|
|
||||||
# Selenium
|
|
||||||
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
|
||||||
|
|
||||||
# SocketPuppetBrowser + Extra for custom browser test
|
|
||||||
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
||||||
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
|
||||||
|
|
||||||
- name: Build changedetection.io container for testing
|
|
||||||
run: |
|
|
||||||
# Build a changedetection.io container and start testing inside
|
|
||||||
docker build --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
|
|
||||||
# Debug info
|
|
||||||
docker run test-changedetectionio bash -c 'pip list'
|
|
||||||
|
|
||||||
- name: Spin up ancillary SMTP+Echo message test server
|
|
||||||
run: |
|
|
||||||
# Debug SMTP server/echo message back server
|
|
||||||
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'python changedetectionio/tests/smtp/smtp-test-server.py'
|
|
||||||
|
|
||||||
- name: Show docker container state and other debug info
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "Running processes in docker..."
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
- name: Test built container with Pytest (generally as requests/plaintext fetching)
|
|
||||||
run: |
|
|
||||||
# Unit tests
|
|
||||||
echo "run test with unittest"
|
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
|
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
|
|
||||||
|
|
||||||
# All tests
|
|
||||||
echo "run test with pytest"
|
|
||||||
# The default pytest logger_level is TRACE
|
|
||||||
# To change logger_level for pytest(test/conftest.py),
|
|
||||||
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
|
||||||
docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
|
||||||
|
|
||||||
# PLAYWRIGHT/NODE-> CDP
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Specific tests in built container
|
|
||||||
run: |
|
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
|
||||||
# tests/visualselector/test_fetch_data.py will do browser steps
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
|
||||||
|
|
||||||
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
|
||||||
run: |
|
|
||||||
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Restock detection
|
|
||||||
run: |
|
|
||||||
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
|
||||||
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
||||||
|
|
||||||
# STRAIGHT TO CDP
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
|
||||||
run: |
|
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
|
||||||
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
|
||||||
run: |
|
|
||||||
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Restock detection
|
|
||||||
run: |
|
|
||||||
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
|
||||||
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
||||||
|
|
||||||
# SELENIUM
|
|
||||||
- name: Specific tests in built container for Selenium
|
|
||||||
run: |
|
|
||||||
# Selenium fetch
|
|
||||||
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
|
||||||
|
|
||||||
- name: Specific tests in built container for headers and requests checks with Selenium
|
|
||||||
run: |
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
# OTHER STUFF
|
|
||||||
- name: Test SMTP notification mime types
|
|
||||||
run: |
|
|
||||||
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
|
||||||
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
|
||||||
|
|
||||||
# @todo Add a test via playwright/puppeteer
|
|
||||||
# squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
|
|
||||||
- name: Test proxy squid style interaction
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_proxy_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test proxy SOCKS5 style interaction
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_socks_proxy_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test custom browser URL
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_custom_browser_url_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test changedetection.io container starts+runs basically without error
|
|
||||||
run: |
|
|
||||||
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
# Should return 0 (no error) when grep finds it
|
|
||||||
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
|
||||||
|
|
||||||
# and IPv6
|
|
||||||
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
|
||||||
|
|
||||||
# Check whether TRACE log is enabled.
|
|
||||||
# Also, check whether TRACE is came from STDERR
|
|
||||||
docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
|
|
||||||
# Check whether DEBUG is came from STDOUT
|
|
||||||
docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
|
|
||||||
|
|
||||||
docker kill test-changedetectionio
|
|
||||||
|
|
||||||
- name: Test changedetection.io SIGTERM and SIGINT signal shutdown
|
|
||||||
run: |
|
|
||||||
|
|
||||||
echo SIGINT Shutdown request test
|
|
||||||
docker run --name sig-test -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
echo ">>> Sending SIGINT to sig-test container"
|
|
||||||
docker kill --signal=SIGINT sig-test
|
|
||||||
sleep 3
|
|
||||||
# invert the check (it should be not 0/not running)
|
|
||||||
docker ps
|
|
||||||
# check signal catch(STDERR) log. Because of
|
|
||||||
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
|
||||||
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
|
|
||||||
test -z "`docker ps|grep sig-test`"
|
|
||||||
if [ $? -ne 0 ]
|
|
||||||
then
|
|
||||||
echo "Looks like container was running when it shouldnt be"
|
|
||||||
docker ps
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
|
||||||
docker rm sig-test
|
|
||||||
|
|
||||||
echo SIGTERM Shutdown request test
|
|
||||||
docker run --name sig-test -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
echo ">>> Sending SIGTERM to sig-test container"
|
|
||||||
docker kill --signal=SIGTERM sig-test
|
|
||||||
sleep 3
|
|
||||||
# invert the check (it should be not 0/not running)
|
|
||||||
docker ps
|
|
||||||
# check signal catch(STDERR) log. Because of
|
|
||||||
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
|
||||||
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
|
|
||||||
test -z "`docker ps|grep sig-test`"
|
|
||||||
if [ $? -ne 0 ]
|
|
||||||
then
|
|
||||||
echo "Looks like container was running when it shouldnt be"
|
|
||||||
docker ps
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
|
||||||
docker rm sig-test
|
|
||||||
|
|
||||||
- name: Dump container log
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
mkdir output-logs
|
|
||||||
docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout.txt
|
|
||||||
docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr.txt
|
|
||||||
|
|
||||||
- name: Store container log
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: test-cdio-basic-tests-output
|
python-version: '3.10'
|
||||||
path: output-logs
|
|
||||||
|
|
||||||
|
test-application-3-11:
|
||||||
|
needs: lint-code
|
||||||
|
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
skip-pypuppeteer: true
|
||||||
|
|
||||||
|
test-application-3-12:
|
||||||
|
needs: lint-code
|
||||||
|
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
skip-pypuppeteer: true
|
||||||
|
|
||||||
|
|||||||
239
.github/workflows/test-stack-reusable-workflow.yml
vendored
Normal file
239
.github/workflows/test-stack-reusable-workflow.yml
vendored
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
name: ChangeDetection.io App Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
python-version:
|
||||||
|
description: 'Python version to use'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: '3.10'
|
||||||
|
skip-pypuppeteer:
|
||||||
|
description: 'Skip PyPuppeteer (not supported in 3.11/3.12)'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-application:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Mainly just for link/flake8
|
||||||
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: Build changedetection.io container for testing under Python ${{ env.PYTHON_VERSION }}
|
||||||
|
run: |
|
||||||
|
echo "---- Building for Python ${{ env.PYTHON_VERSION }} -----"
|
||||||
|
# Build a changedetection.io container and start testing inside
|
||||||
|
docker build --build-arg PYTHON_VERSION=${{ env.PYTHON_VERSION }} --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
|
||||||
|
# Debug info
|
||||||
|
docker run test-changedetectionio bash -c 'pip list'
|
||||||
|
|
||||||
|
- name: We should be Python ${{ env.PYTHON_VERSION }} ...
|
||||||
|
run: |
|
||||||
|
docker run test-changedetectionio bash -c 'python3 --version'
|
||||||
|
|
||||||
|
- name: Spin up ancillary testable services
|
||||||
|
run: |
|
||||||
|
|
||||||
|
docker network create changedet-network
|
||||||
|
|
||||||
|
# Selenium
|
||||||
|
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
||||||
|
|
||||||
|
# SocketPuppetBrowser + Extra for custom browser test
|
||||||
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
|
||||||
|
- name: Spin up ancillary SMTP+Echo message test server
|
||||||
|
run: |
|
||||||
|
# Debug SMTP server/echo message back server
|
||||||
|
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'pip3 install aiosmtpd && python changedetectionio/tests/smtp/smtp-test-server.py'
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
- name: Show docker container state and other debug info
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "Running processes in docker..."
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
- name: Test built container with Pytest (generally as requests/plaintext fetching)
|
||||||
|
run: |
|
||||||
|
# Unit tests
|
||||||
|
echo "run test with unittest"
|
||||||
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
||||||
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
|
||||||
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
|
||||||
|
|
||||||
|
# All tests
|
||||||
|
echo "run test with pytest"
|
||||||
|
# The default pytest logger_level is TRACE
|
||||||
|
# To change logger_level for pytest(test/conftest.py),
|
||||||
|
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
||||||
|
docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
||||||
|
|
||||||
|
# PLAYWRIGHT/NODE-> CDP
|
||||||
|
- name: Playwright and SocketPuppetBrowser - Specific tests in built container
|
||||||
|
run: |
|
||||||
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
|
# tests/visualselector/test_fetch_data.py will do browser steps
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
|
|
||||||
|
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
||||||
|
run: |
|
||||||
|
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
- name: Playwright and SocketPuppetBrowser - Restock detection
|
||||||
|
run: |
|
||||||
|
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
||||||
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
||||||
|
|
||||||
|
# STRAIGHT TO CDP
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
||||||
|
if: ${{ inputs.skip-pypuppeteer == false }}
|
||||||
|
run: |
|
||||||
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
||||||
|
if: ${{ inputs.skip-pypuppeteer == false }}
|
||||||
|
run: |
|
||||||
|
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Restock detection
|
||||||
|
if: ${{ inputs.skip-pypuppeteer == false }}
|
||||||
|
run: |
|
||||||
|
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
||||||
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
||||||
|
|
||||||
|
# SELENIUM
|
||||||
|
- name: Specific tests in built container for Selenium
|
||||||
|
run: |
|
||||||
|
# Selenium fetch
|
||||||
|
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
||||||
|
|
||||||
|
- name: Specific tests in built container for headers and requests checks with Selenium
|
||||||
|
run: |
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
# OTHER STUFF
|
||||||
|
- name: Test SMTP notification mime types
|
||||||
|
run: |
|
||||||
|
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
||||||
|
# "mailserver" hostname defined above
|
||||||
|
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
||||||
|
|
||||||
|
# @todo Add a test via playwright/puppeteer
|
||||||
|
# squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
|
||||||
|
- name: Test proxy squid style interaction
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_proxy_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test proxy SOCKS5 style interaction
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_socks_proxy_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test custom browser URL
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_custom_browser_url_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test changedetection.io container starts+runs basically without error
|
||||||
|
run: |
|
||||||
|
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
# Should return 0 (no error) when grep finds it
|
||||||
|
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
||||||
|
|
||||||
|
# and IPv6
|
||||||
|
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
||||||
|
|
||||||
|
# Check whether TRACE log is enabled.
|
||||||
|
# Also, check whether TRACE is came from STDERR
|
||||||
|
docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
|
||||||
|
# Check whether DEBUG is came from STDOUT
|
||||||
|
docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
|
||||||
|
|
||||||
|
docker kill test-changedetectionio
|
||||||
|
|
||||||
|
- name: Test changedetection.io SIGTERM and SIGINT signal shutdown
|
||||||
|
run: |
|
||||||
|
|
||||||
|
echo SIGINT Shutdown request test
|
||||||
|
docker run --name sig-test -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
echo ">>> Sending SIGINT to sig-test container"
|
||||||
|
docker kill --signal=SIGINT sig-test
|
||||||
|
sleep 3
|
||||||
|
# invert the check (it should be not 0/not running)
|
||||||
|
docker ps
|
||||||
|
# check signal catch(STDERR) log. Because of
|
||||||
|
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
||||||
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
|
||||||
|
test -z "`docker ps|grep sig-test`"
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "Looks like container was running when it shouldnt be"
|
||||||
|
docker ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
||||||
|
docker rm sig-test
|
||||||
|
|
||||||
|
echo SIGTERM Shutdown request test
|
||||||
|
docker run --name sig-test -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
echo ">>> Sending SIGTERM to sig-test container"
|
||||||
|
docker kill --signal=SIGTERM sig-test
|
||||||
|
sleep 3
|
||||||
|
# invert the check (it should be not 0/not running)
|
||||||
|
docker ps
|
||||||
|
# check signal catch(STDERR) log. Because of
|
||||||
|
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
||||||
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
|
||||||
|
test -z "`docker ps|grep sig-test`"
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "Looks like container was running when it shouldnt be"
|
||||||
|
docker ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
||||||
|
docker rm sig-test
|
||||||
|
|
||||||
|
- name: Dump container log
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
mkdir output-logs
|
||||||
|
docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout-${{ env.PYTHON_VERSION }}.txt
|
||||||
|
docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr-${{ env.PYTHON_VERSION }}.txt
|
||||||
|
|
||||||
|
- name: Store container log
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }}
|
||||||
|
path: output-logs
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
# @NOTE! I would love to move to 3.11 but it breaks the async handler in changedetectionio/content_fetchers/puppeteer.py
|
# @NOTE! I would love to move to 3.11 but it breaks the async handler in changedetectionio/content_fetchers/puppeteer.py
|
||||||
# If you know how to fix it, please do! and test it for both 3.10 and 3.11
|
# If you know how to fix it, please do! and test it for both 3.10 and 3.11
|
||||||
FROM python:3.10-slim-bookworm as builder
|
|
||||||
|
ARG PYTHON_VERSION=3.10
|
||||||
|
|
||||||
|
FROM python:${PYTHON_VERSION}-slim-bookworm as builder
|
||||||
|
|
||||||
# See `cryptography` pin comment in requirements.txt
|
# See `cryptography` pin comment in requirements.txt
|
||||||
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
||||||
@@ -32,7 +35,7 @@ RUN pip install --target=/dependencies playwright~=1.41.2 \
|
|||||||
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
||||||
|
|
||||||
# Final image stage
|
# Final image stage
|
||||||
FROM python:3.10-slim-bookworm
|
FROM python:${PYTHON_VERSION}-slim-bookworm
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libxslt1.1 \
|
libxslt1.1 \
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def manage_user_agent(headers, current_ua=''):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# Ask it what the user agent is, if its obviously ChromeHeadless, switch it to the default
|
# Ask it what the user agent is, if its obviously ChromeHeadless, switch it to the default
|
||||||
ua_in_custom_headers = next((v for k, v in headers.items() if k.lower() == "user-agent"), None)
|
ua_in_custom_headers = headers.get('User-Agent')
|
||||||
if ua_in_custom_headers:
|
if ua_in_custom_headers:
|
||||||
return ua_in_custom_headers
|
return ua_in_custom_headers
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,11 @@ class fetcher(Fetcher):
|
|||||||
|
|
||||||
# This user agent is similar to what was used when tweaking the evasions in inject_evasions_into_page(..)
|
# This user agent is similar to what was used when tweaking the evasions in inject_evasions_into_page(..)
|
||||||
user_agent = None
|
user_agent = None
|
||||||
if request_headers:
|
if request_headers and request_headers.get('User-Agent'):
|
||||||
user_agent = next((value for key, value in request_headers.items() if key.lower().strip() == 'user-agent'), None)
|
# Request_headers should now be CaaseInsensitiveDict
|
||||||
if user_agent:
|
|
||||||
await self.page.setUserAgent(user_agent)
|
|
||||||
# Remove it so it's not sent again with headers after
|
# Remove it so it's not sent again with headers after
|
||||||
[request_headers.pop(key) for key in list(request_headers) if key.lower().strip() == 'user-agent'.lower().strip()]
|
user_agent = request_headers.pop('User-Agent').strip()
|
||||||
|
await self.page.setUserAgent(user_agent)
|
||||||
|
|
||||||
if not user_agent:
|
if not user_agent:
|
||||||
# Attempt to strip 'HeadlessChrome' etc
|
# Attempt to strip 'HeadlessChrome' etc
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
|
# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
|
||||||
for uuid, watch in datastore.data['watching'].items():
|
for uuid, watch in datastore.data['watching'].items():
|
||||||
# @todo tag notification_muted skip also (improve Watch model)
|
# @todo tag notification_muted skip also (improve Watch model)
|
||||||
if watch.get('notification_muted'):
|
if datastore.data['settings']['application'].get('rss_hide_muted_watches') and watch.get('notification_muted'):
|
||||||
continue
|
continue
|
||||||
if limit_tag and not limit_tag in watch['tags']:
|
if limit_tag and not limit_tag in watch['tags']:
|
||||||
continue
|
continue
|
||||||
@@ -472,7 +472,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# Don't link to hosting when we're on the hosting environment
|
# Don't link to hosting when we're on the hosting environment
|
||||||
active_tag=active_tag,
|
active_tag=active_tag,
|
||||||
active_tag_uuid=active_tag_uuid,
|
active_tag_uuid=active_tag_uuid,
|
||||||
app_rss_token=datastore.data['settings']['application']['rss_access_token'],
|
app_rss_token=datastore.data['settings']['application'].get('rss_access_token'),
|
||||||
datastore=datastore,
|
datastore=datastore,
|
||||||
errored_count=errored_count,
|
errored_count=errored_count,
|
||||||
form=form,
|
form=form,
|
||||||
|
|||||||
@@ -572,6 +572,8 @@ class globalSettingsApplicationForm(commonSettingsForm):
|
|||||||
removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"})
|
removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"})
|
||||||
render_anchor_tag_content = BooleanField('Render anchor tag content', default=False)
|
render_anchor_tag_content = BooleanField('Render anchor tag content', default=False)
|
||||||
shared_diff_access = BooleanField('Allow access to view diff page when password is enabled', default=False, validators=[validators.Optional()])
|
shared_diff_access = BooleanField('Allow access to view diff page when password is enabled', default=False, validators=[validators.Optional()])
|
||||||
|
rss_hide_muted_watches = BooleanField('Hide muted watches from RSS feed', default=True,
|
||||||
|
validators=[validators.Optional()])
|
||||||
filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification',
|
filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification',
|
||||||
render_kw={"style": "width: 5em;"},
|
render_kw={"style": "width: 5em;"},
|
||||||
validators=[validators.NumberRange(min=0,
|
validators=[validators.NumberRange(min=0,
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class model(dict):
|
|||||||
'pager_size': 50,
|
'pager_size': 50,
|
||||||
'password': False,
|
'password': False,
|
||||||
'render_anchor_tag_content': False,
|
'render_anchor_tag_content': False,
|
||||||
|
'rss_access_token': None,
|
||||||
|
'rss_hide_muted_watches': True,
|
||||||
'schema_version' : 0,
|
'schema_version' : 0,
|
||||||
'shared_diff_access': False,
|
'shared_diff_access': False,
|
||||||
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
||||||
|
|||||||
@@ -333,7 +333,9 @@ class model(dict):
|
|||||||
# Small hack so that we sleep just enough to allow 1 second between history snapshots
|
# Small hack so that we sleep just enough to allow 1 second between history snapshots
|
||||||
# this is because history.txt indexes/keys snapshots by epoch seconds and we dont want dupe keys
|
# this is because history.txt indexes/keys snapshots by epoch seconds and we dont want dupe keys
|
||||||
if self.__newest_history_key and int(timestamp) == int(self.__newest_history_key):
|
if self.__newest_history_key and int(timestamp) == int(self.__newest_history_key):
|
||||||
time.sleep(timestamp - self.__newest_history_key)
|
logger.warning(f"Timestamp {timestamp} already exists, waiting 1 seconds so we have a unique key in history.txt")
|
||||||
|
timestamp = str(int(timestamp) + 1)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024))
|
threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024))
|
||||||
skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False'))
|
skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False'))
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
from copy import deepcopy
|
|
||||||
from changedetectionio.strtobool import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
|
from copy import deepcopy
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
class difference_detection_processor():
|
class difference_detection_processor():
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class difference_detection_processor():
|
|||||||
self.watch = deepcopy(self.datastore.data['watching'].get(watch_uuid))
|
self.watch = deepcopy(self.datastore.data['watching'].get(watch_uuid))
|
||||||
|
|
||||||
def call_browser(self):
|
def call_browser(self):
|
||||||
|
from requests.structures import CaseInsensitiveDict
|
||||||
# Protect against file:// access
|
# Protect against file:// access
|
||||||
if re.search(r'^file://', self.watch.get('url', '').strip(), re.IGNORECASE):
|
if re.search(r'^file://', self.watch.get('url', '').strip(), re.IGNORECASE):
|
||||||
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||||
@@ -93,14 +93,16 @@ class difference_detection_processor():
|
|||||||
self.fetcher.browser_steps_screenshot_path = os.path.join(self.datastore.datastore_path, self.watch.get('uuid'))
|
self.fetcher.browser_steps_screenshot_path = os.path.join(self.datastore.datastore_path, self.watch.get('uuid'))
|
||||||
|
|
||||||
# Tweak the base config with the per-watch ones
|
# Tweak the base config with the per-watch ones
|
||||||
request_headers = self.watch.get('headers', [])
|
request_headers = CaseInsensitiveDict()
|
||||||
request_headers.update(self.datastore.get_all_base_headers())
|
|
||||||
request_headers.update(self.datastore.get_all_headers_in_textfile_for_watch(uuid=self.watch.get('uuid')))
|
|
||||||
|
|
||||||
ua = self.datastore.data['settings']['requests'].get('default_ua')
|
ua = self.datastore.data['settings']['requests'].get('default_ua')
|
||||||
if ua and ua.get(prefer_fetch_backend):
|
if ua and ua.get(prefer_fetch_backend):
|
||||||
request_headers.update({'User-Agent': ua.get(prefer_fetch_backend)})
|
request_headers.update({'User-Agent': ua.get(prefer_fetch_backend)})
|
||||||
|
|
||||||
|
request_headers.update(self.watch.get('headers', {}))
|
||||||
|
request_headers.update(self.datastore.get_all_base_headers())
|
||||||
|
request_headers.update(self.datastore.get_all_headers_in_textfile_for_watch(uuid=self.watch.get('uuid')))
|
||||||
|
|
||||||
# https://github.com/psf/requests/issues/4525
|
# https://github.com/psf/requests/issues/4525
|
||||||
# Requests doesnt yet support brotli encoding, so don't put 'br' here, be totally sure that the user cannot
|
# Requests doesnt yet support brotli encoding, so don't put 'br' here, be totally sure that the user cannot
|
||||||
# do this by accident.
|
# do this by accident.
|
||||||
|
|||||||
@@ -1021,6 +1021,11 @@ ul {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
display: none;
|
display: none;
|
||||||
|
button {
|
||||||
|
/* some space if they wrap the page */
|
||||||
|
margin-bottom: 3px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-uuid {
|
.checkbox-uuid {
|
||||||
|
|||||||
@@ -1127,6 +1127,10 @@ ul {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
display: none; }
|
display: none; }
|
||||||
|
#checkbox-operations button {
|
||||||
|
/* some space if they wrap the page */
|
||||||
|
margin-bottom: 3px;
|
||||||
|
margin-top: 3px; }
|
||||||
|
|
||||||
.checkbox-uuid > * {
|
.checkbox-uuid > * {
|
||||||
vertical-align: middle; }
|
vertical-align: middle; }
|
||||||
|
|||||||
@@ -124,12 +124,12 @@ class ChangeDetectionStore:
|
|||||||
self.__data['app_guid'] = str(uuid_builder.uuid4())
|
self.__data['app_guid'] = str(uuid_builder.uuid4())
|
||||||
|
|
||||||
# Generate the URL access token for RSS feeds
|
# Generate the URL access token for RSS feeds
|
||||||
if not 'rss_access_token' in self.__data['settings']['application']:
|
if not self.__data['settings']['application'].get('rss_access_token'):
|
||||||
secret = secrets.token_hex(16)
|
secret = secrets.token_hex(16)
|
||||||
self.__data['settings']['application']['rss_access_token'] = secret
|
self.__data['settings']['application']['rss_access_token'] = secret
|
||||||
|
|
||||||
# Generate the API access token
|
# Generate the API access token
|
||||||
if not 'api_access_token' in self.__data['settings']['application']:
|
if not self.__data['settings']['application'].get('api_access_token'):
|
||||||
secret = secrets.token_hex(16)
|
secret = secrets.token_hex(16)
|
||||||
self.__data['settings']['application']['api_access_token'] = secret
|
self.__data['settings']['application']['api_access_token'] = secret
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ class ChangeDetectionStore:
|
|||||||
@property
|
@property
|
||||||
def has_unviewed(self):
|
def has_unviewed(self):
|
||||||
for uuid, watch in self.__data['watching'].items():
|
for uuid, watch in self.__data['watching'].items():
|
||||||
if watch.viewed == False:
|
if watch.history_n >= 2 and watch.viewed == False:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
<span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page)
|
<span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
{{ render_checkbox_field(form.application.form.rss_hide_muted_watches) }}
|
||||||
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.application.form.pager_size) }}
|
{{ render_field(form.application.form.pager_size) }}
|
||||||
<span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span>
|
<span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% if not watches|length %}
|
{% if not watches|length %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('import_page')}}" >import a list</a>.</td>
|
<td colspan="6" style="text-wrap: wrap;">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('import_page')}}" >import a list</a>.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
||||||
|
|||||||
@@ -1,42 +1,51 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
import smtpd
|
import asyncio
|
||||||
import asyncore
|
from aiosmtpd.controller import Controller
|
||||||
|
from aiosmtpd.smtp import SMTP
|
||||||
|
|
||||||
# Accept a SMTP message and offer a way to retrieve the last message via TCP Socket
|
# Accept a SMTP message and offer a way to retrieve the last message via TCP Socket
|
||||||
|
|
||||||
last_received_message = b"Nothing"
|
last_received_message = b"Nothing"
|
||||||
|
|
||||||
|
|
||||||
class CustomSMTPServer(smtpd.SMTPServer):
|
class CustomSMTPHandler:
|
||||||
|
async def handle_DATA(self, server, session, envelope):
|
||||||
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
|
|
||||||
global last_received_message
|
global last_received_message
|
||||||
last_received_message = data
|
last_received_message = envelope.content
|
||||||
print('Receiving message from:', peer)
|
print('Receiving message from:', session.peer)
|
||||||
print('Message addressed from:', mailfrom)
|
print('Message addressed from:', envelope.mail_from)
|
||||||
print('Message addressed to :', rcpttos)
|
print('Message addressed to :', envelope.rcpt_tos)
|
||||||
print('Message length :', len(data))
|
print('Message length :', len(envelope.content))
|
||||||
print(data.decode('utf8'))
|
print(envelope.content.decode('utf8'))
|
||||||
return
|
return '250 Message accepted for delivery'
|
||||||
|
|
||||||
|
|
||||||
# Just print out the last message received on plain TCP socket server
|
class EchoServerProtocol(asyncio.Protocol):
|
||||||
class EchoServer(asyncore.dispatcher):
|
def connection_made(self, transport):
|
||||||
|
|
||||||
def __init__(self, host, port):
|
|
||||||
asyncore.dispatcher.__init__(self)
|
|
||||||
self.create_socket()
|
|
||||||
self.set_reuse_addr()
|
|
||||||
self.bind((host, port))
|
|
||||||
self.listen(5)
|
|
||||||
|
|
||||||
def handle_accepted(self, sock, addr):
|
|
||||||
global last_received_message
|
global last_received_message
|
||||||
print('Incoming connection from %s' % repr(addr))
|
self.transport = transport
|
||||||
sock.send(last_received_message)
|
peername = transport.get_extra_info('peername')
|
||||||
|
print('Incoming connection from {}'.format(peername))
|
||||||
|
self.transport.write(last_received_message)
|
||||||
|
|
||||||
last_received_message = b''
|
last_received_message = b''
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
server = CustomSMTPServer(('0.0.0.0', 11025), None) # SMTP mail goes here
|
async def main():
|
||||||
server2 = EchoServer('0.0.0.0', 11080) # Echo back last message received
|
# Start the SMTP server
|
||||||
asyncore.loop()
|
controller = Controller(CustomSMTPHandler(), hostname='0.0.0.0', port=11025)
|
||||||
|
controller.start()
|
||||||
|
|
||||||
|
# Start the TCP Echo server
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
server = await loop.create_server(
|
||||||
|
lambda: EchoServerProtocol(),
|
||||||
|
'0.0.0.0', 11080
|
||||||
|
)
|
||||||
|
async with server:
|
||||||
|
await server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ def get_last_message_from_smtp_server():
|
|||||||
client_socket.connect((smtp_test_server, port)) # connect to the server
|
client_socket.connect((smtp_test_server, port)) # connect to the server
|
||||||
|
|
||||||
data = client_socket.recv(50024).decode() # receive response
|
data = client_socket.recv(50024).decode() # receive response
|
||||||
|
logging.info("get_last_message_from_smtp_server..")
|
||||||
|
logging.info(data)
|
||||||
client_socket.close() # close the connection
|
client_socket.close() # close the connection
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ def test_check_notification_email_formats_default_HTML(client, live_server):
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
set_longer_modified_response()
|
set_longer_modified_response()
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server):
|
|||||||
|
|
||||||
# The email should have two bodies, and the text/html part should be <br>
|
# The email should have two bodies, and the text/html part should be <br>
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(added) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n
|
||||||
assert 'Content-Type: text/html' in msg
|
assert 'Content-Type: text/html' in msg
|
||||||
assert '(added) So let\'s see what happens.<br>' in msg # the html part
|
assert '(added) So let\'s see what happens.<br>' in msg # the html part
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
@@ -135,6 +139,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
set_longer_modified_response()
|
set_longer_modified_response()
|
||||||
|
time.sleep(2)
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
# The email should not have two bodies, should be TEXT only
|
# The email should not have two bodies, should be TEXT only
|
||||||
|
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(added) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n
|
||||||
|
|
||||||
set_original_response()
|
set_original_response()
|
||||||
# Now override as HTML format
|
# Now override as HTML format
|
||||||
@@ -168,7 +173,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
|
|
||||||
# The email should have two bodies, and the text/html part should be <br>
|
# The email should have two bodies, and the text/html part should be <br>
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(removed) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
assert '(removed) So let\'s see what happens.\r\n' in msg # The plaintext part with \n
|
||||||
assert 'Content-Type: text/html' in msg
|
assert 'Content-Type: text/html' in msg
|
||||||
assert '(removed) So let\'s see what happens.<br>' in msg # the html part
|
assert '(removed) So let\'s see what happens.<br>' in msg # the html part
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ def test_check_ldjson_price_autodetect(client, live_server):
|
|||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# Trigger a check
|
# Trigger a check
|
||||||
|
time.sleep(1)
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
# Offer should be gone
|
# Offer should be gone
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
|||||||
# It should have picked up the <title>
|
# It should have picked up the <title>
|
||||||
assert b'head title' in res.data
|
assert b'head title' in res.data
|
||||||
|
|
||||||
|
# Be sure the last_viewed is going to be greater than the last snapshot
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# hit the mark all viewed link
|
# hit the mark all viewed link
|
||||||
res = client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
res = client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ def test_check_notification_error_handling(client, live_server):
|
|||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
set_original_response()
|
set_original_response()
|
||||||
|
|
||||||
# Give the endpoint time to spin up
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Set a URL and fetch it, then set a notification URL which is going to give errors
|
# Set a URL and fetch it, then set a notification URL which is going to give errors
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
|
|||||||
@@ -253,6 +253,62 @@ def test_method_in_request(client, live_server):
|
|||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
|
# Re #2408 - user-agent override test, also should handle case-insensitive header deduplication
|
||||||
|
def test_ua_global_override(client, live_server):
|
||||||
|
# live_server_setup(live_server)
|
||||||
|
test_url = url_for('test_headers', _external=True)
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings_page"),
|
||||||
|
data={
|
||||||
|
"application-fetch_backend": "html_requests",
|
||||||
|
"application-minutes_between_check": 180,
|
||||||
|
"requests-default_ua-html_requests": "html-requests-user-agent"
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b'Settings updated' in res.data
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
data={"urls": test_url},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"html-requests-user-agent" in res.data
|
||||||
|
# default user-agent should have shown by now
|
||||||
|
# now add a custom one in the headers
|
||||||
|
|
||||||
|
|
||||||
|
# Add some headers to a request
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid="first"),
|
||||||
|
data={
|
||||||
|
"url": test_url,
|
||||||
|
"tags": "testtag",
|
||||||
|
"fetch_backend": 'html_requests',
|
||||||
|
# Important - also test case-insensitive
|
||||||
|
"headers": "User-AGent: agent-from-watch"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Updated watch." in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"agent-from-watch" in res.data
|
||||||
|
assert b"html-requests-user-agent" not in res.data
|
||||||
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
def test_headers_textfile_in_request(client, live_server):
|
def test_headers_textfile_in_request(client, live_server):
|
||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
@@ -333,7 +389,7 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
# Not needed anymore
|
# Not needed anymore
|
||||||
os.unlink('test-datastore/headers.txt')
|
os.unlink('test-datastore/headers.txt')
|
||||||
os.unlink('test-datastore/headers-testtag.txt')
|
os.unlink('test-datastore/headers-testtag.txt')
|
||||||
os.unlink('test-datastore/' + extract_UUID_from_client(client) + '/headers.txt')
|
|
||||||
# The service should echo back the request verb
|
# The service should echo back the request verb
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid="first"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Used by Pyppeteer
|
# Used by Pyppeteer
|
||||||
pyee
|
pyee
|
||||||
|
|
||||||
eventlet==0.33.3 # related to dnspython fixes
|
eventlet==0.35.2 # related to dnspython fixes
|
||||||
feedgen~=0.9
|
feedgen~=0.9
|
||||||
flask-compress
|
flask-compress
|
||||||
# 0.6.3 included compatibility fix for werkzeug 3.x (2.x had deprecation of url handlers)
|
# 0.6.3 included compatibility fix for werkzeug 3.x (2.x had deprecation of url handlers)
|
||||||
@@ -29,9 +29,7 @@ chardet>2.3.0
|
|||||||
wtforms~=3.0
|
wtforms~=3.0
|
||||||
jsonpath-ng~=1.5.3
|
jsonpath-ng~=1.5.3
|
||||||
|
|
||||||
# Pinned: module 'eventlet.green.select' has no attribute 'epoll'
|
dnspython==2.6.1
|
||||||
# https://github.com/eventlet/eventlet/issues/805#issuecomment-1640463482
|
|
||||||
dnspython==2.3.0 # related to eventlet fixes
|
|
||||||
|
|
||||||
# jq not available on Windows so must be installed manually
|
# jq not available on Windows so must be installed manually
|
||||||
|
|
||||||
@@ -86,3 +84,5 @@ pytest-flask ~=1.2
|
|||||||
jsonschema==4.17.3
|
jsonschema==4.17.3
|
||||||
|
|
||||||
loguru
|
loguru
|
||||||
|
# Needed for > 3.10, https://github.com/microsoft/playwright-python/issues/2096
|
||||||
|
greenlet >= 3.0.3
|
||||||
Reference in New Issue
Block a user