From 434a1b242e86d00b2cd3e66411313b4203ba3baa Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 17 Jun 2024 09:46:54 +0200 Subject: [PATCH 1/3] Improve testing for Python 3.10, 3.11 and 3.12 --- .github/workflows/test-only.yml | 221 ++-------------- .../test-stack-reusable-workflow.yml | 239 ++++++++++++++++++ Dockerfile | 7 +- .../tests/smtp/smtp-test-server.py | 69 ++--- .../tests/smtp/test_notification_smtp.py | 8 +- requirements.txt | 6 +- 6 files changed, 309 insertions(+), 241 deletions(-) create mode 100644 .github/workflows/test-stack-reusable-workflow.yml diff --git a/.github/workflows/test-only.yml b/.github/workflows/test-only.yml index 5483e6f6..69e42cba 100644 --- a/.github/workflows/test-only.yml +++ b/.github/workflows/test-only.yml @@ -4,17 +4,10 @@ name: ChangeDetection.io App Test on: [push, pull_request] jobs: - test-application: + lint-code: runs-on: ubuntu-latest steps: - 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 run: | pip3 install flake8 @@ -23,202 +16,24 @@ jobs: # 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 - - 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: 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' + test-application-3-10: + needs: lint-code + uses: ./.github/workflows/test-stack-reusable-workflow.yml + with: + python-version: '3.10' - - 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 .. + test-application-3-11: + needs: lint-code + uses: ./.github/workflows/test-stack-reusable-workflow.yml + with: + python-version: '3.11' + skip-pypuppeteer: true - - 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 + test-application-3-12: + needs: lint-code + uses: ./.github/workflows/test-stack-reusable-workflow.yml + with: + python-version: '3.12' + skip-pypuppeteer: true - - name: Store container log - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-cdio-basic-tests-output - path: output-logs diff --git a/.github/workflows/test-stack-reusable-workflow.yml b/.github/workflows/test-stack-reusable-workflow.yml new file mode 100644 index 00000000..a4c7b87c --- /dev/null +++ b/.github/workflows/test-stack-reusable-workflow.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index e592c9bb..5e45880c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 # 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 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." # 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 \ libxslt1.1 \ diff --git a/changedetectionio/tests/smtp/smtp-test-server.py b/changedetectionio/tests/smtp/smtp-test-server.py index 3481ce7e..e294cf01 100755 --- a/changedetectionio/tests/smtp/smtp-test-server.py +++ b/changedetectionio/tests/smtp/smtp-test-server.py @@ -1,42 +1,51 @@ #!/usr/bin/python3 -import smtpd -import asyncore +import asyncio +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 last_received_message = b"Nothing" -class CustomSMTPServer(smtpd.SMTPServer): - - def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): +class CustomSMTPHandler: + async def handle_DATA(self, server, session, envelope): global last_received_message - last_received_message = data - print('Receiving message from:', peer) - print('Message addressed from:', mailfrom) - print('Message addressed to :', rcpttos) - print('Message length :', len(data)) - print(data.decode('utf8')) - return - - -# Just print out the last message received on plain TCP socket server -class EchoServer(asyncore.dispatcher): - - 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): + last_received_message = envelope.content + print('Receiving message from:', session.peer) + print('Message addressed from:', envelope.mail_from) + print('Message addressed to :', envelope.rcpt_tos) + print('Message length :', len(envelope.content)) + print(envelope.content.decode('utf8')) + return '250 Message accepted for delivery' + + +class EchoServerProtocol(asyncio.Protocol): + def connection_made(self, transport): global last_received_message - print('Incoming connection from %s' % repr(addr)) - sock.send(last_received_message) + self.transport = transport + peername = transport.get_extra_info('peername') + print('Incoming connection from {}'.format(peername)) + self.transport.write(last_received_message) + last_received_message = b'' + self.transport.close() + + +async def main(): + # Start the SMTP server + 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() -server = CustomSMTPServer(('0.0.0.0', 11025), None) # SMTP mail goes here -server2 = EchoServer('0.0.0.0', 11080) # Echo back last message received -asyncore.loop() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/changedetectionio/tests/smtp/test_notification_smtp.py b/changedetectionio/tests/smtp/test_notification_smtp.py index 2ab92b4f..c69299fe 100644 --- a/changedetectionio/tests/smtp/test_notification_smtp.py +++ b/changedetectionio/tests/smtp/test_notification_smtp.py @@ -32,6 +32,8 @@ def get_last_message_from_smtp_server(): client_socket.connect((smtp_test_server, port)) # connect to the server 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 return data @@ -83,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
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 '(added) So let\'s see what happens.
' in msg # the html part res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) @@ -150,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 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() # Now override as HTML format @@ -171,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
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 '(removed) So let\'s see what happens.
' in msg # the html part diff --git a/requirements.txt b/requirements.txt index 9fc485e8..a13bc8d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,9 +29,7 @@ chardet>2.3.0 wtforms~=3.0 jsonpath-ng~=1.5.3 -# Pinned: module 'eventlet.green.select' has no attribute 'epoll' -# https://github.com/eventlet/eventlet/issues/805#issuecomment-1640463482 -dnspython==2.3.0 # related to eventlet fixes +dnspython==2.6.1 # jq not available on Windows so must be installed manually @@ -86,3 +84,5 @@ pytest-flask ~=1.2 jsonschema==4.17.3 loguru +# Needed for > 3.10, https://github.com/microsoft/playwright-python/issues/2096 +greenlet >= 3.0.3 \ No newline at end of file From 5e65fb606bc98f98ed24ed076397c6fe4483f6e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:23:50 +0200 Subject: [PATCH 2/3] Bump dnspython from 2.3.0 to 2.6.1 (#2306) --- changedetectionio/tests/test_auth.py | 9 ++++----- .../tests/test_automatic_follow_ldjson_price.py | 2 +- changedetectionio/tests/test_backend.py | 3 --- changedetectionio/tests/test_encoding.py | 13 +++---------- changedetectionio/tests/test_extract_csv.py | 1 + changedetectionio/tests/test_rss.py | 3 ++- requirements.txt | 2 +- 7 files changed, 12 insertions(+), 21 deletions(-) diff --git a/changedetectionio/tests/test_auth.py b/changedetectionio/tests/test_auth.py index b84f8cf7..be817ba4 100644 --- a/changedetectionio/tests/test_auth.py +++ b/changedetectionio/tests/test_auth.py @@ -2,13 +2,12 @@ import time from flask import url_for -from . util import live_server_setup +from .util import live_server_setup, wait_for_all_checks + def test_basic_auth(client, live_server): live_server_setup(live_server) - # Give the endpoint time to spin up - time.sleep(1) # Add our URL to the import page test_url = url_for('test_basicauth_method', _external=True).replace("//","//myuser:mypass@") @@ -19,8 +18,8 @@ def test_basic_auth(client, live_server): follow_redirects=True ) assert b"1 Imported" in res.data + wait_for_all_checks(client) time.sleep(1) - # Check form validation res = client.post( url_for("edit_page", uuid="first"), @@ -29,7 +28,7 @@ def test_basic_auth(client, live_server): ) assert b"Updated watch." in res.data - time.sleep(1) + wait_for_all_checks(client) res = client.get( url_for("preview_page", uuid="first"), follow_redirects=True diff --git a/changedetectionio/tests/test_automatic_follow_ldjson_price.py b/changedetectionio/tests/test_automatic_follow_ldjson_price.py index f0c0d104..c589ea25 100644 --- a/changedetectionio/tests/test_automatic_follow_ldjson_price.py +++ b/changedetectionio/tests/test_automatic_follow_ldjson_price.py @@ -100,7 +100,7 @@ def test_check_ldjson_price_autodetect(client, live_server): # Accept it uuid = extract_UUID_from_client(client) - + time.sleep(1) client.get(url_for('price_data_follower.accept', uuid=uuid, follow_redirects=True)) wait_for_all_checks(client) diff --git a/changedetectionio/tests/test_backend.py b/changedetectionio/tests/test_backend.py index 2d21067e..1e1c6496 100644 --- a/changedetectionio/tests/test_backend.py +++ b/changedetectionio/tests/test_backend.py @@ -62,9 +62,6 @@ def test_check_basic_change_detection_functionality(client, live_server): # Make a change set_modified_response() - res = urlopen(url_for('test_endpoint', _external=True)) - assert b'which has this one new line' in res.read() - # Force recheck res = client.get(url_for("form_watch_checknow"), follow_redirects=True) assert b'1 watches queued for rechecking.' in res.data diff --git a/changedetectionio/tests/test_encoding.py b/changedetectionio/tests/test_encoding.py index 4b273edf..c00a28b8 100644 --- a/changedetectionio/tests/test_encoding.py +++ b/changedetectionio/tests/test_encoding.py @@ -3,7 +3,7 @@ import time from flask import url_for -from .util import live_server_setup +from .util import live_server_setup, wait_for_all_checks import pytest @@ -27,9 +27,6 @@ def set_html_response(): def test_check_encoding_detection(client, live_server): set_html_response() - # Give the endpoint time to spin up - time.sleep(1) - # Add our URL to the import page test_url = url_for('test_endpoint', content_type="text/html", _external=True) client.post( @@ -39,7 +36,7 @@ def test_check_encoding_detection(client, live_server): ) # Give the thread time to pick it up - time.sleep(2) + wait_for_all_checks(client) res = client.get( url_for("preview_page", uuid="first"), @@ -56,9 +53,6 @@ def test_check_encoding_detection(client, live_server): def test_check_encoding_detection_missing_content_type_header(client, live_server): set_html_response() - # Give the endpoint time to spin up - time.sleep(1) - # Add our URL to the import page test_url = url_for('test_endpoint', _external=True) client.post( @@ -67,8 +61,7 @@ def test_check_encoding_detection_missing_content_type_header(client, live_serve follow_redirects=True ) - # Give the thread time to pick it up - time.sleep(2) + wait_for_all_checks(client) res = client.get( url_for("preview_page", uuid="first"), diff --git a/changedetectionio/tests/test_extract_csv.py b/changedetectionio/tests/test_extract_csv.py index 52596a9e..7fcc83c4 100644 --- a/changedetectionio/tests/test_extract_csv.py +++ b/changedetectionio/tests/test_extract_csv.py @@ -29,6 +29,7 @@ def test_check_extract_text_from_diff(client, live_server): # Load in 5 different numbers/changes last_date="" for n in range(5): + time.sleep(1) # Give the thread time to pick it up print("Bumping snapshot and checking.. ", n) last_date = str(time.time()) diff --git a/changedetectionio/tests/test_rss.py b/changedetectionio/tests/test_rss.py index 7f030482..8fe2e84d 100644 --- a/changedetectionio/tests/test_rss.py +++ b/changedetectionio/tests/test_rss.py @@ -69,6 +69,7 @@ def test_rss_and_token(client, live_server): wait_for_all_checks(client) set_modified_response() + time.sleep(1) client.get(url_for("form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) @@ -87,7 +88,7 @@ def test_rss_and_token(client, live_server): assert b"Access denied, bad token" not in res.data assert b"Random content" in res.data - res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) + client.get(url_for("form_delete", uuid="all"), follow_redirects=True) def test_basic_cdata_rss_markup(client, live_server): #live_server_setup(live_server) diff --git a/requirements.txt b/requirements.txt index a13bc8d0..6922ad53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ chardet>2.3.0 wtforms~=3.0 jsonpath-ng~=1.5.3 -dnspython==2.6.1 +dnspython==2.6.1 # related to eventlet fixes # jq not available on Windows so must be installed manually From c9af9b637431b7f07fb80056843401376000058e Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 17 Jun 2024 11:42:41 +0200 Subject: [PATCH 3/3] Filter failure/not found notification threshold - Counter should be reset when editing a watch, clear watch errors on 'save' (#2413) --- changedetectionio/flask_app.py | 5 +++- changedetectionio/model/App.py | 1 + .../tests/test_filter_failure_notification.py | 29 +++++++++---------- changedetectionio/update_worker.py | 3 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 02e041b1..d5ea2460 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -679,7 +679,10 @@ def changedetection_app(config=None, datastore_o=None): if request.method == 'POST' and form.validate(): - extra_update_obj = {} + extra_update_obj = { + 'consecutive_filter_failures': 0, + 'last_error' : False + } if request.args.get('unpause_on_save'): extra_update_obj['paused'] = False diff --git a/changedetectionio/model/App.py b/changedetectionio/model/App.py index e412542b..fdd627ed 100644 --- a/changedetectionio/model/App.py +++ b/changedetectionio/model/App.py @@ -5,6 +5,7 @@ from changedetectionio.notification import ( default_notification_title, ) +# Equal to or greater than this number of FilterNotFoundInResponse exceptions will trigger a filter-not-found notification _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT = 6 DEFAULT_SETTINGS_HEADERS_USERAGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36' diff --git a/changedetectionio/tests/test_filter_failure_notification.py b/changedetectionio/tests/test_filter_failure_notification.py index b25931ea..6d4c3154 100644 --- a/changedetectionio/tests/test_filter_failure_notification.py +++ b/changedetectionio/tests/test_filter_failure_notification.py @@ -21,10 +21,11 @@ def set_response_with_filter(): f.write(test_return_data) return None -def run_filter_test(client, content_filter): +def run_filter_test(client, live_server, content_filter): + + # Response WITHOUT the filter ID element + set_original_response() - # Give the endpoint time to spin up - time.sleep(1) # cleanup for the next client.get( url_for("form_delete", uuid="all"), @@ -79,6 +80,7 @@ def run_filter_test(client, content_filter): "include_filters": content_filter, "fetch_backend": "html_requests"}) + # A POST here will also reset the filter failure counter (filter_failure_notification_threshold_attempts) res = client.post( url_for("edit_page", uuid="first"), data=notification_form_data, @@ -91,20 +93,21 @@ def run_filter_test(client, content_filter): # Now the notification should not exist, because we didnt reach the threshold assert not os.path.isfile("test-datastore/notification.txt") - # -2 because we would have checked twice above (on adding and on edit) + # recheck it up to just before the threshold, including the fact that in the previous POST it would have rechecked (and incremented) for i in range(0, App._FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT-2): - res = 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) - assert not os.path.isfile("test-datastore/notification.txt"), f"test-datastore/notification.txt should not exist - Attempt {i}" + time.sleep(2) # delay for apprise to fire + assert not os.path.isfile("test-datastore/notification.txt"), f"test-datastore/notification.txt should not exist - Attempt {i} when threshold is {App._FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT}" # We should see something in the frontend + res = client.get(url_for("index")) assert b'Warning, no filters were found' in res.data - # One more check should trigger it (see -2 above) - client.get(url_for("form_watch_checknow"), follow_redirects=True) - wait_for_all_checks(client) + # One more check should trigger the _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT threshold client.get(url_for("form_watch_checknow"), follow_redirects=True) wait_for_all_checks(client) + time.sleep(2) # delay for apprise to fire # Now it should exist and contain our "filter not found" alert assert os.path.isfile("test-datastore/notification.txt") @@ -149,13 +152,9 @@ def test_setup(live_server): live_server_setup(live_server) def test_check_include_filters_failure_notification(client, live_server): - set_original_response() - wait_for_all_checks(client) - run_filter_test(client, '#nope-doesnt-exist') + run_filter_test(client, live_server,'#nope-doesnt-exist') def test_check_xpath_filter_failure_notification(client, live_server): - set_original_response() - time.sleep(1) - run_filter_test(client, '//*[@id="nope-doesnt-exist"]') + run_filter_test(client, live_server, '//*[@id="nope-doesnt-exist"]') # Test that notification is never sent diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py index dcb9dbe7..f17d3a28 100644 --- a/changedetectionio/update_worker.py +++ b/changedetectionio/update_worker.py @@ -348,7 +348,7 @@ class update_worker(threading.Thread): # Send notification if we reached the threshold? threshold = self.datastore.data['settings']['application'].get('filter_failure_notification_threshold_attempts', 0) - logger.error(f"Filter for {uuid} not found, consecutive_filter_failures: {c}") + logger.warning(f"Filter for {uuid} not found, consecutive_filter_failures: {c}") if threshold > 0 and c >= threshold: if not self.datastore.data['watching'][uuid].get('notification_muted'): self.send_filter_failure_notification(uuid) @@ -362,7 +362,6 @@ class update_worker(threading.Thread): # Yes fine, so nothing todo, don't continue to process. process_changedetection_results = False changed_detected = False - self.datastore.update_watch(uuid=uuid, update_obj={'last_error': False}) except content_fetchers.exceptions.BrowserConnectError as e: self.datastore.update_watch(uuid=uuid, update_obj={'last_error': e.msg