mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-12-03 14:52:34 +00:00
Compare commits
88 Commits
playwright
...
diff-propo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef24c42db | ||
|
|
0f6afb9ce8 | ||
|
|
ea2fcee4ad | ||
|
|
bd79c5decd | ||
|
|
74428372c3 | ||
|
|
e6cdb57db0 | ||
|
|
ac3de58116 | ||
|
|
e11c6aeb5f | ||
|
|
294bb7be15 | ||
|
|
c2c8bb4de8 | ||
|
|
35d950fa74 | ||
|
|
d24111f3a6 | ||
|
|
7011a04399 | ||
|
|
4364521cfc | ||
|
|
748328453e | ||
|
|
e867e89303 | ||
|
|
3e7fd9570a | ||
|
|
99f3b01013 | ||
|
|
43c2e71961 | ||
|
|
9946ee66d0 | ||
|
|
9f722cc76b | ||
|
|
62b6645810 | ||
|
|
e5e8b3bbbd | ||
|
|
852a698629 | ||
|
|
76fd27dfab | ||
|
|
83161e4fa3 | ||
|
|
296c7c46cb | ||
|
|
0a2644d0c3 | ||
|
|
495e322c9e | ||
|
|
0d5820932f | ||
|
|
408be08a48 | ||
|
|
bad0909cc2 | ||
|
|
c80f46308a | ||
|
|
802daa6296 | ||
|
|
2f641da182 | ||
|
|
4951721286 | ||
|
|
a50d6db0b2 | ||
|
|
f55f7967ef | ||
|
|
13a96e93a2 | ||
|
|
ed93d51ae8 | ||
|
|
db28b30b1b | ||
|
|
6bdcdfbaea | ||
|
|
0efc504c5d | ||
|
|
628cb2ad44 | ||
|
|
604f2eaf02 | ||
|
|
2a649afd22 | ||
|
|
526f8fac45 | ||
|
|
e76f5efee3 | ||
|
|
7ac0620099 | ||
|
|
14765b46bd | ||
|
|
4f3a15e68d | ||
|
|
c6207f729d | ||
|
|
fcc1a72d30 | ||
|
|
6f2b7ceddb | ||
|
|
1e265b312e | ||
|
|
f379dda13d | ||
|
|
4a88589a27 | ||
|
|
cac53a76c0 | ||
|
|
8dbf2257d3 | ||
|
|
c0fb051dde | ||
|
|
cf09f03d32 | ||
|
|
237cf7db4f | ||
|
|
a8e24dab01 | ||
|
|
5c9b7353d4 | ||
|
|
1e22949e3d | ||
|
|
68e1a64474 | ||
|
|
151c2dab3a | ||
|
|
3e43d7ad1a | ||
|
|
58cb7fbc2a | ||
|
|
23452a1599 | ||
|
|
7fb432bf06 | ||
|
|
dc3fc6cfdf | ||
|
|
8ee42d2403 | ||
|
|
8d9cac4c38 | ||
|
|
374bb3824f | ||
|
|
91d8600b19 | ||
|
|
7b0ddc23d3 | ||
|
|
ab74377be0 | ||
|
|
2196d120a9 | ||
|
|
5dca59a4a0 | ||
|
|
ee8042b54e | ||
|
|
4c3f233d21 | ||
|
|
159b062cb3 | ||
|
|
83565787ae | ||
|
|
bdab4f5e09 | ||
|
|
69075a81c5 | ||
|
|
04746cc706 | ||
|
|
234494d907 |
31
.github/test/Dockerfile-alpine
vendored
31
.github/test/Dockerfile-alpine
vendored
@@ -1,31 +0,0 @@
|
||||
# Taken from https://github.com/linuxserver/docker-changedetection.io/blob/main/Dockerfile
|
||||
# Test that we can still build on Alpine (musl modified libc https://musl.libc.org/)
|
||||
# Some packages wont install via pypi because they dont have a wheel available under this architecture.
|
||||
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.16
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
COPY requirements.txt /requirements.txt
|
||||
|
||||
RUN \
|
||||
apk add --update --no-cache --virtual=build-dependencies \
|
||||
cargo \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
libffi-dev \
|
||||
libxslt-dev \
|
||||
make \
|
||||
openssl-dev \
|
||||
py3-wheel \
|
||||
python3-dev \
|
||||
zlib-dev && \
|
||||
apk add --update --no-cache \
|
||||
libxslt \
|
||||
python3 \
|
||||
py3-pip && \
|
||||
echo "**** pip3 install test of changedetection.io ****" && \
|
||||
pip3 install -U pip wheel setuptools && \
|
||||
pip3 install -U --no-cache-dir --find-links https://wheel-index.linuxserver.io/alpine-3.16/ -r /requirements.txt && \
|
||||
apk del --purge \
|
||||
build-dependencies
|
||||
11
.github/workflows/test-container-build.yml
vendored
11
.github/workflows/test-container-build.yml
vendored
@@ -43,16 +43,6 @@ jobs:
|
||||
version: latest
|
||||
driver-opts: image=moby/buildkit:master
|
||||
|
||||
# https://github.com/dgtlmoon/changedetection.io/pull/1067
|
||||
# Check we can still build under alpine/musl
|
||||
- name: Test that the docker containers can build (musl via alpine check)
|
||||
id: docker_build_musl
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./.github/test/Dockerfile-alpine
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Test that the docker containers can build
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
@@ -63,4 +53,3 @@ jobs:
|
||||
platforms: linux/arm/v7,linux/arm/v6,linux/amd64,linux/arm64,
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
|
||||
@@ -23,10 +23,14 @@ RUN pip install --target=/dependencies -r /requirements.txt
|
||||
|
||||
# Playwright is an alternative to Selenium
|
||||
# Excluded this package from requirements.txt to prevent arm/v6 and arm/v7 builds from failing
|
||||
# https://github.com/dgtlmoon/changedetection.io/pull/1067 also musl/alpine (not supported)
|
||||
RUN pip install --target=/dependencies playwright~=1.26 \
|
||||
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
||||
|
||||
|
||||
RUN pip install --target=/dependencies jq~=1.3 \
|
||||
|| echo "WARN: Failed to install JQ. The application can still run, but the Jq: filter option will be disabled."
|
||||
|
||||
|
||||
# Final image stage
|
||||
FROM python:3.8-slim
|
||||
|
||||
|
||||
@@ -167,6 +167,9 @@ One big advantage of `jq` is that you can use logic in your JSON filter, such as
|
||||
|
||||
See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/JSON-Selector-Filter-help for more information and examples
|
||||
|
||||
Note: `jq` library must be added separately (`pip3 install jq`)
|
||||
|
||||
|
||||
### Parse JSON embedded in HTML!
|
||||
|
||||
When you enable a `json:` or `jq:` filter, you can even automatically extract and parse embedded JSON inside a HTML page! Amazingly handy for sites that build content based on JSON, such as many e-commerce websites.
|
||||
|
||||
@@ -33,7 +33,7 @@ from flask_wtf import CSRFProtect
|
||||
from changedetectionio import html_tools
|
||||
from changedetectionio.api import api_v1
|
||||
|
||||
__version__ = '0.39.21'
|
||||
__version__ = '0.39.20.4'
|
||||
|
||||
datastore = None
|
||||
|
||||
@@ -199,6 +199,8 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Setup cors headers to allow all domains
|
||||
# https://flask-cors.readthedocs.io/en/latest/
|
||||
# CORS(app)
|
||||
@@ -1307,8 +1309,8 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
|
||||
threading.Thread(target=notification_runner).start()
|
||||
|
||||
# Check for new release version, but not when running in test/build or pytest
|
||||
if not os.getenv("GITHUB_REF", False) and not config.get('disable_checkver') == True:
|
||||
# Check for new release version, but not when running in test/build
|
||||
if not os.getenv("GITHUB_REF", False):
|
||||
threading.Thread(target=check_for_new_version).start()
|
||||
|
||||
return app
|
||||
|
||||
@@ -2,14 +2,14 @@ import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib3
|
||||
import difflib
|
||||
|
||||
|
||||
from changedetectionio import content_fetcher, html_tools
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
# Some common stuff here that can be moved to a base class
|
||||
# (set_proxy_from_list)
|
||||
class perform_site_check():
|
||||
@@ -185,6 +185,9 @@ class perform_site_check():
|
||||
elif is_source:
|
||||
stripped_text_from_html = html_content
|
||||
|
||||
# Re #340 - return the content before the 'ignore text' was applied
|
||||
text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8')
|
||||
|
||||
# Re #340 - return the content before the 'ignore text' was applied
|
||||
text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8')
|
||||
|
||||
@@ -286,8 +289,23 @@ class perform_site_check():
|
||||
else:
|
||||
logging.debug("check_unique_lines: UUID {} had unique content".format(uuid))
|
||||
|
||||
# Always record the new checksum
|
||||
if changed_detected:
|
||||
if not watch.get("trigger_add", True) or not watch.get("trigger_del", True): # if we are supposed to filter any diff types
|
||||
# get the diff types present in the watch
|
||||
diff_types = watch.get_diff_types(text_content_before_ignored_filter)
|
||||
print("Diff components found: " + str(diff_types))
|
||||
|
||||
# Only Additions (deletions are turned off)
|
||||
if not watch["trigger_del"] and diff_types["del"] and not diff_types["add"]:
|
||||
changed_detected = False
|
||||
|
||||
# Only Deletions (additions are turned off)
|
||||
elif not watch["trigger_add"] and diff_types["add"] and not diff_types["del"]:
|
||||
changed_detected = False
|
||||
|
||||
# Always record the new checksum and the new text
|
||||
update_obj["previous_md5"] = fetched_md5
|
||||
watch.save_previous_text(text_content_before_ignored_filter)
|
||||
|
||||
# On the first run of a site, watch['previous_md5'] will be None, set it the current one.
|
||||
if not watch.get('previous_md5'):
|
||||
|
||||
@@ -323,6 +323,18 @@ class ValidateCSSJSONXPATHInput(object):
|
||||
except:
|
||||
raise ValidationError("A system-error occurred when validating your jq expression")
|
||||
|
||||
class ValidateDiffFilters(object):
|
||||
"""
|
||||
Validates that at least one filter checkbox is selected
|
||||
"""
|
||||
def __init__(self, message=None):
|
||||
self.message = message
|
||||
|
||||
def __call__(self, form, field):
|
||||
if not form.trigger_add.data and not form.trigger_del.data:
|
||||
message = field.gettext('At least one filter checkbox must be selected')
|
||||
raise ValidationError(message)
|
||||
|
||||
|
||||
class quickWatchForm(Form):
|
||||
url = fields.URLField('URL', validators=[validateURL()])
|
||||
@@ -365,6 +377,8 @@ class watchForm(commonSettingsForm):
|
||||
check_unique_lines = BooleanField('Only trigger when new lines appear', default=False)
|
||||
trigger_text = StringListField('Trigger/wait for text', [validators.Optional(), ValidateListRegex()])
|
||||
text_should_not_be_present = StringListField('Block change-detection if text matches', [validators.Optional(), ValidateListRegex()])
|
||||
trigger_add = BooleanField('Additions', [ValidateDiffFilters()], default=True)
|
||||
trigger_del = BooleanField('Deletions', [ValidateDiffFilters()], default=True)
|
||||
|
||||
webdriver_js_execute_code = TextAreaField('Execute JavaScript before change detection', render_kw={"rows": "5"}, validators=[validators.Optional()])
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ class model(dict):
|
||||
'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine.
|
||||
'extract_title_as_title': False,
|
||||
'check_unique_lines': False, # On change-detected, compare against all history if its something new
|
||||
'trigger_add': True,
|
||||
'trigger_del': True,
|
||||
'proxy': None, # Preferred proxy connection
|
||||
# Re #110, so then if this is set to None, we know to use the default value instead
|
||||
# Requires setting to None on submit if it's the same as the default
|
||||
@@ -185,12 +187,6 @@ class model(dict):
|
||||
def save_history_text(self, contents, timestamp):
|
||||
|
||||
self.ensure_data_dir_exists()
|
||||
|
||||
# Small hack so that we sleep just enough to allow 1 second between history snapshots
|
||||
# this is because history.txt indexes/keys snapshots by epoch seconds and we dont want dupe keys
|
||||
if self.__newest_history_key and int(timestamp) == int(self.__newest_history_key):
|
||||
time.sleep(timestamp - self.__newest_history_key)
|
||||
|
||||
snapshot_fname = "{}.txt".format(str(uuid.uuid4()))
|
||||
|
||||
# in /diff/ and /preview/ we are going to assume for now that it's UTF-8 when reading
|
||||
@@ -212,6 +208,35 @@ class model(dict):
|
||||
# @todo bump static cache of the last timestamp so we dont need to examine the file to set a proper ''viewed'' status
|
||||
return snapshot_fname
|
||||
|
||||
# Save previous text snapshot for diffing - used for calculating additions and deletions
|
||||
def save_previous_text(self, contents):
|
||||
import logging
|
||||
|
||||
output_path = os.path.join(self.__datastore_path, self['uuid'])
|
||||
|
||||
# Incase the operator deleted it, check and create.
|
||||
self.ensure_data_dir_exists()
|
||||
|
||||
snapshot_fname = os.path.join(self.watch_data_dir, "previous.txt")
|
||||
logging.debug("Saving previous text {}".format(snapshot_fname))
|
||||
|
||||
with open(snapshot_fname, 'wb') as f:
|
||||
f.write(contents)
|
||||
|
||||
return snapshot_fname
|
||||
|
||||
# Get previous text snapshot for diffing - used for calculating additions and deletions
|
||||
def get_previous_text(self):
|
||||
|
||||
snapshot_fname = os.path.join(self.watch_data_dir, "previous.txt")
|
||||
if self.history_n < 1:
|
||||
return ""
|
||||
|
||||
with open(snapshot_fname, 'rb') as f:
|
||||
contents = f.read()
|
||||
|
||||
return contents
|
||||
|
||||
@property
|
||||
def has_empty_checktime(self):
|
||||
# using all() + dictionary comprehension
|
||||
@@ -241,6 +266,31 @@ class model(dict):
|
||||
# if not, something new happened
|
||||
return not local_lines.issubset(existing_history)
|
||||
|
||||
# Get diff types (addition, deletion, modification) from the previous snapshot and new_text
|
||||
# uses similar algorithm to customSequenceMatcher in diff.py
|
||||
# Returns a dict of diff types and wether they are present in the diff
|
||||
def get_diff_types(self, new_text):
|
||||
import difflib
|
||||
|
||||
diff_types = {
|
||||
'add': False,
|
||||
'del': False,
|
||||
}
|
||||
|
||||
# get diff types using difflib
|
||||
cruncher = difflib.SequenceMatcher(isjunk=lambda x: x in " \\t", a=str(self.get_previous_text()), b=str(new_text))
|
||||
|
||||
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
|
||||
if tag == 'delete':
|
||||
diff_types["del"] = True
|
||||
elif tag == 'insert':
|
||||
diff_types["add"] = True
|
||||
elif tag == 'replace':
|
||||
diff_types["del"] = True
|
||||
diff_types["add"] = True
|
||||
|
||||
return diff_types
|
||||
|
||||
def get_screenshot(self):
|
||||
fname = os.path.join(self.watch_data_dir, "last-screenshot.png")
|
||||
if os.path.isfile(fname):
|
||||
|
||||
@@ -24,6 +24,14 @@ echo "RUNNING WITH BASE_URL SET"
|
||||
export BASE_URL="https://really-unique-domain.io"
|
||||
pytest tests/test_notification.py
|
||||
|
||||
|
||||
## JQ + JSON: filter test
|
||||
# jq is not available on windows and we should just test it when the package is installed
|
||||
# this will re-test with jq support
|
||||
pip3 install jq~=1.3
|
||||
pytest tests/test_jsonpath_jq_selector.py
|
||||
|
||||
|
||||
# Now for the selenium and playwright/browserless fetchers
|
||||
# Note - this is not UI functional tests - just checking that each one can fetch the content
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ class ChangeDetectionStore:
|
||||
# For when we edit, we should write to disk
|
||||
needs_write_urgent = False
|
||||
|
||||
__version_check = True
|
||||
|
||||
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)
|
||||
@@ -39,6 +37,7 @@ class ChangeDetectionStore:
|
||||
self.proxy_list = None
|
||||
self.start_time = time.time()
|
||||
self.stop_thread = False
|
||||
|
||||
# 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={}))
|
||||
@@ -549,6 +548,10 @@ class ChangeDetectionStore:
|
||||
# `last_changed` not needed, we pull that information from the history.txt index
|
||||
def update_4(self):
|
||||
for uuid, watch in self.data['watching'].items():
|
||||
# Be sure it's recalculated
|
||||
p = watch.history
|
||||
if watch.history_n < 2:
|
||||
watch['last_changed'] = 0
|
||||
try:
|
||||
# Remove it from the struct
|
||||
del(watch['last_changed'])
|
||||
@@ -584,3 +587,23 @@ class ChangeDetectionStore:
|
||||
for v in ['User-Agent', 'Accept', 'Accept-Encoding', 'Accept-Language']:
|
||||
if self.data['settings']['headers'].get(v):
|
||||
del self.data['settings']['headers'][v]
|
||||
|
||||
# Generate a previous.txt for all watches that do not have one and contain history
|
||||
def update_8(self):
|
||||
for uuid, watch in self.data['watching'].items():
|
||||
# Make sure we actually have history
|
||||
if (watch.history_n == 0):
|
||||
continue
|
||||
latest_file_name = watch.history[watch.newest_history_key]
|
||||
|
||||
|
||||
# Check if the previous.txt exists
|
||||
if not os.path.exists(os.path.join(watch.watch_data_dir, "previous.txt")):
|
||||
# Generate a previous.txt
|
||||
with open(os.path.join(watch.watch_data_dir, "previous.txt"), "wb") as f:
|
||||
# Fill it with the latest history
|
||||
latest_file_name = watch.history[watch.newest_history_key]
|
||||
with open(latest_file_name, "rb") as f2:
|
||||
f.write(f2.read())
|
||||
|
||||
|
||||
|
||||
@@ -173,6 +173,16 @@ User-Agent: wonderbra 1.0") }}
|
||||
<span class="pure-form-message-inline">Good for websites that just move the content around, and you want to know when NEW content is added, compares new lines against all history for this watch.</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
<label for="trigger-type">Filter and restrict change detection of content to</label>
|
||||
{{ render_checkbox_field(form.trigger_add, class="trigger-type") }}
|
||||
{{ render_checkbox_field(form.trigger_del, class="trigger-type") }}
|
||||
<span class="pure-form-message-inline">
|
||||
Filters the change-detection of this watch to only this type of content change. <strong>Replacements</strong> (neither additions nor deletions) are always included. The 'diff' will still include all changes.
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="pure-control-group">
|
||||
{% set field = render_field(form.css_filter,
|
||||
placeholder=".class-name or #some-id, or other CSS selector rule.",
|
||||
|
||||
@@ -41,7 +41,7 @@ def app(request):
|
||||
|
||||
cleanup(datastore_path)
|
||||
|
||||
app_config = {'datastore_path': datastore_path, 'disable_checkver' : True}
|
||||
app_config = {'datastore_path': datastore_path}
|
||||
cleanup(app_config['datastore_path'])
|
||||
datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], include_default_watches=False)
|
||||
app = changedetection_app(app_config, datastore)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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
|
||||
from .util import set_original_response, set_modified_response, live_server_setup
|
||||
|
||||
sleep_time_for_fetch_thread = 3
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
# Give the thread time to pick it up
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# It should report nothing found (no new 'unviewed' class)
|
||||
res = client.get(url_for("index"))
|
||||
@@ -69,7 +69,7 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
||||
res = client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
assert b'1 watches are queued for rechecking.' in res.data
|
||||
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# Now something should be ready, indicated by having a 'unviewed' class
|
||||
res = client.get(url_for("index"))
|
||||
@@ -98,14 +98,14 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
||||
assert b'which has this one new line' in res.data
|
||||
assert b'Which is across multiple lines' not in res.data
|
||||
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(2)
|
||||
|
||||
# Do this a few times.. ensures we dont accidently set the status
|
||||
for n in range(2):
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
# Give the thread time to pick it up
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# It should report nothing found (no new 'unviewed' class)
|
||||
res = client.get(url_for("index"))
|
||||
@@ -125,7 +125,7 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
||||
)
|
||||
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' in res.data
|
||||
|
||||
@@ -46,6 +46,6 @@ def test_backup(client, live_server):
|
||||
uuid4hex = re.compile('^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}.*txt', re.I)
|
||||
newlist = list(filter(uuid4hex.match, l)) # Read Note below
|
||||
|
||||
# Should be two txt files in the archive (history and the snapshot)
|
||||
assert len(newlist) == 2
|
||||
# Should be three txt files in the archive (history and the snapshot)
|
||||
assert len(newlist) == 3
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/python3
|
||||
# @NOTE: THIS RELIES ON SOME MIDDLEWARE TO MAKE CHECKBOXES WORK WITH WTFORMS UNDER TEST CONDITION, see changedetectionio/tests/util.py
|
||||
import time
|
||||
from flask import url_for
|
||||
from .util import live_server_setup
|
||||
|
||||
def set_original_response():
|
||||
test_return_data = """
|
||||
Here
|
||||
is
|
||||
some
|
||||
text
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def set_response_with_deleted_word():
|
||||
test_return_data = """
|
||||
Here
|
||||
is
|
||||
text
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def set_response_with_changed_word():
|
||||
test_return_data = """
|
||||
Here
|
||||
ix
|
||||
some
|
||||
text
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def test_diff_filter_changes_as_add_delete(client, live_server):
|
||||
live_server_setup(live_server)
|
||||
|
||||
sleep_time_for_fetch_thread = 3
|
||||
|
||||
set_original_response()
|
||||
# Give the endpoint time to spin up
|
||||
time.sleep(1)
|
||||
|
||||
# Add our URL to the import page
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
res = client.post(
|
||||
url_for("import_page"),
|
||||
data={"urls": test_url},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"1 Imported" in res.data
|
||||
# Wait for it to read the original version
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# Make a change that ONLY includes deletes
|
||||
set_response_with_deleted_word()
|
||||
res = client.post(
|
||||
url_for("edit_page", uuid="first"),
|
||||
data={"trigger_add": "y",
|
||||
"trigger_del": "n",
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests"},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# We should NOT see a change because we chose to not know about any Deletions
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' not in res.data
|
||||
# Recheck to be sure
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' not in res.data
|
||||
|
||||
|
||||
# Now set the original response, which will include the word, which should trigger Added (because trigger_add ==y)
|
||||
set_original_response()
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' in res.data
|
||||
|
||||
# Now check 'changes' are always going to be triggered
|
||||
set_original_response()
|
||||
client.post(
|
||||
url_for("edit_page", uuid="first"),
|
||||
# Neither trigger add nor del? then we should see changes still
|
||||
data={"trigger_add": "n",
|
||||
"trigger_del": "n",
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests"},
|
||||
follow_redirects=True
|
||||
)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
||||
set_response_with_changed_word()
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' in res.data
|
||||
83
changedetectionio/tests/test_diff_filter_only_additions.py
Normal file
83
changedetectionio/tests/test_diff_filter_only_additions.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import time
|
||||
from flask import url_for
|
||||
from .util import live_server_setup
|
||||
|
||||
def set_original_response():
|
||||
test_return_data = """
|
||||
A few new lines
|
||||
Where there is more lines originally
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def set_delete_response():
|
||||
test_return_data = """
|
||||
A few new lines
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def test_diff_filtering_no_del(client, live_server):
|
||||
live_server_setup(live_server)
|
||||
|
||||
sleep_time_for_fetch_thread = 3
|
||||
|
||||
set_original_response()
|
||||
# Give the endpoint time to spin up
|
||||
time.sleep(1)
|
||||
|
||||
# Add our URL to the import page
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
res = client.post(
|
||||
url_for("import_page"),
|
||||
data={"urls": test_url},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"1 Imported" in res.data
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# Add our URL to the import page
|
||||
res = client.post(
|
||||
url_for("edit_page", uuid="first"),
|
||||
data={"trigger_add": "y",
|
||||
"trigger_del": "n",
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests"},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
assert b'unviewed' not in res.data
|
||||
|
||||
# Make an delete change
|
||||
set_delete_response()
|
||||
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
# Trigger a check
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
# Give the thread time to pick it up
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# We should NOT see the change
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' not in res.data
|
||||
|
||||
# Make an delete change
|
||||
set_original_response()
|
||||
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
# Trigger a check
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
|
||||
# Give the thread time to pick it up
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# We should see the change
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' in res.data
|
||||
|
||||
72
changedetectionio/tests/test_diff_filter_only_deletions.py
Normal file
72
changedetectionio/tests/test_diff_filter_only_deletions.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import time
|
||||
from flask import url_for
|
||||
from .util import live_server_setup
|
||||
|
||||
def set_original_response():
|
||||
test_return_data = """
|
||||
A few new lines
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def set_add_response():
|
||||
test_return_data = """
|
||||
A few new lines
|
||||
Where there is more lines than before
|
||||
"""
|
||||
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def test_diff_filtering_no_add(client, live_server):
|
||||
live_server_setup(live_server)
|
||||
|
||||
sleep_time_for_fetch_thread = 3
|
||||
|
||||
set_original_response()
|
||||
# Give the endpoint time to spin up
|
||||
time.sleep(1)
|
||||
|
||||
# Add our URL to the import page
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
res = client.post(
|
||||
url_for("import_page"),
|
||||
data={"urls": test_url},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
assert b"1 Imported" in res.data
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# Add our URL to the import page
|
||||
res = client.post(
|
||||
url_for("edit_page", uuid="first"),
|
||||
data={"trigger_add": "n",
|
||||
"trigger_del": "y",
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests"},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
assert b'unviewed' not in res.data
|
||||
|
||||
# Make an add change
|
||||
set_add_response()
|
||||
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
# Trigger a check
|
||||
|
||||
# Give the thread time to pick it up
|
||||
time.sleep(sleep_time_for_fetch_thread)
|
||||
|
||||
# We should NOT see the change
|
||||
res = client.get(url_for("index"))
|
||||
# save res.data to a file
|
||||
|
||||
|
||||
|
||||
assert b'unviewed' not in res.data
|
||||
|
||||
@@ -81,4 +81,4 @@ def test_consistent_history(client, live_server):
|
||||
|
||||
|
||||
|
||||
assert len(files_in_watch_dir) == 2, "Should be just two files in the dir, history.txt and the snapshot"
|
||||
assert len(files_in_watch_dir) == 3, "Should be just three files in the dir, history.txt, previous.txt, and the snapshot"
|
||||
|
||||
@@ -4,6 +4,12 @@ from flask import make_response, request
|
||||
from flask import url_for
|
||||
import logging
|
||||
import time
|
||||
from werkzeug import Request
|
||||
import io
|
||||
|
||||
# This is a fix for macOS running tests.
|
||||
import multiprocessing
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
def set_original_response():
|
||||
test_return_data = """<html>
|
||||
@@ -86,7 +92,6 @@ def extract_UUID_from_client(client):
|
||||
def wait_for_all_checks(client):
|
||||
# Loop waiting until done..
|
||||
attempt=0
|
||||
time.sleep(0.1)
|
||||
while attempt < 60:
|
||||
time.sleep(1)
|
||||
res = client.get(url_for("index"))
|
||||
@@ -160,6 +165,38 @@ def live_server_setup(live_server):
|
||||
ret = " ".join([auth.username, auth.password, auth.type])
|
||||
return ret
|
||||
|
||||
# Make sure any checkboxes that are supposed to be defaulted to true are set during the post request
|
||||
# This is due to the fact that defaults are set in the HTML which we are not using during tests.
|
||||
# This does not affect the server when running outside of a test
|
||||
class DefaultCheckboxMiddleware(object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
if request.method == "POST" and "/edit" in request.path:
|
||||
body = environ['wsgi.input'].read()
|
||||
|
||||
# if the checkboxes are not set, set them to true
|
||||
if b"trigger_add" not in body:
|
||||
body += b'&trigger_add=y'
|
||||
|
||||
if b"trigger_del" not in body:
|
||||
body += b'&trigger_del=y'
|
||||
|
||||
# remove any checkboxes set to "n" so wtforms processes them correctly
|
||||
body = body.replace(b"trigger_add=n", b"")
|
||||
body = body.replace(b"trigger_del=n", b"")
|
||||
body = body.replace(b"&&", b"&")
|
||||
|
||||
new_stream = io.BytesIO(body)
|
||||
environ["CONTENT_LENGTH"] = len(body)
|
||||
environ['wsgi.input'] = new_stream
|
||||
|
||||
return self.app(environ, start_response)
|
||||
|
||||
live_server.app.wsgi_app = DefaultCheckboxMiddleware(live_server.app.wsgi_app)
|
||||
|
||||
# Just return some GET var
|
||||
@live_server.app.route('/test-return-query', methods=['GET'])
|
||||
def test_return_query():
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
flask~=2.0
|
||||
flask ~= 2.0
|
||||
flask_wtf
|
||||
eventlet>=0.31.0
|
||||
eventlet >= 0.31.0
|
||||
validators
|
||||
timeago~=1.0
|
||||
inscriptis~=2.2
|
||||
feedgen~=0.9
|
||||
flask-login~=0.5
|
||||
timeago ~= 1.0
|
||||
inscriptis ~= 2.2
|
||||
feedgen ~= 0.9
|
||||
flask-login ~= 0.5
|
||||
flask_restful
|
||||
pytz
|
||||
|
||||
# Set these versions together to avoid a RequestsDependencyWarning
|
||||
# >= 2.26 also adds Brotli support if brotli is installed
|
||||
brotli~=1.0
|
||||
requests[socks] ~=2.28
|
||||
brotli ~= 1.0
|
||||
requests[socks] ~= 2.28
|
||||
|
||||
urllib3>1.26
|
||||
chardet>2.3.0
|
||||
urllib3 > 1.26
|
||||
chardet > 2.3.0
|
||||
|
||||
wtforms~=3.0
|
||||
jsonpath-ng~=1.5.3
|
||||
wtforms ~= 3.0
|
||||
jsonpath-ng ~= 1.5.3
|
||||
|
||||
# jq not available on Windows so must be installed manually
|
||||
|
||||
# Notification library
|
||||
apprise~=1.1.0
|
||||
apprise ~= 1.1.0
|
||||
|
||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
||||
paho-mqtt
|
||||
|
||||
# Pinned version of cryptography otherwise
|
||||
# ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly
|
||||
cryptography~=3.4
|
||||
cryptography ~= 3.4
|
||||
|
||||
# Used for CSS filtering
|
||||
bs4
|
||||
@@ -39,20 +39,16 @@ bs4
|
||||
lxml
|
||||
|
||||
# 3.141 was missing socksVersion, 3.150 was not in pypi, so we try 4.1.0
|
||||
selenium~=4.1.0
|
||||
selenium ~= 4.1.0
|
||||
|
||||
# https://stackoverflow.com/questions/71652965/importerror-cannot-import-name-safe-str-cmp-from-werkzeug-security/71653849#71653849
|
||||
# ImportError: cannot import name 'safe_str_cmp' from 'werkzeug.security'
|
||||
# need to revisit flask login versions
|
||||
werkzeug~=2.0.0
|
||||
werkzeug ~= 2.0.0
|
||||
|
||||
# Templating, so far just in the URLs but in the future can be for the notifications also
|
||||
jinja2~=3.1
|
||||
jinja2 ~= 3.1
|
||||
jinja2-time
|
||||
|
||||
# https://peps.python.org/pep-0508/#environment-markers
|
||||
# https://github.com/dgtlmoon/changedetection.io/pull/1009
|
||||
jq~=1.3 ;python_version >= "3.8" and sys_platform == "linux"
|
||||
|
||||
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
||||
|
||||
|
||||
Reference in New Issue
Block a user