mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-10-31 06:37:41 +00:00 
			
		
		
		
	Compare commits
	
		
			43 Commits
		
	
	
		
			0.50.31
			...
			parallel-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ca63dad896 | ||
|   | bd9b72dbfa | ||
|   | 8473da4bdb | ||
|   | 762e2dacb2 | ||
|   | 62ea1f9b24 | ||
|   | 14a6ced8f4 | ||
|   | 465e5e2ecc | ||
|   | ada63a3200 | ||
|   | eef5425908 | ||
|   | 096bd21663 | ||
|   | 0f53233272 | ||
|   | faaa9937d6 | ||
|   | 950d59ccfa | ||
|   | bd3f0360e4 | ||
|   | 57347fd55c | ||
|   | 8ef782760a | ||
|   | 4e20fce82c | ||
|   | 7d8c127e1f | ||
|   | 0ca2acd38c | ||
|   | 2a0131d0f4 | ||
|   | 9ed236434e | ||
|   | ab0b85d088 | ||
|   | 66aec365c2 | ||
|   | e09cea60ef | ||
|   | f304ae19db | ||
|   | 2116b2cb93 | ||
|   | 8f580ac96b | ||
|   | a8cadc3d16 | ||
|   | c9290d73e0 | ||
|   | 2db5e906e9 | ||
|   | 0751bd371a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3ffa0805e9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3335270692 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a7573b10ec | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | df945ad743 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4536e95205 | ||
|   | 1479d7bd46 | ||
|   | 9ba2094f75 | ||
|   | 8aa012ba8e | ||
|   | 8bc6b10db1 | ||
|   | 76d799c95b | ||
|   | 7c8bdfcc9f | ||
|   | 01a938d7ce | 
							
								
								
									
										51
									
								
								.github/actions/extract-memory-report/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								.github/actions/extract-memory-report/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| name: 'Extract Memory Test Report' | ||||
| description: 'Extracts and displays memory test report from a container' | ||||
| inputs: | ||||
|   container-name: | ||||
|     description: 'Name of the container to extract logs from' | ||||
|     required: true | ||||
|   python-version: | ||||
|     description: 'Python version for artifact naming' | ||||
|     required: true | ||||
|   output-dir: | ||||
|     description: 'Directory to store output logs' | ||||
|     required: false | ||||
|     default: 'output-logs' | ||||
|  | ||||
| runs: | ||||
|   using: "composite" | ||||
|   steps: | ||||
|     - name: Create output directory | ||||
|       shell: bash | ||||
|       run: | | ||||
|         mkdir -p ${{ inputs.output-dir }} | ||||
|  | ||||
|     - name: Dump container log | ||||
|       shell: bash | ||||
|       run: | | ||||
|         echo "Disabled for now" | ||||
| #        return | ||||
| #        docker logs ${{ inputs.container-name }} > ${{ inputs.output-dir }}/${{ inputs.container-name }}-stdout-${{ inputs.python-version }}.txt 2>&1 || echo "Could not get stdout" | ||||
| #        docker logs ${{ inputs.container-name }} 2> ${{ inputs.output-dir }}/${{ inputs.container-name }}-stderr-${{ inputs.python-version }}.txt || echo "Could not get stderr" | ||||
|  | ||||
|     - name: Extract and display memory test report | ||||
|       shell: bash | ||||
|       run: | | ||||
|         echo "Disabled for now" | ||||
| #        echo "Extracting test-memory.log from container..." | ||||
| #        docker cp ${{ inputs.container-name }}:/app/changedetectionio/test-memory.log ${{ inputs.output-dir }}/test-memory-${{ inputs.python-version }}.log || echo "test-memory.log not found in container" | ||||
| # | ||||
| #        echo "=== Top 10 Highest Peak Memory Tests ===" | ||||
| #        if [ -f ${{ inputs.output-dir }}/test-memory-${{ inputs.python-version }}.log ]; then | ||||
| #          grep "Peak memory:" ${{ inputs.output-dir }}/test-memory-${{ inputs.python-version }}.log | \ | ||||
| #            sed 's/.*Peak memory: //' | \ | ||||
| #            paste -d'|' - <(grep "Peak memory:" ${{ inputs.output-dir }}/test-memory-${{ inputs.python-version }}.log) | \ | ||||
| #            sort -t'|' -k1 -nr | \ | ||||
| #            cut -d'|' -f2 | \ | ||||
| #            head -10 | ||||
| #          echo "" | ||||
| #          echo "=== Full Memory Test Report ===" | ||||
| #          cat ${{ inputs.output-dir }}/test-memory-${{ inputs.python-version }}.log | ||||
| #        else | ||||
| #          echo "No memory log available" | ||||
| #        fi | ||||
							
								
								
									
										8
									
								
								.github/workflows/containers.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/containers.yml
									
									
									
									
										vendored
									
									
								
							| @@ -45,6 +45,14 @@ jobs: | ||||
|         with: | ||||
|           python-version: 3.11 | ||||
|  | ||||
|       - name: Cache pip packages | ||||
|         uses: actions/cache@v4 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-pip- | ||||
|  | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           python -m pip install --upgrade pip | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/pypi-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/pypi-release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|     - name: Build a binary wheel and a source tarball | ||||
|       run: python3 -m build | ||||
|     - name: Store the distribution packages | ||||
|       uses: actions/upload-artifact@v4 | ||||
|       uses: actions/upload-artifact@v5 | ||||
|       with: | ||||
|         name: python-package-distributions | ||||
|         path: dist/ | ||||
| @@ -34,7 +34,7 @@ jobs: | ||||
|     - build | ||||
|     steps: | ||||
|     - name: Download all the dists | ||||
|       uses: actions/download-artifact@v5 | ||||
|       uses: actions/download-artifact@v6 | ||||
|       with: | ||||
|         name: python-package-distributions | ||||
|         path: dist/ | ||||
| @@ -93,7 +93,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|     - name: Download all the dists | ||||
|       uses: actions/download-artifact@v5 | ||||
|       uses: actions/download-artifact@v6 | ||||
|       with: | ||||
|         name: python-package-distributions | ||||
|         path: dist/ | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/test-container-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/test-container-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -50,6 +50,14 @@ jobs: | ||||
|           with: | ||||
|             python-version: 3.11 | ||||
|  | ||||
|         - name: Cache pip packages | ||||
|           uses: actions/cache@v4 | ||||
|           with: | ||||
|             path: ~/.cache/pip | ||||
|             key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} | ||||
|             restore-keys: | | ||||
|               ${{ runner.os }}-pip- | ||||
|  | ||||
|         # Just test that the build works, some libraries won't compile on ARM/rPi etc | ||||
|         - name: Set up QEMU | ||||
|           uses: docker/setup-qemu-action@v3 | ||||
|   | ||||
							
								
								
									
										470
									
								
								.github/workflows/test-stack-reusable-workflow.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										470
									
								
								.github/workflows/test-stack-reusable-workflow.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,141 +15,294 @@ on: | ||||
|         default: false | ||||
|  | ||||
| jobs: | ||||
|   test-application: | ||||
|   # Build the Docker image once and share it with all test jobs | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       # Mainly just for link/flake8 | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v6 | ||||
|         with: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Cache pip packages | ||||
|         uses: actions/cache@v4 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: ${{ runner.os }}-pip-py${{ env.PYTHON_VERSION }}-${{ hashFiles('requirements.txt') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-pip-py${{ env.PYTHON_VERSION }}- | ||||
|             ${{ runner.os }}-pip- | ||||
|  | ||||
|       - 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'          | ||||
|           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 | ||||
|           docker run test-changedetectionio bash -c 'python3 --version' | ||||
|  | ||||
|       - name: Spin up ancillary SMTP+Echo message test server | ||||
|       - name: Save Docker image | ||||
|         run: | | ||||
|           # Debug SMTP server/echo message back server, telnet 11080 to it should immediately bounce back the most recent message that tried to send (then you can see if cdio tried to send, the format, etc) | ||||
|           # 11025 is the SMTP port for testing | ||||
|           # apprise example would be 'mailto://changedetection@localhost:11025/?to=fff@home.com  (it will also echo to STDOUT) | ||||
|           # telnet localhost 11080 | ||||
|           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 | ||||
|           docker save test-changedetectionio -o /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Show docker container state and other debug info | ||||
|       - name: Upload Docker image artifact | ||||
|         uses: actions/upload-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp/test-changedetectionio.tar | ||||
|           retention-days: 1 | ||||
|  | ||||
|   # Unit tests (lightweight, no ancillary services needed) | ||||
|   unit-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           set -x | ||||
|           echo "Running processes in docker..." | ||||
|           docker ps | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Run Unit Tests | ||||
|         run: | | ||||
|           # Unit tests | ||||
|           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' | ||||
|           docker run test-changedetectionio  bash -c 'python3 -m unittest changedetectionio.tests.unit.test_semver' | ||||
|           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' | ||||
|           docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_semver' | ||||
|  | ||||
|       - name: Test built container with Pytest (generally as requests/plaintext fetching) | ||||
|   # Basic pytest tests with ancillary services | ||||
|   basic-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 25 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           # 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' | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
| # PLAYWRIGHT/NODE-> CDP | ||||
|       - name: Playwright and SocketPuppetBrowser - Specific tests in built container | ||||
|       - name: Test built container with Pytest | ||||
|         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  -vv --capture=tee-sys --showlocals --tb=long --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  -vv --capture=tee-sys --showlocals --tb=long --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  -vv --capture=tee-sys --showlocals --tb=long --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  -vv --capture=tee-sys --showlocals --tb=long --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py' | ||||
|           docker network inspect changedet-network >/dev/null 2>&1 || docker network create changedet-network | ||||
|           docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh' | ||||
|  | ||||
|       - name: Extract memory report and logs | ||||
|         if: always() | ||||
|         uses: ./.github/actions/extract-memory-report | ||||
|         with: | ||||
|           container-name: test-cdio-basic-tests | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - 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 'find .; cd changedetectionio; pytest --live-server-host=0.0.0.0  --live-server-port=5004 tests/test_request.py; pwd;find .' | ||||
|       - name: Store test artifacts | ||||
|         if: always() | ||||
|         uses: actions/upload-artifact@v5 | ||||
|         with: | ||||
|           name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }} | ||||
|           path: output-logs | ||||
|  | ||||
|       - 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' | ||||
|   # Playwright tests | ||||
|   playwright-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
| # STRAIGHT TO CDP | ||||
|       - name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container | ||||
|         if: ${{ inputs.skip-pypuppeteer == false }} | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         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' | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks | ||||
|         if: ${{ inputs.skip-pypuppeteer == false }} | ||||
|       - name: Spin up ancillary services | ||||
|         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' | ||||
|           docker network create changedet-network | ||||
|           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: 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' | ||||
|       - name: Playwright - Specific tests in built container | ||||
|         run: | | ||||
|           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 -vv --capture=tee-sys --showlocals --tb=long --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 -vv --capture=tee-sys --showlocals --tb=long --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 -vv --capture=tee-sys --showlocals --tb=long --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 -vv --capture=tee-sys --showlocals --tb=long --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py' | ||||
|  | ||||
|       - name: Playwright - Headers and requests | ||||
|         run: | | ||||
|           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 'find .; cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py; pwd;find .' | ||||
|  | ||||
|       - name: Playwright - Restock detection | ||||
|         run: | | ||||
|           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' | ||||
|  | ||||
|   # Pyppeteer tests | ||||
|   pyppeteer-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     if: ${{ inputs.skip-pypuppeteer == false }} | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Spin up ancillary services | ||||
|         run: | | ||||
|           docker network create changedet-network | ||||
|           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 | ||||
|  | ||||
|       - name: Pyppeteer - Specific tests in built container | ||||
|         run: | | ||||
|           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 - Headers and requests checks | ||||
|         run: | | ||||
|           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 - Restock detection | ||||
|         run: | | ||||
|           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 tests | ||||
|   selenium-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Spin up ancillary services | ||||
|         run: | | ||||
|           docker network create changedet-network | ||||
|           docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4 | ||||
|           sleep 3 | ||||
|  | ||||
|       - name: Specific tests 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' | ||||
|  | ||||
| # 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' | ||||
|           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 | ||||
|  | ||||
|   # SMTP tests | ||||
|   smtp-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         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' | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Spin up SMTP test server | ||||
|         run: | | ||||
|           docker network create changedet-network | ||||
|           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' | ||||
|  | ||||
| # 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 | ||||
|   # Proxy tests | ||||
|   proxy-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Spin up services | ||||
|         run: | | ||||
|           docker network create changedet-network | ||||
|           docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4 | ||||
|           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: Test proxy Squid style interaction | ||||
|         run: | | ||||
|           cd changedetectionio | ||||
|           ./run_proxy_tests.sh | ||||
|           docker ps | ||||
|           cd .. | ||||
|  | ||||
|       - name: Test proxy SOCKS5 style interaction | ||||
| @@ -158,28 +311,65 @@ jobs: | ||||
|           ./run_socks_proxy_tests.sh | ||||
|           cd .. | ||||
|  | ||||
|   # Custom browser URL tests | ||||
|   custom-browser-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Spin up ancillary services | ||||
|         run: | | ||||
|           docker network create changedet-network | ||||
|           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: Test custom browser URL | ||||
|         run: | | ||||
|           cd changedetectionio | ||||
|           ./run_custom_browser_url_tests.sh | ||||
|           cd .. | ||||
|  | ||||
|       - name: Test changedetection.io container starts+runs basically without error | ||||
|   # Container startup tests | ||||
|   container-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker run --name test-changedetectionio -p 5556:5000  -d test-changedetectionio | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Test 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 came from STDOUT | ||||
|           curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid | ||||
|           curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid | ||||
|           docker logs test-changedetectionio 2>/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 HTTPS SSL mode | ||||
| @@ -187,102 +377,66 @@ jobs: | ||||
|           openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost" | ||||
|           docker run --name test-changedetectionio-ssl --rm -e SSL_CERT_FILE=cert.pem -e SSL_PRIVKEY_FILE=privkey.pem -p 5000:5000 -v ./cert.pem:/app/cert.pem -v ./privkey.pem:/app/privkey.pem -d test-changedetectionio | ||||
|           sleep 3 | ||||
|           # Should return 0 (no error) when grep finds it | ||||
|           # -k because its self-signed | ||||
|           curl --retry-connrefused --retry 6 -k https://localhost:5000 -v|grep -q checkbox-uuid | ||||
|        | ||||
|           docker kill test-changedetectionio-ssl | ||||
|  | ||||
|       - name: Test IPv6 Mode | ||||
|         run: | | ||||
|           # IPv6 - :: bind to all interfaces inside container (like 0.0.0.0), ::1 would be localhost only | ||||
|           docker run --name test-changedetectionio-ipv6 --rm -p 5000:5000 -e LISTEN_HOST=:: -d test-changedetectionio | ||||
|           sleep 3 | ||||
|           # Should return 0 (no error) when grep finds it on localhost | ||||
|           curl --retry-connrefused --retry 6 http://[::1]:5000 -v|grep -q checkbox-uuid | ||||
|           docker kill test-changedetectionio-ipv6 | ||||
|  | ||||
|       - name: Test changedetection.io SIGTERM and SIGINT signal shutdown | ||||
|   # Signal tests | ||||
|   signal-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: build | ||||
|     timeout-minutes: 10 | ||||
|     env: | ||||
|       PYTHON_VERSION: ${{ inputs.python-version }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5 | ||||
|  | ||||
|       - name: Download Docker image artifact | ||||
|         uses: actions/download-artifact@v5 | ||||
|         with: | ||||
|           name: test-changedetectionio-${{ env.PYTHON_VERSION }} | ||||
|           path: /tmp | ||||
|  | ||||
|       - name: Load Docker image | ||||
|         run: | | ||||
|           docker load -i /tmp/test-changedetectionio.tar | ||||
|  | ||||
|       - name: Test 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 | ||||
|           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 | ||||
|           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: Extract and display memory test report | ||||
|         if: always() | ||||
|         run: | | ||||
|           # Extract test-memory.log from the container | ||||
|           echo "Extracting test-memory.log from container..." | ||||
|           docker cp test-cdio-basic-tests:/app/changedetectionio/test-memory.log output-logs/test-memory-${{ env.PYTHON_VERSION }}.log || echo "test-memory.log not found in container" | ||||
|  | ||||
|           # Display the memory log contents for immediate visibility in workflow output | ||||
|           echo "=== Top 10 Highest Peak Memory Tests ===" | ||||
|           if [ -f output-logs/test-memory-${{ env.PYTHON_VERSION }}.log ]; then | ||||
|             # Sort by peak memory value (extract number before MB and sort numerically, reverse order) | ||||
|             grep "Peak memory:" output-logs/test-memory-${{ env.PYTHON_VERSION }}.log | \ | ||||
|               sed 's/.*Peak memory: //' | \ | ||||
|               paste -d'|' - <(grep "Peak memory:" output-logs/test-memory-${{ env.PYTHON_VERSION }}.log) | \ | ||||
|               sort -t'|' -k1 -nr | \ | ||||
|               cut -d'|' -f2 | \ | ||||
|               head -10 | ||||
|             echo "" | ||||
|             echo "=== Full Memory Test Report ===" | ||||
|             cat output-logs/test-memory-${{ env.PYTHON_VERSION }}.log | ||||
|           else | ||||
|             echo "No memory log available" | ||||
|           fi | ||||
|  | ||||
|       - name: Store everything including test-datastore | ||||
|         if: always() | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }} | ||||
|           path: . | ||||
|   | ||||
| @@ -36,6 +36,7 @@ ENV OPENSSL_INCLUDE_DIR="/usr/include/openssl" | ||||
| ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 | ||||
| RUN --mount=type=cache,target=/tmp/pip-cache \ | ||||
|     pip install \ | ||||
|     --prefer-binary \ | ||||
|     --extra-index-url https://www.piwheels.org/simple \ | ||||
|     --extra-index-url https://pypi.anaconda.org/ARM-software/simple \ | ||||
|     --cache-dir=/tmp/pip-cache \ | ||||
| @@ -47,6 +48,7 @@ RUN --mount=type=cache,target=/tmp/pip-cache \ | ||||
| # https://github.com/dgtlmoon/changedetection.io/pull/1067 also musl/alpine (not supported) | ||||
| RUN --mount=type=cache,target=/tmp/pip-cache \ | ||||
|     pip install \ | ||||
|     --prefer-binary \ | ||||
|     --cache-dir=/tmp/pip-cache \ | ||||
|     --target=/dependencies \ | ||||
|     playwright~=1.48.0 \ | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| # Read more https://github.com/dgtlmoon/changedetection.io/wiki | ||||
|  | ||||
| __version__ = '0.50.31' | ||||
| __version__ = '0.50.33' | ||||
|  | ||||
| from changedetectionio.strtobool import strtobool | ||||
| from json.decoder import JSONDecodeError | ||||
|   | ||||
| @@ -1,9 +1,22 @@ | ||||
| import os | ||||
| from changedetectionio.strtobool import strtobool | ||||
| from flask_restful import abort, Resource | ||||
| from flask import request | ||||
| import validators | ||||
| from functools import wraps | ||||
| from . import auth, validate_openapi_request | ||||
| from ..validate_url import is_safe_valid_url | ||||
|  | ||||
|  | ||||
| def default_content_type(content_type='text/plain'): | ||||
|     """Decorator to set a default Content-Type header if none is provided.""" | ||||
|     def decorator(f): | ||||
|         @wraps(f) | ||||
|         def wrapper(*args, **kwargs): | ||||
|             if not request.content_type: | ||||
|                 # Set default content type in the request environment | ||||
|                 request.environ['CONTENT_TYPE'] = content_type | ||||
|             return f(*args, **kwargs) | ||||
|         return wrapper | ||||
|     return decorator | ||||
|  | ||||
|  | ||||
| class Import(Resource): | ||||
| @@ -12,6 +25,7 @@ class Import(Resource): | ||||
|         self.datastore = kwargs['datastore'] | ||||
|  | ||||
|     @auth.check_token | ||||
|     @default_content_type('text/plain') #3547 #3542 | ||||
|     @validate_openapi_request('importWatches') | ||||
|     def post(self): | ||||
|         """Import a list of watched URLs.""" | ||||
| @@ -35,14 +49,13 @@ class Import(Resource): | ||||
|  | ||||
|         urls = request.get_data().decode('utf8').splitlines() | ||||
|         added = [] | ||||
|         allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False')) | ||||
|         for url in urls: | ||||
|             url = url.strip() | ||||
|             if not len(url): | ||||
|                 continue | ||||
|  | ||||
|             # If hosts that only contain alphanumerics are allowed ("localhost" for example) | ||||
|             if not validators.url(url, simple_host=allow_simplehost): | ||||
|             if not is_safe_valid_url(url): | ||||
|                 return f"Invalid or unsupported URL - {url}", 400 | ||||
|  | ||||
|             if dedupe and self.datastore.url_exists(url): | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import os | ||||
| from changedetectionio.strtobool import strtobool | ||||
|  | ||||
| from changedetectionio.validate_url import is_safe_valid_url | ||||
|  | ||||
| from flask_expects_json import expects_json | ||||
| from changedetectionio import queuedWatchMetaData | ||||
| from changedetectionio import worker_handler | ||||
| from flask_restful import abort, Resource | ||||
| from flask import request, make_response, send_from_directory | ||||
| import validators | ||||
| from . import auth | ||||
| import copy | ||||
|  | ||||
| @@ -121,6 +121,10 @@ class Watch(Resource): | ||||
|         if validation_error: | ||||
|             return validation_error, 400 | ||||
|  | ||||
|         # XSS etc protection | ||||
|         if request.json.get('url') and not is_safe_valid_url(request.json.get('url')): | ||||
|             return "Invalid URL", 400 | ||||
|  | ||||
|         watch.update(request.json) | ||||
|  | ||||
|         return "OK", 200 | ||||
| @@ -226,9 +230,7 @@ class CreateWatch(Resource): | ||||
|         json_data = request.get_json() | ||||
|         url = json_data['url'].strip() | ||||
|  | ||||
|         # If hosts that only contain alphanumerics are allowed ("localhost" for example) | ||||
|         allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False')) | ||||
|         if not validators.url(url, simple_host=allow_simplehost): | ||||
|         if not is_safe_valid_url(url): | ||||
|             return "Invalid or unsupported URL", 400 | ||||
|  | ||||
|         if json_data.get('proxy'): | ||||
|   | ||||
| @@ -240,9 +240,7 @@ nav | ||||
|                     <p> | ||||
|                        {{ render_field(form.application.form.scheduler_timezone_default) }} | ||||
|                         <datalist id="timezones" style="display: none;"> | ||||
|                             {% for tz_name in available_timezones %} | ||||
|                                 <option value="{{ tz_name }}">{{ tz_name }}</option> | ||||
|                             {% endfor %} | ||||
|                             {%- for timezone in available_timezones -%}<option value="{{ timezone }}">{{ timezone }}</option>{%- endfor -%} | ||||
|                         </datalist> | ||||
|                     </p> | ||||
|                 </div> | ||||
|   | ||||
| @@ -76,14 +76,14 @@ def _handle_operations(op, uuids, datastore, worker_handler, update_q, queuedWat | ||||
|  | ||||
|     elif (op == 'notification-default'): | ||||
|         from changedetectionio.notification import ( | ||||
|             default_notification_format_for_watch | ||||
|             USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|         ) | ||||
|         for uuid in uuids: | ||||
|             if datastore.data['watching'].get(uuid): | ||||
|                 datastore.data['watching'][uuid]['notification_title'] = None | ||||
|                 datastore.data['watching'][uuid]['notification_body'] = None | ||||
|                 datastore.data['watching'][uuid]['notification_urls'] = [] | ||||
|                 datastore.data['watching'][uuid]['notification_format'] = default_notification_format_for_watch | ||||
|                 datastore.data['watching'][uuid]['notification_format'] = USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|         if emit_flash: | ||||
|             flash(f"{len(uuids)} watches set to use default notification settings") | ||||
|  | ||||
|   | ||||
| @@ -75,7 +75,6 @@ class Fetcher(): | ||||
|         self.screenshot = None | ||||
|         self.xpath_data = None | ||||
|         # Keep headers and status_code as they're small | ||||
|         logger.trace("Fetcher content cleared from memory") | ||||
|  | ||||
|     @abstractmethod | ||||
|     def get_error(self): | ||||
|   | ||||
| @@ -2,10 +2,17 @@ import difflib | ||||
| from typing import List, Iterator, Union | ||||
|  | ||||
| # https://github.com/dgtlmoon/changedetection.io/issues/821#issuecomment-1241837050 | ||||
| HTML_ADDED_STYLE = "background-color: #d2f7c2; color: #255d00;" | ||||
| HTML_CHANGED_INTO_STYLE = "background-color: #dafbe1; color: #116329;" | ||||
| HTML_CHANGED_STYLE = "background-color: #ffd6cc; color: #7a2000;" | ||||
| HTML_REMOVED_STYLE = "background-color: #ffebe9; color: #82071e;" | ||||
| #HTML_ADDED_STYLE = "background-color: #d2f7c2; color: #255d00;" | ||||
| #HTML_CHANGED_INTO_STYLE = "background-color: #dafbe1; color: #116329;" | ||||
| #HTML_CHANGED_STYLE = "background-color: #ffd6cc; color: #7a2000;" | ||||
| #HTML_REMOVED_STYLE = "background-color: #ffebe9; color: #82071e;" | ||||
|  | ||||
| # @todo - In the future we can make this configurable | ||||
| HTML_ADDED_STYLE = "background-color: #eaf2c2; color: #406619" | ||||
| HTML_REMOVED_STYLE = "background-color: #fadad7; color: #b30000" | ||||
| HTML_CHANGED_STYLE = HTML_REMOVED_STYLE | ||||
| HTML_CHANGED_INTO_STYLE = HTML_ADDED_STYLE | ||||
|  | ||||
|  | ||||
| # These get set to html or telegram type or discord compatible or whatever in handler.py | ||||
| # Something that cant get escaped to HTML by accident | ||||
|   | ||||
| @@ -133,6 +133,11 @@ def get_socketio_path(): | ||||
|     # Socket.IO will be available at {prefix}/socket.io/ | ||||
|     return prefix | ||||
|  | ||||
| @app.template_global('is_safe_valid_url') | ||||
| def _is_safe_valid_url(test_url): | ||||
|     from .validate_url import is_safe_valid_url | ||||
|     return is_safe_valid_url(test_url) | ||||
|  | ||||
|  | ||||
| @app.template_filter('format_number_locale') | ||||
| def _jinja2_filter_format_number_locale(value: float) -> str: | ||||
| @@ -382,7 +387,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             # We would sometimes get login loop errors on sites hosted in sub-paths | ||||
|  | ||||
|             # note for the future: | ||||
|             #            if not is_safe_url(next): | ||||
|             #            if not is_safe_valid_url(next): | ||||
|             #                return flask.abort(400) | ||||
|             return redirect(url_for('watchlist.index')) | ||||
|  | ||||
|   | ||||
| @@ -28,11 +28,8 @@ from wtforms.utils import unset_value | ||||
|  | ||||
| from wtforms.validators import ValidationError | ||||
|  | ||||
| from validators.url import url as url_validator | ||||
|  | ||||
| from changedetectionio.widgets import TernaryNoneBooleanField | ||||
|  | ||||
|  | ||||
| # default | ||||
| # each select <option data-enabled="enabled-0-0" | ||||
| from changedetectionio.blueprint.browser_steps.browser_steps import browser_step_ui_config | ||||
| @@ -541,19 +538,10 @@ class validateURL(object): | ||||
|  | ||||
|  | ||||
| def validate_url(test_url): | ||||
|     # If hosts that only contain alphanumerics are allowed ("localhost" for example) | ||||
|     try: | ||||
|         url_validator(test_url, simple_host=allow_simplehost) | ||||
|     except validators.ValidationError: | ||||
|         #@todo check for xss | ||||
|         message = f"'{test_url}' is not a valid URL." | ||||
|     from changedetectionio.validate_url import is_safe_valid_url | ||||
|     if not is_safe_valid_url(test_url): | ||||
|         # This should be wtforms.validators. | ||||
|         raise ValidationError(message) | ||||
|  | ||||
|     from .model.Watch import is_safe_url | ||||
|     if not is_safe_url(test_url): | ||||
|         # This should be wtforms.validators. | ||||
|         raise ValidationError('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format') | ||||
|         raise ValidationError('Watch protocol is not permitted or invalid URL format') | ||||
|  | ||||
|  | ||||
| class ValidateSinglePythonRegexString(object): | ||||
| @@ -741,7 +729,6 @@ class quickWatchForm(Form): | ||||
|     edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|  | ||||
|  | ||||
|  | ||||
| # Common to a single watch and the global settings | ||||
| class commonSettingsForm(Form): | ||||
|     from . import processors | ||||
| @@ -754,7 +741,7 @@ class commonSettingsForm(Form): | ||||
|  | ||||
|     fetch_backend = RadioField(u'Fetch Method', choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) | ||||
|     notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) | ||||
|     notification_format = SelectField('Notification format', choices=valid_notification_formats.keys()) | ||||
|     notification_format = SelectField('Notification format', choices=list(valid_notification_formats.items())) | ||||
|     notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()]) | ||||
|     notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()]) | ||||
|     processor = RadioField( label=u"Processor - What do you want to achieve?", choices=processors.available_processors(), default="text_json_diff") | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from functools import lru_cache | ||||
|  | ||||
| from loguru import logger | ||||
| from typing import List | ||||
| import html | ||||
| @@ -13,7 +15,6 @@ TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S) | ||||
| META_CS  = re.compile(r'<meta[^>]+charset=["\']?\s*([a-z0-9_\-:+.]+)', re.I) | ||||
| META_CT  = re.compile(r'<meta[^>]+http-equiv=["\']?content-type["\']?[^>]*content=["\'][^>]*charset=([a-z0-9_\-:+.]+)', re.I) | ||||
|  | ||||
|  | ||||
| # 'price' , 'lowPrice', 'highPrice' are usually under here | ||||
| # All of those may or may not appear on different websites - I didnt find a way todo case-insensitive searching here | ||||
| LD_JSON_PRODUCT_OFFER_SELECTORS = ["json:$..offers", "json:$..Offers"] | ||||
| @@ -22,9 +23,9 @@ class JSONNotFound(ValueError): | ||||
|     def __init__(self, msg): | ||||
|         ValueError.__init__(self, msg) | ||||
|  | ||||
|  | ||||
| # Doesn't look like python supports forward slash auto enclosure in re.findall | ||||
| # So convert it to inline flag "(?i)foobar" type configuration | ||||
| @lru_cache(maxsize=100) | ||||
| def perl_style_slash_enclosed_regex_to_options(regex): | ||||
|  | ||||
|     res = re.search(PERL_STYLE_REGEX, regex, re.IGNORECASE) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from os import getenv | ||||
| from copy import deepcopy | ||||
|  | ||||
| from changedetectionio.blueprint.rss import RSS_FORMAT_TYPES | ||||
|  | ||||
| @@ -74,7 +75,8 @@ class model(dict): | ||||
|  | ||||
|     def __init__(self, *arg, **kw): | ||||
|         super(model, self).__init__(*arg, **kw) | ||||
|         self.update(self.base_config) | ||||
|         # CRITICAL: deepcopy to avoid sharing mutable objects between instances | ||||
|         self.update(deepcopy(self.base_config)) | ||||
|  | ||||
|  | ||||
| def parse_headers_from_text_file(filepath): | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from blinker import signal | ||||
| from changedetectionio.validate_url import is_safe_valid_url | ||||
|  | ||||
| from changedetectionio.strtobool import strtobool | ||||
| from changedetectionio.jinja2_custom import render as jinja_render | ||||
| @@ -12,32 +13,12 @@ from .. import jinja2_custom as safe_jinja | ||||
| from ..diff import ADDED_PLACEMARKER_OPEN | ||||
| from ..html_tools import TRANSLATE_WHITESPACE_TABLE | ||||
|  | ||||
| # Allowable protocols, protects against javascript: etc | ||||
| # file:// is further checked by ALLOW_FILE_URI | ||||
| SAFE_PROTOCOL_REGEX='^(http|https|ftp|file):' | ||||
| FAVICON_RESAVE_THRESHOLD_SECONDS=86400 | ||||
|  | ||||
|  | ||||
| minimum_seconds_recheck_time = int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3)) | ||||
| mtable = {'seconds': 1, 'minutes': 60, 'hours': 3600, 'days': 86400, 'weeks': 86400 * 7} | ||||
|  | ||||
|  | ||||
| def is_safe_url(test_url): | ||||
|     # See https://github.com/dgtlmoon/changedetection.io/issues/1358 | ||||
|  | ||||
|     # Remove 'source:' prefix so we dont get 'source:javascript:' etc | ||||
|     # 'source:' is a valid way to tell us to return the source | ||||
|  | ||||
|     r = re.compile(re.escape('source:'), re.IGNORECASE) | ||||
|     test_url = r.sub('', test_url) | ||||
|  | ||||
|     pattern = re.compile(os.getenv('SAFE_PROTOCOL_REGEX', SAFE_PROTOCOL_REGEX), re.IGNORECASE) | ||||
|     if not pattern.match(test_url.strip()): | ||||
|         return False | ||||
|  | ||||
|     return True | ||||
|  | ||||
|  | ||||
| class model(watch_base): | ||||
|     __newest_history_key = None | ||||
|     __history_n = 0 | ||||
| @@ -80,7 +61,7 @@ class model(watch_base): | ||||
|     def link(self): | ||||
|  | ||||
|         url = self.get('url', '') | ||||
|         if not is_safe_url(url): | ||||
|         if not is_safe_valid_url(url): | ||||
|             return 'DISABLED' | ||||
|  | ||||
|         ready_url = url | ||||
| @@ -101,7 +82,7 @@ class model(watch_base): | ||||
|             ready_url=ready_url.replace('source:', '') | ||||
|  | ||||
|         # Also double check it after any Jinja2 formatting just incase | ||||
|         if not is_safe_url(ready_url): | ||||
|         if not is_safe_valid_url(ready_url): | ||||
|             return 'DISABLED' | ||||
|         return ready_url | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import os | ||||
| import uuid | ||||
|  | ||||
| from changedetectionio import strtobool | ||||
| default_notification_format_for_watch = 'System default' | ||||
| USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH = 'System default' | ||||
| CONDITIONS_MATCH_LOGIC_DEFAULT = 'ALL' | ||||
|  | ||||
| class watch_base(dict): | ||||
| @@ -44,7 +44,7 @@ class watch_base(dict): | ||||
|             'method': 'GET', | ||||
|             'notification_alert_count': 0, | ||||
|             'notification_body': None, | ||||
|             'notification_format': default_notification_format_for_watch, | ||||
|             'notification_format': USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, | ||||
|             'notification_muted': False, | ||||
|             'notification_screenshot': False,  # Include the latest screenshot if available and supported by the apprise URL | ||||
|             'notification_title': None, | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| from changedetectionio.model import default_notification_format_for_watch | ||||
| from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|  | ||||
| default_notification_format = 'HTML Color' | ||||
| default_notification_format = 'htmlcolor' | ||||
| default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' | ||||
| default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' | ||||
|  | ||||
| # The values (markdown etc) are from apprise NotifyFormat, | ||||
| # But to avoid importing the whole heavy module just use the same strings here. | ||||
| valid_notification_formats = { | ||||
|     'Plain Text': 'text', | ||||
|     'HTML': 'html', | ||||
|     'HTML Color': 'htmlcolor', | ||||
|     'Markdown to HTML': 'markdown', | ||||
|     'text': 'Plain Text', | ||||
|     'html': 'HTML', | ||||
|     'htmlcolor': 'HTML Color', | ||||
|     'markdown': 'Markdown to HTML', | ||||
|     # Used only for editing a watch (not for global) | ||||
|     default_notification_format_for_watch: default_notification_format_for_watch | ||||
|     USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -195,25 +195,16 @@ def apprise_http_custom_handler( | ||||
|  | ||||
|     url = re.sub(rf"^{schema}", "https" if schema.endswith("s") else "http", parsed_url.get("url")) | ||||
|  | ||||
|     try: | ||||
|         response = requests.request( | ||||
|             method=method, | ||||
|             url=url, | ||||
|             auth=auth, | ||||
|             headers=headers, | ||||
|             params=params, | ||||
|             data=body.encode("utf-8") if isinstance(body, str) else body, | ||||
|         ) | ||||
|     response = requests.request( | ||||
|         method=method, | ||||
|         url=url, | ||||
|         auth=auth, | ||||
|         headers=headers, | ||||
|         params=params, | ||||
|         data=body.encode("utf-8") if isinstance(body, str) else body, | ||||
|     ) | ||||
|  | ||||
|         response.raise_for_status() | ||||
|     response.raise_for_status() | ||||
|  | ||||
|         logger.info(f"Successfully sent custom notification to {url}") | ||||
|         return True | ||||
|  | ||||
|     except requests.RequestException as e: | ||||
|         logger.error(f"Remote host error while sending custom notification to {url}: {e}") | ||||
|         return False | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error(f"Unexpected error occurred while sending custom notification to {url}: {e}") | ||||
|         return False | ||||
|     logger.info(f"Successfully sent custom notification to {url}") | ||||
|     return True | ||||
|   | ||||
							
								
								
									
										42
									
								
								changedetectionio/notification/email_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								changedetectionio/notification/email_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| def as_monospaced_html_email(content: str, title: str) -> str: | ||||
|     """ | ||||
|     Wraps `content` in a minimal, email-safe HTML template | ||||
|     that forces monospace rendering across Gmail, Hotmail, Apple Mail, etc. | ||||
|  | ||||
|     Args: | ||||
|         content: The body text (plain text or HTML-like). | ||||
|         title: The title plaintext | ||||
|     Returns: | ||||
|         A complete HTML document string suitable for sending as an email body. | ||||
|     """ | ||||
|  | ||||
|     # All line feed types should be removed and then this function should only be fed <br>'s | ||||
|     # Then it works with our <pre> styling without double linefeeds | ||||
|     content = content.translate(str.maketrans('', '', '\r\n')) | ||||
|  | ||||
|     if title: | ||||
|         import html | ||||
|         title = html.escape(title) | ||||
|     else: | ||||
|         title = '' | ||||
|     # 2. Full email-safe HTML | ||||
|     html_email = f"""<!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta name="x-apple-disable-message-reformatting"> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|   <!--[if mso]> | ||||
|     <style> | ||||
|       body, div, pre, td {{ font-family: "Courier New", Courier, monospace !important; }} | ||||
|     </style> | ||||
|   <![endif]--> | ||||
|   <title>{title}</title> | ||||
| </head> | ||||
| <body style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;"> | ||||
|   <pre role="article" aria-roledescription="email" lang="en" | ||||
|        style="font-family: monospace, 'Courier New', Courier; font-size: 0.8em; | ||||
|               white-space: pre-wrap; word-break: break-word;">{content}</pre> | ||||
| </body> | ||||
| </html>""" | ||||
|     return html_email | ||||
| @@ -6,6 +6,7 @@ from loguru import logger | ||||
| from urllib.parse import urlparse | ||||
| from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL | ||||
| from .apprise_plugin.custom_handlers import SUPPORTED_HTTP_METHODS | ||||
| from .email_helpers import as_monospaced_html_email | ||||
| from ..diff import HTML_REMOVED_STYLE, REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED, ADDED_PLACEMARKER_OPEN, HTML_ADDED_STYLE, \ | ||||
|     ADDED_PLACEMARKER_CLOSED, CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED, CHANGED_PLACEMARKER_OPEN, \ | ||||
|     CHANGED_PLACEMARKER_CLOSED, HTML_CHANGED_STYLE, HTML_CHANGED_INTO_STYLE | ||||
| @@ -62,13 +63,13 @@ def notification_format_align_with_apprise(n_format : str): | ||||
|     :return: | ||||
|     """ | ||||
|  | ||||
|     if n_format.lower().startswith('html'): | ||||
|     if n_format.startswith('html'): | ||||
|         # Apprise only knows 'html' not 'htmlcolor' etc, which shouldnt matter here | ||||
|         n_format = NotifyFormat.HTML.value | ||||
|     elif n_format.lower().startswith('markdown'): | ||||
|     elif n_format.startswith('markdown'): | ||||
|         # probably the same but just to be safe | ||||
|         n_format = NotifyFormat.MARKDOWN.value | ||||
|     elif n_format.lower().startswith('text'): | ||||
|     elif n_format.startswith('text'): | ||||
|         # probably the same but just to be safe | ||||
|         n_format = NotifyFormat.TEXT.value | ||||
|     else: | ||||
| @@ -76,6 +77,55 @@ def notification_format_align_with_apprise(n_format : str): | ||||
|  | ||||
|     return n_format | ||||
|  | ||||
| def apply_discord_markdown_to_body(n_body): | ||||
|     """ | ||||
|     Discord does not support <del> but it supports non-standard ~~strikethrough~~ | ||||
|     :param n_body: | ||||
|     :return: | ||||
|     """ | ||||
|     import re | ||||
|     # Define the mapping between your placeholders and markdown markers | ||||
|     replacements = [ | ||||
|         (REMOVED_PLACEMARKER_OPEN, '~~', REMOVED_PLACEMARKER_CLOSED, '~~'), | ||||
|         (ADDED_PLACEMARKER_OPEN, '**', ADDED_PLACEMARKER_CLOSED, '**'), | ||||
|         (CHANGED_PLACEMARKER_OPEN, '~~', CHANGED_PLACEMARKER_CLOSED, '~~'), | ||||
|         (CHANGED_INTO_PLACEMARKER_OPEN, '**', CHANGED_INTO_PLACEMARKER_CLOSED, '**'), | ||||
|     ] | ||||
|     # So that the markdown gets added without any whitespace following it which would break it | ||||
|     for open_tag, open_md, close_tag, close_md in replacements: | ||||
|         # Regex: match opening tag, optional whitespace, capture the content, optional whitespace, then closing tag | ||||
|         pattern = re.compile( | ||||
|             re.escape(open_tag) + r'(\s*)(.*?)?(\s*)' + re.escape(close_tag), | ||||
|             flags=re.DOTALL | ||||
|         ) | ||||
|         n_body = pattern.sub(lambda m: f"{m.group(1)}{open_md}{m.group(2)}{close_md}{m.group(3)}", n_body) | ||||
|     return n_body | ||||
|  | ||||
| def apply_standard_markdown_to_body(n_body): | ||||
|     """ | ||||
|     Apprise does not support ~~strikethrough~~ but it will convert <del> to HTML strikethrough. | ||||
|     :param n_body: | ||||
|     :return: | ||||
|     """ | ||||
|     import re | ||||
|     # Define the mapping between your placeholders and markdown markers | ||||
|     replacements = [ | ||||
|         (REMOVED_PLACEMARKER_OPEN, '<del>', REMOVED_PLACEMARKER_CLOSED, '</del>'), | ||||
|         (ADDED_PLACEMARKER_OPEN, '**', ADDED_PLACEMARKER_CLOSED, '**'), | ||||
|         (CHANGED_PLACEMARKER_OPEN, '<del>', CHANGED_PLACEMARKER_CLOSED, '</del>'), | ||||
|         (CHANGED_INTO_PLACEMARKER_OPEN, '**', CHANGED_INTO_PLACEMARKER_CLOSED, '**'), | ||||
|     ] | ||||
|  | ||||
|     # So that the markdown gets added without any whitespace following it which would break it | ||||
|     for open_tag, open_md, close_tag, close_md in replacements: | ||||
|         # Regex: match opening tag, optional whitespace, capture the content, optional whitespace, then closing tag | ||||
|         pattern = re.compile( | ||||
|             re.escape(open_tag) + r'(\s*)(.*?)?(\s*)' + re.escape(close_tag), | ||||
|             flags=re.DOTALL | ||||
|         ) | ||||
|         n_body = pattern.sub(lambda m: f"{m.group(1)}{open_md}{m.group(2)}{close_md}{m.group(3)}", n_body) | ||||
|     return n_body | ||||
|  | ||||
|  | ||||
| def apply_service_tweaks(url, n_body, n_title, requested_output_format): | ||||
|  | ||||
| @@ -106,7 +156,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): | ||||
|         n_body = n_body.replace('<br>', '\n') | ||||
|         n_body = n_body.replace('</br>', '\n') | ||||
|         n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '\n') | ||||
|          | ||||
|  | ||||
|         # Use strikethrough for removed content, bold for added content | ||||
|         n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '<s>') | ||||
|         n_body = n_body.replace(REMOVED_PLACEMARKER_CLOSED, '</s>') | ||||
| @@ -140,15 +190,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): | ||||
|         if requested_output_format == 'html': | ||||
|             # No diff placeholders, use Discord markdown for any other formatting | ||||
|             # Use Discord markdown: strikethrough for removed, bold for added | ||||
|             n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '~~') | ||||
|             n_body = n_body.replace(REMOVED_PLACEMARKER_CLOSED, '~~') | ||||
|             n_body = n_body.replace(ADDED_PLACEMARKER_OPEN, '**') | ||||
|             n_body = n_body.replace(ADDED_PLACEMARKER_CLOSED, '**') | ||||
|             # Handle changed/replaced lines (old → new) | ||||
|             n_body = n_body.replace(CHANGED_PLACEMARKER_OPEN, '~~') | ||||
|             n_body = n_body.replace(CHANGED_PLACEMARKER_CLOSED, '~~') | ||||
|             n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_OPEN, '**') | ||||
|             n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_CLOSED, '**') | ||||
|             n_body = apply_discord_markdown_to_body(n_body=n_body) | ||||
|  | ||||
|             # Apply 2000 char limit for plain content | ||||
|             payload_max_size = 1700 | ||||
| @@ -180,6 +222,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): | ||||
|         n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_OPEN, f'(into) ') | ||||
|         n_body = n_body.replace(CHANGED_INTO_PLACEMARKER_CLOSED, f'') | ||||
|         n_body = n_body.replace('\n', f'{CUSTOM_LINEBREAK_PLACEHOLDER}\n') | ||||
|     elif requested_output_format == 'markdown': | ||||
|         # Markdown to HTML - Apprise will convert this to HTML | ||||
|         n_body = apply_standard_markdown_to_body(n_body=n_body) | ||||
|  | ||||
|     else: #plaintext etc default | ||||
|         n_body = n_body.replace(REMOVED_PLACEMARKER_OPEN, '(removed) ') | ||||
| @@ -196,7 +241,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): | ||||
|  | ||||
| def process_notification(n_object: NotificationContextData, datastore): | ||||
|     from changedetectionio.jinja2_custom import render as jinja_render | ||||
|     from . import default_notification_format_for_watch, default_notification_format, valid_notification_formats | ||||
|     from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats | ||||
|     # be sure its registered | ||||
|     from .apprise_plugin.custom_handlers import apprise_http_custom_handler | ||||
|     # Register custom Discord plugin | ||||
| @@ -212,18 +257,17 @@ def process_notification(n_object: NotificationContextData, datastore): | ||||
|     # Insert variables into the notification content | ||||
|     notification_parameters = create_notification_parameters(n_object, datastore) | ||||
|  | ||||
|     requested_output_format = valid_notification_formats.get( | ||||
|         n_object.get('notification_format', default_notification_format), | ||||
|         valid_notification_formats[default_notification_format], | ||||
|     ) | ||||
|     requested_output_format = n_object.get('notification_format', default_notification_format) | ||||
|     logger.debug(f"Requested notification output format: '{requested_output_format}'") | ||||
|  | ||||
|     # If we arrived with 'System default' then look it up | ||||
|     if requested_output_format == default_notification_format_for_watch and datastore.data['settings']['application'].get('notification_format') != default_notification_format_for_watch: | ||||
|     if requested_output_format == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: | ||||
|         # Initially text or whatever | ||||
|         requested_output_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]).lower() | ||||
|         requested_output_format = datastore.data['settings']['application'].get('notification_format', default_notification_format) | ||||
|  | ||||
|     requested_output_format_original = requested_output_format | ||||
|  | ||||
|     # Now clean it up so it fits perfectly with apprise | ||||
|     requested_output_format = notification_format_align_with_apprise(n_format=requested_output_format) | ||||
|  | ||||
|     logger.trace(f"Complete notification body including Jinja and placeholders calculated in  {time.time() - now:.2f}s") | ||||
| @@ -257,7 +301,6 @@ def process_notification(n_object: NotificationContextData, datastore): | ||||
|             if n_object.get('markup_text_links_to_html_links'): | ||||
|                 n_body = markup_text_links_to_html(body=n_body) | ||||
|  | ||||
|  | ||||
|             n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters) | ||||
|  | ||||
|             url = url.strip() | ||||
| @@ -279,6 +322,11 @@ def process_notification(n_object: NotificationContextData, datastore): | ||||
|                     from markupsafe import escape | ||||
|                     n_body = str(escape(n_body)) | ||||
|  | ||||
|             if 'html' in requested_output_format: | ||||
|                 # Since the n_body is always some kind of text from the 'diff' engine, attempt to preserve whitespaces that get sent to the HTML output | ||||
|                 # But only where its more than 1 consecutive whitespace, otherwise "and this" becomes "and this" etc which is too much. | ||||
|                 n_body = n_body.replace('  ', '  ') | ||||
|  | ||||
|             (url, n_body, n_title) = apply_service_tweaks(url=url, n_body=n_body, n_title=n_title, requested_output_format=requested_output_format_original) | ||||
|  | ||||
|             apprise_input_format = "NO-THANKS-WE-WILL-MANAGE-ALL-OF-THIS" | ||||
| @@ -296,24 +344,45 @@ def process_notification(n_object: NotificationContextData, datastore): | ||||
|                     apprise_input_format = NotifyFormat.TEXT.value | ||||
|  | ||||
|                 elif requested_output_format == NotifyFormat.MARKDOWN.value: | ||||
|                     # This actually means we request "Markdown to HTML", we want HTML output | ||||
|                     # Convert markdown to HTML ourselves since not all plugins do this | ||||
|                     from apprise.conversion import markdown_to_html | ||||
|                     # Make sure there are paragraph breaks around horizontal rules | ||||
|                     n_body = n_body.replace('---', '\n\n---\n\n') | ||||
|                     n_body = markdown_to_html(n_body) | ||||
|                     url = f"{url}{prefix_add_to_url}format={NotifyFormat.HTML.value}" | ||||
|                     requested_output_format = NotifyFormat.HTML.value | ||||
|                     apprise_input_format = NotifyFormat.MARKDOWN.value | ||||
|  | ||||
|                     apprise_input_format = NotifyFormat.HTML.value  # Changed from MARKDOWN to HTML | ||||
|  | ||||
|                 # Could have arrived at any stage, so we dont end up running .escape on it | ||||
|                 if 'html' in requested_output_format: | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '<br>\n') | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '<br>\r\n') | ||||
|                 else: | ||||
|                     # Just incase | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '') | ||||
|                     # texty types | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '\r\n') | ||||
|  | ||||
|             else: | ||||
|                 # ?format was IN the apprise URL, they are kind of on their own here, we will try our best | ||||
|                 if 'format=html' in url: | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '<br>\r\n') | ||||
|                     # This will also prevent apprise from doing conversion | ||||
|                     apprise_input_format = NotifyFormat.HTML.value | ||||
|                     requested_output_format = NotifyFormat.HTML.value | ||||
|                 elif 'format=text' in url: | ||||
|                     n_body = n_body.replace(CUSTOM_LINEBREAK_PLACEHOLDER, '\r\n') | ||||
|                     apprise_input_format = NotifyFormat.TEXT.value | ||||
|                     requested_output_format = NotifyFormat.TEXT.value | ||||
|  | ||||
|             sent_objs.append({'title': n_title, | ||||
|                               'body': n_body, | ||||
|                               'url': url}) | ||||
|             apobj.add(url) | ||||
|  | ||||
|             # Since the output is always based on the plaintext of the 'diff' engine, wrap it nicely. | ||||
|             # It should always be similar to the 'history' part of the UI. | ||||
|             if url.startswith('mail') and 'html' in requested_output_format: | ||||
|                 if not '<pre' in n_body and not '<body' in n_body: # No custom HTML-ish body was setup already | ||||
|                     n_body = as_monospaced_html_email(content=n_body, title=n_title) | ||||
|  | ||||
|         apobj.notify( | ||||
|             title=n_title, | ||||
|             body=n_body, | ||||
|   | ||||
| @@ -9,7 +9,8 @@ for both sync and async workers | ||||
| from loguru import logger | ||||
| import time | ||||
|  | ||||
| from changedetectionio.notification import default_notification_format | ||||
| from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
| from changedetectionio.notification import default_notification_format, valid_notification_formats | ||||
|  | ||||
| # This gets modified on notification time (handler.py) depending on the required notification output | ||||
| CUSTOM_LINEBREAK_PLACEHOLDER='@BR@' | ||||
| @@ -48,15 +49,28 @@ class NotificationContextData(dict): | ||||
|         if kwargs: | ||||
|             self.update(kwargs) | ||||
|  | ||||
|         n_format = self.get('notification_format') | ||||
|         if n_format and not valid_notification_formats.get(n_format): | ||||
|             raise ValueError(f'Invalid notification format: "{n_format}"') | ||||
|  | ||||
|     def set_random_for_validation(self): | ||||
|         import random, string | ||||
|         """Randomly fills all dict keys with random strings (for validation/testing).""" | ||||
|         """Randomly fills all dict keys with random strings (for validation/testing).  | ||||
|         So we can test the output in the notification body | ||||
|         """ | ||||
|         for key in self.keys(): | ||||
|             if key in ['uuid', 'time', 'watch_uuid']: | ||||
|                 continue | ||||
|             rand_str = 'RANDOM-PLACEHOLDER-'+''.join(random.choices(string.ascii_letters + string.digits, k=12)) | ||||
|             self[key] = rand_str | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         if key == 'notification_format' and isinstance(value, str) and not value.startswith('RANDOM-PLACEHOLDER-'): | ||||
|             if not valid_notification_formats.get(value): | ||||
|                 raise ValueError(f'Invalid notification format: "{value}"') | ||||
|  | ||||
|         super().__setitem__(key, value) | ||||
|  | ||||
| class NotificationService: | ||||
|     """ | ||||
|     Standalone notification service that handles all notification functionality | ||||
| @@ -72,7 +86,7 @@ class NotificationService: | ||||
|         Queue a notification for a watch with full diff rendering and template variables | ||||
|         """ | ||||
|         from changedetectionio import diff | ||||
|         from changedetectionio.notification import default_notification_format_for_watch | ||||
|         from changedetectionio.notification import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|  | ||||
|         if not isinstance(n_object, NotificationContextData): | ||||
|             raise TypeError(f"Expected NotificationContextData, got {type(n_object)}") | ||||
| @@ -94,7 +108,7 @@ class NotificationService: | ||||
|             snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." | ||||
|  | ||||
|         # If we ended up here with "System default" | ||||
|         if n_object.get('notification_format') == default_notification_format_for_watch: | ||||
|         if n_object.get('notification_format') == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: | ||||
|             n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format') | ||||
|  | ||||
|  | ||||
| @@ -141,7 +155,7 @@ class NotificationService: | ||||
|         Individual watch settings > Tag settings > Global settings | ||||
|         """ | ||||
|         from changedetectionio.notification import ( | ||||
|             default_notification_format_for_watch, | ||||
|             USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, | ||||
|             default_notification_body, | ||||
|             default_notification_title | ||||
|         ) | ||||
| @@ -149,7 +163,7 @@ class NotificationService: | ||||
|         # Would be better if this was some kind of Object where Watch can reference the parent datastore etc | ||||
|         v = watch.get(var_name) | ||||
|         if v and not watch.get('notification_muted'): | ||||
|             if var_name == 'notification_format' and v == default_notification_format_for_watch: | ||||
|             if var_name == 'notification_format' and v == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: | ||||
|                 return self.datastore.data['settings']['application'].get('notification_format') | ||||
|  | ||||
|             return v | ||||
| @@ -166,7 +180,7 @@ class NotificationService: | ||||
|  | ||||
|         # Otherwise could be defaults | ||||
|         if var_name == 'notification_format': | ||||
|             return default_notification_format_for_watch | ||||
|             return USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|         if var_name == 'notification_body': | ||||
|             return default_notification_body | ||||
|         if var_name == 'notification_title': | ||||
| @@ -221,7 +235,6 @@ class NotificationService: | ||||
|         if not watch: | ||||
|             return | ||||
|  | ||||
|         n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format) | ||||
|         filter_list = ", ".join(watch['include_filters']) | ||||
|         # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed | ||||
|         body = f"""Hello, | ||||
| @@ -238,9 +251,9 @@ Thanks - Your omniscient changedetection.io installation. | ||||
|         n_object = NotificationContextData({ | ||||
|             'notification_title': 'Changedetection.io - Alert - CSS/xPath filter was not present in the page', | ||||
|             'notification_body': body, | ||||
|             'notification_format': n_format, | ||||
|             'markup_text_links_to_html_links': n_format.lower().startswith('html') | ||||
|             'notification_format': self._check_cascading_vars('notification_format', watch), | ||||
|         }) | ||||
|         n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') | ||||
|  | ||||
|         if len(watch['notification_urls']): | ||||
|             n_object['notification_urls'] = watch['notification_urls'] | ||||
| @@ -268,7 +281,7 @@ Thanks - Your omniscient changedetection.io installation. | ||||
|         if not watch: | ||||
|             return | ||||
|         threshold = self.datastore.data['settings']['application'].get('filter_failure_notification_threshold_attempts') | ||||
|         n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format).lower() | ||||
|  | ||||
|         step = step_n + 1 | ||||
|         # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed | ||||
|  | ||||
| @@ -287,9 +300,9 @@ Thanks - Your omniscient changedetection.io installation. | ||||
|         n_object = NotificationContextData({ | ||||
|             'notification_title': f"Changedetection.io - Alert - Browser step at position {step} could not be run", | ||||
|             'notification_body': body, | ||||
|             'notification_format': n_format, | ||||
|             'markup_text_links_to_html_links': n_format.lower().startswith('html') | ||||
|             'notification_format': self._check_cascading_vars('notification_format', watch), | ||||
|         }) | ||||
|         n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') | ||||
|  | ||||
|         if len(watch['notification_urls']): | ||||
|             n_object['notification_urls'] = watch['notification_urls'] | ||||
|   | ||||
| @@ -91,6 +91,8 @@ class difference_detection_processor(): | ||||
|             else: | ||||
|                 logger.debug("Skipping adding proxy data when custom Browser endpoint is specified. ") | ||||
|  | ||||
|         logger.debug(f"Using proxy '{proxy_url}' for {self.watch['uuid']}") | ||||
|  | ||||
|         # Now call the fetcher (playwright/requests/etc) with arguments that only a fetcher would need. | ||||
|         # When browser_connection_url is None, it method should default to working out whats the best defaults (os env vars etc) | ||||
|         self.fetcher = fetcher_obj(proxy_override=proxy_url, | ||||
|   | ||||
| @@ -88,7 +88,7 @@ class guess_stream_type(): | ||||
|                         magic_content_header = mime | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error getting a more precise mime type from 'puremagic' library ({str(e)}), using content-based detection") | ||||
|             logger.warning(f"Error getting a more precise mime type from 'puremagic' library ({str(e)}), using content-based detection") | ||||
|  | ||||
|         # Content-based detection (most reliable for text formats) | ||||
|         # Check for HTML patterns first - if found, override magic's text/plain | ||||
|   | ||||
| @@ -32,7 +32,7 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data): | ||||
|     '''Used by @app.route("/edit/<string:uuid>/preview-rendered", methods=['POST'])''' | ||||
|     from changedetectionio import forms, html_tools | ||||
|     from changedetectionio.model.Watch import model as watch_model | ||||
|     from concurrent.futures import ProcessPoolExecutor | ||||
|     from concurrent.futures import ThreadPoolExecutor | ||||
|     from copy import deepcopy | ||||
|     from flask import request | ||||
|     import brotli | ||||
| @@ -76,13 +76,16 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data): | ||||
|             update_handler.fetcher.headers['content-type'] = tmp_watch.get('content-type') | ||||
|  | ||||
|             # Process our watch with filters and the HTML from disk, and also a blank watch with no filters but also with the same HTML from disk | ||||
|             # Do this as a parallel process because it could take some time | ||||
|             with ProcessPoolExecutor(max_workers=2) as executor: | ||||
|                 future1 = executor.submit(_task, tmp_watch, update_handler) | ||||
|                 future2 = executor.submit(_task, blank_watch_no_filters, update_handler) | ||||
|             # Do this as parallel threads (not processes) to avoid pickle issues with Lock objects | ||||
|             try: | ||||
|                 with ThreadPoolExecutor(max_workers=2) as executor: | ||||
|                     future1 = executor.submit(_task, tmp_watch, update_handler) | ||||
|                     future2 = executor.submit(_task, blank_watch_no_filters, update_handler) | ||||
|  | ||||
|                 text_after_filter = future1.result() | ||||
|                 text_before_filter = future2.result() | ||||
|                     text_after_filter = future1.result() | ||||
|                     text_before_filter = future2.result() | ||||
|             except Exception as e: | ||||
|                 x=1 | ||||
|  | ||||
|     try: | ||||
|         trigger_line_numbers = html_tools.strip_ignore_text(content=text_after_filter, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| [pytest] | ||||
| addopts = --no-start-live-server --live-server-port=5005 | ||||
| addopts = --no-start-live-server --live-server-port=0 | ||||
| #testpaths = tests pytest_invenio | ||||
| #live_server_scope = function | ||||
|  | ||||
|   | ||||
| @@ -11,19 +11,16 @@ set -e | ||||
|  | ||||
| SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) | ||||
|  | ||||
| find tests/test_*py -type f|while read test_name | ||||
| do | ||||
|   echo "TEST RUNNING $test_name" | ||||
|   # REMOVE_REQUESTS_OLD_SCREENSHOTS disabled so that we can write a screenshot and send it in test_notifications.py without a real browser | ||||
|   REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 --tb=long $test_name | ||||
| done | ||||
| # REMOVE_REQUESTS_OLD_SCREENSHOTS disabled so that we can write a screenshot and send it in test_notifications.py without a real browser | ||||
| REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -n 30 --dist load  tests/test_*.py | ||||
|  | ||||
| #time pytest -n auto --dist loadfile -vv --tb=long tests/test_*.py | ||||
| echo "RUNNING WITH BASE_URL SET" | ||||
|  | ||||
| # Now re-run some tests with BASE_URL enabled | ||||
| # Re #65 - Ability to include a link back to the installation, in the notification. | ||||
| export BASE_URL="https://really-unique-domain.io" | ||||
| REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 tests/test_notification.py | ||||
| REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv --maxfail=1 tests/test_notification.py | ||||
|  | ||||
|  | ||||
| # Re-run with HIDE_REFERER set - could affect login | ||||
|   | ||||
| @@ -6,6 +6,8 @@ | ||||
|  | ||||
| # enable debug | ||||
| set -x | ||||
| docker network inspect changedet-network >/dev/null 2>&1 || docker network create changedet-network | ||||
| docker run --network changedet-network -d --hostname selenium  -p 4444:4444 --rm --shm-size="2g"  selenium/standalone-chrome:4 | ||||
|  | ||||
| # A extra browser is configured, but we never chose to use it, so it should NOT show in the logs | ||||
| docker run --rm -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio  bash -c 'cd changedetectionio;pytest tests/custom_browser_url/test_custom_browser_url.py::test_request_not_via_custom_browser_url' | ||||
|   | ||||
| @@ -19,12 +19,13 @@ docker run --network changedet-network -d \ | ||||
|   -v `pwd`/tests/proxy_list/squid-passwords.txt:/etc/squid3/passwords \ | ||||
|   ubuntu/squid:4.13-21.10_edge | ||||
|  | ||||
|  | ||||
| sleep 5 | ||||
| ## 2nd test actually choose the preferred proxy from proxies.json | ||||
| # This will force a request via "proxy-two" | ||||
| docker run --network changedet-network \ | ||||
|   -v `pwd`/tests/proxy_list/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \ | ||||
|   -v `pwd`/tests/proxy_list/proxies.json-example:/tmp/proxies.json \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_multiple_proxy.py' | ||||
|   bash -c 'cd changedetectionio && pytest -s tests/proxy_list/test_multiple_proxy.py --datastore-path /tmp' | ||||
|  | ||||
| set +e | ||||
| echo "- Looking for chosen.changedetection.io request in squid-one - it should NOT be here" | ||||
| @@ -48,8 +49,10 @@ fi | ||||
| # Test the UI configurable proxies | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py' | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py --datastore-path /tmp' | ||||
|  | ||||
| # Give squid proxies a moment to flush their logs | ||||
| sleep 2 | ||||
|  | ||||
| # Should see a request for one.changedetection.io in there | ||||
| echo "- Looking for .changedetection.io request in squid-custom" | ||||
| @@ -63,7 +66,10 @@ fi | ||||
| # Test "no-proxy" option | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_noproxy.py' | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_noproxy.py --datastore-path /tmp' | ||||
|  | ||||
| # Give squid proxies a moment to flush their logs | ||||
| sleep 2 | ||||
|  | ||||
| # We need to handle grep returning 1 | ||||
| set +e | ||||
| @@ -80,6 +86,8 @@ for c in $(echo "squid-one squid-two squid-custom"); do | ||||
|   fi | ||||
| done | ||||
|  | ||||
| echo "docker ps output" | ||||
| docker ps | ||||
|  | ||||
| docker kill squid-one squid-two squid-custom | ||||
|  | ||||
| @@ -88,19 +96,19 @@ docker kill squid-one squid-two squid-custom | ||||
| # Requests | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_proxy_noconnect.py' | ||||
|   bash -c 'cd changedetectionio && pytest tests/proxy_list/test_proxy_noconnect.py --datastore-path /tmp' | ||||
|  | ||||
| # Playwright | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000 pytest tests/proxy_list/test_proxy_noconnect.py' | ||||
|   bash -c 'cd changedetectionio && PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000 pytest tests/proxy_list/test_proxy_noconnect.py --datastore-path /tmp' | ||||
|  | ||||
| # Puppeteer fast | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && FAST_PUPPETEER_CHROME_FETCHER=1 PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000 pytest tests/proxy_list/test_proxy_noconnect.py' | ||||
|   bash -c 'cd changedetectionio && FAST_PUPPETEER_CHROME_FETCHER=1 PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000 pytest tests/proxy_list/test_proxy_noconnect.py --datastore-path /tmp' | ||||
|  | ||||
| # Selenium | ||||
| docker run --network changedet-network \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && WEBDRIVER_URL=http://selenium:4444/wd/hub pytest tests/proxy_list/test_proxy_noconnect.py' | ||||
|   bash -c 'cd changedetectionio && WEBDRIVER_URL=http://selenium:4444/wd/hub pytest tests/proxy_list/test_proxy_noconnect.py --datastore-path /tmp' | ||||
|   | ||||
| @@ -5,6 +5,7 @@ set -e | ||||
| # enable debug | ||||
| set -x | ||||
|  | ||||
| docker network inspect changedet-network >/dev/null 2>&1 || docker network create changedet-network | ||||
|  | ||||
| # SOCKS5 related - start simple Socks5 proxy server | ||||
| # SOCKSTEST=xyz should show in the logs of this service to confirm it fetched | ||||
| @@ -14,13 +15,13 @@ docker run --network changedet-network -d --hostname socks5proxy-noauth --rm -p | ||||
| echo "---------------------------------- SOCKS5 -------------------" | ||||
| # SOCKS5 related - test from proxies.json | ||||
| docker run --network changedet-network \ | ||||
|   -v `pwd`/tests/proxy_socks5/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \ | ||||
|   -v `pwd`/tests/proxy_socks5/proxies.json-example:/tmp/proxies.json \ | ||||
|   --rm \ | ||||
|   -e "FLASK_SERVER_NAME=cdio" \ | ||||
|   --hostname cdio \ | ||||
|   -e "SOCKSTEST=proxiesjson" \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy_sources.py' | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy_sources.py  --datastore-path /tmp' | ||||
|  | ||||
| # SOCKS5 related - by manually entering in UI | ||||
| docker run --network changedet-network \ | ||||
| @@ -29,18 +30,18 @@ docker run --network changedet-network \ | ||||
|   --hostname cdio \ | ||||
|   -e "SOCKSTEST=manual" \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy.py' | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy.py --datastore-path /tmp' | ||||
|  | ||||
| # SOCKS5 related - test from proxies.json via playwright - NOTE- PLAYWRIGHT DOESNT SUPPORT AUTHENTICATING PROXY | ||||
| docker run --network changedet-network \ | ||||
|   -e "SOCKSTEST=manual-playwright" \ | ||||
|   --hostname cdio \ | ||||
|   -e "FLASK_SERVER_NAME=cdio" \ | ||||
|   -v `pwd`/tests/proxy_socks5/proxies.json-example-noauth:/app/changedetectionio/test-datastore/proxies.json \ | ||||
|   -v `pwd`/tests/proxy_socks5/proxies.json-example-noauth:/tmp/proxies.json \ | ||||
|   -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" \ | ||||
|   --rm \ | ||||
|   test-changedetectionio \ | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy_sources.py' | ||||
|   bash -c 'cd changedetectionio && pytest --live-server-host=0.0.0.0 --live-server-port=5004  -s tests/proxy_socks5/test_socks5_proxy_sources.py --datastore-path /tmp' | ||||
|  | ||||
| echo "socks5 server logs" | ||||
| docker logs socks5proxy | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| from changedetectionio.strtobool import strtobool | ||||
|  | ||||
| from changedetectionio.validate_url import is_safe_valid_url | ||||
|  | ||||
| from flask import ( | ||||
|     flash | ||||
| ) | ||||
|  | ||||
| from .html_tools import TRANSLATE_WHITESPACE_TABLE | ||||
| from . model import App, Watch | ||||
| from .model import App, Watch, USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
| from copy import deepcopy, copy | ||||
| from os import path, unlink | ||||
| from threading import Lock | ||||
| @@ -40,17 +42,24 @@ class ChangeDetectionStore: | ||||
|     needs_write_urgent = False | ||||
|  | ||||
|     __version_check = True | ||||
|     save_data_thread = None | ||||
|  | ||||
|     def __init__(self, datastore_path="/datastore", include_default_watches=True, version_tag="0.0.0"): | ||||
|         # Should only be active for docker | ||||
|         # logging.basicConfig(filename='/dev/stdout', level=logging.INFO) | ||||
|         self.__data = App.model() | ||||
|         self.datastore_path = datastore_path | ||||
|         self.json_store_path = os.path.join(self.datastore_path, "url-watches.json") | ||||
|         logger.info(f"Datastore path is '{self.json_store_path}'") | ||||
|  | ||||
|         self.needs_write = False | ||||
|         self.start_time = time.time() | ||||
|         self.stop_thread = False | ||||
|         self.reload_state(datastore_path=datastore_path, include_default_watches=include_default_watches, version_tag=version_tag) | ||||
|  | ||||
|  | ||||
|     def reload_state(self, datastore_path, include_default_watches, version_tag): | ||||
|         logger.info(f"Datastore path is '{datastore_path}'") | ||||
|  | ||||
|         self.__data = App.model() | ||||
|         self.datastore_path = datastore_path | ||||
|         self.json_store_path = os.path.join(self.datastore_path, "url-watches.json") | ||||
|         # Base definition for all watchers | ||||
|         # deepcopy part of #569 - not sure why its needed exactly | ||||
|         self.generic_definition = deepcopy(Watch.model(datastore_path = datastore_path, default={})) | ||||
| @@ -143,7 +152,10 @@ class ChangeDetectionStore: | ||||
|         self.needs_write = True | ||||
|  | ||||
|         # Finally start the thread that will manage periodic data saves to JSON | ||||
|         save_data_thread = threading.Thread(target=self.save_datastore).start() | ||||
|         # Only start if thread is not already running (reload_state might be called multiple times) | ||||
|         if not self.save_data_thread or not self.save_data_thread.is_alive(): | ||||
|             self.save_data_thread = threading.Thread(target=self.save_datastore) | ||||
|             self.save_data_thread.start() | ||||
|  | ||||
|     def rehydrate_entity(self, uuid, entity, processor_override=None): | ||||
|         """Set the dict back to the dict Watch object""" | ||||
| @@ -340,9 +352,10 @@ class ChangeDetectionStore: | ||||
|                 logger.error(f"Error fetching metadata for shared watch link {url} {str(e)}") | ||||
|                 flash("Error fetching metadata for {}".format(url), 'error') | ||||
|                 return False | ||||
|         from .model.Watch import is_safe_url | ||||
|         if not is_safe_url(url): | ||||
|             flash('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX', 'error') | ||||
|  | ||||
|         if not is_safe_valid_url(url): | ||||
|             flash('Watch protocol is not permitted or invalid URL format', 'error') | ||||
|  | ||||
|             return None | ||||
|  | ||||
|         if tag and type(tag) == str: | ||||
| @@ -408,7 +421,6 @@ class ChangeDetectionStore: | ||||
|             self.sync_to_json() | ||||
|             return | ||||
|         else: | ||||
|  | ||||
|             try: | ||||
|                 # Re #286  - First write to a temp file, then confirm it looks OK and rename it | ||||
|                 # This is a fairly basic strategy to deal with the case that the file is corrupted, | ||||
| @@ -438,7 +450,7 @@ class ChangeDetectionStore: | ||||
|                 logger.remove() | ||||
|                 logger.add(sys.stderr) | ||||
|  | ||||
|                 logger.critical("Shutting down datastore thread") | ||||
|                 logger.info(f"Shutting down datastore '{self.datastore_path}' thread") | ||||
|                 return | ||||
|  | ||||
|             if self.needs_write or self.needs_write_urgent: | ||||
| @@ -987,10 +999,35 @@ class ChangeDetectionStore: | ||||
|             self.data['settings']['application']['ui']['use_page_title_in_list'] = self.data['settings']['application'].get('extract_title_as_title') | ||||
|  | ||||
|     def update_21(self): | ||||
|         self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') | ||||
|         del self.data['settings']['application']['timezone'] | ||||
|         if self.data['settings']['application'].get('timezone'): | ||||
|             self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') | ||||
|             del self.data['settings']['application']['timezone'] | ||||
|  | ||||
|  | ||||
|     # Some notification formats got the wrong name type | ||||
|     def update_22(self): | ||||
|         from .notification import valid_notification_formats | ||||
|  | ||||
|         sys_n_format = self.data['settings']['application'].get('notification_format') | ||||
|         key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == sys_n_format), None) | ||||
|         if key_exists_as_value: # key of "Plain text" | ||||
|             logger.success(f"['settings']['application']['notification_format'] '{sys_n_format}' -> '{key_exists_as_value}'") | ||||
|             self.data['settings']['application']['notification_format'] = key_exists_as_value | ||||
|  | ||||
|         for uuid, watch in self.data['watching'].items(): | ||||
|             n_format = self.data['watching'][uuid].get('notification_format') | ||||
|             key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) | ||||
|             if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH:  # key of "Plain text" | ||||
|                 logger.success(f"['watching'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") | ||||
|                 self.data['watching'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever | ||||
|  | ||||
|         for uuid, tag in self.data['settings']['application']['tags'].items(): | ||||
|             n_format = self.data['settings']['application']['tags'][uuid].get('notification_format') | ||||
|             key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) | ||||
|             if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH:  # key of "Plain text" | ||||
|                 logger.success(f"['settings']['application']['tags'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") | ||||
|                 self.data['settings']['application']['tags'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever | ||||
|  | ||||
|     def add_notification_url(self, notification_url): | ||||
|          | ||||
|         logger.debug(f">>> Adding new notification_url - '{notification_url}'") | ||||
|   | ||||
| @@ -266,9 +266,7 @@ | ||||
|             <li id="timezone-info"> | ||||
|                 {{ render_field(form.time_schedule_limit.timezone, placeholder=timezone_default_config) }} <span id="local-time-in-tz"></span> | ||||
|                 <datalist id="timezones" style="display: none;"> | ||||
|                     {% for timezone in available_timezones %} | ||||
|                         <option value="{{ timezone }}">{{ timezone }}</option> | ||||
|                     {% endfor %} | ||||
|                     {%- for timezone in available_timezones -%}<option value="{{ timezone }}">{{ timezone }}</option>{%- endfor -%} | ||||
|                 </datalist> | ||||
|             </li> | ||||
|         </ul> | ||||
|   | ||||
| @@ -53,7 +53,7 @@ | ||||
|           <a class="pure-menu-heading" href="{{url_for('watchlist.index')}}"> | ||||
|             <strong>Change</strong>Detection.io</a> | ||||
|         {% endif %} | ||||
|         {% if current_diff_url %} | ||||
|         {% if current_diff_url and is_safe_valid_url(current_diff_url) %} | ||||
|           <a class="current-diff-url" href="{{ current_diff_url }}"> | ||||
|             <span style="max-width: 30%; overflow: hidden">{{ current_diff_url }}</span></a> | ||||
|         {% else %} | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import os | ||||
| import sys | ||||
| from loguru import logger | ||||
|  | ||||
| from changedetectionio.flask_app import init_app_secret | ||||
| from changedetectionio.tests.util import live_server_setup, new_live_server_setup | ||||
|  | ||||
| # https://github.com/pallets/flask/blob/1.1.2/examples/tutorial/tests/test_auth.py | ||||
| @@ -87,7 +88,6 @@ def measure_memory_usage(request): | ||||
| def cleanup(datastore_path): | ||||
|     import glob | ||||
|     # Unlink test output files | ||||
|  | ||||
|     for g in ["*.txt", "*.json", "*.pdf"]: | ||||
|         files = glob.glob(os.path.join(datastore_path, g)) | ||||
|         for f in files: | ||||
| @@ -97,34 +97,121 @@ def cleanup(datastore_path): | ||||
|             if os.path.isfile(f): | ||||
|                 os.unlink(f) | ||||
|  | ||||
| @pytest.fixture(scope='function', autouse=True) | ||||
| def prepare_test_function(live_server): | ||||
| def pytest_addoption(parser): | ||||
|     """Add custom command-line options for pytest. | ||||
|  | ||||
|     Provides --datastore-path option for specifying custom datastore location. | ||||
|     Note: Cannot use -d short option as it's reserved by pytest for debug mode. | ||||
|     """ | ||||
|     parser.addoption( | ||||
|         "--datastore-path", | ||||
|         action="store", | ||||
|         default=None, | ||||
|         help="Custom datastore path for tests" | ||||
|     ) | ||||
|  | ||||
| @pytest.fixture(scope='session') | ||||
| def datastore_path(tmp_path_factory, request): | ||||
|     """Provide datastore path unique to this worker. | ||||
|  | ||||
|     Supports custom path via --datastore-path/-d flag (mirrors main app). | ||||
|  | ||||
|     CRITICAL for xdist isolation: | ||||
|     - Each WORKER gets its own directory | ||||
|     - Tests on same worker run SEQUENTIALLY and cleanup between tests | ||||
|     - No subdirectories needed since tests don't overlap on same worker | ||||
|     - Example: /tmp/test-datastore-gw0/ for worker gw0 | ||||
|     """ | ||||
|     # Check for custom path first (mirrors main app's -d flag) | ||||
|     custom_path = request.config.getoption("--datastore-path") | ||||
|     if custom_path: | ||||
|         # Ensure the directory exists | ||||
|         os.makedirs(custom_path, exist_ok=True) | ||||
|         logger.info(f"Using custom datastore path: {custom_path}") | ||||
|         return custom_path | ||||
|  | ||||
|     # Otherwise use default tmp_path_factory logic | ||||
|     worker_id = getattr(request.config, 'workerinput', {}).get('workerid', 'master') | ||||
|     if worker_id == 'master': | ||||
|         path = tmp_path_factory.mktemp("test-datastore") | ||||
|     else: | ||||
|         path = tmp_path_factory.mktemp(f"test-datastore-{worker_id}") | ||||
|     return str(path) | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope='function', autouse=True) | ||||
| def prepare_test_function(live_server, datastore_path): | ||||
|     """Prepare each test with complete isolation. | ||||
|  | ||||
|     CRITICAL for xdist per-test isolation: | ||||
|     - Reuses the SAME datastore instance (so blueprint references stay valid) | ||||
|     - Clears all watches and state for a clean slate | ||||
|     - First watch will get uuid="first" | ||||
|     """ | ||||
|     routes = [rule.rule for rule in live_server.app.url_map.iter_rules()] | ||||
|     if '/test-random-content-endpoint' not in routes: | ||||
|         logger.debug("Setting up test URL routes") | ||||
|         new_live_server_setup(live_server) | ||||
|  | ||||
|     # CRITICAL: Point app to THIS test's unique datastore directory | ||||
|     live_server.app.config['TEST_DATASTORE_PATH'] = datastore_path | ||||
|  | ||||
|     # CRITICAL: Get datastore and stop it from writing stale data | ||||
|     datastore = live_server.app.config.get('DATASTORE') | ||||
|  | ||||
|     # Prevent background thread from writing during cleanup/reload | ||||
|     datastore.needs_write = False | ||||
|     datastore.needs_write_urgent = False | ||||
|  | ||||
|     # CRITICAL: Clean up any files from previous tests | ||||
|     # This ensures a completely clean directory | ||||
|     cleanup(datastore_path) | ||||
|  | ||||
|     # CRITICAL: Reload the EXISTING datastore instead of creating a new one | ||||
|     # This keeps blueprint references valid (they capture datastore at construction) | ||||
|     # reload_state() completely resets the datastore to a clean state | ||||
|  | ||||
|     # Reload state with clean data (no default watches) | ||||
|     datastore.reload_state( | ||||
|         datastore_path=datastore_path, | ||||
|         include_default_watches=False, | ||||
|         version_tag=datastore.data.get('version_tag', '0.0.0') | ||||
|     ) | ||||
|     live_server.app.secret_key = init_app_secret(datastore_path) | ||||
|     logger.debug(f"prepare_test_function: Reloaded datastore at {hex(id(datastore))}") | ||||
|     logger.debug(f"prepare_test_function: Path {datastore.datastore_path}") | ||||
|  | ||||
|     yield | ||||
|     # Then cleanup/shutdown | ||||
|     live_server.app.config['DATASTORE'].data['watching']={} | ||||
|     time.sleep(0.3) | ||||
|     live_server.app.config['DATASTORE'].data['watching']={} | ||||
|  | ||||
|     # Cleanup: Clear watches again after test | ||||
|     try: | ||||
|         datastore.data['watching'] = {} | ||||
|         datastore.needs_write = True | ||||
|     except Exception as e: | ||||
|         logger.warning(f"Error during datastore cleanup: {e}") | ||||
|  | ||||
|  | ||||
| # So the app can also know which test name it was | ||||
| @pytest.fixture(autouse=True) | ||||
| def set_test_name(request): | ||||
|   """Automatically set TEST_NAME env var for every test""" | ||||
|   test_name = request.node.name | ||||
|   os.environ['PYTEST_CURRENT_TEST'] = test_name | ||||
|   yield | ||||
|   # Cleanup if needed | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope='session') | ||||
| def app(request): | ||||
|     """Create application for the tests.""" | ||||
|     datastore_path = "./test-datastore" | ||||
| def app(request, datastore_path): | ||||
|     """Create application once per worker (session). | ||||
|  | ||||
|     Note: Actual per-test isolation is handled by: | ||||
|     - prepare_test_function() recreates datastore and cleans directory | ||||
|     - All tests on same worker use same directory (cleaned between tests) | ||||
|     """ | ||||
|     # So they don't delay in fetching | ||||
|     os.environ["MINIMUM_SECONDS_RECHECK_TIME"] = "0" | ||||
|     try: | ||||
|         os.mkdir(datastore_path) | ||||
|     except FileExistsError: | ||||
|         pass | ||||
|  | ||||
|     logger.debug(f"Testing with datastore_path={datastore_path}") | ||||
|     cleanup(datastore_path) | ||||
|  | ||||
|     app_config = {'datastore_path': datastore_path, 'disable_checkver' : True} | ||||
| @@ -147,6 +234,8 @@ def app(request): | ||||
|     # Disable CSRF while running tests | ||||
|     app.config['WTF_CSRF_ENABLED'] = False | ||||
|     app.config['STOP_THREADS'] = True | ||||
|     # Store datastore_path so Flask routes can access it | ||||
|     app.config['TEST_DATASTORE_PATH'] = datastore_path | ||||
|  | ||||
|     def teardown(): | ||||
|         # Stop all threads and services | ||||
|   | ||||
| @@ -73,13 +73,13 @@ def do_test(client, live_server, make_test_use_extra_browser=False): | ||||
|  | ||||
|  | ||||
| # Requires playwright to be installed | ||||
| def test_request_via_custom_browser_url(client, live_server, measure_memory_usage): | ||||
| def test_request_via_custom_browser_url(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     # We do this so we can grep the logs of the custom container and see if the request actually went through that container | ||||
|     do_test(client, live_server, make_test_use_extra_browser=True) | ||||
|  | ||||
|  | ||||
| def test_request_not_via_custom_browser_url(client, live_server, measure_memory_usage): | ||||
| def test_request_not_via_custom_browser_url(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     # We do this so we can grep the logs of the custom container and see if the request actually went through that container | ||||
|     do_test(client, live_server, make_test_use_extra_browser=False) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import logging | ||||
|  | ||||
|  | ||||
| # Requires playwright to be installed | ||||
| def test_fetch_webdriver_content(client, live_server, measure_memory_usage): | ||||
| def test_fetch_webdriver_content(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     ##################### | ||||
|   | ||||
| @@ -3,7 +3,7 @@ from flask import url_for | ||||
| from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client | ||||
|  | ||||
|  | ||||
| def test_execute_custom_js(client, live_server, measure_memory_usage): | ||||
| def test_execute_custom_js(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test" | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from flask import url_for | ||||
| from ..util import live_server_setup, wait_for_all_checks | ||||
|  | ||||
|  | ||||
| def test_preferred_proxy(client, live_server, measure_memory_usage): | ||||
| def test_preferred_proxy(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     url = "http://chosen.changedetection.io" | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from flask import url_for | ||||
| from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client | ||||
|  | ||||
|  | ||||
| def test_noproxy_option(client, live_server, measure_memory_usage): | ||||
| def test_noproxy_option(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     # Run by run_proxy_tests.sh | ||||
|     # Call this URL then scan the containers that it never went through them | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from flask import url_for | ||||
| from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client | ||||
|  | ||||
| # just make a request, we will grep in the docker logs to see it actually got called | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage): | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     res = client.post( | ||||
|         url_for("imports.import_page"), | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from ... import strtobool | ||||
| # FAST_PUPPETEER_CHROME_FETCHER=True PLAYWRIGHT_DRIVER_URL=ws://127.0.0.1:3000 pytest tests/proxy_list/test_proxy_noconnect.py | ||||
| # WEBDRIVER_URL=http://127.0.0.1:4444/wd/hub pytest tests/proxy_list/test_proxy_noconnect.py | ||||
|  | ||||
| def test_proxy_noconnect_custom(client, live_server, measure_memory_usage): | ||||
| def test_proxy_noconnect_custom(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     # Goto settings, add our custom one | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from ..util import live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
| # just make a request, we will grep in the docker logs to see it actually got called | ||||
| def test_select_custom(client, live_server, measure_memory_usage): | ||||
| def test_select_custom(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     # Goto settings, add our custom one | ||||
| @@ -50,7 +50,7 @@ def test_select_custom(client, live_server, measure_memory_usage): | ||||
|     # Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default | ||||
|  | ||||
|  | ||||
| def test_custom_proxy_validation(client, live_server, measure_memory_usage): | ||||
| def test_custom_proxy_validation(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     # Goto settings, add our custom one | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from flask import url_for | ||||
| from changedetectionio.tests.util import live_server_setup, wait_for_all_checks, extract_UUID_from_client, delete_all_watches | ||||
|  | ||||
|  | ||||
| def set_response(): | ||||
| def set_response(datastore_path): | ||||
|     import time | ||||
|     data = """<html> | ||||
|        <body> | ||||
| @@ -15,13 +15,13 @@ def set_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|     time.sleep(1) | ||||
|  | ||||
| def test_socks5(client, live_server, measure_memory_usage): | ||||
| def test_socks5(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_response() | ||||
|     set_response(datastore_path) | ||||
|  | ||||
|     # Setup a proxy | ||||
|     res = client.post( | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from flask import url_for | ||||
| from changedetectionio.tests.util import live_server_setup, wait_for_all_checks | ||||
|  | ||||
|  | ||||
| def set_response(): | ||||
| def set_response(datastore_path): | ||||
|     import time | ||||
|     data = """<html> | ||||
|        <body> | ||||
| @@ -14,15 +14,15 @@ def set_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|     time.sleep(1) | ||||
|  | ||||
| # should be proxies.json mounted from run_proxy_tests.sh already | ||||
| # -v `pwd`/tests/proxy_socks5/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json | ||||
| def test_socks5_from_proxiesjson_file(client, live_server, measure_memory_usage): | ||||
| def test_socks5_from_proxiesjson_file(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_response() | ||||
|     set_response(datastore_path) | ||||
|     # Because the socks server should connect back to us | ||||
|     test_url = url_for('test_endpoint', _external=True) + f"?socks-test-tag={os.getenv('SOCKSTEST', '')}" | ||||
|     test_url = test_url.replace('localhost.localdomain', 'cdio') | ||||
|   | ||||
| @@ -11,7 +11,7 @@ from changedetectionio.notification import ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|        <section id=header style="padding: 50px; height: 350px">This is the header which should be ignored always - <span>add to cart</span></section> | ||||
| @@ -26,13 +26,13 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_back_in_stock_response(): | ||||
| def set_back_in_stock_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -45,14 +45,14 @@ def set_back_in_stock_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| # Add a site in paused mode, add an invalid filter, we should still have visual selector data ready | ||||
| def test_restock_detection(client, live_server, measure_memory_usage): | ||||
| def test_restock_detection(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     #assert os.getenv('PLAYWRIGHT_DRIVER_URL'), "Needs PLAYWRIGHT_DRIVER_URL set for this test" | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     ##################### | ||||
| @@ -88,24 +88,25 @@ def test_restock_detection(client, live_server, measure_memory_usage): | ||||
|     assert b'not-in-stock' in res.data # should be out of stock | ||||
|  | ||||
|     # Is it correctly shown as in stock | ||||
|     set_back_in_stock_response() | ||||
|     set_back_in_stock_response(datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|     assert b'not-in-stock' not in res.data | ||||
|  | ||||
|     # We should have a notification | ||||
|     wait_for_notification_endpoint_output() | ||||
|     assert os.path.isfile("test-datastore/notification.txt"), "Notification received" | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     notification_file = os.path.join(datastore_path, "notification.txt") | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|     assert os.path.isfile(notification_file), "Notification received" | ||||
|     os.unlink(notification_file) | ||||
|  | ||||
|     # Default behaviour is to only fire notification when it goes OUT OF STOCK -> IN STOCK | ||||
|     # So here there should be no file, because we go IN STOCK -> OUT OF STOCK | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     time.sleep(5) | ||||
|     assert not os.path.isfile("test-datastore/notification.txt"), "No notification should have fired when it went OUT OF STOCK by default" | ||||
|     assert not os.path.isfile(notification_file), "No notification should have fired when it went OUT OF STOCK by default" | ||||
|  | ||||
|     # BUT we should see that it correctly shows "not in stock" | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|   | ||||
| @@ -32,6 +32,8 @@ class CustomSMTPHandler: | ||||
|  | ||||
|             # Parse the email message | ||||
|             msg = message_from_bytes(envelope.content, policy=default) | ||||
|             with open('/tmp/last.eml', 'wb') as f: | ||||
|                 f.write(envelope.content) | ||||
|  | ||||
|             # Write parts to files based on content type | ||||
|             if msg.is_multipart(): | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from email import message_from_string | ||||
| from email.policy import default as email_policy | ||||
|  | ||||
| from changedetectionio.diff import HTML_REMOVED_STYLE, HTML_ADDED_STYLE, HTML_CHANGED_STYLE | ||||
| from changedetectionio.notification_service import NotificationContextData | ||||
| from changedetectionio.notification_service import NotificationContextData, CUSTOM_LINEBREAK_PLACEHOLDER | ||||
| from changedetectionio.tests.util import set_original_response, set_modified_response, set_more_modified_response, live_server_setup, \ | ||||
|     wait_for_all_checks, \ | ||||
|     set_longer_modified_response, delete_all_watches | ||||
| @@ -40,9 +40,10 @@ def get_last_message_from_smtp_server(): | ||||
|  | ||||
| # Requires running the test SMTP server | ||||
|  | ||||
| def test_check_notification_email_formats_default_HTML(client, live_server, measure_memory_usage): | ||||
| def test_check_notification_email_formats_default_HTML(client, live_server, measure_memory_usage, datastore_path): | ||||
|     ##  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|  | ||||
| @@ -53,7 +54,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": "some text\nfallback-body<br> " + default_notification_body, | ||||
|               "application-notification_format": 'HTML', | ||||
|               "application-notification_format": 'html', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -71,7 +72,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_longer_modified_response() | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -99,6 +100,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas | ||||
|     text_content = text_part.get_content() | ||||
|     assert '(added) So let\'s see what happens.\r\n' in text_content  # The plaintext part | ||||
|     assert 'fallback-body\r\n' in text_content  # The plaintext part | ||||
|     assert CUSTOM_LINEBREAK_PLACEHOLDER not in text_content | ||||
|  | ||||
|     # Second part should be text/html | ||||
|     html_part = parts[1] | ||||
| @@ -107,11 +109,13 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas | ||||
|     assert 'some text<br>' in html_content  # We converted \n from the notification body | ||||
|     assert 'fallback-body<br>' in html_content  # kept the original <br> | ||||
|     assert '(added) So let\'s see what happens.<br>' in html_content  # the html part | ||||
|     assert CUSTOM_LINEBREAK_PLACEHOLDER not in html_content | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_check_notification_plaintext_format(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_notification_plaintext_format(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|  | ||||
| @@ -122,7 +126,7 @@ def test_check_notification_plaintext_format(client, live_server, measure_memory | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": "some text\n" + default_notification_body, | ||||
|               "application-notification_format": 'Plain Text', | ||||
|               "application-notification_format": 'text', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -136,7 +140,7 @@ def test_check_notification_plaintext_format(client, live_server, measure_memory | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     set_longer_modified_response() | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| @@ -162,8 +166,9 @@ def test_check_notification_plaintext_format(client, live_server, measure_memory | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_check_notification_html_color_format(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_notification_html_color_format(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|  | ||||
| @@ -174,7 +179,7 @@ def test_check_notification_html_color_format(client, live_server, measure_memor | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": f"some text\n{default_notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", | ||||
|               "application-notification_format": 'HTML Color', | ||||
|               "application-notification_format": 'htmlcolor', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -193,7 +198,7 @@ def test_check_notification_html_color_format(client, live_server, measure_memor | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_longer_modified_response() | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -233,8 +238,9 @@ def test_check_notification_html_color_format(client, live_server, measure_memor | ||||
|     assert 'some text<br>' in html_content | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_notification_markdown_format(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_notification_markdown_format(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|  | ||||
| @@ -245,7 +251,7 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": "*header*\n\nsome text\n" + default_notification_body, | ||||
|               "application-notification_format": 'Markdown to HTML', | ||||
|               "application-notification_format": 'markdown', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -264,7 +270,7 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_longer_modified_response() | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -290,7 +296,8 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ | ||||
|     text_part = parts[0] | ||||
|     assert text_part.get_content_type() == 'text/plain' | ||||
|     text_content = text_part.get_content() | ||||
|     assert '(added) So let\'s see what happens.\r\n' in text_content  # The plaintext part | ||||
|     # We wont see anything in the "FALLBACK" text but that's OK (no added/strikethrough etc) | ||||
|     assert 'So let\'s see what happens.\r\n' in text_content  # The plaintext part | ||||
|  | ||||
|  | ||||
|     # Second part should be text/html and roughly converted from markdown to HTML | ||||
| @@ -298,16 +305,17 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ | ||||
|     assert html_part.get_content_type() == 'text/html' | ||||
|     html_content = html_part.get_content() | ||||
|     assert '<p><em>header</em></p>' in html_content | ||||
|     assert '(added) So let\'s see what happens.<br' in html_content | ||||
|     assert '<strong>So let\'s see what happens.</strong><br>' in html_content # Additions are <strong> in markdown | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Custom notification body with HTML, that is either sent as HTML or rendered to plaintext and sent | ||||
| def test_check_notification_email_formats_default_Text_override_HTML(client, live_server, measure_memory_usage): | ||||
| def test_check_notification_email_formats_default_Text_override_HTML(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # HTML problems? see this | ||||
|     # https://github.com/caronc/apprise/issues/633 | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|     notification_body = f"""<!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| @@ -328,7 +336,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": notification_body, | ||||
|               "application-notification_format": 'Plain Text', | ||||
|               "application-notification_format": 'text', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -347,7 +355,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv | ||||
|  | ||||
|     #################################### FIRST SITUATION, PLAIN TEXT NOTIFICATION IS WANTED BUT WE HAVE HTML IN OUR TEMPLATE AND CONTENT ########## | ||||
|     wait_for_all_checks(client) | ||||
|     set_longer_modified_response() | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(2) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
| @@ -372,13 +380,14 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv | ||||
|  | ||||
|  | ||||
|     #################################### SECOND SITUATION, HTML IS CORRECTLY PASSED THROUGH TO THE EMAIL #################### | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Now override as HTML format | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_edit.edit_page", uuid="first"), | ||||
|         data={ | ||||
|             "url": test_url, | ||||
|             "notification_format": 'HTML', | ||||
|             "notification_format": 'html', | ||||
|             'fetch_backend': "html_requests", | ||||
|             "time_between_check_use_default": "y"}, | ||||
|         follow_redirects=True | ||||
| @@ -421,10 +430,11 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_plaintext_document_plaintext_notification_smtp(client, live_server, measure_memory_usage): | ||||
| def test_check_plaintext_document_plaintext_notification_smtp(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """When following a plaintext document, notification in Plain Text format is sent correctly""" | ||||
|     import os | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nover here\n") | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
| @@ -437,7 +447,7 @@ def test_check_plaintext_document_plaintext_notification_smtp(client, live_serve | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", | ||||
|               "application-notification_format": 'Plain Text', | ||||
|               "application-notification_format": 'text', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -451,7 +461,7 @@ def test_check_plaintext_document_plaintext_notification_smtp(client, live_serve | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Change the content | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") | ||||
|  | ||||
|  | ||||
| @@ -470,14 +480,15 @@ def test_check_plaintext_document_plaintext_notification_smtp(client, live_serve | ||||
|     assert '(added)' in body | ||||
|     assert '<br' not in body | ||||
|     assert '<' not in body | ||||
|  | ||||
|     assert '<pre' not in body | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_plaintext_document_html_notifications(client, live_server, measure_memory_usage): | ||||
| def test_check_plaintext_document_html_notifications(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """When following a plaintext document, notification in Plain Text format is sent correctly""" | ||||
|     import os | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nover here\n") | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("    Some nice plain text\nwhich we add some extra data\nover here\n") | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|     notification_body = f"""{default_notification_body}""" | ||||
| @@ -489,7 +500,7 @@ def test_check_plaintext_document_html_notifications(client, live_server, measur | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", | ||||
|               "application-notification_format": 'HTML', | ||||
|               "application-notification_format": 'html', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -503,8 +514,8 @@ def test_check_plaintext_document_html_notifications(client, live_server, measur | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Change the content | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("    Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") | ||||
|  | ||||
|  | ||||
|     time.sleep(2) | ||||
| @@ -541,16 +552,21 @@ def test_check_plaintext_document_html_notifications(client, live_server, measur | ||||
|     assert 'talk about <title>' in html_content | ||||
|     # Should be the HTML, but not HTML Color | ||||
|     assert 'background-color' not in html_content | ||||
|     assert '<br>\r\n(added) And let's talk about <title> tags<br>' in html_content | ||||
|     assert '<br>(added) And let's talk about <title> tags<br>' in html_content | ||||
|     assert '<br' not in html_content | ||||
|     assert '<pre role="article"' in html_content # Should have got wrapped nicely in email_helpers.py | ||||
|  | ||||
|     # And now for the whitespace retention | ||||
|     assert '    Some nice plain text' in html_content | ||||
|     assert '(added) And let' in html_content # just to show a single whitespace didnt get touched | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_check_plaintext_document_html_color_notifications(client, live_server, measure_memory_usage): | ||||
| def test_check_plaintext_document_html_color_notifications(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """When following a plaintext document, notification in Plain Text format is sent correctly""" | ||||
|     import os | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nover here\n") | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
| @@ -563,7 +579,7 @@ def test_check_plaintext_document_html_color_notifications(client, live_server, | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", | ||||
|               "application-notification_format": 'HTML Color', | ||||
|               "application-notification_format": 'htmlcolor', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
| @@ -578,7 +594,7 @@ def test_check_plaintext_document_html_color_notifications(client, live_server, | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Change the content | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Some nice plain text\nwhich we add some extra data\nAnd let's talk about <title> tags\nover here\n") | ||||
|  | ||||
|     time.sleep(1) | ||||
| @@ -616,5 +632,134 @@ def test_check_plaintext_document_html_color_notifications(client, live_server, | ||||
|     assert '(added) And let' not in html_content | ||||
|     assert '<br' not in html_content | ||||
|     assert '<br>' in html_content | ||||
|     assert '<pre role="article"' in html_content # Should have got wrapped nicely in email_helpers.py | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_html_document_plaintext_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """When following a HTML document, notification in Plain Text format is sent correctly""" | ||||
|     import os | ||||
|  | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html><body>some stuff<br>and more stuff<br>and even more stuff<br></body></html>") | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' | ||||
|     notification_body = f"""{default_notification_body}""" | ||||
|  | ||||
|     ##################### | ||||
|     # Set this up for when we remove the notification from the watch, it should fallback with these details | ||||
|     res = client.post( | ||||
|         url_for("settings.settings_page"), | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", | ||||
|               "application-notification_format": 'text', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b"Settings updated." in res.data | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', content_type="text/html", _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html><body>sxome stuff<br>and more stuff<br>lets slip this in<br>and this in<br>and even more stuff<br><tag></body></html>") | ||||
|  | ||||
|     time.sleep(0.1) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|  | ||||
|     # Parse the email properly using Python's email library | ||||
|     msg = message_from_string(get_last_message_from_smtp_server(), policy=email_policy) | ||||
|  | ||||
|     assert not msg.is_multipart() | ||||
|     assert msg.get_content_type() == 'text/plain' | ||||
|     body = msg.get_content() | ||||
|  | ||||
|     assert '<tag>' in body # Should have got converted from original HTML to plaintext | ||||
|     assert '(changed) some stuff\r\n' in body | ||||
|     assert '(into) sxome stuff\r\n' in body | ||||
|     assert '(added) lets slip this in\r\n' in body | ||||
|     assert '(added) and this in\r\n' in body | ||||
|     assert ' ' not in body | ||||
|  | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_check_html_notification_with_apprise_format_is_html(client, live_server, measure_memory_usage, datastore_path): | ||||
|     ##  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com&format=html' | ||||
|  | ||||
|     ##################### | ||||
|     # Set this up for when we remove the notification from the watch, it should fallback with these details | ||||
|     res = client.post( | ||||
|         url_for("settings.settings_page"), | ||||
|         data={"application-notification_urls": notification_url, | ||||
|               "application-notification_title": "fallback-title " + default_notification_title, | ||||
|               "application-notification_body": "some text\nfallback-body<br> " + default_notification_body, | ||||
|               "application-notification_format": 'html', | ||||
|               "requests-time_between_check-minutes": 180, | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert b"Settings updated." in res.data | ||||
|  | ||||
|     # Add a watch and trigger a HTTP POST | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": test_url, "tags": 'nice one'}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_longer_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     time.sleep(3) | ||||
|  | ||||
|     msg_raw = get_last_message_from_smtp_server() | ||||
|     assert len(msg_raw) >= 1 | ||||
|  | ||||
|     # Parse the email properly using Python's email library | ||||
|     msg = message_from_string(msg_raw, policy=email_policy) | ||||
|  | ||||
|     # The email should have two bodies (multipart/alternative with text/plain and text/html) | ||||
|     assert msg.is_multipart() | ||||
|     assert msg.get_content_type() == 'multipart/alternative' | ||||
|  | ||||
|     # Get the parts | ||||
|     parts = list(msg.iter_parts()) | ||||
|     assert len(parts) == 2 | ||||
|  | ||||
|     # First part should be text/plain (the auto-generated plaintext version) | ||||
|     text_part = parts[0] | ||||
|     assert text_part.get_content_type() == 'text/plain' | ||||
|     text_content = text_part.get_content() | ||||
|     assert '(added) So let\'s see what happens.\r\n' in text_content  # The plaintext part | ||||
|     assert 'fallback-body\r\n' in text_content  # The plaintext part | ||||
|     assert CUSTOM_LINEBREAK_PLACEHOLDER not in text_content | ||||
|  | ||||
|     # Second part should be text/html | ||||
|     html_part = parts[1] | ||||
|     assert html_part.get_content_type() == 'text/html' | ||||
|     html_content = html_part.get_content() | ||||
|     assert 'some text<br>' in html_content  # We converted \n from the notification body | ||||
|     assert 'fallback-body<br>' in html_content  # kept the original <br> | ||||
|     assert '(added) So let\'s see what happens.<br>' in html_content  # the html part | ||||
|     assert CUSTOM_LINEBREAK_PLACEHOLDER not in html_content | ||||
|     delete_all_watches(client) | ||||
| @@ -2,7 +2,7 @@ from .util import live_server_setup, wait_for_all_checks | ||||
| from flask import url_for | ||||
| import time | ||||
|  | ||||
| def test_check_access_control(app, client, live_server, measure_memory_usage): | ||||
| def test_check_access_control(app, client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Still doesnt work, but this is closer. | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import os.path | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, wait_for_notification_endpoint_output, delete_all_watches | ||||
| @@ -9,7 +10,7 @@ import time | ||||
| from ..diff import ADDED_PLACEMARKER_OPEN | ||||
|  | ||||
|  | ||||
| def set_original(excluding=None, add_line=None): | ||||
| def set_original(datastore_path, excluding=None, add_line=None): | ||||
|     test_return_data = """<html> | ||||
|      <body> | ||||
|      <p>Some initial text</p> | ||||
| @@ -35,16 +36,16 @@ def set_original(excluding=None, add_line=None): | ||||
|  | ||||
|         test_return_data = output | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_check_removed_line_contains_trigger(client, live_server, measure_memory_usage): | ||||
| def test_check_removed_line_contains_trigger(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     set_original() | ||||
|     set_original(datastore_path=datastore_path) | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
| @@ -64,9 +65,10 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory | ||||
|               "time_between_check_use_default": "y"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b"Updated watch." in res.data | ||||
|     wait_for_all_checks(client) | ||||
|     set_original(excluding='Something irrelevant') | ||||
|     set_original(excluding='Something irrelevant', datastore_path=datastore_path) | ||||
|  | ||||
|     # A line thats not the trigger should not trigger anything | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -77,7 +79,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # The trigger line is REMOVED,  this should trigger | ||||
|     set_original(excluding='The golden line') | ||||
|     set_original(excluding='The golden line', datastore_path=datastore_path) | ||||
|  | ||||
|     # Check in the processor here what's going on, its triggering empty-reply and no change. | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -92,7 +94,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory | ||||
|     time.sleep(0.2) | ||||
|  | ||||
|     time.sleep(1) | ||||
|     set_original(excluding=None) | ||||
|     set_original(excluding=None, datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     time.sleep(1) | ||||
| @@ -100,7 +102,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # Remove it again, and we should get a trigger | ||||
|     set_original(excluding='The golden line') | ||||
|     set_original(excluding='The golden line', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -109,7 +111,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_check_add_line_contains_trigger(client, live_server, measure_memory_usage): | ||||
| def test_check_add_line_contains_trigger(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     delete_all_watches(client) | ||||
|     time.sleep(1) | ||||
| @@ -124,7 +126,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa | ||||
|               "application-notification_body": 'triggered text was -{{triggered_text}}- ### 网站监测 内容更新了 ####', | ||||
|               # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation | ||||
|               "application-notification_urls": test_notification_url, | ||||
|               "application-notification_format": 'Plain Text', | ||||
|               "application-notification_format": 'text', | ||||
|               "application-minutes_between_check": 180, | ||||
|               "application-fetch_backend": "html_requests" | ||||
|               }, | ||||
| @@ -132,7 +134,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa | ||||
|     ) | ||||
|     assert b'Settings updated' in res.data | ||||
|  | ||||
|     set_original() | ||||
|     set_original(datastore_path=datastore_path) | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
| @@ -155,7 +157,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa | ||||
|     ) | ||||
|     assert b"Updated watch." in res.data | ||||
|     wait_for_all_checks(client) | ||||
|     set_original(excluding='Something irrelevant') | ||||
|     set_original(excluding='Something irrelevant', datastore_path=datastore_path) | ||||
|  | ||||
|     # A line thats not the trigger should not trigger anything | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -166,7 +168,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # The trigger line is ADDED,  this should trigger | ||||
|     set_original(add_line='<p>Oh yes please</p>') | ||||
|     set_original(add_line='<p>Oh yes please</p>', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -174,9 +176,9 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa | ||||
|     assert b'has-unread-changes' in res.data | ||||
|  | ||||
|     # Takes a moment for apprise to fire | ||||
|     wait_for_notification_endpoint_output() | ||||
|     assert os.path.isfile("test-datastore/notification.txt"), "Notification fired because I can see the output file" | ||||
|     with open("test-datastore/notification.txt", 'rb') as f: | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification.txt")), "Notification fired because I can see the output file" | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'rb') as f: | ||||
|         response = f.read() | ||||
|         assert ADDED_PLACEMARKER_OPEN.encode('utf-8') not in response #  _apply_diff_filtering shouldnt add something here | ||||
|         assert b'-Oh yes please' in response | ||||
|   | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| import os | ||||
|  | ||||
| import json | ||||
| import uuid | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -21,12 +22,12 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -39,7 +40,7 @@ def set_modified_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     return None | ||||
| @@ -52,17 +53,17 @@ def is_valid_uuid(val): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|  | ||||
| def test_api_simple(client, live_server, measure_memory_usage): | ||||
| def test_api_simple(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
|     # Create a watch | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Validate bad URL | ||||
|     test_url = url_for('test_endpoint', _external=True ) | ||||
| @@ -111,7 +112,7 @@ def test_api_simple(client, live_server, measure_memory_usage): | ||||
|     time.sleep(1) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|     # Trigger recheck of all ?recheck_all=1 | ||||
|     client.get( | ||||
|         url_for("createwatch", recheck_all='1'), | ||||
| @@ -244,7 +245,7 @@ def test_api_simple(client, live_server, measure_memory_usage): | ||||
|     ) | ||||
|     assert len(res.json) == 0, "Watch list should be empty" | ||||
|  | ||||
| def test_access_denied(client, live_server, measure_memory_usage): | ||||
| def test_access_denied(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # `config_api_token_enabled` Should be On by default | ||||
|     res = client.get( | ||||
|         url_for("createwatch") | ||||
| @@ -289,11 +290,11 @@ def test_access_denied(client, live_server, measure_memory_usage): | ||||
|     ) | ||||
|     assert b"Settings updated." in res.data | ||||
|  | ||||
| def test_api_watch_PUT_update(client, live_server, measure_memory_usage): | ||||
| def test_api_watch_PUT_update(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|     # Create a watch | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     # Create new | ||||
| @@ -370,7 +371,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     ###################################################### | ||||
|  | ||||
|     # HTTP PUT try a field that doenst exist | ||||
|     # HTTP PUT try a field that doesn't exist | ||||
|  | ||||
|     # HTTP PUT an update | ||||
|     res = client.put( | ||||
| @@ -383,18 +384,30 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage): | ||||
|     # Message will come from `flask_expects_json` | ||||
|     assert b'Additional properties are not allowed' in res.data | ||||
|  | ||||
|  | ||||
|     # Try a XSS URL | ||||
|     res = client.put( | ||||
|         url_for("watch", uuid=watch_uuid), | ||||
|         headers={'x-api-key': api_key, 'content-type': 'application/json'}, | ||||
|         data=json.dumps({ | ||||
|             'url': 'javascript:alert(document.domain)' | ||||
|         }), | ||||
|     ) | ||||
|     assert res.status_code == 400 | ||||
|  | ||||
|     # Cleanup everything | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_api_import(client, live_server, measure_memory_usage): | ||||
| def test_api_import(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("import") + "?tag=import-test", | ||||
|         data='https://website1.com\r\nhttps://website2.com', | ||||
|         headers={'x-api-key': api_key, 'content-type': 'text/plain'}, | ||||
|         # We removed  'content-type': 'text/plain', the Import API should assume this if none is set #3547 #3542 | ||||
|         headers={'x-api-key': api_key}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
| @@ -408,7 +421,7 @@ def test_api_import(client, live_server, measure_memory_usage): | ||||
|     res = client.get(url_for('tags.tags_overview_page')) | ||||
|     assert b'import-test' in res.data | ||||
|  | ||||
| def test_api_conflict_UI_password(client, live_server, measure_memory_usage): | ||||
| def test_api_conflict_UI_password(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|      | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
| @@ -426,7 +439,7 @@ def test_api_conflict_UI_password(client, live_server, measure_memory_usage): | ||||
|     assert b"Password protection enabled." in res.data | ||||
|  | ||||
|     # Create a watch | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     # Create new | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from flask import url_for | ||||
| from .util import live_server_setup | ||||
| import json | ||||
|  | ||||
| def test_api_notifications_crud(client, live_server, measure_memory_usage): | ||||
| def test_api_notifications_crud(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_invalid_content_type_on_create_watch(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_invalid_content_type_on_create_watch(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that creating a watch with invalid content-type triggers OpenAPI validation error.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -29,7 +29,7 @@ def test_openapi_validation_invalid_content_type_on_create_watch(client, live_se | ||||
|     assert b"OpenAPI validation failed" in res.data, "Should contain OpenAPI validation error message" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_missing_required_field_create_watch(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_missing_required_field_create_watch(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that creating a watch without required URL field triggers OpenAPI validation error.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -46,7 +46,7 @@ def test_openapi_validation_missing_required_field_create_watch(client, live_ser | ||||
|     assert b"OpenAPI validation failed" in res.data, "Should contain OpenAPI validation error message" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_invalid_field_in_request_body(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_invalid_field_in_request_body(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that including invalid fields triggers OpenAPI validation error.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -83,7 +83,7 @@ def test_openapi_validation_invalid_field_in_request_body(client, live_server, m | ||||
|     assert b"Additional properties are not allowed" in res.data, "Should contain validation error about additional properties" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_import_wrong_content_type(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_import_wrong_content_type(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that import endpoint with wrong content-type triggers OpenAPI validation error.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -100,7 +100,7 @@ def test_openapi_validation_import_wrong_content_type(client, live_server, measu | ||||
|     assert b"OpenAPI validation failed" in res.data, "Should contain OpenAPI validation error message" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_import_correct_content_type_succeeds(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_import_correct_content_type_succeeds(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that import endpoint with correct content-type succeeds (positive test).""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -117,7 +117,7 @@ def test_openapi_validation_import_correct_content_type_succeeds(client, live_se | ||||
|     assert len(res.json) == 2, "Should import 2 URLs" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_get_requests_bypass_validation(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_get_requests_bypass_validation(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that GET requests bypass OpenAPI validation entirely.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -141,7 +141,7 @@ def test_openapi_validation_get_requests_bypass_validation(client, live_server, | ||||
|     assert isinstance(res.json, dict), "Should return JSON dictionary for watch list" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_create_tag_missing_required_title(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_create_tag_missing_required_title(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that creating a tag without required title triggers OpenAPI validation error.""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
| @@ -158,7 +158,7 @@ def test_openapi_validation_create_tag_missing_required_title(client, live_serve | ||||
|     assert b"OpenAPI validation failed" in res.data, "Should contain OpenAPI validation error message" | ||||
|  | ||||
|  | ||||
| def test_openapi_validation_watch_update_allows_partial_updates(client, live_server, measure_memory_usage): | ||||
| def test_openapi_validation_watch_update_allows_partial_updates(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that watch updates allow partial updates without requiring all fields (positive test).""" | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import time | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
|  | ||||
|  | ||||
| def test_api_search(client, live_server, measure_memory_usage): | ||||
| def test_api_search(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|  | ||||
|   | ||||
| @@ -5,13 +5,14 @@ from .util import live_server_setup, wait_for_all_checks, set_original_response | ||||
| import json | ||||
| import time | ||||
|  | ||||
| def test_api_tags_listing(client, live_server, measure_memory_usage): | ||||
| def test_api_tags_listing(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token') | ||||
|     tag_title = 'Test Tag' | ||||
|  | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     res = client.get( | ||||
|         url_for("tags"), | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
|  | ||||
| # test pages with http://username@password:foobar.com/ work | ||||
| def test_basic_auth(client, live_server, measure_memory_usage): | ||||
| def test_basic_auth(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,9 +3,10 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, extract_UUID_from_client, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_response_with_ldjson(): | ||||
| def set_response_with_ldjson(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -55,11 +56,11 @@ def set_response_with_ldjson(): | ||||
|      </html> | ||||
| """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def set_response_without_ldjson(): | ||||
| def set_response_without_ldjson(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -72,17 +73,17 @@ def set_response_without_ldjson(): | ||||
|      </html> | ||||
| """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| # actually only really used by the distll.io importer, but could be handy too | ||||
| def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage): | ||||
| def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     set_response_with_ldjson() | ||||
|     set_response_with_ldjson(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -121,7 +122,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage | ||||
|  | ||||
|     ########################################################################################## | ||||
|     # And we shouldnt see the offer | ||||
|     set_response_without_ldjson() | ||||
|     set_response_without_ldjson(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -151,7 +152,7 @@ def _test_runner_check_bad_format_ignored(live_server, client, has_ldjson_price_ | ||||
|     client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) | ||||
|  | ||||
|  | ||||
| def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usage): | ||||
| def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     test_return_data = """ | ||||
|             <html> | ||||
| @@ -181,7 +182,7 @@ def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usa | ||||
|             <div class="yes">Some extra stuff</div> | ||||
|             </body></html> | ||||
|      """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     _test_runner_check_bad_format_ignored(live_server=live_server, client=client, has_ldjson_price_data=True) | ||||
| @@ -215,7 +216,7 @@ def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usa | ||||
|     #         <div class="yes">Some extra stuff</div> | ||||
|     #         </body></html> | ||||
|     #  """ | ||||
|     # with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     # with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|     #     f.write(test_return_data) | ||||
|     # | ||||
|     # _test_runner_check_bad_format_ignored(live_server=live_server, client=client, has_ldjson_price_data=False) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import os | ||||
|  | ||||
| import time | ||||
| from flask import url_for | ||||
| @@ -16,8 +17,8 @@ def test_inscriptus(): | ||||
|     assert stripped_text_from_html == 'test!\nok man' | ||||
|  | ||||
|  | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -60,7 +61,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|     assert b'foobar-detection' not in res.data | ||||
|  | ||||
|     # Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Force recheck | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -121,7 +122,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|         assert b'test-endpoint' in res.data | ||||
|  | ||||
|     # Recheck it but only with a title change, content wasnt changed | ||||
|     set_original_response(extra_title=" and more") | ||||
|     set_original_response(datastore_path=datastore_path, extra_title=" and more") | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
| @@ -167,7 +168,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|  | ||||
|  | ||||
| # Server says its plaintext, we should always treat it as plaintext, and then if they have a filter, try to apply that | ||||
| def test_requests_timeout(client, live_server, measure_memory_usage): | ||||
| def test_requests_timeout(client, live_server, measure_memory_usage, datastore_path): | ||||
|     delay = 2 | ||||
|     test_url = url_for('test_endpoint', delay=delay, _external=True) | ||||
|  | ||||
| @@ -205,7 +206,7 @@ def test_requests_timeout(client, live_server, measure_memory_usage): | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|     assert b'Read timed out' not in res.data | ||||
|  | ||||
| def test_non_text_mime_or_downloads(client, live_server, measure_memory_usage): | ||||
| def test_non_text_mime_or_downloads(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """ | ||||
|  | ||||
|     https://github.com/dgtlmoon/changedetection.io/issues/3434 | ||||
| @@ -220,7 +221,7 @@ def test_non_text_mime_or_downloads(client, live_server, measure_memory_usage): | ||||
|     :param measure_memory_usage: | ||||
|     :return: | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""some random text that should be split by line | ||||
| and not parsed with html_to_text | ||||
| this way we know that it correctly parsed as plain text | ||||
| @@ -264,7 +265,7 @@ got it\r\n | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_standard_text_plain(client, live_server, measure_memory_usage): | ||||
| def test_standard_text_plain(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """ | ||||
|  | ||||
|     https://github.com/dgtlmoon/changedetection.io/issues/3434 | ||||
| @@ -279,7 +280,7 @@ def test_standard_text_plain(client, live_server, measure_memory_usage): | ||||
|     :param measure_memory_usage: | ||||
|     :return: | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""some random text that should be split by line | ||||
| and not parsed with html_to_text | ||||
| <title>Even this title should stay because we are just plain text</title> | ||||
| @@ -325,9 +326,9 @@ got it\r\n | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Server says its plaintext, we should always treat it as plaintext | ||||
| def test_plaintext_even_if_xml_content(client, live_server, measure_memory_usage): | ||||
| def test_plaintext_even_if_xml_content(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<?xml version="1.0" encoding="utf-8"?> | ||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <!--Activity and fragment titles--> | ||||
| @@ -353,10 +354,10 @@ def test_plaintext_even_if_xml_content(client, live_server, measure_memory_usage | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Server says its plaintext, we should always treat it as plaintext, and then if they have a filter, try to apply that | ||||
| def test_plaintext_even_if_xml_content_and_can_apply_filters(client, live_server, measure_memory_usage): | ||||
| def test_plaintext_even_if_xml_content_and_can_apply_filters(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<?xml version="1.0" encoding="utf-8"?> | ||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <!--Activity and fragment titles--> | ||||
|   | ||||
| @@ -8,13 +8,11 @@ import re | ||||
| import time | ||||
|  | ||||
|  | ||||
| def test_backup(client, live_server, measure_memory_usage): | ||||
| def test_backup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     res = client.post( | ||||
| @@ -31,7 +29,7 @@ def test_backup(client, live_server, measure_memory_usage): | ||||
|         url_for("backups.request_backup"), | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     time.sleep(2) | ||||
|     time.sleep(4) | ||||
|  | ||||
|     res = client.get( | ||||
|         url_for("backups.index"), | ||||
|   | ||||
| @@ -10,11 +10,12 @@ from .util import ( | ||||
| ) | ||||
| from loguru import logger | ||||
|  | ||||
| def run_socketio_watch_update_test(client, live_server, password_mode=""): | ||||
| def run_socketio_watch_update_test(client, live_server, password_mode="", datastore_path=""): | ||||
|     """Test that the socketio emits a watch update event when content changes""" | ||||
|  | ||||
|     # Set up the test server | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Get the SocketIO instance from the app | ||||
|     from changedetectionio.flask_app import app | ||||
| @@ -47,7 +48,7 @@ def run_socketio_watch_update_test(client, live_server, password_mode=""): | ||||
|     socketio_test_client.get_received() | ||||
|  | ||||
|     # Make a change to trigger an update | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Force recheck | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -105,11 +106,11 @@ def run_socketio_watch_update_test(client, live_server, password_mode=""): | ||||
|     # Clean up | ||||
|     client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) | ||||
|  | ||||
| def test_everything(live_server, client): | ||||
| def test_everything(live_server, client, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     run_socketio_watch_update_test(password_mode="", live_server=live_server, client=client) | ||||
|     run_socketio_watch_update_test(password_mode="", live_server=live_server, client=client, datastore_path=datastore_path) | ||||
|  | ||||
|     ############################ Password required auth check ############################## | ||||
|  | ||||
| @@ -124,7 +125,7 @@ def test_everything(live_server, client): | ||||
|  | ||||
|     assert b"Password protection enabled." in res.data | ||||
|  | ||||
|     run_socketio_watch_update_test(password_mode="not logged in, should exit on connect", live_server=live_server, client=client) | ||||
|     run_socketio_watch_update_test(password_mode="not logged in, should exit on connect", live_server=live_server, client=client, datastore_path=datastore_path) | ||||
|     res = client.post( | ||||
|         url_for("login"), | ||||
|         data={"password": "foobar"}, | ||||
| @@ -133,4 +134,4 @@ def test_everything(live_server, client): | ||||
|  | ||||
|     # Yes we are correctly logged in | ||||
|     assert b"LOG OUT" in res.data | ||||
|     run_socketio_watch_update_test(password_mode="should be like normal", live_server=live_server, client=client) | ||||
|     run_socketio_watch_update_test(password_mode="should be like normal", live_server=live_server, client=client, datastore_path=datastore_path) | ||||
|   | ||||
| @@ -4,8 +4,9 @@ import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| from changedetectionio import html_tools | ||||
| import os | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -17,11 +18,11 @@ def set_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_modified_original_ignore_response(): | ||||
| def set_modified_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some NEW nice initial text<br> | ||||
| @@ -36,12 +37,12 @@ def set_modified_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| # Is the same but includes ZZZZZ, 'ZZZZZ' is the last line in ignore_text | ||||
| def set_modified_response_minus_block_text(): | ||||
| def set_modified_response_minus_block_text(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some NEW nice initial text<br> | ||||
| @@ -56,16 +57,16 @@ def set_modified_response_minus_block_text(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def test_check_block_changedetection_text_NOT_present(client, live_server, measure_memory_usage): | ||||
| def test_check_block_changedetection_text_NOT_present(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     # Use a mix of case in ZzZ to prove it works case-insensitive. | ||||
|     ignore_text = "out of stoCk\r\nfoobar" | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -109,7 +110,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu | ||||
|     assert b'/test-endpoint' in res.data | ||||
|  | ||||
|     # The page changed, BUT the text is still there, just the rest of it changes, we should not see a change | ||||
|     set_modified_original_ignore_response() | ||||
|     set_modified_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -123,7 +124,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu | ||||
|  | ||||
|     # 2548 | ||||
|     # Going back to the ORIGINAL should NOT trigger a change | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -131,10 +132,11 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu | ||||
|  | ||||
|  | ||||
|     # Now we set a change where the text is gone AND its different content, it should now trigger | ||||
|     set_modified_response_minus_block_text() | ||||
|     set_modified_response_minus_block_text(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|  | ||||
|     assert b'has-unread-changes' in res.data | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
| def test_clone_functionality(client, live_server, measure_memory_usage): | ||||
| def test_clone_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html><body>Some content</body></html>") | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import json | ||||
| import time | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| from ..model import CONDITIONS_MATCH_LOGIC_DEFAULT | ||||
|  | ||||
|  | ||||
| def set_original_response(number="50"): | ||||
| def set_original_response(datastore_path, number="50"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      <h1>Test Page for Conditions</h1> | ||||
| @@ -17,10 +18,10 @@ def set_original_response(number="50"): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
| def set_number_in_range_response(number="75"): | ||||
| def set_number_in_range_response(datastore_path, number="75"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      <h1>Test Page for Conditions</h1> | ||||
| @@ -30,10 +31,10 @@ def set_number_in_range_response(number="75"): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
| def set_number_out_of_range_response(number="150"): | ||||
| def set_number_out_of_range_response(datastore_path, number="150"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      <h1>Test Page for Conditions</h1> | ||||
| @@ -43,18 +44,18 @@ def set_number_out_of_range_response(number="150"): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that both text and number conditions work together with AND logic.""" | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_conditions_with_text_and_number(client, live_server, measure_memory_usage): | ||||
| def test_conditions_with_text_and_number(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that both text and number conditions work together with AND logic.""" | ||||
|      | ||||
|     set_original_response("50") | ||||
|     set_original_response(datastore_path=datastore_path, number="50") | ||||
|      | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -114,7 +115,7 @@ def test_conditions_with_text_and_number(client, live_server, measure_memory_usa | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Case 1 | ||||
|     set_number_in_range_response("70.5") | ||||
|     set_number_in_range_response(datastore_path=datastore_path, number="70.5") | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| @@ -129,7 +130,7 @@ def test_conditions_with_text_and_number(client, live_server, measure_memory_usa | ||||
|     client.get(url_for("ui.mark_all_viewed"), follow_redirects=True) | ||||
|     time.sleep(0.2) | ||||
|  | ||||
|     set_number_out_of_range_response("150.5") | ||||
|     set_number_out_of_range_response(datastore_path=datastore_path, number="150.5") | ||||
|  | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -142,9 +143,9 @@ def test_conditions_with_text_and_number(client, live_server, measure_memory_usa | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # The 'validate' button next to each rule row | ||||
| def test_condition_validate_rule_row(client, live_server, measure_memory_usage): | ||||
| def test_condition_validate_rule_row(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     set_original_response("50") | ||||
|     set_original_response(datastore_path=datastore_path, number="50") | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
| @@ -203,7 +204,7 @@ def test_condition_validate_rule_row(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
| # If there was only a change in the whitespacing, then we shouldnt have a change detected | ||||
| def test_wordcount_conditions_plugin(client, live_server, measure_memory_usage): | ||||
| def test_wordcount_conditions_plugin(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     test_return_data = """<html> | ||||
| @@ -216,7 +217,7 @@ def test_wordcount_conditions_plugin(client, live_server, measure_memory_usage): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -242,10 +243,10 @@ def test_wordcount_conditions_plugin(client, live_server, measure_memory_usage): | ||||
|     ) | ||||
|  | ||||
| # If there was only a change in the whitespacing, then we shouldnt have a change detected | ||||
| def test_lev_conditions_plugin(client, live_server, measure_memory_usage): | ||||
|      | ||||
| def test_lev_conditions_plugin(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # This should break.. | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -297,7 +298,7 @@ def test_lev_conditions_plugin(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     ############### Now change it a LITTLE bit... | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -326,7 +327,7 @@ def test_lev_conditions_plugin(client, live_server, measure_memory_usage): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     assert b'Queued 1 watch for rechecking.' in res.data | ||||
|   | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
| from ..html_tools import * | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -21,11 +22,11 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -38,7 +39,7 @@ def set_modified_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     return None | ||||
| @@ -69,12 +70,12 @@ def test_include_filters_output(): | ||||
|  | ||||
|  | ||||
| # Tests the whole stack works with the CSS Filter | ||||
| def test_check_markup_include_filters_restriction(client, live_server, measure_memory_usage): | ||||
| def test_check_markup_include_filters_restriction(client, live_server, measure_memory_usage, datastore_path): | ||||
|     sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|     include_filters = "#sametext" | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
| @@ -105,7 +106,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m | ||||
|     # Give the thread time to pick it up | ||||
|     time.sleep(sleep_time_for_fetch_thread) | ||||
|     #  Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -119,11 +120,11 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m | ||||
|  | ||||
|  | ||||
| # Tests the whole stack works with the CSS Filter | ||||
| def test_check_multiple_filters(client, live_server, measure_memory_usage): | ||||
| def test_check_multiple_filters(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     include_filters = "#blob-a\r\nxpath://*[contains(@id,'blob-b')]" | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<html><body> | ||||
|      <div id="blob-a">Blob A</div> | ||||
|      <div id="blob-b">Blob B</div> | ||||
| @@ -168,12 +169,12 @@ def test_check_multiple_filters(client, live_server, measure_memory_usage): | ||||
| # The filter exists, but did not contain anything useful | ||||
| # Mainly used when the filter contains just an IMG, this can happen when someone selects an image in the visual-selector | ||||
| # Tests fetcher can throw a "ReplyWithContentButNoText" exception after applying filter and extracting text | ||||
| def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usage): | ||||
| def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     include_filters = "#blob-a" | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<html><body> | ||||
|          <div id="blob-a"> | ||||
|            <img src="something.jpg"> | ||||
| @@ -216,7 +217,7 @@ def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usa | ||||
|  | ||||
|     ### Just an empty selector, no image | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("""<html><body> | ||||
|          <div id="blob-a"> | ||||
|            <!-- doo doo --> | ||||
|   | ||||
							
								
								
									
										19
									
								
								changedetectionio/tests/test_datastore_isolation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								changedetectionio/tests/test_datastore_isolation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """Test to verify client and live_server share the same datastore""" | ||||
|  | ||||
| def test_client_and_live_server_share_datastore(client, live_server): | ||||
|     """Verify that client and live_server use the same app and datastore.""" | ||||
|  | ||||
|     # They should be the SAME object | ||||
|     assert client.application is live_server.app, "client.application and live_server.app should be the SAME object!" | ||||
|  | ||||
|     # They should share the same datastore | ||||
|     client_datastore = client.application.config.get('DATASTORE') | ||||
|     server_datastore = live_server.app.config.get('DATASTORE') | ||||
|  | ||||
|     assert client_datastore is server_datastore, \ | ||||
|         f"Datastores are DIFFERENT objects! client={hex(id(client_datastore))} server={hex(id(server_datastore))}" | ||||
|  | ||||
|     print(f"✓ client.application and live_server.app are the SAME object") | ||||
|     print(f"✓ Both use the same DATASTORE at {hex(id(client_datastore))}") | ||||
|     print(f"✓ Datastore path: {client_datastore.datastore_path}") | ||||
| @@ -1,6 +1,7 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import time | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
|  | ||||
| @@ -10,7 +11,7 @@ from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_response_with_multiple_index(): | ||||
| def set_response_with_multiple_index(datastore_path): | ||||
|     data= """<!DOCTYPE html> | ||||
| <html> | ||||
| <body> | ||||
| @@ -36,11 +37,11 @@ def set_response_with_multiple_index(): | ||||
| </body> | ||||
| </html> | ||||
| """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|     <header> | ||||
|     <h2>Header</h2> | ||||
| @@ -65,11 +66,11 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|     <header> | ||||
|     <h2>Header changed</h2> | ||||
| @@ -94,7 +95,7 @@ def set_modified_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| @@ -146,10 +147,10 @@ across multiple lines | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_element_removal_full(client, live_server, measure_memory_usage): | ||||
| def test_element_removal_full(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -194,7 +195,7 @@ def test_element_removal_full(client, live_server, measure_memory_usage): | ||||
|     client.get(url_for("ui.ui_views.diff_history_page", uuid="first")) | ||||
|  | ||||
|     #  Make a change to header/footer/nav | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -208,9 +209,9 @@ def test_element_removal_full(client, live_server, measure_memory_usage): | ||||
|     assert b"unviewed" not in res.data | ||||
|  | ||||
| # Re #2752 | ||||
| def test_element_removal_nth_offset_no_shift(client, live_server, measure_memory_usage): | ||||
| def test_element_removal_nth_offset_no_shift(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     set_response_with_multiple_index() | ||||
|     set_response_with_multiple_index(datastore_path=datastore_path) | ||||
|     subtractive_selectors_data = [ | ||||
| ### css style ### | ||||
| """body > table > tr:nth-child(1) > th:nth-child(2) | ||||
|   | ||||
| @@ -5,26 +5,27 @@ import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, extract_UUID_from_client | ||||
| import pytest | ||||
| import os | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_html_response(): | ||||
| def set_html_response(datastore_path): | ||||
|     test_return_data = """ | ||||
| <html><body><span class="nav_second_img_text"> | ||||
|                          铸大国重器,挺制造脊梁,致力能源未来,赋能美好生活。 | ||||
|                                   </span> | ||||
| </body></html> | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| # In the case the server does not issue a charset= or doesnt have content_type header set | ||||
| def test_check_encoding_detection(client, live_server, measure_memory_usage): | ||||
|     set_html_response() | ||||
| def test_check_encoding_detection(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_html_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', content_type="text/html", _external=True) | ||||
| @@ -51,8 +52,8 @@ def test_check_encoding_detection(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
| # In the case the server does not issue a charset= or doesnt have content_type header set | ||||
| def test_check_encoding_detection_missing_content_type_header(client, live_server, measure_memory_usage): | ||||
|     set_html_response() | ||||
| def test_check_encoding_detection_missing_content_type_header(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_html_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import time | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| @@ -8,9 +9,9 @@ from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
|  | ||||
|  | ||||
|  | ||||
| def _runner_test_http_errors(client, live_server, http_code, expected_text): | ||||
| def _runner_test_http_errors(client, live_server, http_code, expected_text, datastore_path): | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Now you going to get a {} error code\n".format(http_code)) | ||||
|  | ||||
|  | ||||
| @@ -46,17 +47,15 @@ def _runner_test_http_errors(client, live_server, http_code, expected_text): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_http_error_handler(client, live_server, measure_memory_usage): | ||||
|     _runner_test_http_errors(client, live_server, 403, 'Access denied') | ||||
|     _runner_test_http_errors(client, live_server, 404, 'Page not found') | ||||
|     _runner_test_http_errors(client, live_server, 500, '(Internal server error) received') | ||||
|     _runner_test_http_errors(client, live_server, 400, 'Error - Request returned a HTTP error code 400') | ||||
| def test_http_error_handler(client, live_server, measure_memory_usage, datastore_path): | ||||
|     _runner_test_http_errors(client, live_server, 403, 'Access denied', datastore_path=datastore_path) | ||||
|     _runner_test_http_errors(client, live_server, 404, 'Page not found', datastore_path=datastore_path) | ||||
|     _runner_test_http_errors(client, live_server, 500, '(Internal server error) received', datastore_path=datastore_path) | ||||
|     _runner_test_http_errors(client, live_server, 400, 'Error - Request returned a HTTP error code 400', datastore_path=datastore_path) | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Just to be sure error text is properly handled | ||||
| def test_DNS_errors(client, live_server, measure_memory_usage): | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
| def test_DNS_errors(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     res = client.post( | ||||
| @@ -84,12 +83,9 @@ def test_DNS_errors(client, live_server, measure_memory_usage): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Re 1513 | ||||
| def test_low_level_errors_clear_correctly(client, live_server, measure_memory_usage): | ||||
|      | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
| def test_low_level_errors_clear_correctly(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html><body><div id=here>Hello world</div></body></html>") | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|   | ||||
| @@ -4,14 +4,15 @@ import time | ||||
| from flask import url_for | ||||
| from urllib.request import urlopen | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
| sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_check_extract_text_from_diff(client, live_server, measure_memory_usage): | ||||
| def test_check_extract_text_from_diff(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import time | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("Now it's {} seconds since epoch, time flies!".format(str(time.time()))) | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
| @@ -33,7 +34,7 @@ def test_check_extract_text_from_diff(client, live_server, measure_memory_usage) | ||||
|         # Give the thread time to pick it up | ||||
|         print("Bumping snapshot and checking.. ", n) | ||||
|         last_date = str(time.time()) | ||||
|         with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|         with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|             f.write("Now it's {} seconds since epoch, time flies!".format(last_date)) | ||||
|  | ||||
|         client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|   | ||||
| @@ -3,11 +3,12 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| import os | ||||
|  | ||||
| from ..html_tools import * | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -20,12 +21,12 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -39,13 +40,13 @@ def set_modified_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def set_multiline_response(): | ||||
| def set_multiline_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|       | ||||
| @@ -61,18 +62,18 @@ def set_multiline_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     return None | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_check_filter_multiline(client, live_server, measure_memory_usage): | ||||
| def test_check_filter_multiline(client, live_server, measure_memory_usage, datastore_path): | ||||
|    ##  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_multiline_response() | ||||
|     set_multiline_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -119,11 +120,11 @@ def test_check_filter_multiline(client, live_server, measure_memory_usage): | ||||
|     # but the last one, which also says 'lines' shouldnt be here (non-greedy match checking) | ||||
|     assert b'aaand something lines' not in res.data | ||||
|  | ||||
| def test_check_filter_and_regex_extract(client, live_server, measure_memory_usage): | ||||
| def test_check_filter_and_regex_extract(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     include_filters = ".changetext" | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -159,7 +160,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag | ||||
|     assert b'not at the start of the expression' not in res.data | ||||
|  | ||||
|     #  Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -198,7 +199,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_regex_error_handling(client, live_server, measure_memory_usage): | ||||
| def test_regex_error_handling(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|      | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from .util import set_original_response, live_server_setup, wait_for_notificatio | ||||
| from changedetectionio.model import App | ||||
|  | ||||
|  | ||||
| def set_response_without_filter(): | ||||
| def set_response_without_filter(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -20,12 +20,12 @@ def set_response_without_filter(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def set_response_with_filter(): | ||||
| def set_response_with_filter(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -37,11 +37,11 @@ def set_response_with_filter(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_server, measure_memory_usage): | ||||
| def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
| #  Filter knowingly doesn't exist, like someone setting up a known filter to see if some cinema tickets are on sale again | ||||
| #  And the page has that filter available | ||||
| #  Then I should get a notification | ||||
| @@ -50,7 +50,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
|     set_response_without_filter() | ||||
|     set_response_without_filter(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -86,7 +86,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se | ||||
|                                                    "Diff Full: {{diff_full}}\n" | ||||
|                                                    "Diff as Patch: {{diff_patch}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_format": 'Plain Text'} | ||||
|                               "notification_format": 'text'} | ||||
|  | ||||
|     notification_form_data.update({ | ||||
|         "url": test_url, | ||||
| @@ -105,20 +105,20 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert b"Updated watch." in res.data | ||||
|     wait_for_notification_endpoint_output() | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|  | ||||
|     # Shouldn't exist, shouldn't have fired | ||||
|     assert not os.path.isfile("test-datastore/notification.txt") | ||||
|     assert not os.path.isfile(os.path.join(datastore_path, "notification.txt")) | ||||
|     # Now the filter should exist | ||||
|     set_response_with_filter() | ||||
|     set_response_with_filter(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|  | ||||
|     wait_for_notification_endpoint_output() | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|  | ||||
|     assert os.path.isfile("test-datastore/notification.txt") | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         notification = f.read() | ||||
|  | ||||
|     assert 'Ticket now on sale' in notification | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from .util import set_original_response,  wait_for_all_checks, wait_for_notifica | ||||
| from ..notification import valid_notification_formats | ||||
|  | ||||
|  | ||||
| def set_response_with_filter(): | ||||
| def set_response_with_filter(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -17,14 +17,14 @@ def set_response_with_filter(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def run_filter_test(client, live_server, content_filter, app_notification_format): | ||||
| def run_filter_test(client, live_server, content_filter, app_notification_format, datastore_path): | ||||
|  | ||||
|     # Response WITHOUT the filter ID element | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     live_server.app.config['DATASTORE'].data['settings']['application']['notification_format'] = app_notification_format | ||||
|  | ||||
|     # Goto the edit page, add our ignore text | ||||
| @@ -38,10 +38,16 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|         url_for("ui.form_delete", uuid="all"), | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     if os.path.isfile("test-datastore/notification.txt"): | ||||
|         os.unlink("test-datastore/notification.txt") | ||||
|     notification_file = os.path.join(datastore_path, "notification.txt") | ||||
|     if os.path.isfile(notification_file): | ||||
|         os.unlink(notification_file) | ||||
|  | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|  | ||||
|     assert b'No website watches configured' not in res.data | ||||
|  | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| @@ -63,7 +69,7 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|                                        "Diff Full: {{diff_full}}\n" | ||||
|                                        "Diff as Patch: {{diff_patch}}\n" | ||||
|                                        ":-)", | ||||
|                   "notification_format": 'Plain Text', | ||||
|                   "notification_format": 'text', | ||||
|                   "fetch_backend": "html_requests", | ||||
|                   "filter_failure_notification_send": 'y', | ||||
|                   "time_between_check_use_default": "y", | ||||
| @@ -79,6 +85,7 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|         data=watch_data, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b"Updated watch." in res.data | ||||
|     wait_for_all_checks(client) | ||||
|     assert live_server.app.config['DATASTORE'].data['watching'][uuid]['consecutive_filter_failures'] == 0, "No filter = No filter failure" | ||||
| @@ -95,7 +102,7 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|     # It should have checked once so far and given this error (because we hit SAVE) | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     assert not os.path.isfile("test-datastore/notification.txt") | ||||
|     assert not os.path.isfile(notification_file) | ||||
|  | ||||
|     # Hitting [save] would have triggered a recheck, and we have a filter, so this would be ONE failure | ||||
|     assert live_server.app.config['DATASTORE'].data['watching'][uuid]['consecutive_filter_failures'] == 1, "Should have been checked once" | ||||
| @@ -110,20 +117,20 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|         wait_for_all_checks(client) | ||||
|         res = client.get(url_for("watchlist.index")) | ||||
|         assert b'Warning, no filters were found' in res.data | ||||
|         assert not os.path.isfile("test-datastore/notification.txt") | ||||
|         assert not os.path.isfile(notification_file) | ||||
|         time.sleep(1) | ||||
|          | ||||
|  | ||||
|     assert live_server.app.config['DATASTORE'].data['watching'][uuid]['consecutive_filter_failures'] == 5 | ||||
|  | ||||
|     time.sleep(2) | ||||
|     # One more check should trigger the _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT threshold | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     wait_for_notification_endpoint_output() | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|  | ||||
|     # Now it should exist and contain our "filter not found" alert | ||||
|     assert os.path.isfile("test-datastore/notification.txt") | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     assert os.path.isfile(notification_file) | ||||
|     with open(notification_file, 'r') as f: | ||||
|         notification = f.read() | ||||
|  | ||||
|     assert 'Your configured CSS/xPath filters' in notification | ||||
| @@ -146,19 +153,19 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|  | ||||
|     # Remove it and prove that it doesn't trigger when not expected | ||||
|     # It should register a change, but no 'filter not found' | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     set_response_with_filter() | ||||
|     os.unlink(notification_file) | ||||
|     set_response_with_filter(datastore_path) | ||||
|  | ||||
|     # Try several times, it should NOT have 'filter not found' | ||||
|     for i in range(0, ATTEMPT_THRESHOLD_SETTING + 2): | ||||
|         client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|         wait_for_all_checks(client) | ||||
|  | ||||
|     wait_for_notification_endpoint_output() | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|     # It should have sent a notification, but.. | ||||
|     assert os.path.isfile("test-datastore/notification.txt") | ||||
|     assert os.path.isfile(notification_file) | ||||
|     # but it should not contain the info about a failed filter (because there was none in this case) | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(notification_file, 'r') as f: | ||||
|         notification = f.read() | ||||
|     assert not 'CSS/xPath filter was not present in the page' in notification | ||||
|  | ||||
| @@ -170,22 +177,22 @@ def run_filter_test(client, live_server, content_filter, app_notification_format | ||||
|         url_for("ui.form_delete", uuid="all"), | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(notification_file) | ||||
|  | ||||
|  | ||||
| def test_check_include_filters_failure_notification(client, live_server, measure_memory_usage): | ||||
| def test_check_include_filters_failure_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #   #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('HTML Color')) | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('htmlcolor'), datastore_path=datastore_path) | ||||
|     # Check markup send conversion didnt affect plaintext preference | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('Plain Text')) | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('text'), datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_xpath_filter_failure_notification(client, live_server, measure_memory_usage): | ||||
| def test_check_xpath_filter_failure_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #   #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='//*[@id="nope-doesnt-exist"]', app_notification_format=valid_notification_formats.get('HTML Color')) | ||||
|     run_filter_test(client=client, live_server=live_server, content_filter='//*[@id="nope-doesnt-exist"]', app_notification_format=valid_notification_formats.get('htmlcolor'), datastore_path=datastore_path) | ||||
|  | ||||
| # Test that notification is never sent | ||||
|  | ||||
| def test_basic_markup_from_text(client, live_server, measure_memory_usage): | ||||
| def test_basic_markup_from_text(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Test the notification error templates convert to HTML if needed (link activate) | ||||
|     from ..notification.handler import markup_text_links_to_html | ||||
|     x = markup_text_links_to_html("hello https://google.com") | ||||
|   | ||||
| @@ -6,10 +6,10 @@ from .util import live_server_setup, wait_for_all_checks, extract_rss_token_from | ||||
| import os | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -20,11 +20,11 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -35,13 +35,13 @@ def set_modified_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def test_setup_group_tag(client, live_server, measure_memory_usage): | ||||
| def test_setup_group_tag(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add a tag with some config, import a tag and it should roughly work | ||||
|     res = client.post( | ||||
| @@ -116,7 +116,7 @@ def test_setup_group_tag(client, live_server, measure_memory_usage): | ||||
|     ) | ||||
|     assert b"1 Imported" in res.data | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     rss_token = extract_rss_token_from_UI(client) | ||||
| @@ -129,7 +129,7 @@ def test_setup_group_tag(client, live_server, measure_memory_usage): | ||||
|     assert b"first-imported=1" in res.data | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_tag_import_singular(client, live_server, measure_memory_usage): | ||||
| def test_tag_import_singular(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -148,7 +148,7 @@ def test_tag_import_singular(client, live_server, measure_memory_usage): | ||||
|     assert res.data.count(b'test-tag') == 1 | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_tag_add_in_ui(client, live_server, measure_memory_usage): | ||||
| def test_tag_add_in_ui(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
| # | ||||
|     res = client.post( | ||||
| @@ -164,9 +164,9 @@ def test_tag_add_in_ui(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_group_tag_notification(client, live_server, measure_memory_usage): | ||||
| def test_group_tag_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     res = client.post( | ||||
| @@ -195,7 +195,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage): | ||||
|                                                    "Diff as Patch: {{diff_patch}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_screenshot": True, | ||||
|                               "notification_format": 'Plain Text', | ||||
|                               "notification_format": 'text', | ||||
|                               "title": "test-tag"} | ||||
|  | ||||
|     res = client.post( | ||||
| @@ -207,16 +207,16 @@ def test_group_tag_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     time.sleep(3) | ||||
|  | ||||
|     assert os.path.isfile("test-datastore/notification.txt") | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     # Verify what was sent as a notification, this file should exist | ||||
|     with open("test-datastore/notification.txt", "r") as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), "r") as f: | ||||
|         notification_submission = f.read() | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     # Did we see the URL that had a change, in the notification? | ||||
|     # Diff was correctly executed | ||||
| @@ -231,7 +231,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage): | ||||
|     #@todo Test that each of multiple notifications with different settings | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_limit_tag_ui(client, live_server, measure_memory_usage): | ||||
| def test_limit_tag_ui(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     test_url = url_for('test_random_content_endpoint', _external=True) | ||||
|  | ||||
| @@ -269,7 +269,7 @@ def test_limit_tag_ui(client, live_server, measure_memory_usage): | ||||
|     res = client.get(url_for("tags.delete_all"), follow_redirects=True) | ||||
|     assert b'All tags deleted' in res.data | ||||
|  | ||||
| def test_clone_tag_on_import(client, live_server, measure_memory_usage): | ||||
| def test_clone_tag_on_import(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     res = client.post( | ||||
| @@ -294,7 +294,7 @@ def test_clone_tag_on_import(client, live_server, measure_memory_usage): | ||||
|     assert res.data.count(b'another-tag') == 3 | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usage): | ||||
| def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -324,7 +324,7 @@ def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usa | ||||
|     res = client.get(url_for("tags.delete_all"), follow_redirects=True) | ||||
|     assert b'All tags deleted' in res.data | ||||
|  | ||||
| def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measure_memory_usage): | ||||
| def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # Add a tag with some config, import a tag and it should roughly work | ||||
|     res = client.post( | ||||
| @@ -378,7 +378,7 @@ def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measu | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(d) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|   | ||||
| @@ -7,7 +7,7 @@ from flask import url_for | ||||
| from .util import wait_for_all_checks, delete_all_watches | ||||
| from urllib.parse import urlparse, parse_qs | ||||
|  | ||||
| def test_consistent_history(client, live_server, measure_memory_usage): | ||||
| def test_consistent_history(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     workers = int(os.getenv("FETCH_WORKERS", 10)) | ||||
|     r = range(1, 10+workers) | ||||
| @@ -80,9 +80,9 @@ def test_consistent_history(client, live_server, measure_memory_usage): | ||||
|         assert '"default"' not in f.read(), "'default' probably shouldnt be here, it came from when the 'default' Watch vars were accidently being saved" | ||||
|  | ||||
|  | ||||
| def test_check_text_history_view(client, live_server, measure_memory_usage): | ||||
| def test_check_text_history_view(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html>test-one</html>") | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -94,7 +94,7 @@ def test_check_text_history_view(client, live_server, measure_memory_usage): | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Set second version, Make a change | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html>test-two</html>") | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -105,7 +105,7 @@ def test_check_text_history_view(client, live_server, measure_memory_usage): | ||||
|     assert b'test-two' in res.data | ||||
|  | ||||
|     # Set third version, Make a change | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("<html>test-three</html>") | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|   | ||||
| @@ -5,8 +5,9 @@ from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
| from changedetectionio import html_tools | ||||
| from . util import  extract_UUID_from_client | ||||
| import os | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -19,13 +20,13 @@ def set_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def test_ignore(client, live_server, measure_memory_usage): | ||||
| def test_ignore(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path) | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -55,9 +56,9 @@ def test_ignore(client, live_server, measure_memory_usage): | ||||
|     assert b'csrftoken' in res.data | ||||
|  | ||||
|  | ||||
| def test_strip_ignore_lines(client, live_server, measure_memory_usage): | ||||
| def test_strip_ignore_lines(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path) | ||||
|  | ||||
|  | ||||
|     # Goto the settings page, add our ignore text | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| from changedetectionio import html_tools | ||||
| import os | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -31,7 +32,7 @@ def test_strip_text_func(): | ||||
|     stripped_content = html_tools.strip_ignore_text(test_content, ignore) | ||||
|     assert stripped_content == "Some initial text\n\nWhich is across multiple lines\n\n\n\nSo let's see what happens." | ||||
|  | ||||
| def set_original_ignore_response(ver_stamp="123"): | ||||
| def set_original_ignore_response(datastore_path, ver_stamp="123"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -44,11 +45,11 @@ def set_original_ignore_response(ver_stamp="123"): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_modified_original_ignore_response(ver_stamp="123"): | ||||
| def set_modified_original_ignore_response(datastore_path, ver_stamp="123"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      Some NEW nice initial text<br> | ||||
| @@ -63,12 +64,12 @@ def set_modified_original_ignore_response(ver_stamp="123"): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| # Is the same but includes ZZZZZ, 'ZZZZZ' is the last line in ignore_text | ||||
| def set_modified_ignore_response(ver_stamp="123"): | ||||
| def set_modified_ignore_response(datastore_path, ver_stamp="123"): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -82,17 +83,17 @@ def set_modified_ignore_response(ver_stamp="123"): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| # Ignore text now just removes it entirely, is a LOT more simpler code this way | ||||
|  | ||||
| def test_check_ignore_text_functionality(client, live_server, measure_memory_usage): | ||||
| def test_check_ignore_text_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # Use a mix of case in ZzZ to prove it works case-insensitive. | ||||
|     ignore_text = "XXXXX\r\nYYYYY\r\nzZzZZ\r\nnew ignore stuff" | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -130,7 +131,7 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa | ||||
|     assert b'/test-endpoint' in res.data | ||||
|  | ||||
|     #  Make a change | ||||
|     set_modified_ignore_response() | ||||
|     set_modified_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -145,7 +146,7 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa | ||||
|  | ||||
|  | ||||
|     # Just to be sure.. set a regular modified change.. | ||||
|     set_modified_original_ignore_response() | ||||
|     set_modified_original_ignore_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| @@ -162,10 +163,10 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # When adding some ignore text, it should not trigger a change, even if something else on that line changes | ||||
| def _run_test_global_ignore(client, as_source=False, extra_ignore=""): | ||||
| def _run_test_global_ignore(client, datastore_path, as_source=False, extra_ignore=""): | ||||
|     ignore_text = "XXXXX\r\nYYYYY\r\nZZZZZ\r\n"+extra_ignore | ||||
|  | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Goto the settings page, add our ignore text | ||||
|     res = client.post( | ||||
| @@ -222,7 +223,7 @@ def _run_test_global_ignore(client, as_source=False, extra_ignore=""): | ||||
|     # Make a change which includes the ignore text, it should be ignored and no 'change' triggered | ||||
|     # It adds text with "ZZZZzzzz" and "ZZZZ" is in the ignore list | ||||
|     # And tweaks the ver_stamp which should be picked up by global regex ignore | ||||
|     set_modified_ignore_response(ver_stamp=time.time()) | ||||
|     set_modified_ignore_response(ver_stamp=time.time(), datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -236,7 +237,7 @@ def _run_test_global_ignore(client, as_source=False, extra_ignore=""): | ||||
|     assert b'/test-endpoint' in res.data | ||||
|  | ||||
|     # Just to be sure.. set a regular modified change that will trigger it | ||||
|     set_modified_original_ignore_response() | ||||
|     set_modified_original_ignore_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -244,10 +245,10 @@ def _run_test_global_ignore(client, as_source=False, extra_ignore=""): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_global_ignore_text_functionality(client, live_server, measure_memory_usage): | ||||
| def test_check_global_ignore_text_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     _run_test_global_ignore(client, as_source=False) | ||||
|     _run_test_global_ignore(client, as_source=False, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_global_ignore_text_functionality_as_source(client, live_server, measure_memory_usage): | ||||
| def test_check_global_ignore_text_functionality_as_source(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     _run_test_global_ignore(client, as_source=True, extra_ignore='/\?v=\d/') | ||||
|     _run_test_global_ignore(client, as_source=True, extra_ignore='/\?v=\d/', datastore_path=datastore_path) | ||||
|   | ||||
| @@ -4,9 +4,10 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -17,13 +18,13 @@ def set_original_ignore_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| # Should be the same as set_original_ignore_response() but with a different | ||||
| # Should be the same as set_original_ignore_response(datastore_path=datastore_path) but with a different | ||||
| # link | ||||
| def set_modified_ignore_response(): | ||||
| def set_modified_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -34,10 +35,10 @@ def set_modified_ignore_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
| def test_render_anchor_tag_content_true(client, live_server, measure_memory_usage): | ||||
| def test_render_anchor_tag_content_true(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Testing that the link changes are detected when | ||||
|     render_anchor_tag_content setting is set to true""" | ||||
|     sleep_time_for_fetch_thread = 3 | ||||
| @@ -46,7 +47,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag | ||||
|     time.sleep(1) | ||||
|  | ||||
|     # set original html text | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Goto the settings page, choose to ignore links (dont select/send "application-render_anchor_tag_content") | ||||
|     res = client.post( | ||||
| @@ -72,7 +73,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|  | ||||
|     # set a new html text with a modified link | ||||
|     set_modified_ignore_response() | ||||
|     set_modified_ignore_response(datastore_path=datastore_path) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Trigger a check | ||||
|   | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -19,11 +20,11 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_some_changed_response(): | ||||
| def set_some_changed_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -34,17 +35,17 @@ def set_some_changed_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def test_normal_page_check_works_with_ignore_status_code(client, live_server, measure_memory_usage): | ||||
| def test_normal_page_check_works_with_ignore_status_code(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Goto the settings page, add our ignore text | ||||
|     res = client.post( | ||||
| @@ -65,7 +66,7 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server, me | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     set_some_changed_response() | ||||
|     set_some_changed_response(datastore_path=datastore_path) | ||||
|     wait_for_all_checks(client) | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -80,10 +81,10 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server, me | ||||
|  | ||||
|  | ||||
| # Tests the whole stack works with staus codes ignored | ||||
| def test_403_page_check_works_with_ignore_status_code(client, live_server, measure_memory_usage): | ||||
| def test_403_page_check_works_with_ignore_status_code(client, live_server, measure_memory_usage, datastore_path): | ||||
|     sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
| @@ -109,7 +110,7 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server, measu | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     #  Make a change | ||||
|     set_some_changed_response() | ||||
|     set_some_changed_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|   | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from . util import live_server_setup | ||||
| import os | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # Should be the same as set_original_ignore_response() but with a little more whitespacing | ||||
| def set_original_ignore_response_but_with_whitespace(): | ||||
| # Should be the same as set_original_ignore_response(datastore_path=datastore_path) but with a little more whitespacing | ||||
| def set_original_ignore_response_but_with_whitespace(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -26,11 +27,11 @@ def set_original_ignore_response_but_with_whitespace(): | ||||
|      </html> | ||||
|  | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -42,19 +43,19 @@ def set_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
|  | ||||
| # If there was only a change in the whitespacing, then we shouldnt have a change detected | ||||
| def test_check_ignore_whitespace(client, live_server, measure_memory_usage): | ||||
| def test_check_ignore_whitespace(client, live_server, measure_memory_usage, datastore_path): | ||||
|     sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(1) | ||||
|  | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Goto the settings page, add our ignore text | ||||
|     res = client.post( | ||||
| @@ -77,7 +78,7 @@ def test_check_ignore_whitespace(client, live_server, measure_memory_usage): | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|  | ||||
|     set_original_ignore_response_but_with_whitespace() | ||||
|     set_original_ignore_response_but_with_whitespace(datastore_path) | ||||
|     time.sleep(sleep_time_for_fetch_thread) | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|   | ||||
| @@ -8,10 +8,10 @@ from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_import(client, live_server, measure_memory_usage): | ||||
| def test_import(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Give the endpoint time to spin up | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| @@ -34,7 +34,7 @@ https://example.com tag1, other tag""" | ||||
|     res = client.get( url_for("watchlist.index")) | ||||
|     res = client.get( url_for("watchlist.index")) | ||||
|  | ||||
| def xtest_import_skip_url(client, live_server, measure_memory_usage): | ||||
| def xtest_import_skip_url(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
| @@ -57,7 +57,7 @@ def xtest_import_skip_url(client, live_server, measure_memory_usage): | ||||
|     # Clear flask alerts | ||||
|     res = client.get( url_for("watchlist.index")) | ||||
|  | ||||
| def test_import_distillio(client, live_server, measure_memory_usage): | ||||
| def test_import_distillio(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     distill_data=''' | ||||
| { | ||||
| @@ -123,7 +123,7 @@ def test_import_distillio(client, live_server, measure_memory_usage): | ||||
|     # Clear flask alerts | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|  | ||||
| def test_import_custom_xlsx(client, live_server, measure_memory_usage): | ||||
| def test_import_custom_xlsx(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test can upload a excel spreadsheet and the watches are created correctly""" | ||||
|  | ||||
|      | ||||
| @@ -171,7 +171,7 @@ def test_import_custom_xlsx(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_import_watchete_xlsx(client, live_server, measure_memory_usage): | ||||
| def test_import_watchete_xlsx(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test can upload a excel spreadsheet and the watches are created correctly""" | ||||
|  | ||||
|      | ||||
|   | ||||
| @@ -7,11 +7,11 @@ from .util import live_server_setup, wait_for_all_checks | ||||
| from ..jinja2_custom import render | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    # #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| # If there was only a change in the whitespacing, then we shouldnt have a change detected | ||||
| def test_jinja2_in_url_query(client, live_server, measure_memory_usage): | ||||
| def test_jinja2_in_url_query(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -36,7 +36,7 @@ def test_jinja2_in_url_query(client, live_server, measure_memory_usage): | ||||
|     assert b'date=2' in res.data | ||||
|  | ||||
| # Test for issue #1493 - jinja2-time offset functionality | ||||
| def test_jinja2_time_offset_in_url_query(client, live_server, measure_memory_usage): | ||||
| def test_jinja2_time_offset_in_url_query(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """Test that jinja2 time offset expressions work in watch URLs (issue #1493).""" | ||||
|  | ||||
|     # Add our URL to the import page with time offset expression | ||||
| @@ -64,29 +64,21 @@ def test_jinja2_time_offset_in_url_query(client, live_server, measure_memory_usa | ||||
|     # Should not have template error | ||||
|     assert b'Invalid template' not in res.data | ||||
|  | ||||
| # https://techtonics.medium.com/secure-templating-with-jinja2-understanding-ssti-and-jinja2-sandbox-environment-b956edd60456 | ||||
| def test_jinja2_security_url_query(client, live_server, measure_memory_usage): | ||||
|      | ||||
|  | ||||
| # https://techtonics.medium.com/secure-templating-with-jinja2-understanding-ssti-and-jinja2-sandbox-environment-b956edd60456 | ||||
| def test_jinja2_security_url_query(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_return_query', _external=True) | ||||
|  | ||||
|     # because url_for() will URL-encode the var, but we dont here | ||||
|     full_url = "{}?{}".format(test_url, | ||||
|                               "date={{ ''.__class__.__mro__[1].__subclasses__()}}", ) | ||||
|     full_url = test_url + "?date={{ ''.__class__.__mro__[1].__subclasses__()}}" | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": full_url, "tags": "test"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert b"Watch added" in res.data | ||||
|     wait_for_all_checks(client) | ||||
|     assert b"Watch added" not in res.data | ||||
|  | ||||
|     # It should report nothing found (no new 'has-unread-changes' class) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|     assert b'is invalid and cannot be used' in res.data | ||||
|     # Some of the spewed output from the subclasses | ||||
|     assert b'dict_values' not in res.data | ||||
|  | ||||
| def test_timezone(mocker): | ||||
|     """Verify that timezone is parsed.""" | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from flask import url_for | ||||
| from markupsafe import escape | ||||
| from . util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| import pytest | ||||
| import os | ||||
| jq_support = True | ||||
|  | ||||
| try: | ||||
| @@ -92,7 +93,7 @@ def test_unittest_inline_extract_body(): | ||||
|     text = html_tools.extract_json_as_string(content, "json:$.testKey") | ||||
|     assert text == '42' | ||||
|  | ||||
| def set_original_ext_response(): | ||||
| def set_original_ext_response(datastore_path): | ||||
|     data = """ | ||||
|         [ | ||||
|         { | ||||
| @@ -109,11 +110,11 @@ def set_original_ext_response(): | ||||
|     ] | ||||
|         """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|     return None | ||||
|  | ||||
| def set_modified_ext_response(): | ||||
| def set_modified_ext_response(datastore_path): | ||||
|     # This should get reformatted | ||||
|     data = """ [ { "isPriceLowered": false,  "status": "Sold",  "statusOrig": "sold" }, { | ||||
|         "_id": "5e7b3e1fb3262d306323ff1e", | ||||
| @@ -124,11 +125,11 @@ def set_modified_ext_response(): | ||||
| ] | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|     return None | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """ | ||||
|     { | ||||
|       "employees": [ | ||||
| @@ -149,12 +150,12 @@ def set_original_response(): | ||||
|       "available": true | ||||
|     } | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def set_json_response_with_html(): | ||||
| def set_json_response_with_html(datastore_path): | ||||
|     test_return_data = """ | ||||
|     { | ||||
|       "test": [ | ||||
| @@ -164,11 +165,11 @@ def set_json_response_with_html(): | ||||
|       ] | ||||
|     } | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def set_modified_response(): | ||||
| def set_modified_response(datastore_path): | ||||
|     test_return_data = """ | ||||
|     { | ||||
|       "employees": [ | ||||
| @@ -190,15 +191,15 @@ def set_modified_response(): | ||||
|     } | ||||
|         """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|     return None | ||||
|  | ||||
| def test_check_json_without_filter(client, live_server, measure_memory_usage): | ||||
| def test_check_json_without_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Request a JSON document from a application/json source containing HTML | ||||
|     # and be sure it doesn't get chewed up by instriptis | ||||
|     set_json_response_with_html() | ||||
|     set_json_response_with_html(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', content_type="application/json", _external=True) | ||||
| @@ -219,8 +220,8 @@ def test_check_json_without_filter(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def check_json_filter(json_filter, client, live_server): | ||||
|     set_original_response() | ||||
| def check_json_filter(json_filter, client, live_server, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -240,7 +241,7 @@ def check_json_filter(json_filter, client, live_server): | ||||
|     # Give the thread time to pick it up | ||||
|     wait_for_all_checks(client) | ||||
|     #  Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -260,19 +261,19 @@ def check_json_filter(json_filter, client, live_server): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_jsonpath_filter(client, live_server, measure_memory_usage): | ||||
|     check_json_filter('json:boss.name', client, live_server) | ||||
| def test_check_jsonpath_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     check_json_filter('json:boss.name', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jq_filter(client, live_server, measure_memory_usage): | ||||
| def test_check_jq_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_filter('jq:.boss.name', client, live_server) | ||||
|         check_json_filter('jq:.boss.name', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jqraw_filter(client, live_server, measure_memory_usage): | ||||
| def test_check_jqraw_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_filter('jqraw:.boss.name', client, live_server) | ||||
|         check_json_filter('jqraw:.boss.name', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def check_json_filter_bool_val(json_filter, client, live_server): | ||||
|     set_original_response() | ||||
| def check_json_filter_bool_val(json_filter, client, live_server, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', content_type="application/json", _external=True) | ||||
|  | ||||
| @@ -281,7 +282,7 @@ def check_json_filter_bool_val(json_filter, client, live_server): | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     #  Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -294,24 +295,24 @@ def check_json_filter_bool_val(json_filter, client, live_server): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_jsonpath_filter_bool_val(client, live_server, measure_memory_usage): | ||||
|     check_json_filter_bool_val("json:$['available']", client, live_server) | ||||
| def test_check_jsonpath_filter_bool_val(client, live_server, measure_memory_usage, datastore_path): | ||||
|     check_json_filter_bool_val("json:$['available']", client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jq_filter_bool_val(client, live_server, measure_memory_usage): | ||||
| def test_check_jq_filter_bool_val(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_filter_bool_val("jq:.available", client, live_server) | ||||
|         check_json_filter_bool_val("jq:.available", client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jqraw_filter_bool_val(client, live_server, measure_memory_usage): | ||||
| def test_check_jqraw_filter_bool_val(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_filter_bool_val("jq:.available", client, live_server) | ||||
|         check_json_filter_bool_val("jq:.available", client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| # Re #265 - Extended JSON selector test | ||||
| # Stuff to consider here | ||||
| # - Selector should be allowed to return empty when it doesnt match (people might wait for some condition) | ||||
| # - The 'diff' tab could show the old and new content | ||||
| # - Form should let us enter a selector that doesnt (yet) match anything | ||||
| def check_json_ext_filter(json_filter, client, live_server): | ||||
|     set_original_ext_response() | ||||
| def check_json_ext_filter(json_filter, client, live_server, datastore_path): | ||||
|     set_original_ext_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', content_type="application/json", _external=True) | ||||
| @@ -343,7 +344,7 @@ def check_json_ext_filter(json_filter, client, live_server): | ||||
|     # Give the thread time to pick it up | ||||
|     wait_for_all_checks(client) | ||||
|     #  Make a change | ||||
|     set_modified_ext_response() | ||||
|     set_modified_ext_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -376,10 +377,10 @@ def check_json_ext_filter(json_filter, client, live_server): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_ignore_json_order(client, live_server, measure_memory_usage): | ||||
| def test_ignore_json_order(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # A change in order shouldn't trigger a notification | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write('{"hello" : 123, "world": 123}') | ||||
|  | ||||
|  | ||||
| @@ -390,7 +391,7 @@ def test_ignore_json_order(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write('{"world" : 123, "hello": 123}') | ||||
|  | ||||
|     # Trigger a check | ||||
| @@ -401,7 +402,7 @@ def test_ignore_json_order(client, live_server, measure_memory_usage): | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # Just to be sure it still works | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write('{"world" : 123, "hello": 124}') | ||||
|  | ||||
|     # Trigger a check | ||||
| @@ -413,10 +414,10 @@ def test_ignore_json_order(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_correct_header_detect(client, live_server, measure_memory_usage): | ||||
| def test_correct_header_detect(client, live_server, measure_memory_usage, datastore_path): | ||||
|     # Like in https://github.com/dgtlmoon/changedetection.io/pull/1593 | ||||
|     # Specify extra html that JSON is sometimes wrapped in - when using SockpuppetBrowser / Puppeteer / Playwrightetc | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write('<html><body>{ "world": 123, "hello" : 123}') | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -450,18 +451,18 @@ def test_correct_header_detect(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_check_jsonpath_ext_filter(client, live_server, measure_memory_usage): | ||||
|     check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server) | ||||
| def test_check_jsonpath_ext_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jq_ext_filter(client, live_server, measure_memory_usage): | ||||
| def test_check_jq_ext_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server) | ||||
|         check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_check_jqraw_ext_filter(client, live_server, measure_memory_usage): | ||||
| def test_check_jqraw_ext_filter(client, live_server, measure_memory_usage, datastore_path): | ||||
|     if jq_support: | ||||
|         check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server) | ||||
|         check_json_ext_filter('jq:.[] | select(.status | contains("Sold"))', client, live_server, datastore_path=datastore_path) | ||||
|  | ||||
| def test_jsonpath_BOM_utf8(client, live_server, measure_memory_usage): | ||||
| def test_jsonpath_BOM_utf8(client, live_server, measure_memory_usage, datastore_path): | ||||
|     from .. import html_tools | ||||
|  | ||||
|     # JSON string with BOM and correct double-quoted keys | ||||
|   | ||||
| @@ -2,9 +2,10 @@ | ||||
|  | ||||
| from flask import url_for | ||||
| from changedetectionio.tests.util import live_server_setup, wait_for_all_checks, extract_UUID_from_client, delete_all_watches | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_response(): | ||||
| def set_response(datastore_path): | ||||
|  | ||||
|     data = """<html> | ||||
|        <body>Awesome, you made it<br> | ||||
| @@ -15,12 +16,12 @@ something to trigger<br> | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(data) | ||||
|  | ||||
| def test_content_filter_live_preview(client, live_server, measure_memory_usage): | ||||
| def test_content_filter_live_preview(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_response() | ||||
|     set_response(datastore_path=datastore_path) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|   | ||||
| @@ -3,9 +3,10 @@ | ||||
| from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| import time | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_nonrenderable_response(): | ||||
| def set_nonrenderable_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|     <head><title>modified head title</title></head> | ||||
|     <!-- like when some angular app was broken and doesnt render or whatever --> | ||||
| @@ -13,20 +14,20 @@ def set_nonrenderable_response(): | ||||
|      </body> | ||||
|      </html> | ||||
|     """ | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     time.sleep(1) | ||||
|  | ||||
|     return None | ||||
|  | ||||
| def set_zero_byte_response(): | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
| def set_zero_byte_response(datastore_path): | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write("") | ||||
|     time.sleep(1) | ||||
|     return None | ||||
|  | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -55,7 +56,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|     ) | ||||
|  | ||||
|     # this should not trigger a change, because no good text could be converted from the HTML | ||||
|     set_nonrenderable_response() | ||||
|     set_nonrenderable_response(datastore_path) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|  | ||||
| @@ -84,7 +85,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|               'application-fetch_backend': "html_requests"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -100,7 +101,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure | ||||
|  | ||||
|  | ||||
|     # A totally zero byte (#2528) response should also not trigger an error | ||||
|     set_zero_byte_response() | ||||
|     set_zero_byte_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     # 2877 | ||||
|   | ||||
| @@ -13,17 +13,17 @@ import base64 | ||||
| from changedetectionio.notification import ( | ||||
|     default_notification_body, | ||||
|     default_notification_format, | ||||
|     default_notification_title, | ||||
|     valid_notification_formats, | ||||
|     default_notification_title, valid_notification_formats | ||||
| ) | ||||
| from ..diff import HTML_CHANGED_STYLE | ||||
| from ..model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH | ||||
|  | ||||
|  | ||||
| # Hard to just add more live server URLs when one test is already running (I think) | ||||
| # So we add our test here (was in a different file) | ||||
| def test_check_notification(client, live_server, measure_memory_usage): | ||||
| def test_check_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Re 360 - new install should have defaults set | ||||
|     res = client.get(url_for("settings.settings_page")) | ||||
| @@ -47,6 +47,14 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     assert b"Settings updated." in res.data | ||||
|  | ||||
|     res = client.get(url_for("settings.settings_page")) | ||||
|     for k,v in valid_notification_formats.items(): | ||||
|         if k == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: | ||||
|             continue | ||||
|         assert f'value="{k}"'.encode() in res.data # Should be by key NOT value | ||||
|         assert f'value="{v}"'.encode() not in res.data # Should be by key NOT value | ||||
|  | ||||
|  | ||||
|     # When test mode is in BASE_URL env mode, we should see this already configured | ||||
|     env_base_url = os.getenv('BASE_URL', '').strip() | ||||
|     if len(env_base_url): | ||||
| @@ -75,8 +83,7 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     uuid = next(iter(live_server.app.config['DATASTORE'].data['watching'])) | ||||
|     datastore = 'test-datastore' | ||||
|     with open(os.path.join(datastore, str(uuid), 'last-screenshot.png'), 'wb') as f: | ||||
|     with open(os.path.join(datastore_path, str(uuid), 'last-screenshot.png'), 'wb') as f: | ||||
|         f.write(base64.b64decode(testimage_png)) | ||||
|  | ||||
|     # Goto the edit page, add our ignore text | ||||
| @@ -101,7 +108,7 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|                                                    "Diff as Patch: {{diff_patch}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_screenshot": True, | ||||
|                               "notification_format": 'Plain Text'} | ||||
|                               "notification_format": 'text'} | ||||
|  | ||||
|     notification_form_data.update({ | ||||
|         "url": test_url, | ||||
| @@ -130,7 +137,7 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     ## Now recheck, and it should have sent the notification | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -143,9 +150,9 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     # Verify what was sent as a notification, this file should exist | ||||
|     with open("test-datastore/notification.txt", "r") as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), "r") as f: | ||||
|         notification_submission = f.read() | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     # Did we see the URL that had a change, in the notification? | ||||
|     # Diff was correctly executed | ||||
| @@ -189,18 +196,18 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     # This should insert the {current_snapshot} | ||||
|     set_more_modified_response() | ||||
|     set_more_modified_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     time.sleep(3) | ||||
|     # Verify what was sent as a notification, this file should exist | ||||
|     with open("test-datastore/notification.txt", "r") as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), "r") as f: | ||||
|         notification_submission = f.read() | ||||
|     assert "Ohh yeah awesome" in notification_submission | ||||
|  | ||||
|  | ||||
|     # Prove that "content constantly being marked as Changed with no Updating causes notification" is not a thing | ||||
|     # https://github.com/dgtlmoon/changedetection.io/discussions/192 | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -209,13 +216,13 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|     wait_for_all_checks(client) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     assert os.path.exists("test-datastore/notification.txt") == False | ||||
|     assert os.path.exists(os.path.join(datastore_path, "notification.txt")) == False | ||||
|  | ||||
|     res = client.get(url_for("settings.notification_logs")) | ||||
|     # be sure we see it in the output log | ||||
|     assert b'New ChangeDetection.io Notification - ' + test_url.encode('utf-8') in res.data | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_edit.edit_page", uuid="first"), | ||||
|         data={ | ||||
| @@ -235,7 +242,7 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|     time.sleep(2) | ||||
|  | ||||
|     # Verify what was sent as a notification, this file should exist | ||||
|     with open("test-datastore/notification.txt", "r") as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), "r") as f: | ||||
|         notification_submission = f.read() | ||||
|     assert "fallback-title" in notification_submission | ||||
|     assert "fallback-body" in notification_submission | ||||
| @@ -246,7 +253,7 @@ def test_check_notification(client, live_server, measure_memory_usage): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
| def test_notification_validation(client, live_server, measure_memory_usage): | ||||
| def test_notification_validation(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     time.sleep(1) | ||||
|  | ||||
| @@ -267,7 +274,7 @@ def test_notification_validation(client, live_server, measure_memory_usage): | ||||
| #        data={"notification_urls": 'json://localhost/foobar', | ||||
| #              "notification_title": "", | ||||
| #              "notification_body": "", | ||||
| #              "notification_format": 'Plain Text', | ||||
| #              "notification_format": 'text', | ||||
| #              "url": test_url, | ||||
| #              "tag": "my tag", | ||||
| #              "title": "my title", | ||||
| @@ -284,7 +291,7 @@ def test_notification_validation(client, live_server, measure_memory_usage): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_notification_urls_jinja2_apprise_integration(client, live_server, measure_memory_usage): | ||||
| def test_notification_urls_jinja2_apprise_integration(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     # | ||||
|     # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#header-manipulation | ||||
| @@ -306,14 +313,14 @@ def test_notification_urls_jinja2_apprise_integration(client, live_server, measu | ||||
|     assert b'Settings updated' in res.data | ||||
|  | ||||
|  | ||||
| def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_memory_usage): | ||||
| def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     # test_endpoint - that sends the contents of a file | ||||
|     # test_notification_endpoint - that takes a POST and writes it to file (test-datastore/notification.txt) | ||||
|  | ||||
|     # CUSTOM JSON BODY CHECK for POST:// | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#header-manipulation | ||||
|     test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?status_code=204&watch_uuid={{ watch_uuid }}&xxx={{ watch_url }}&now={% now 'Europe/London', '%Y-%m-%d' %}&+custom-header=123&+second=hello+world%20%22space%22" | ||||
|  | ||||
| @@ -344,7 +351,7 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me | ||||
|     watch_uuid = next(iter(live_server.app.config['DATASTORE'].data['watching'])) | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
| @@ -356,7 +363,7 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|     assert b'notification-error' not in res.data | ||||
|  | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         x = f.read() | ||||
|         j = json.loads(x) | ||||
|         assert j['url'].startswith('http://localhost') | ||||
| @@ -365,8 +372,8 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me | ||||
|  | ||||
|  | ||||
|     # URL check, this will always be converted to lowercase | ||||
|     assert os.path.isfile("test-datastore/notification-url.txt") | ||||
|     with open("test-datastore/notification-url.txt", 'r') as f: | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification-url.txt")) | ||||
|     with open(os.path.join(datastore_path, "notification-url.txt"), 'r') as f: | ||||
|         notification_url = f.read() | ||||
|         assert 'xxx=http' in notification_url | ||||
|         # apprise style headers should be stripped | ||||
| @@ -377,18 +384,18 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me | ||||
|         assert f'watch_uuid={watch_uuid}' in notification_url | ||||
|  | ||||
|  | ||||
|     with open("test-datastore/notification-headers.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification-headers.txt"), 'r') as f: | ||||
|         notification_headers = f.read() | ||||
|         assert 'custom-header: 123' in notification_headers.lower() | ||||
|         assert 'second: hello world "space"' in notification_headers.lower() | ||||
|  | ||||
|  | ||||
|     # Should always be automatically detected as JSON content type even when we set it as 'Plain Text' (default) | ||||
|     assert os.path.isfile("test-datastore/notification-content-type.txt") | ||||
|     with open("test-datastore/notification-content-type.txt", 'r') as f: | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification-content-type.txt")) | ||||
|     with open(os.path.join(datastore_path, "notification-content-type.txt"), 'r') as f: | ||||
|         assert 'application/json' in f.read() | ||||
|  | ||||
|     os.unlink("test-datastore/notification-url.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification-url.txt")) | ||||
|  | ||||
|     client.get( | ||||
|         url_for("ui.form_delete", uuid="all"), | ||||
| @@ -397,12 +404,12 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me | ||||
|  | ||||
|  | ||||
| #2510 | ||||
| def test_global_send_test_notification(client, live_server, measure_memory_usage): | ||||
| def test_global_send_test_notification(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|      | ||||
|     set_original_response() | ||||
|     if os.path.isfile("test-datastore/notification.txt"): | ||||
|         os.unlink("test-datastore/notification.txt") \ | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     if os.path.isfile(os.path.join(datastore_path, "notification.txt")): | ||||
|         os.unlink(os.path.join(datastore_path, "notification.txt")) \ | ||||
|  | ||||
|     # 1995 UTF-8 content should be encoded | ||||
|     test_body = 'change detection is cool 网站监测 内容更新了' | ||||
| @@ -443,11 +450,11 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage | ||||
|     assert res.status_code != 400 | ||||
|     assert res.status_code != 500 | ||||
|  | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         x = f.read() | ||||
|         assert test_body in x | ||||
|  | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     ######### Test group/tag settings | ||||
|     res = client.post( | ||||
| @@ -462,11 +469,30 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage | ||||
|     # Give apprise time to fire | ||||
|     time.sleep(4) | ||||
|  | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         x = f.read() | ||||
|         # Should come from notification.py default handler when there is no notification body to pull from | ||||
|         assert 'change detection is cool 网站监测 内容更新了' in x | ||||
|  | ||||
|     ## Check that 'test' catches errors | ||||
|     test_notification_url = 'post://akjsdfkjasdkfjasdkfjasdkjfas232323/should-error' | ||||
|  | ||||
|     ######### Test global/system settings | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_notification.ajax_callback_send_notification_test")+"?mode=global-settings", | ||||
|         data={"notification_urls": test_notification_url}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert res.status_code == 400 | ||||
|     assert ( | ||||
|         b"No address found" in res.data or | ||||
|         b"Name or service not known" in res.data or | ||||
|         b"nodename nor servname provided" in res.data or | ||||
|         b"Temporary failure in name resolution" in res.data or | ||||
|         b"Failed to establish a new connection" in res.data or | ||||
|         b"Connection error occurred" in res.data | ||||
|     ) | ||||
|      | ||||
|     client.get( | ||||
|         url_for("ui.form_delete", uuid="all"), | ||||
|         follow_redirects=True | ||||
| @@ -483,12 +509,13 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage | ||||
|     assert b"Error: You must have atleast one watch configured for 'test notification' to work" in res.data | ||||
|  | ||||
|  | ||||
| def _test_color_notifications(client, notification_body_token): | ||||
|  | ||||
|     set_original_response() | ||||
| def _test_color_notifications(client, notification_body_token, datastore_path): | ||||
|  | ||||
|     if os.path.isfile("test-datastore/notification.txt"): | ||||
|         os.unlink("test-datastore/notification.txt") | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     if os.path.isfile(os.path.join(datastore_path, "notification.txt")): | ||||
|         os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|  | ||||
|     test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}&+custom-header=123" | ||||
| @@ -501,7 +528,7 @@ def _test_color_notifications(client, notification_body_token): | ||||
|             "application-fetch_backend": "html_requests", | ||||
|             "application-minutes_between_check": 180, | ||||
|             "application-notification_body": notification_body_token, | ||||
|             "application-notification_format": "HTML Color", | ||||
|             "application-notification_format": "htmlcolor", | ||||
|             "application-notification_urls": test_notification_url, | ||||
|             "application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", | ||||
|         }, | ||||
| @@ -520,7 +547,7 @@ def _test_color_notifications(client, notification_body_token): | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -529,7 +556,7 @@ def _test_color_notifications(client, notification_body_token): | ||||
|     wait_for_all_checks(client) | ||||
|     time.sleep(3) | ||||
|  | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         x = f.read() | ||||
|         s =  f'<span style="{HTML_CHANGED_STYLE}" role="note" aria-label="Changed text" title="Changed text">Which is across multiple lines' | ||||
|         assert s in x | ||||
| @@ -541,6 +568,7 @@ def _test_color_notifications(client, notification_body_token): | ||||
|     ) | ||||
|  | ||||
| # Just checks the format of the colour notifications was correct | ||||
| def test_html_color_notifications(client, live_server, measure_memory_usage): | ||||
|     _test_color_notifications(client, '{{diff}}') | ||||
|     _test_color_notifications(client, '{{diff_full}}') | ||||
| def test_html_color_notifications(client, live_server, measure_memory_usage, datastore_path): | ||||
|     _test_color_notifications(client, '{{diff}}',datastore_path=datastore_path) | ||||
|     _test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path) | ||||
|  | ||||
|   | ||||
| @@ -4,10 +4,10 @@ from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks | ||||
| import logging | ||||
|  | ||||
| def test_check_notification_error_handling(client, live_server, measure_memory_usage): | ||||
| def test_check_notification_error_handling(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # 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) | ||||
| @@ -19,7 +19,7 @@ def test_check_notification_error_handling(client, live_server, measure_memory_u | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     working_notification_url = url_for('test_notification_endpoint', _external=True).replace('http', 'json') | ||||
|     broken_notification_url = "jsons://broken-url-xxxxxxxx123/test" | ||||
| @@ -30,7 +30,7 @@ def test_check_notification_error_handling(client, live_server, measure_memory_u | ||||
|         data={"notification_urls": f"{broken_notification_url}\r\n{working_notification_url}", | ||||
|               "notification_title": "xxx", | ||||
|               "notification_body": "xxxxx", | ||||
|               "notification_format": 'Plain Text', | ||||
|               "notification_format": 'text', | ||||
|               "url": test_url, | ||||
|               "tags": "", | ||||
|               "title": "", | ||||
| @@ -73,9 +73,9 @@ def test_check_notification_error_handling(client, live_server, measure_memory_u | ||||
|     assert found_name_resolution_error | ||||
|  | ||||
|     # And the working one, which is after the 'broken' one should still have fired | ||||
|     with open("test-datastore/notification.txt", "r") as f: | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), "r") as f: | ||||
|         notification_submission = f.read() | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|     assert 'xxxxx' in notification_submission | ||||
|  | ||||
|     client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) | ||||
|   | ||||
| @@ -3,9 +3,10 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      <span>The price is</span><span>$<!-- -->90<!-- -->.<!-- -->74</span> | ||||
| @@ -14,12 +15,12 @@ def set_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def test_obfuscations(client, live_server, measure_memory_usage): | ||||
|     set_original_ignore_response() | ||||
| def test_obfuscations(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_ignore_response(datastore_path) | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     time.sleep(1) | ||||
|     # Add our URL to the import page | ||||
|   | ||||
| @@ -3,15 +3,16 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
| # `subtractive_selectors` should still work in `source:` type requests | ||||
| def test_fetch_pdf(client, live_server, measure_memory_usage): | ||||
| def test_fetch_pdf(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import shutil | ||||
|     import os | ||||
|  | ||||
|     shutil.copy("tests/test.pdf", "test-datastore/endpoint-test.pdf") | ||||
|     first_version_size = os.path.getsize("test-datastore/endpoint-test.pdf") | ||||
|     shutil.copy("tests/test.pdf", os.path.join(datastore_path, "endpoint-test.pdf")) | ||||
|     first_version_size = os.path.getsize(os.path.join(datastore_path, "endpoint-test.pdf")) | ||||
|  | ||||
|     test_url = url_for('test_pdf_endpoint', _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
| @@ -35,14 +36,14 @@ def test_fetch_pdf(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     # So we know if the file changes in other ways | ||||
|     import hashlib | ||||
|     original_md5 = hashlib.md5(open("test-datastore/endpoint-test.pdf", 'rb').read()).hexdigest().upper() | ||||
|     original_md5 = hashlib.md5(open(os.path.join(datastore_path, "endpoint-test.pdf"), 'rb').read()).hexdigest().upper() | ||||
|     # We should have one | ||||
|     assert len(original_md5) >0 | ||||
|     # And it's going to be in the document | ||||
|     assert f'Document checksum - {original_md5}' in snapshot_contents | ||||
|  | ||||
|     shutil.copy("tests/test2.pdf", "test-datastore/endpoint-test.pdf") | ||||
|     changed_md5 = hashlib.md5(open("test-datastore/endpoint-test.pdf", 'rb').read()).hexdigest().upper() | ||||
|     shutil.copy("tests/test2.pdf", os.path.join(datastore_path, "endpoint-test.pdf")) | ||||
|     changed_md5 = hashlib.md5(open(os.path.join(datastore_path, "endpoint-test.pdf"), 'rb').read()).hexdigest().upper() | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     assert b'Queued 1 watch for rechecking.' in res.data | ||||
|  | ||||
| @@ -76,9 +77,9 @@ def test_fetch_pdf(client, live_server, measure_memory_usage): | ||||
|     # new snapshot was also OK, no HTML | ||||
|     snapshot_contents = watch.get_history_snapshot(dates[1]) | ||||
|     assert 'html' not in snapshot_contents.lower() | ||||
|     assert f'Original file size - {os.path.getsize("test-datastore/endpoint-test.pdf")}' in snapshot_contents | ||||
|     assert f'Original file size - {os.path.getsize(os.path.join(datastore_path, "endpoint-test.pdf"))}' in snapshot_contents | ||||
|     assert f'here is a change' in snapshot_contents | ||||
|     assert os.path.getsize("test-datastore/endpoint-test.pdf") != first_version_size # And the disk change worked | ||||
|     assert os.path.getsize(os.path.join(datastore_path, "endpoint-test.pdf")) != first_version_size # And the disk change worked | ||||
|  | ||||
|  | ||||
|      | ||||
| @@ -3,12 +3,13 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
| # `subtractive_selectors` should still work in `source:` type requests | ||||
| def test_fetch_pdf(client, live_server, measure_memory_usage): | ||||
| def test_fetch_pdf(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import shutil | ||||
|     shutil.copy("tests/test.pdf", "test-datastore/endpoint-test.pdf") | ||||
|     shutil.copy("tests/test.pdf", os.path.join(datastore_path, "endpoint-test.pdf")) | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|     test_url = url_for('test_pdf_endpoint', _external=True) | ||||
| @@ -29,14 +30,14 @@ def test_fetch_pdf(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     # So we know if the file changes in other ways | ||||
|     import hashlib | ||||
|     original_md5 = hashlib.md5(open("test-datastore/endpoint-test.pdf", 'rb').read()).hexdigest().upper() | ||||
|     original_md5 = hashlib.md5(open(os.path.join(datastore_path, "endpoint-test.pdf"), 'rb').read()).hexdigest().upper() | ||||
|     # We should have one | ||||
|     assert len(original_md5) > 0 | ||||
|     # And it's going to be in the document | ||||
|     assert b'Document checksum - ' + bytes(str(original_md5).encode('utf-8')) in res.data | ||||
|  | ||||
|     shutil.copy("tests/test2.pdf", "test-datastore/endpoint-test.pdf") | ||||
|     changed_md5 = hashlib.md5(open("test-datastore/endpoint-test.pdf", 'rb').read()).hexdigest().upper() | ||||
|     shutil.copy("tests/test2.pdf", os.path.join(datastore_path, "endpoint-test.pdf")) | ||||
|     changed_md5 = hashlib.md5(open(os.path.join(datastore_path, "endpoint-test.pdf"), 'rb').read()).hexdigest().upper() | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     assert b'Queued 1 watch for rechecking.' in res.data | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from . util import set_original_response, set_modified_response, live_server_set | ||||
|  | ||||
| # Hard to just add more live server URLs when one test is already running (I think) | ||||
| # So we add our test here (was in a different file) | ||||
| def test_headers_in_request(client, live_server, measure_memory_usage): | ||||
| def test_headers_in_request(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #ve_server_setup(live_server) | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_headers', _external=True) | ||||
| @@ -76,7 +76,8 @@ def test_headers_in_request(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_body_in_request(client, live_server, measure_memory_usage): | ||||
| def test_body_in_request(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import os | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_body', _external=True) | ||||
| @@ -141,7 +142,7 @@ def test_body_in_request(client, live_server, measure_memory_usage): | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     watches_with_body = 0 | ||||
|     with open('test-datastore/url-watches.json') as f: | ||||
|     with open(os.path.join(datastore_path, 'url-watches.json')) as f: | ||||
|         app_struct = json.load(f) | ||||
|         for uuid in app_struct['watching']: | ||||
|             if app_struct['watching'][uuid]['body']==body_value: | ||||
| @@ -165,7 +166,8 @@ def test_body_in_request(client, live_server, measure_memory_usage): | ||||
|     assert b"Body must be empty when Request Method is set to GET" in res.data | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_method_in_request(client, live_server, measure_memory_usage): | ||||
| def test_method_in_request(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import os | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_method', _external=True) | ||||
|     if os.getenv('PLAYWRIGHT_DRIVER_URL'): | ||||
| @@ -223,7 +225,7 @@ def test_method_in_request(client, live_server, measure_memory_usage): | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     watches_with_method = 0 | ||||
|     with open('test-datastore/url-watches.json') as f: | ||||
|     with open(os.path.join(datastore_path, 'url-watches.json')) as f: | ||||
|         app_struct = json.load(f) | ||||
|         for uuid in app_struct['watching']: | ||||
|             if app_struct['watching'][uuid]['method'] == 'PATCH': | ||||
| @@ -235,7 +237,7 @@ def test_method_in_request(client, live_server, measure_memory_usage): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # Re #2408 - user-agent override test, also should handle case-insensitive header deduplication | ||||
| def test_ua_global_override(client, live_server, measure_memory_usage): | ||||
| def test_ua_global_override(client, live_server, measure_memory_usage, datastore_path): | ||||
|     ##  live_server_setup(live_server) # Setup on conftest per function | ||||
|     test_url = url_for('test_headers', _external=True) | ||||
|  | ||||
| @@ -286,8 +288,9 @@ def test_ua_global_override(client, live_server, measure_memory_usage): | ||||
|     assert b"html-requests-user-agent" not in res.data | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_headers_textfile_in_request(client, live_server, measure_memory_usage): | ||||
|      | ||||
| def test_headers_textfile_in_request(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import os | ||||
|  | ||||
|     # Add our URL to the import page | ||||
|  | ||||
|     webdriver_ua = "Hello fancy webdriver UA 1.0" | ||||
| @@ -343,14 +346,14 @@ def test_headers_textfile_in_request(client, live_server, measure_memory_usage): | ||||
|     assert b"Updated watch." in res.data | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     with open('test-datastore/headers-testtag.txt', 'w') as f: | ||||
|     with open(os.path.join(datastore_path, 'headers-testtag.txt'), 'w') as f: | ||||
|         f.write("tag-header: test\r\nurl-header: http://example.com") | ||||
|  | ||||
|     with open('test-datastore/headers.txt', 'w') as f: | ||||
|     with open(os.path.join(datastore_path, 'headers.txt'), 'w') as f: | ||||
|         f.write("global-header: nice\r\nnext-global-header: nice\r\nurl-header-global: http://example.com/global") | ||||
|  | ||||
|     uuid = next(iter(live_server.app.config['DATASTORE'].data['watching'])) | ||||
|     with open(f'test-datastore/{uuid}/headers.txt', 'w') as f: | ||||
|     with open(os.path.join(datastore_path, uuid, 'headers.txt'), 'w') as f: | ||||
|         f.write("watch-header: nice\r\nurl-header-watch: http://example.com/watch") | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
| @@ -368,8 +371,8 @@ def test_headers_textfile_in_request(client, live_server, measure_memory_usage): | ||||
|     assert b"Extra headers file found and will be added to this watch" in res.data | ||||
|  | ||||
|     # Not needed anymore | ||||
|     os.unlink('test-datastore/headers.txt') | ||||
|     os.unlink('test-datastore/headers-testtag.txt') | ||||
|     os.unlink(os.path.join(datastore_path, 'headers.txt')) | ||||
|     os.unlink(os.path.join(datastore_path, 'headers-testtag.txt')) | ||||
|  | ||||
|     # The service should echo back the request verb | ||||
|     res = client.get( | ||||
| @@ -395,7 +398,7 @@ def test_headers_textfile_in_request(client, live_server, measure_memory_usage): | ||||
|     # unlink headers.txt on start/stop | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_headers_validation(client, live_server, measure_memory_usage): | ||||
| def test_headers_validation(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     test_url = url_for('test_headers', _external=True) | ||||
|   | ||||
| @@ -21,8 +21,7 @@ out_of_stock_props = [ | ||||
|     '<script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","url":"https://www.medimops.de/","potentialAction":{"@type":"SearchAction","target":"https://www.medimops.de/produkte-C0/?fcIsSearch=1&searchparam={searchparam}","query-input":"required name=searchparam"}}</script><script type="application/ld+json">{"@context":"http://schema.org","@type":"Product","name":"Horsetrader: Robert Sangster and the Rise and Fall of the Sport of Kings","image":"https://images2.medimops.eu/product/43a982/M00002551322-large.jpg","productID":"isbn:9780002551328","gtin13":"9780002551328","category":"Livres en langue étrangère","offers":{"@type":"Offer","priceCurrency":"EUR","price":$$PRICE$$,"itemCondition":"UsedCondition","availability":"OutOfStock"},"brand":{"@type":"Thing","name":"Patrick Robinson","url":"https://www.momox-shop.fr/,patrick-robinson/"}}</script>' | ||||
| ] | ||||
|  | ||||
| def set_original_response(props_markup='', price="121.95"): | ||||
|  | ||||
| def set_original_response(datastore_path, props_markup='', price="121.95"): | ||||
|     props_markup=props_markup.replace('$$PRICE$$', price) | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
| @@ -36,28 +35,19 @@ def set_original_response(props_markup='', price="121.95"): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     time.sleep(1) | ||||
|     return None | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_restock_itemprop_basic(client, live_server, measure_memory_usage): | ||||
|  | ||||
|      | ||||
| def test_restock_itemprop_basic(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     # By default it should enable ('in_stock_processing') == 'all_changes' | ||||
|  | ||||
|     for p in instock_props: | ||||
|         set_original_response(props_markup=p) | ||||
|         set_original_response(props_markup=p, datastore_path=datastore_path) | ||||
|         client.post( | ||||
|             url_for("ui.ui_views.form_quick_watch_add"), | ||||
|             data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'}, | ||||
| @@ -73,7 +63,7 @@ def test_restock_itemprop_basic(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     for p in out_of_stock_props: | ||||
|         set_original_response(props_markup=p) | ||||
|         set_original_response(props_markup=p, datastore_path=datastore_path) | ||||
|         client.post( | ||||
|             url_for("ui.ui_views.form_quick_watch_add"), | ||||
|             data={"url": test_url, "tags": '', 'processor': 'restock_diff'}, | ||||
| @@ -86,13 +76,13 @@ def test_restock_itemprop_basic(client, live_server, measure_memory_usage): | ||||
|  | ||||
|         delete_all_watches(client) | ||||
|  | ||||
| def test_itemprop_price_change(client, live_server, measure_memory_usage): | ||||
| def test_itemprop_price_change(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     # Out of the box 'Follow price changes' should be ON | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     set_original_response(props_markup=instock_props[0], price="190.95") | ||||
|     set_original_response(props_markup=instock_props[0], price="190.95", datastore_path=datastore_path) | ||||
|     client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'}, | ||||
| @@ -105,7 +95,7 @@ def test_itemprop_price_change(client, live_server, measure_memory_usage): | ||||
|     assert b'190.95' in res.data | ||||
|  | ||||
|     # basic price change, look for notification | ||||
|     set_original_response(props_markup=instock_props[0], price='180.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='180.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -116,7 +106,7 @@ def test_itemprop_price_change(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     # turning off price change trigger, but it should show the new price, with no change notification | ||||
|     set_original_response(props_markup=instock_props[0], price='120.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='120.45', datastore_path=datastore_path) | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_edit.edit_page", uuid="first"), | ||||
|         data={"restock_settings-follow_price_changes": "", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, | ||||
| @@ -132,13 +122,13 @@ def test_itemprop_price_change(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
| def _run_test_minmax_limit(client, extra_watch_edit_form, datastore_path): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95") | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95", datastore_path=datastore_path) | ||||
|     client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'}, | ||||
| @@ -166,7 +156,7 @@ def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
|     client.get(url_for("ui.mark_all_viewed")) | ||||
|  | ||||
|     # price changed to something greater than min (900), BUT less than max (1100).. should be no change | ||||
|     set_original_response(props_markup=instock_props[0], price='1000.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='1000.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow")) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -177,7 +167,7 @@ def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # price changed to something LESS than min (900), SHOULD be a change | ||||
|     set_original_response(props_markup=instock_props[0], price='890.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='890.45', datastore_path=datastore_path) | ||||
|  | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     assert b'Queued 1 watch for rechecking.' in res.data | ||||
| @@ -190,7 +180,7 @@ def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
|  | ||||
|  | ||||
|     # 2715 - Price detection (once it crosses the "lower" threshold) again with a lower price - should trigger again! | ||||
|     set_original_response(props_markup=instock_props[0], price='820.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='820.45', datastore_path=datastore_path) | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     assert b'Queued 1 watch for rechecking.' in res.data | ||||
|     wait_for_all_checks(client) | ||||
| @@ -200,7 +190,7 @@ def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
|     client.get(url_for("ui.mark_all_viewed")) | ||||
|  | ||||
|     # price changed to something MORE than max (1100.10), SHOULD be a change | ||||
|     set_original_response(props_markup=instock_props[0], price='1890.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='1890.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -211,16 +201,16 @@ def _run_test_minmax_limit(client, extra_watch_edit_form): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_restock_itemprop_minmax(client, live_server, measure_memory_usage): | ||||
| def test_restock_itemprop_minmax(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     extras = { | ||||
|         "restock_settings-follow_price_changes": "y", | ||||
|         "restock_settings-price_change_min": 900.0, | ||||
|         "restock_settings-price_change_max": 1100.10 | ||||
|     } | ||||
|     _run_test_minmax_limit(client, extra_watch_edit_form=extras) | ||||
|     _run_test_minmax_limit(client, extra_watch_edit_form=extras, datastore_path=datastore_path) | ||||
|  | ||||
| def test_restock_itemprop_with_tag(client, live_server, measure_memory_usage): | ||||
| def test_restock_itemprop_with_tag(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     res = client.post( | ||||
| @@ -245,18 +235,18 @@ def test_restock_itemprop_with_tag(client, live_server, measure_memory_usage): | ||||
|         "tags": "test-tag" | ||||
|     } | ||||
|  | ||||
|     _run_test_minmax_limit(client, extra_watch_edit_form=extras) | ||||
|     _run_test_minmax_limit(client, extra_watch_edit_form=extras,datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_itemprop_percent_threshold(client, live_server, measure_memory_usage): | ||||
| def test_itemprop_percent_threshold(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|  | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95") | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95", datastore_path=datastore_path) | ||||
|     client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'}, | ||||
| @@ -283,7 +273,7 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|     # Basic change should not trigger | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow")) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -291,7 +281,7 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage): | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # Bigger INCREASE change than the threshold should trigger | ||||
|     set_original_response(props_markup=instock_props[0], price='1960.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='1960.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow")) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -301,7 +291,7 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     # Small decrease should NOT trigger | ||||
|     client.get(url_for("ui.mark_all_viewed")) | ||||
|     set_original_response(props_markup=instock_props[0], price='1950.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='1950.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow")) | ||||
|     wait_for_all_checks(client) | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
| @@ -315,14 +305,14 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage): | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_change_with_notification_values(client, live_server, measure_memory_usage): | ||||
| def test_change_with_notification_values(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     if os.path.isfile("test-datastore/notification.txt"): | ||||
|         os.unlink("test-datastore/notification.txt") | ||||
|     if os.path.isfile(os.path.join(datastore_path, "notification.txt")): | ||||
|         os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45', datastore_path=datastore_path) | ||||
|  | ||||
|     notification_url = url_for('test_notification_endpoint', _external=True).replace('http', 'json') | ||||
|  | ||||
| @@ -363,34 +353,34 @@ def test_change_with_notification_values(client, live_server, measure_memory_usa | ||||
|     assert b"Settings updated." in res.data | ||||
|  | ||||
|  | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='960.45', datastore_path=datastore_path) | ||||
|     # A change in price, should trigger a change by default | ||||
|     set_original_response(props_markup=instock_props[0], price='1950.45') | ||||
|     set_original_response(props_markup=instock_props[0], price='1950.45', datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow")) | ||||
|     wait_for_all_checks(client) | ||||
|     wait_for_notification_endpoint_output() | ||||
|     assert os.path.isfile("test-datastore/notification.txt"), "Notification received" | ||||
|     with open("test-datastore/notification.txt", 'r') as f: | ||||
|     wait_for_notification_endpoint_output(datastore_path=datastore_path) | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification.txt")), "Notification received" | ||||
|     with open(os.path.join(datastore_path, "notification.txt"), 'r') as f: | ||||
|         notification = f.read() | ||||
|         assert "new price 1950.45" in notification | ||||
|         assert "title new price 1950.45" in notification | ||||
|  | ||||
|     ## Now test the "SEND TEST NOTIFICATION" is working | ||||
|     os.unlink("test-datastore/notification.txt") | ||||
|     os.unlink(os.path.join(datastore_path, "notification.txt")) | ||||
|     uuid = next(iter(live_server.app.config['DATASTORE'].data['watching'])) | ||||
|     res = client.post(url_for("ui.ui_notification.ajax_callback_send_notification_test", watch_uuid=uuid), data={}, follow_redirects=True) | ||||
|     time.sleep(5) | ||||
|     assert os.path.isfile("test-datastore/notification.txt"), "Notification received" | ||||
|     assert os.path.isfile(os.path.join(datastore_path, "notification.txt")), "Notification received" | ||||
|  | ||||
|  | ||||
| def test_data_sanity(client, live_server, measure_memory_usage): | ||||
| def test_data_sanity(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     test_url2 = url_for('test_endpoint2', _external=True) | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95") | ||||
|     set_original_response(props_markup=instock_props[0], price="950.95", datastore_path=datastore_path) | ||||
|     client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'}, | ||||
| @@ -429,7 +419,7 @@ def test_data_sanity(client, live_server, measure_memory_usage): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| # All examples should give a prive of 666.66 | ||||
| def test_special_prop_examples(client, live_server, measure_memory_usage): | ||||
| def test_special_prop_examples(client, live_server, measure_memory_usage, datastore_path): | ||||
|     import glob | ||||
|      | ||||
|  | ||||
| @@ -439,7 +429,7 @@ def test_special_prop_examples(client, live_server, measure_memory_usage): | ||||
|     assert files | ||||
|     for test_example_filename in files: | ||||
|         with open(test_example_filename, 'r') as example_f: | ||||
|             with open("test-datastore/endpoint-content.txt", "w") as test_f: | ||||
|             with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as test_f: | ||||
|                 test_f.write(f"<html><body>{example_f.read()}</body></html>") | ||||
|  | ||||
|             # Now fetch it and check the price worked | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import os | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI, \ | ||||
|     extract_UUID_from_client, delete_all_watches | ||||
|  | ||||
|  | ||||
| def set_original_cdata_xml(): | ||||
| def set_original_cdata_xml(datastore_path): | ||||
|     test_return_data = """<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> | ||||
|     <channel> | ||||
|     <title>Gizi</title> | ||||
| @@ -45,12 +45,12 @@ def set_original_cdata_xml(): | ||||
|     </rss> | ||||
|             """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
|  | ||||
| def set_html_content(content): | ||||
| def set_html_content(datastore_path, content): | ||||
|     test_return_data = f"""<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -62,16 +62,16 @@ def set_html_content(content): | ||||
|     """ | ||||
|  | ||||
|     # Write as UTF-8 encoded bytes | ||||
|     with open("test-datastore/endpoint-content.txt", "wb") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "wb") as f: | ||||
|         f.write(test_return_data.encode('utf-8')) | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_rss_and_token(client, live_server, measure_memory_usage): | ||||
| def test_rss_and_token(client, live_server, measure_memory_usage, datastore_path): | ||||
|     #   #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     set_original_response() | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     rss_token = extract_rss_token_from_UI(client) | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -84,7 +84,7 @@ def test_rss_and_token(client, live_server, measure_memory_usage): | ||||
|     assert b"1 Imported" in res.data | ||||
|  | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|     time.sleep(1) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
| @@ -106,10 +106,10 @@ def test_rss_and_token(client, live_server, measure_memory_usage): | ||||
|  | ||||
|     client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) | ||||
|  | ||||
| def test_basic_cdata_rss_markup(client, live_server, measure_memory_usage): | ||||
| def test_basic_cdata_rss_markup(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     set_original_cdata_xml() | ||||
|     set_original_cdata_xml(datastore_path) | ||||
|     # Rarely do endpoints give the right header, usually just text/xml, so we check also for <rss | ||||
|     # This also triggers the automatic CDATA text parser so the RSS goes back a nice content list | ||||
|     test_url = url_for('test_endpoint', content_type="text/xml; charset=UTF-8", _external=True) | ||||
| @@ -130,10 +130,10 @@ def test_basic_cdata_rss_markup(client, live_server, measure_memory_usage): | ||||
|     assert b'The days of Terminator' in res.data | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_rss_xpath_filtering(client, live_server, measure_memory_usage): | ||||
| def test_rss_xpath_filtering(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     set_original_cdata_xml() | ||||
|     set_original_cdata_xml(datastore_path) | ||||
|  | ||||
|     test_url = url_for('test_endpoint', content_type="application/atom+xml; charset=UTF-8", _external=True) | ||||
|  | ||||
| @@ -179,7 +179,7 @@ def test_rss_xpath_filtering(client, live_server, measure_memory_usage): | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_rss_bad_chars_breaking(client, live_server, measure_memory_usage): | ||||
| def test_rss_bad_chars_breaking(client, live_server, measure_memory_usage, datastore_path): | ||||
|     """This should absolutely trigger the RSS builder to go into worst state mode | ||||
|  | ||||
|     - source: prefix means no html conversion (which kinda filters out the bad stuff) | ||||
| @@ -190,7 +190,7 @@ def test_rss_bad_chars_breaking(client, live_server, measure_memory_usage): | ||||
|     """ | ||||
|      | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         ten_kb_string = "A" * 10_000 | ||||
|         f.write(ten_kb_string) | ||||
|  | ||||
| @@ -204,7 +204,7 @@ def test_rss_bad_chars_breaking(client, live_server, measure_memory_usage): | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
|     # Set the bad content | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         jpeg_bytes = "\xff\xd8\xff\xe0\x00\x10XXXXXXXX\x00\x01\x02\x00\x00\x01\x00\x01\x00\x00"  # JPEG header | ||||
|         jpeg_bytes += "A" * 10_000 | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import time | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
| from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI, \ | ||||
|     extract_UUID_from_client, delete_all_watches | ||||
|  | ||||
|  | ||||
| def set_original_cdata_xml(): | ||||
| def set_original_cdata_xml(datastore_path): | ||||
|     test_return_data = """<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"> | ||||
| <channel> | ||||
| <title>Security Bulletins on wetscale</title> | ||||
| @@ -40,13 +42,13 @@ def set_original_cdata_xml(): | ||||
|     </rss> | ||||
|             """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_rss_reader_mode(client, live_server, measure_memory_usage): | ||||
|     set_original_cdata_xml() | ||||
| def test_rss_reader_mode(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_cdata_xml(datastore_path=datastore_path) | ||||
|  | ||||
|     # Rarely do endpoints give the right header, usually just text/xml, so we check also for <rss | ||||
|     # This also triggers the automatic CDATA text parser so the RSS goes back a nice content list | ||||
| @@ -71,8 +73,8 @@ def test_rss_reader_mode(client, live_server, measure_memory_usage): | ||||
|     assert 'PubDate: Thu, 07 Aug 2025 00:00:00 GMT' in snapshot_contents | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_rss_reader_mode_with_css_filters(client, live_server, measure_memory_usage): | ||||
|     set_original_cdata_xml() | ||||
| def test_rss_reader_mode_with_css_filters(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_cdata_xml(datastore_path=datastore_path) | ||||
|  | ||||
|     # Rarely do endpoints give the right header, usually just text/xml, so we check also for <rss | ||||
|     # This also triggers the automatic CDATA text parser so the RSS goes back a nice content list | ||||
|   | ||||
| @@ -9,10 +9,10 @@ from .util import  live_server_setup, wait_for_all_checks, extract_UUID_from_cli | ||||
| from ..forms import REQUIRE_ATLEAST_ONE_TIME_PART_MESSAGE_DEFAULT, REQUIRE_ATLEAST_ONE_TIME_PART_WHEN_NOT_GLOBAL_DEFAULT | ||||
|  | ||||
|  | ||||
| # def test_setup(client, live_server, measure_memory_usage): | ||||
| # def test_setup(client, live_server, measure_memory_usage, datastore_path): | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
| def test_check_basic_scheduler_functionality(client, live_server, measure_memory_usage): | ||||
| def test_check_basic_scheduler_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] | ||||
|     test_url = url_for('test_random_content_endpoint', _external=True) | ||||
| @@ -90,7 +90,7 @@ def test_check_basic_scheduler_functionality(client, live_server, measure_memory | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_check_basic_global_scheduler_functionality(client, live_server, measure_memory_usage): | ||||
| def test_check_basic_global_scheduler_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] | ||||
|     test_url = url_for('test_random_content_endpoint', _external=True) | ||||
| @@ -172,7 +172,7 @@ def test_check_basic_global_scheduler_functionality(client, live_server, measure | ||||
|     delete_all_watches(client) | ||||
|  | ||||
|  | ||||
| def test_validation_time_interval_field(client, live_server, measure_memory_usage): | ||||
| def test_validation_time_interval_field(client, live_server, measure_memory_usage, datastore_path): | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import time | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_basic_search(client, live_server, measure_memory_usage): | ||||
| def test_basic_search(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     urls = ['https://localhost:12300?first-result=1', | ||||
| @@ -37,7 +37,7 @@ def test_basic_search(client, live_server, measure_memory_usage): | ||||
|     assert urls[1].encode('utf-8') not in res.data | ||||
|  | ||||
|  | ||||
| def test_search_in_tag_limit(client, live_server, measure_memory_usage): | ||||
| def test_search_in_tag_limit(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     urls = ['https://localhost:12300?first-result=1 tag-one', | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import os | ||||
|  | ||||
| from flask import url_for | ||||
|  | ||||
| from changedetectionio.tests.util import set_modified_response | ||||
| from .util import live_server_setup, wait_for_all_checks, delete_all_watches | ||||
| from .. import strtobool | ||||
|  | ||||
|  | ||||
| def set_original_response(): | ||||
| def set_original_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|     <head><title>head title</title></head> | ||||
|     <body> | ||||
| @@ -18,12 +20,12 @@ def set_original_response(): | ||||
|      </html> | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|     return None | ||||
|  | ||||
| def test_bad_access(client, live_server, measure_memory_usage): | ||||
|      | ||||
| def test_bad_access(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("imports.import_page"), | ||||
|         data={"urls": 'https://localhost'}, | ||||
| @@ -46,7 +48,7 @@ def test_bad_access(client, live_server, measure_memory_usage): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data | ||||
|     assert b'Watch protocol is not permitted or invalid URL format' in res.data | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
| @@ -54,7 +56,7 @@ def test_bad_access(client, live_server, measure_memory_usage): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data | ||||
|     assert b'Watch protocol is not permitted or invalid URL format' in res.data | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
| @@ -62,7 +64,7 @@ def test_bad_access(client, live_server, measure_memory_usage): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data | ||||
|     assert b'Watch protocol is not permitted or invalid URL format' in res.data | ||||
|  | ||||
|  | ||||
|     res = client.post( | ||||
| @@ -71,8 +73,15 @@ def test_bad_access(client, live_server, measure_memory_usage): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data | ||||
|     assert b'Watch protocol is not permitted or invalid URL format' in res.data | ||||
|  | ||||
|     res = client.post( | ||||
|         url_for("ui.ui_views.form_quick_watch_add"), | ||||
|         data={"url": 'https://i-wanna-xss-you.com?hereis=<script>alert(1)</script>', "tags": ''}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b'Watch protocol is not permitted or invalid URL format' in res.data | ||||
|  | ||||
| def _runner_test_various_file_slash(client, file_uri): | ||||
|  | ||||
| @@ -102,17 +111,17 @@ def _runner_test_various_file_slash(client, file_uri): | ||||
|  | ||||
|     delete_all_watches(client) | ||||
|  | ||||
| def test_file_slash_access(client, live_server, measure_memory_usage): | ||||
| def test_file_slash_access(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|  | ||||
|     # file: is NOT permitted by default, so it will be caught by ALLOW_FILE_URI check | ||||
|  | ||||
|     test_file_path = os.path.abspath(__file__) | ||||
|     _runner_test_various_file_slash(client, file_uri=f"file://{test_file_path}") | ||||
|     _runner_test_various_file_slash(client, file_uri=f"file:/{test_file_path}") | ||||
|     _runner_test_various_file_slash(client, file_uri=f"file:{test_file_path}") # CVE-2024-56509 | ||||
| #    _runner_test_various_file_slash(client, file_uri=f"file:/{test_file_path}") | ||||
| #    _runner_test_various_file_slash(client, file_uri=f"file:{test_file_path}") # CVE-2024-56509 | ||||
|  | ||||
| def test_xss(client, live_server, measure_memory_usage): | ||||
| def test_xss(client, live_server, measure_memory_usage, datastore_path): | ||||
|      | ||||
|     from changedetectionio.notification import ( | ||||
|         default_notification_format | ||||
| @@ -132,9 +141,29 @@ def test_xss(client, live_server, measure_memory_usage): | ||||
|     assert b"<img src=x onerror=alert(" not in res.data | ||||
|     assert b"<img" in res.data | ||||
|  | ||||
|     # Check that even forcing an update directly still doesnt get to the frontend | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     XSS_HACK = 'javascript:alert(document.domain)' | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=url_for('test_endpoint', _external=True)) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|  | ||||
| def test_xss_watch_last_error(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
|     live_server.app.config['DATASTORE'].data['watching'][uuid]['url']=XSS_HACK | ||||
|  | ||||
|  | ||||
|     res = client.get(url_for("ui.ui_views.preview_page", uuid=uuid)) | ||||
|     assert XSS_HACK.encode('utf-8') not in res.data and res.status_code == 200 | ||||
|     client.get(url_for("ui.ui_views.diff_history_page", uuid=uuid)) | ||||
|     assert XSS_HACK.encode('utf-8') not in res.data and res.status_code == 200 | ||||
|     res = client.get(url_for("watchlist.index")) | ||||
|     assert XSS_HACK.encode('utf-8') not in res.data and res.status_code == 200 | ||||
|  | ||||
|  | ||||
| def test_xss_watch_last_error(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|     # Add our URL to the import page | ||||
|     res = client.post( | ||||
|         url_for("imports.import_page"), | ||||
|   | ||||
| @@ -9,8 +9,9 @@ import re | ||||
| sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|  | ||||
| def test_share_watch(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_share_watch(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|   | ||||
| @@ -9,8 +9,9 @@ sleep_time_for_fetch_thread = 3 | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_check_basic_change_detection_functionality_source(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_basic_change_detection_functionality_source(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     test_url = 'source:'+url_for('test_endpoint', _external=True) | ||||
|     # Add our URL to the import page | ||||
|     uuid = client.application.config.get('DATASTORE').add_watch(url=test_url) | ||||
| @@ -30,7 +31,7 @@ def test_check_basic_change_detection_functionality_source(client, live_server, | ||||
|     assert b'foobar-detection' in res.data | ||||
|  | ||||
|     # Make a change | ||||
|     set_modified_response() | ||||
|     set_modified_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Force recheck | ||||
|     res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -52,8 +53,9 @@ def test_check_basic_change_detection_functionality_source(client, live_server, | ||||
|  | ||||
|  | ||||
| # `subtractive_selectors` should still work in `source:` type requests | ||||
| def test_check_ignore_elements(client, live_server, measure_memory_usage): | ||||
|     set_original_response() | ||||
| def test_check_ignore_elements(client, live_server, measure_memory_usage, datastore_path): | ||||
|     set_original_response(datastore_path=datastore_path) | ||||
|  | ||||
|     time.sleep(1) | ||||
|     test_url = 'source:'+url_for('test_endpoint', _external=True) | ||||
|     # Add our URL to the import page | ||||
|   | ||||
| @@ -3,9 +3,10 @@ | ||||
| import time | ||||
| from flask import url_for | ||||
| from .util import live_server_setup, wait_for_all_checks | ||||
| import os | ||||
|  | ||||
|  | ||||
| def set_original_ignore_response(): | ||||
| def set_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some initial text<br> | ||||
| @@ -17,11 +18,11 @@ def set_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_modified_original_ignore_response(): | ||||
| def set_modified_original_ignore_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some NEW nice initial text<br> | ||||
| @@ -33,11 +34,11 @@ def set_modified_original_ignore_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def set_modified_with_trigger_text_response(): | ||||
| def set_modified_with_trigger_text_response(datastore_path): | ||||
|     test_return_data = """<html> | ||||
|        <body> | ||||
|      Some NEW nice initial text<br> | ||||
| @@ -51,16 +52,16 @@ def set_modified_with_trigger_text_response(): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     with open("test-datastore/endpoint-content.txt", "w") as f: | ||||
|     with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f: | ||||
|         f.write(test_return_data) | ||||
|  | ||||
|  | ||||
| def test_trigger_functionality(client, live_server, measure_memory_usage): | ||||
| def test_trigger_functionality(client, live_server, measure_memory_usage, datastore_path): | ||||
|  | ||||
|    #  live_server_setup(live_server) # Setup on conftest per function | ||||
|  | ||||
|     trigger_text = "Add to cart" | ||||
|     set_original_ignore_response() | ||||
|     set_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|  | ||||
|     # Add our URL to the import page | ||||
| @@ -106,7 +107,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage): | ||||
|     assert b'/test-endpoint' in res.data | ||||
|  | ||||
|     #  Make a change | ||||
|     set_modified_original_ignore_response() | ||||
|     set_modified_original_ignore_response(datastore_path=datastore_path) | ||||
|  | ||||
|     # Trigger a check | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
| @@ -117,7 +118,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage): | ||||
|     assert b'has-unread-changes' not in res.data | ||||
|  | ||||
|     # Now set the content which contains the trigger text | ||||
|     set_modified_with_trigger_text_response() | ||||
|     set_modified_with_trigger_text_response(datastore_path=datastore_path) | ||||
|  | ||||
|     client.get(url_for("ui.form_watch_checknow"), follow_redirects=True) | ||||
|     wait_for_all_checks(client) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user