Skip to content

Commit 26531d8

Browse files
tobixenclaude
andcommitted
Expand search tests and remove fragile mocked tests
This commit enhances the CalDAV search functionality testing and improves test maintainability: ## Search Test Improvements - Add SearchMixIn class with search_find_set() helper method to reduce boilerplate in search feature testing - Expand search feature coverage: - Text search (case-sensitive, case-insensitive, substring) - Property filter with CalDAVSearcher (is-not-defined operator) - Category search and substring matching - Combined search filters (logical AND behavior) - Add post_filter=False to all server behavior tests to ensure we're testing actual server responses, not client-side filtering - Improve comp-type-optional test with additional text search validation - Add test event without summary property to validate optional fields ## Test Maintenance - Remove test_ai_checks_with_mocks.py - these AI-generated mocked tests were fragile and difficult to maintain. The file itself documented that it should be deleted if it breaks. All meaningful test coverage is provided by the remaining unit tests. ## Bug Fixes - Fix create-calendar feature detection to not mark mkcol method as standard calendar creation - Capitalize event summary to match actual test data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2a1b907 commit 26531d8

2 files changed

Lines changed: 100 additions & 677 deletions

File tree

src/caldav_server_tester/checks.py

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from caldav.compatibility_hints import FeatureSet
99
from caldav.lib.error import NotFoundError, AuthorizationError, ReportError, DAVError
1010
from caldav.calendarobjectresource import Event, Todo, Journal
11+
from caldav.search import CalDAVSearcher
1112

1213
from .checks_base import Check
1314

@@ -105,7 +106,10 @@ def _try_make_calendar(self, cal_id, **kwargs):
105106
## calendar creation must have gone OK.
106107
calmade = True
107108
self.checker.principal.calendar(cal_id=cal_id).events()
108-
self.set_feature("create-calendar")
109+
## the caller takes care of setting quirk flag if mkcol
110+
## (todo - does this make sense? Actually the whole _try_make_calendar looks messy to me and should probably be refactored)
111+
if kwargs.get('method', 'mkcalendar') != 'mkcol':
112+
self.set_feature("create-calendar")
109113
if kwargs.get("name"):
110114
try:
111115
name = "A calendar with this name should not exist"
@@ -344,7 +348,7 @@ def add_if_not_existing(*largs, **kwargs):
344348

345349
simple_event = add_if_not_existing(
346350
Event,
347-
summary="simple event with a start time and an end time",
351+
summary="Simple event with a start time and an end time",
348352
uid="csc_simple_event1",
349353
dtstart=datetime(2000, 1, 1, 12, 0, 0, tzinfo=utc),
350354
dtend=datetime(2000, 1, 1, 13, 0, 0, tzinfo=utc),
@@ -486,71 +490,129 @@ def add_if_not_existing(*largs, **kwargs):
486490
END:VCALENDAR""",
487491
)
488492

493+
simple_event = add_if_not_existing(
494+
Event,
495+
description="Simple event without a summary",
496+
uid="csc_simple_event_no_summary",
497+
dtstart=datetime(2000, 3, 1, 12, 0, 0, tzinfo=utc),
498+
dtend=datetime(2000, 3, 1, 13, 0, 0, tzinfo=utc),
499+
)
500+
489501
## No more existing IDs in the calendar from 2000 ... otherwise,
490502
## more work is needed to ensure those won't pollute the tests nor be
491503
## deleted by accident
492504
assert not object_by_uid
493505
assert self.checker.calendar.events()
494506
assert self.checker.tasklist.todos()
495507

508+
class SearchMixIn:
509+
## Boilerplate
510+
def search_find_set(self, cal_or_searcher, feature, num_expected=None, **search_args):
511+
try:
512+
results = cal_or_searcher.search(**search_args, post_filter=False)
513+
cnt = len(results)
514+
if num_expected is None:
515+
is_good = cnt > 0
516+
else:
517+
is_good = cnt==num_expected
518+
self.set_feature(feature, is_good)
519+
except ReportError:
520+
self.set_feature(feature, "ungraceful")
521+
496522

497-
class CheckSearch(Check):
523+
class CheckSearch(Check, SearchMixIn):
498524
depends_on = {PrepareCalendar}
499525
features_to_be_checked = {
500526
"search.time-range.event",
501-
"search.category",
502-
"search.category.fullstring",
503-
"search.category.fullstring.smart",
504527
"search.time-range.todo",
528+
"search.text",
529+
"search.text.case-sensitive",
530+
"search.text.case-insensitive",
531+
"search.text.substring",
532+
"search.is-not-defined",
533+
"search.text.category",
534+
"search.text.category.substring",
505535
"search.comp-type-optional",
506536
"search.combined-is-logical-and",
507537
} ## TODO: we can do so much better than this
508538

509539
def _run_check(self):
510540
cal = self.checker.calendar
511541
tasklist = self.checker.tasklist
512-
events = cal.search(
542+
self.search_find_set(
543+
cal, "search.time-range.event", 1,
513544
start=datetime(2000, 1, 1, tzinfo=utc),
514545
end=datetime(2000, 1, 2, tzinfo=utc),
515546
event=True,
516547
)
517-
self.set_feature("search.time-range.event", len(events) == 1)
518-
tasks = tasklist.search(
548+
self.search_find_set(
549+
tasklist, "search.time-range.todo", 1,
519550
start=datetime(2000, 1, 9, tzinfo=utc),
520551
end=datetime(2000, 1, 10, tzinfo=utc),
521552
todo=True,
522553
include_completed=True,
523554
)
524-
self.set_feature("search.time-range.todo", len(tasks) == 1)
525555

526-
## search.category
527-
try:
528-
events = cal.search(category="hands", event=True)
529-
self.set_feature("search.category", len(events) == 1)
530-
except ReportError:
531-
self.set_feature("search.category", "ungraceful")
532-
if self.feature_checked("search.category", str) != 'ungraceful':
533-
events = cal.search(category="hands,feet,head", event=True)
534-
self.set_feature("search.category.fullstring", len(events) == 1)
535-
if len(events) == 1:
536-
events = cal.search(category="feet,head,hands", event=True)
537-
self.set_feature("search.category.fullstring.smart", len(events) == 1)
556+
## summary search
557+
self.search_find_set(
558+
cal, "search.text", 1,
559+
summary="Simple event with a start time and an end time",
560+
event=True)
561+
562+
## summary search is by default case sensitive
563+
self.search_find_set(
564+
cal, "search.text.case-sensitive", 0,
565+
summary="simple event with a start time and an end time",
566+
event=True)
567+
568+
## summary search, case insensitive
569+
searcher = CalDAVSearcher(event=True)
570+
searcher.add_property_filter('summary', "simple event with a start time and an end time", case_sensitive=False)
571+
self.search_find_set(
572+
searcher, "search.text.case-insensitive", 1, calendar=cal)
573+
574+
## is not defined search
575+
searcher = CalDAVSearcher(event=True)
576+
searcher.add_property_filter('summary', None, operator="undef")
577+
self.search_find_set(
578+
searcher, "search.is-not-defined", 1, calendar=cal)
579+
580+
## summary search, substring
581+
## The RFC says that TextMatch is a subetext search
582+
self.search_find_set(
583+
cal, "search.text.substring", 1,
584+
summary="Simple event with a start time and",
585+
event=True)
586+
587+
## search.text.category
588+
self.search_find_set(
589+
cal, "search.text.category", 1,
590+
category="hands", event=True)
538591

539592
## search.combined
540-
if self.feature_checked("search.category"):
541-
events1 = cal.search(category="hands", event=True, start=datetime(2000, 1, 1, 11, 0, 0), end=datetime(2000, 1, 13, 14, 0, 0))
542-
events2 = cal.search(category="hands", event=True, start=datetime(2000, 1, 1, 9, 0, 0), end=datetime(2000, 1, 6, 14, 0, 0))
593+
if self.feature_checked("search.text.category"):
594+
events1 = cal.search(category="hands", event=True, start=datetime(2000, 1, 1, 11, 0, 0), end=datetime(2000, 1, 13, 14, 0, 0), post_filter=False)
595+
events2 = cal.search(category="hands", event=True, start=datetime(2000, 1, 1, 9, 0, 0), end=datetime(2000, 1, 6, 14, 0, 0), post_filter=False)
543596
self.set_feature("search.combined-is-logical-and", len(events1) == 1 and len(events2) == 0)
544-
597+
self.search_find_set(
598+
cal, "search.text.category.substring", 1,
599+
category="eet",
600+
event=True)
545601
try:
602+
summary = "Simple event with a start time and"
603+
## Text search with and without comptype
604+
tswc = cal.search(summary=summary, event=True, post_filter=False)
605+
tswoc = cal.search(summary=summary, post_filter=False)
606+
## Testing if search without comp-type filter returns both events and tasks
546607
if self.feature_checked("search.time-range.todo"):
547608
objects = cal.search(
548609
start=datetime(2000, 1, 1, tzinfo=utc),
549610
end=datetime(2001, 1, 1, tzinfo=utc),
611+
post_filter=False,
550612
)
551613
else:
552-
objects = _filter_2000(cal.search())
553-
if len(objects) == 0:
614+
objects = _filter_2000(cal.search(post_filter=False))
615+
if len(objects) == 0 and not tswoc:
554616
self.set_feature(
555617
"search.comp-type-optional",
556618
{
@@ -570,12 +632,15 @@ def _run_check(self):
570632
cal != tasklist
571633
and len(objects)
572634
+ len(
635+
## Also search tasklist without comp-type to see if we get all objects
573636
tasklist.search(
574637
start=datetime(2000, 1, 1, tzinfo=utc),
575638
end=datetime(2001, 1, 1, tzinfo=utc),
639+
post_filter=False,
576640
)
577641
)
578-
== self.checker.cnt
642+
== self.checker.cnt and
643+
(tswoc or not tswc)
579644
):
580645
self.set_feature(
581646
"search.comp-type-optional",
@@ -584,7 +649,7 @@ def _run_check(self):
584649
"description": "comp-filter is redundant in search as a calendar can only hold one kind of components",
585650
},
586651
)
587-
elif len(objects) == self.checker.cnt:
652+
elif len(objects) == self.checker.cnt and (tswoc or not tswc):
588653
self.set_feature("search.comp-type-optional")
589654
else:
590655
## TODO ... we need to do more testing on search to conclude certainly on this one. But at least we get something out.
@@ -599,7 +664,7 @@ def _run_check(self):
599664
self.set_feature("search.comp-type-optional", {"support": "ungraceful"})
600665

601666

602-
class CheckRecurrenceSearch(Check):
667+
class CheckRecurrenceSearch(Check, SearchMixIn):
603668
depends_on = {CheckSearch}
604669
features_to_be_checked = {
605670
"search.recurrences.includes-implicit.todo",
@@ -661,6 +726,10 @@ def _run_check(self):
661726
event=True,
662727
post_filter=False,
663728
)
729+
## Xandikos version 0.2.12 breaks here for me.
730+
## It didn't break earlier.
731+
## Everything is exactly the same here. Same data on the server, same query
732+
## There must be some local state in xandikos causing some bug to happen
664733
assert len(exception) == 1
665734
far_future_recurrence = cal.search(
666735
start=datetime(2045, 3, 12, tzinfo=utc),

0 commit comments

Comments
 (0)