feat: add safe calendar import and export
Add explicit noninteractive CLI transfer modes with full event validation, idempotent duplicate handling, collision refusal, atomic import backups, and no-overwrite export creation. Cover pure merge semantics and end-to-end rollback paths for unsupported content, invalid identities, conflicts, option combinations, and existing destinations.
This commit is contained in:
279
tests/calendar_transfer_tests.cpp
Normal file
279
tests/calendar_transfer_tests.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
#include "nocal/domain/calendar_transfer.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void check(const bool condition, const std::string_view message)
|
||||
{
|
||||
if (!condition) {
|
||||
++failures;
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
template <class Function>
|
||||
void check_invalid_argument(Function&& function, const std::string_view message)
|
||||
{
|
||||
try {
|
||||
std::forward<Function>(function)();
|
||||
check(false, message);
|
||||
} catch (const std::invalid_argument&) {
|
||||
} catch (...) {
|
||||
check(false, message);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] nocal::TimePoint at(const int hour, const int minute = 0)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
return nocal::TimePoint{hours{24 * 20'000 + hour} + minutes{minute}};
|
||||
}
|
||||
|
||||
[[nodiscard]] nocal::Event complete_event(std::string uid)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
nocal::Event event;
|
||||
event.uid = std::move(uid);
|
||||
event.title = "Planning";
|
||||
event.start = at(9);
|
||||
event.end = at(10, 30);
|
||||
event.all_day = false;
|
||||
event.location = "Room 3";
|
||||
event.description = "Discuss launch";
|
||||
event.calendar_id = "work";
|
||||
event.color = "blue";
|
||||
event.time_basis = nocal::TimeBasis::zoned;
|
||||
event.time_zone = "Europe/London";
|
||||
nocal::RecurrenceRule recurrence;
|
||||
recurrence.frequency = nocal::RecurrenceFrequency::weekly;
|
||||
recurrence.interval = 2;
|
||||
recurrence.count = 6;
|
||||
recurrence.until = at(24);
|
||||
recurrence.by_weekday = {Monday, Wednesday};
|
||||
recurrence.by_month_day = {1, -1};
|
||||
recurrence.by_month = {1, 7};
|
||||
event.recurrence = std::move(recurrence);
|
||||
event.recurrence_exceptions = {at(16), at(18)};
|
||||
return event;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool equal(const nocal::Event& left, const nocal::Event& right)
|
||||
{
|
||||
return left.uid == right.uid && left.title == right.title &&
|
||||
left.start == right.start && left.end == right.end &&
|
||||
left.all_day == right.all_day && left.location == right.location &&
|
||||
left.description == right.description &&
|
||||
left.calendar_id == right.calendar_id && left.color == right.color &&
|
||||
left.time_basis == right.time_basis &&
|
||||
left.time_zone == right.time_zone &&
|
||||
left.recurrence == right.recurrence &&
|
||||
left.recurrence_exceptions == right.recurrence_exceptions;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool equal(const std::vector<nocal::Event>& left,
|
||||
const std::vector<nocal::Event>& right)
|
||||
{
|
||||
if (left.size() != right.size()) return false;
|
||||
for (std::size_t index = 0; index < left.size(); ++index) {
|
||||
if (!equal(left[index], right[index])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void test_export_validation()
|
||||
{
|
||||
nocal::validate_events_for_export({});
|
||||
const std::vector valid{complete_event("one"), complete_event("two")};
|
||||
nocal::validate_events_for_export(valid);
|
||||
|
||||
auto missing_uid = valid;
|
||||
missing_uid[0].uid.clear();
|
||||
check_invalid_argument(
|
||||
[&] { nocal::validate_events_for_export(missing_uid); },
|
||||
"export rejects an empty UID");
|
||||
|
||||
auto duplicate_uid = valid;
|
||||
duplicate_uid[1].uid = duplicate_uid[0].uid;
|
||||
check_invalid_argument(
|
||||
[&] { nocal::validate_events_for_export(duplicate_uid); },
|
||||
"export rejects duplicate UIDs even when event fields differ");
|
||||
|
||||
auto backwards = valid;
|
||||
backwards[0].end = backwards[0].start - std::chrono::minutes{1};
|
||||
check_invalid_argument([&] { nocal::validate_events_for_export(backwards); },
|
||||
"export rejects a backwards interval");
|
||||
|
||||
auto zero_duration = complete_event("instant");
|
||||
zero_duration.end = zero_duration.start;
|
||||
nocal::validate_events_for_export(std::vector{zero_duration});
|
||||
}
|
||||
|
||||
void test_merge_success_order_and_counts()
|
||||
{
|
||||
auto first = complete_event("target-one");
|
||||
auto second = complete_event("target-two");
|
||||
second.title = "Second target";
|
||||
auto duplicate = second;
|
||||
auto third = complete_event("source-one");
|
||||
third.title = "First import";
|
||||
auto fourth = complete_event("source-two");
|
||||
fourth.title = "Second import";
|
||||
|
||||
const std::vector target{first, second};
|
||||
const std::vector source{third, duplicate, fourth};
|
||||
const auto result = nocal::merge_events_for_import(target, source);
|
||||
|
||||
check(result.events.size() == 4, "merge returns target and new source events");
|
||||
check(result.imported == 2, "merge reports the number imported");
|
||||
check(result.duplicates_skipped == 1,
|
||||
"merge reports exact duplicates skipped");
|
||||
check(result.events.size() == 4 && result.events[0].uid == "target-one" &&
|
||||
result.events[1].uid == "target-two" &&
|
||||
result.events[2].uid == "source-one" &&
|
||||
result.events[3].uid == "source-two",
|
||||
"merge preserves target order and appends source order");
|
||||
check(equal(result.events[1], duplicate),
|
||||
"an all-field exact duplicate is skipped without replacing target");
|
||||
}
|
||||
|
||||
void test_every_event_field_participates_in_collision_equality()
|
||||
{
|
||||
using Mutation = std::pair<std::string_view, std::function<void(nocal::Event&)>>;
|
||||
const std::vector<Mutation> mutations{
|
||||
{"title", [](auto& event) { event.title += " changed"; }},
|
||||
{"start", [](auto& event) { event.start += std::chrono::minutes{1}; }},
|
||||
{"end", [](auto& event) { event.end += std::chrono::minutes{1}; }},
|
||||
{"all-day", [](auto& event) { event.all_day = !event.all_day; }},
|
||||
{"location", [](auto& event) { event.location += " changed"; }},
|
||||
{"description", [](auto& event) { event.description += " changed"; }},
|
||||
{"calendar ID", [](auto& event) { event.calendar_id += "-other"; }},
|
||||
{"color", [](auto& event) { event.color += "-other"; }},
|
||||
{"time basis", [](auto& event) { event.time_basis = nocal::TimeBasis::utc; }},
|
||||
{"time zone", [](auto& event) { event.time_zone = "America/New_York"; }},
|
||||
{"recurrence", [](auto& event) { event.recurrence->interval += 1; }},
|
||||
{"recurrence exclusions",
|
||||
[](auto& event) { event.recurrence_exceptions.push_back(at(19)); }},
|
||||
};
|
||||
|
||||
const std::vector target{complete_event("same-uid")};
|
||||
for (const auto& [field, mutate] : mutations) {
|
||||
auto collision = target[0];
|
||||
mutate(collision);
|
||||
check_invalid_argument(
|
||||
[&] {
|
||||
(void)nocal::merge_events_for_import(
|
||||
target, std::vector<nocal::Event>{collision});
|
||||
},
|
||||
std::string{"UID collision rejects a differing "} +
|
||||
std::string{field});
|
||||
}
|
||||
|
||||
auto removed_recurrence = target[0];
|
||||
removed_recurrence.recurrence.reset();
|
||||
check_invalid_argument(
|
||||
[&] {
|
||||
(void)nocal::merge_events_for_import(
|
||||
target, std::vector<nocal::Event>{removed_recurrence});
|
||||
},
|
||||
"UID collision rejects recurrence presence mismatch");
|
||||
}
|
||||
|
||||
void test_merge_validates_both_collections_before_result()
|
||||
{
|
||||
const auto target_event = complete_event("target");
|
||||
const auto source_event = complete_event("source");
|
||||
|
||||
auto empty_target_uid = std::vector{target_event};
|
||||
empty_target_uid[0].uid.clear();
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import(empty_target_uid, {}); },
|
||||
"merge rejects an empty target UID");
|
||||
|
||||
const std::vector duplicate_target{target_event, target_event};
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import(duplicate_target, {}); },
|
||||
"merge rejects duplicate target UIDs");
|
||||
|
||||
auto empty_source_uid = std::vector{source_event};
|
||||
empty_source_uid[0].uid.clear();
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import({}, empty_source_uid); },
|
||||
"merge rejects an empty source UID");
|
||||
|
||||
const std::vector duplicate_source{source_event, source_event};
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import({}, duplicate_source); },
|
||||
"merge rejects duplicate source UIDs");
|
||||
|
||||
auto invalid_target = std::vector{target_event};
|
||||
invalid_target[0].end = invalid_target[0].start - std::chrono::seconds{1};
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import(invalid_target, {}); },
|
||||
"merge rejects a backwards target interval");
|
||||
|
||||
auto invalid_source = std::vector{source_event};
|
||||
invalid_source[0].end = invalid_source[0].start - std::chrono::seconds{1};
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import({}, invalid_source); },
|
||||
"merge rejects a backwards source interval");
|
||||
}
|
||||
|
||||
void test_inputs_are_not_mutated_on_failure()
|
||||
{
|
||||
auto target_event = complete_event("collision");
|
||||
auto source_event = target_event;
|
||||
source_event.description = "Different data";
|
||||
source_event.recurrence_exceptions.push_back(at(21));
|
||||
|
||||
std::vector target{target_event};
|
||||
std::vector source{source_event};
|
||||
const auto target_before = target;
|
||||
const auto source_before = source;
|
||||
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import(target, source); },
|
||||
"a differing collision fails the whole merge");
|
||||
check(equal(target, target_before), "failed merge does not mutate target input");
|
||||
check(equal(source, source_before), "failed merge does not mutate source input");
|
||||
|
||||
source.push_back(complete_event("new-before-invalid"));
|
||||
auto invalid = complete_event("invalid-after-new");
|
||||
invalid.end = invalid.start - std::chrono::hours{1};
|
||||
source.push_back(invalid);
|
||||
const auto source_with_invalid_before = source;
|
||||
check_invalid_argument(
|
||||
[&] { (void)nocal::merge_events_for_import(target, source); },
|
||||
"source validation fails before exposing an earlier import");
|
||||
check(equal(source, source_with_invalid_before),
|
||||
"validation failure after a valid source item leaves source unchanged");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_export_validation();
|
||||
test_merge_success_order_and_counts();
|
||||
test_every_event_field_participates_in_collision_equality();
|
||||
test_merge_validates_both_collections_before_result();
|
||||
test_inputs_are_not_mutated_on_failure();
|
||||
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " calendar transfer test(s) failed\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::cout << "calendar transfer tests passed\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
280
tests/cli_transfer_test.sh
Normal file
280
tests/cli_transfer_test.sh
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: cli_transfer_test.sh /path/to/nocal" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
nocal=$1
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/nocal-cli-transfer.XXXXXX")
|
||||
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
expect_failure()
|
||||
{
|
||||
description=$1
|
||||
shift
|
||||
if "$@" >"$tmpdir/out" 2>"$tmpdir/err"; then
|
||||
fail "$description unexpectedly succeeded"
|
||||
fi
|
||||
}
|
||||
|
||||
write_target()
|
||||
{
|
||||
path=$1
|
||||
cat >"$path" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
UID:existing@nocal.test
|
||||
DTSTART:20260720T090000
|
||||
DTEND:20260720T100000
|
||||
SUMMARY:Existing appointment
|
||||
LOCATION:Desk
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
}
|
||||
|
||||
source_calendar=$tmpdir/source.ics
|
||||
cat >"$source_calendar" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
UID:existing@nocal.test
|
||||
DTSTART:20260720T090000
|
||||
DTEND:20260720T100000
|
||||
SUMMARY:Existing appointment
|
||||
LOCATION:Desk
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:imported@nocal.test
|
||||
DTSTART:20260721T140000Z
|
||||
DTEND:20260721T150000Z
|
||||
SUMMARY:Imported appointment
|
||||
DESCRIPTION:Transferred deterministically
|
||||
X-NOCAL-CALENDAR-ID:work
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
|
||||
# Import merges new appointments, skips exact duplicates, and preserves the
|
||||
# exact pre-import bytes as the adjacent backup.
|
||||
target=$tmpdir/target.ics
|
||||
write_target "$target"
|
||||
cp "$target" "$tmpdir/target-before.ics"
|
||||
import_summary=$(
|
||||
"$nocal" --import "$source_calendar" --calendar "$target"
|
||||
)
|
||||
printf '%s\n' "$import_summary" | grep -E 'Imported[[:space:]]+1([^0-9]|$)' >/dev/null ||
|
||||
fail "import summary does not report one imported appointment"
|
||||
printf '%s\n' "$import_summary" | grep -E '1[[:space:]]+duplicate' >/dev/null ||
|
||||
fail "import summary does not report one skipped duplicate"
|
||||
cmp "$target.bak" "$tmpdir/target-before.ics" >/dev/null ||
|
||||
fail "successful import backup does not contain the exact prior target bytes"
|
||||
grep -F 'UID:existing@nocal.test' "$target" >/dev/null
|
||||
grep -F 'UID:imported@nocal.test' "$target" >/dev/null
|
||||
|
||||
# A source containing only an exact duplicate must not rewrite the target or
|
||||
# create a backup.
|
||||
duplicate_source=$tmpdir/duplicate-source.ics
|
||||
write_target "$duplicate_source"
|
||||
duplicate_target=$tmpdir/duplicate-target.ics
|
||||
write_target "$duplicate_target"
|
||||
cp "$duplicate_target" "$tmpdir/duplicate-target-before.ics"
|
||||
duplicate_summary=$(
|
||||
"$nocal" --import "$duplicate_source" --calendar "$duplicate_target"
|
||||
)
|
||||
printf '%s\n' "$duplicate_summary" | grep -E 'Imported[[:space:]]+0([^0-9]|$)' >/dev/null ||
|
||||
fail "no-op import summary does not report zero imported appointments"
|
||||
printf '%s\n' "$duplicate_summary" | grep -E '1[[:space:]]+duplicate' >/dev/null ||
|
||||
fail "no-op import summary does not report one skipped duplicate"
|
||||
cmp "$duplicate_target" "$tmpdir/duplicate-target-before.ics" >/dev/null ||
|
||||
fail "exact-duplicate import rewrote the target"
|
||||
[ ! -e "$duplicate_target.bak" ] ||
|
||||
fail "exact-duplicate import created a backup despite performing no write"
|
||||
|
||||
# A differing appointment with the same UID is a conflict. No target or
|
||||
# backup may be produced by the failed mutation.
|
||||
conflict_source=$tmpdir/conflict-source.ics
|
||||
cat >"$conflict_source" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
UID:existing@nocal.test
|
||||
DTSTART:20260720T090000
|
||||
DTEND:20260720T100000
|
||||
SUMMARY:Conflicting appointment
|
||||
LOCATION:Desk
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
conflict_target=$tmpdir/conflict-target.ics
|
||||
write_target "$conflict_target"
|
||||
cp "$conflict_target" "$tmpdir/conflict-target-before.ics"
|
||||
expect_failure "conflicting-UID import" \
|
||||
"$nocal" --import "$conflict_source" --calendar "$conflict_target"
|
||||
cmp "$conflict_target" "$tmpdir/conflict-target-before.ics" >/dev/null ||
|
||||
fail "conflicting-UID import changed the target"
|
||||
[ ! -e "$conflict_target.bak" ] ||
|
||||
fail "conflicting-UID import created a backup"
|
||||
|
||||
missing_uid=$tmpdir/missing-uid.ics
|
||||
cat >"$missing_uid" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20260722T110000
|
||||
DTEND:20260722T120000
|
||||
SUMMARY:Missing identity
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
|
||||
duplicate_uid=$tmpdir/duplicate-uid.ics
|
||||
cat >"$duplicate_uid" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate@nocal.test
|
||||
DTSTART:20260722T110000
|
||||
DTEND:20260722T120000
|
||||
SUMMARY:First duplicate identity
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:duplicate@nocal.test
|
||||
DTSTART:20260723T110000
|
||||
DTEND:20260723T120000
|
||||
SUMMARY:Second duplicate identity
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
|
||||
unsupported=$tmpdir/unsupported.ics
|
||||
cat >"$unsupported" <<'EOF'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Nocal transfer CLI test//EN
|
||||
BEGIN:VEVENT
|
||||
UID:unsupported@nocal.test
|
||||
DTSTART:20260722T110000
|
||||
DTEND:20260722T120000
|
||||
SUMMARY:Unsupported appointment
|
||||
ATTENDEE:mailto:person@example.test
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOF
|
||||
|
||||
# Unsafe import sources are rejected before the target persistence boundary.
|
||||
for unsafe_source in "$missing_uid" "$duplicate_uid" "$unsupported"; do
|
||||
unsafe_name=$(basename "$unsafe_source" .ics)
|
||||
unsafe_target=$tmpdir/import-$unsafe_name-target.ics
|
||||
write_target "$unsafe_target"
|
||||
cp "$unsafe_target" "$tmpdir/import-$unsafe_name-before.ics"
|
||||
expect_failure "$unsafe_name import" \
|
||||
"$nocal" --import "$unsafe_source" --calendar "$unsafe_target"
|
||||
cmp "$unsafe_target" "$tmpdir/import-$unsafe_name-before.ics" >/dev/null ||
|
||||
fail "$unsafe_name import changed the target"
|
||||
[ ! -e "$unsafe_target.bak" ] ||
|
||||
fail "$unsafe_name import created a backup"
|
||||
done
|
||||
|
||||
# Identity validation applies equally to the target and leaves its exact bytes
|
||||
# untouched when it fails.
|
||||
duplicate_uid_target=$tmpdir/duplicate-uid-target.ics
|
||||
cp "$duplicate_uid" "$duplicate_uid_target"
|
||||
cp "$duplicate_uid_target" "$tmpdir/duplicate-uid-target-before.ics"
|
||||
expect_failure "duplicate-UID target import" \
|
||||
"$nocal" --import "$source_calendar" --calendar "$duplicate_uid_target"
|
||||
cmp "$duplicate_uid_target" "$tmpdir/duplicate-uid-target-before.ics" >/dev/null ||
|
||||
fail "import changed a duplicate-UID target"
|
||||
[ ! -e "$duplicate_uid_target.bak" ] ||
|
||||
fail "import into a duplicate-UID target created a backup"
|
||||
|
||||
# An unsafe target is read-only even when the source is valid.
|
||||
read_only_target=$tmpdir/read-only-target.ics
|
||||
cp "$unsupported" "$read_only_target"
|
||||
cp "$read_only_target" "$tmpdir/read-only-target-before.ics"
|
||||
expect_failure "read-only target import" \
|
||||
"$nocal" --import "$source_calendar" --calendar "$read_only_target"
|
||||
cmp "$read_only_target" "$tmpdir/read-only-target-before.ics" >/dev/null ||
|
||||
fail "import changed a read-only target"
|
||||
[ ! -e "$read_only_target.bak" ] ||
|
||||
fail "import into a read-only target created a backup"
|
||||
|
||||
# Export creates a new parseable calendar. Re-exporting that calendar yields
|
||||
# identical canonical bytes, demonstrating equivalent parsed appointments.
|
||||
exported=$tmpdir/exported.ics
|
||||
export_summary=$(
|
||||
"$nocal" --export "$exported" --calendar "$source_calendar"
|
||||
)
|
||||
printf '%s\n' "$export_summary" | grep -E 'Exported[[:space:]]+2([^0-9]|$)' >/dev/null ||
|
||||
fail "export summary does not report two exported appointments"
|
||||
[ -f "$exported" ] || fail "successful export did not create its destination"
|
||||
[ ! -e "$exported.bak" ] || fail "successful new export unexpectedly created a backup"
|
||||
canonical_copy=$tmpdir/exported-again.ics
|
||||
"$nocal" --export "$canonical_copy" --calendar "$exported" >"$tmpdir/out"
|
||||
cmp "$exported" "$canonical_copy" >/dev/null ||
|
||||
fail "exported calendar did not parse and round-trip equivalently"
|
||||
|
||||
# Existing export destinations are never overwritten or backed up.
|
||||
existing_destination=$tmpdir/existing-destination.ics
|
||||
printf '%s\n' 'sentinel destination bytes' >"$existing_destination"
|
||||
cp "$existing_destination" "$tmpdir/existing-destination-before"
|
||||
expect_failure "export to existing destination" \
|
||||
"$nocal" --export "$existing_destination" --calendar "$source_calendar"
|
||||
cmp "$existing_destination" "$tmpdir/existing-destination-before" >/dev/null ||
|
||||
fail "failed export changed its existing destination"
|
||||
[ ! -e "$existing_destination.bak" ] ||
|
||||
fail "failed export created a backup for its existing destination"
|
||||
|
||||
# Unsafe export sources fail without creating any destination or backup.
|
||||
for unsafe_source in "$missing_uid" "$duplicate_uid" "$unsupported"; do
|
||||
unsafe_name=$(basename "$unsafe_source" .ics)
|
||||
unsafe_destination=$tmpdir/export-$unsafe_name-output.ics
|
||||
expect_failure "$unsafe_name export" \
|
||||
"$nocal" --export "$unsafe_destination" --calendar "$unsafe_source"
|
||||
[ ! -e "$unsafe_destination" ] ||
|
||||
fail "$unsafe_name export created an output file"
|
||||
[ ! -e "$unsafe_destination.bak" ] ||
|
||||
fail "$unsafe_name export created an output backup"
|
||||
done
|
||||
|
||||
# Transfer actions are exclusive and cannot be combined with interactive,
|
||||
# print, demo, or recovery actions. These parse failures must occur without a
|
||||
# destination write or target backup.
|
||||
option_target=$tmpdir/option-target.ics
|
||||
write_target "$option_target"
|
||||
cp "$option_target" "$tmpdir/option-target-before.ics"
|
||||
option_export=$tmpdir/option-export.ics
|
||||
|
||||
expect_failure "combined import and export" \
|
||||
"$nocal" --import "$source_calendar" --export "$option_export" \
|
||||
--calendar "$option_target"
|
||||
|
||||
for conflicting_option in --demo --print --restore-backup; do
|
||||
expect_failure "import with $conflicting_option" \
|
||||
"$nocal" --import "$source_calendar" "$conflicting_option" \
|
||||
--calendar "$option_target"
|
||||
expect_failure "export with $conflicting_option" \
|
||||
"$nocal" --export "$option_export" "$conflicting_option" \
|
||||
--calendar "$option_target"
|
||||
done
|
||||
|
||||
cmp "$option_target" "$tmpdir/option-target-before.ics" >/dev/null ||
|
||||
fail "invalid transfer option combination changed the calendar"
|
||||
[ ! -e "$option_target.bak" ] ||
|
||||
fail "invalid transfer option combination created a calendar backup"
|
||||
[ ! -e "$option_export" ] ||
|
||||
fail "invalid transfer option combination created an export destination"
|
||||
Reference in New Issue
Block a user