calendav 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -2
- data/README.md +183 -39
- data/lib/calendav/calendar.rb +18 -42
- data/lib/calendav/client.rb +3 -5
- data/lib/calendav/clients/calendars_client.rb +60 -18
- data/lib/calendav/clients/events_client.rb +32 -13
- data/lib/calendav/contextual_url.rb +6 -3
- data/lib/calendav/endpoint.rb +43 -13
- data/lib/calendav/errors.rb +28 -0
- data/lib/calendav/event.rb +13 -35
- data/lib/calendav/multi_response.rb +27 -0
- data/lib/calendav/namespaces.rb +10 -0
- data/lib/calendav/parsers/calendar_xml.rb +58 -0
- data/lib/calendav/parsers/event_xml.rb +37 -0
- data/lib/calendav/parsers/response_xml.rb +48 -0
- data/lib/calendav/parsers/sync_xml.rb +57 -0
- data/lib/calendav/requests/calendar_home_set.rb +2 -2
- data/lib/calendav/requests/current_user_principal.rb +2 -2
- data/lib/calendav/requests/list_calendars.rb +32 -7
- data/lib/calendav/requests/list_events.rb +22 -10
- data/lib/calendav/requests/make_calendar.rb +8 -2
- data/lib/calendav/requests/sync_collection.rb +36 -0
- data/lib/calendav/requests/update_calendar.rb +63 -0
- data/lib/calendav/sync_collection.rb +13 -0
- data/spec/acceptance/apple_spec.rb +217 -0
- data/spec/acceptance/google_spec.rb +167 -26
- data/spec/spec_helper.rb +2 -0
- data/spec/support/encoded_matchers.rb +29 -0
- metadata +34 -7
- data/lib/calendav/xml_processor.rb +0 -42
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
require_relative "../sync_collection"
|
6
|
+
|
7
|
+
module Calendav
|
8
|
+
module Parsers
|
9
|
+
class SyncXML
|
10
|
+
def self.call(...)
|
11
|
+
new(...).call
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(calendar_url, multi_response)
|
15
|
+
@calendar_url = calendar_url
|
16
|
+
@multi_response = multi_response
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
SyncCollection.new(events, deleted_urls, token)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :calendar_url, :multi_response
|
26
|
+
|
27
|
+
def deleted_urls
|
28
|
+
individual_responses
|
29
|
+
.select { |node| node.xpath("./dav:status").text["404 Not Found"] }
|
30
|
+
.collect { |node| response_url(node) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def events
|
34
|
+
individual_responses
|
35
|
+
.reject { |node| node.xpath("./dav:status").text["404 Not Found"] }
|
36
|
+
.collect { |node| Event.from_xml(calendar_url, node) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def calendar_path
|
40
|
+
@calendar_path ||= URI(calendar_url).path
|
41
|
+
end
|
42
|
+
|
43
|
+
def individual_responses
|
44
|
+
multi_response
|
45
|
+
.reject { |node| node.xpath("./dav:href").text == calendar_path }
|
46
|
+
end
|
47
|
+
|
48
|
+
def token
|
49
|
+
multi_response.xpath("/dav:multistatus/dav:sync-token").text
|
50
|
+
end
|
51
|
+
|
52
|
+
def response_url(response)
|
53
|
+
ContextualURL.call(calendar_url, response.xpath("./dav:href").text)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
4
|
|
5
|
-
require_relative "../
|
5
|
+
require_relative "../namespaces"
|
6
6
|
|
7
7
|
module Calendav
|
8
8
|
module Requests
|
@@ -13,7 +13,7 @@ module Calendav
|
|
13
13
|
|
14
14
|
def call
|
15
15
|
Nokogiri::XML::Builder.new do |xml|
|
16
|
-
xml["dav"].propfind(
|
16
|
+
xml["dav"].propfind(NAMESPACES) do
|
17
17
|
xml["dav"].prop do
|
18
18
|
xml["caldav"].public_send(:"calendar-home-set")
|
19
19
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
4
|
|
5
|
-
require_relative "../
|
5
|
+
require_relative "../namespaces"
|
6
6
|
|
7
7
|
module Calendav
|
8
8
|
module Requests
|
@@ -13,7 +13,7 @@ module Calendav
|
|
13
13
|
|
14
14
|
def call
|
15
15
|
Nokogiri::XML::Builder.new do |xml|
|
16
|
-
xml["dav"].propfind(
|
16
|
+
xml["dav"].propfind(NAMESPACES) do
|
17
17
|
xml["dav"].prop do
|
18
18
|
xml["dav"].public_send(:"current-user-principal")
|
19
19
|
end
|
@@ -2,28 +2,53 @@
|
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
4
|
|
5
|
-
require_relative "../
|
5
|
+
require_relative "../namespaces"
|
6
6
|
|
7
7
|
module Calendav
|
8
8
|
module Requests
|
9
9
|
class ListCalendars
|
10
|
+
PROPERTIES = [
|
11
|
+
{ key: :display_name, namespace: "dav", name: "displayname" },
|
12
|
+
{ key: :resource_type, namespace: "dav", name: "resourcetype" },
|
13
|
+
{ key: :etag, namespace: "dav", name: "getetag" },
|
14
|
+
{ key: :ctag, namespace: "cs", name: "getctag" },
|
15
|
+
{ key: :color, namespace: "apple", name: "calendar-color" },
|
16
|
+
{ key: :sync_token, namespace: "dav", name: "sync-token" },
|
17
|
+
{ key: :reports, namespace: "dav", name: "supported-report-set" },
|
18
|
+
{
|
19
|
+
key: :components,
|
20
|
+
namespace: "caldav",
|
21
|
+
name: "supported-calendar-component-set"
|
22
|
+
}
|
23
|
+
].freeze
|
24
|
+
|
10
25
|
def self.call(...)
|
11
26
|
new(...).call
|
12
27
|
end
|
13
28
|
|
29
|
+
def initialize(attributes)
|
30
|
+
@attributes = attributes
|
31
|
+
end
|
32
|
+
|
14
33
|
def call
|
15
34
|
Nokogiri::XML::Builder.new do |xml|
|
16
|
-
xml["dav"].propfind(
|
35
|
+
xml["dav"].propfind(NAMESPACES) do
|
17
36
|
xml["dav"].prop do
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
xml["cs"].getctag
|
22
|
-
xml["apple"].public_send(:"calendar-color")
|
37
|
+
properties.each do |hash|
|
38
|
+
xml[hash[:namespace]].public_send(hash[:name].to_sym)
|
39
|
+
end
|
23
40
|
end
|
24
41
|
end
|
25
42
|
end
|
26
43
|
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :attributes
|
48
|
+
|
49
|
+
def properties
|
50
|
+
PROPERTIES.select { |hash| attributes.include?(hash[:key]) }
|
51
|
+
end
|
27
52
|
end
|
28
53
|
end
|
29
54
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
4
|
|
5
|
-
require_relative "../
|
5
|
+
require_relative "../namespaces"
|
6
6
|
|
7
7
|
module Calendav
|
8
8
|
module Requests
|
@@ -18,9 +18,7 @@ module Calendav
|
|
18
18
|
|
19
19
|
def call
|
20
20
|
Nokogiri::XML::Builder.new do |xml|
|
21
|
-
xml["caldav"].public_send(
|
22
|
-
"calendar-query", XMLProcessor::NAMESPACES
|
23
|
-
) do
|
21
|
+
xml["caldav"].public_send("calendar-query", NAMESPACES) do
|
24
22
|
xml["dav"].prop do
|
25
23
|
xml["dav"].getetag
|
26
24
|
xml["caldav"].public_send(:"calendar-data")
|
@@ -28,11 +26,11 @@ module Calendav
|
|
28
26
|
xml["caldav"].filter do
|
29
27
|
xml["caldav"].public_send(:"comp-filter", name: "VCALENDAR") do
|
30
28
|
xml["caldav"].public_send(:"comp-filter", name: "VEVENT") do
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
if range?
|
30
|
+
xml["caldav"].public_send(
|
31
|
+
:"time-range", start: from, end: to
|
32
|
+
)
|
33
|
+
end
|
36
34
|
end
|
37
35
|
end
|
38
36
|
end
|
@@ -42,7 +40,21 @@ module Calendav
|
|
42
40
|
|
43
41
|
private
|
44
42
|
|
45
|
-
|
43
|
+
def from
|
44
|
+
return nil if @from.nil?
|
45
|
+
|
46
|
+
@from.utc.iso8601.delete(":-")
|
47
|
+
end
|
48
|
+
|
49
|
+
def to
|
50
|
+
return nil if @to.nil?
|
51
|
+
|
52
|
+
@to.utc.iso8601.delete(":-")
|
53
|
+
end
|
54
|
+
|
55
|
+
def range?
|
56
|
+
to || from
|
57
|
+
end
|
46
58
|
end
|
47
59
|
end
|
48
60
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
4
|
|
5
|
-
require_relative "../
|
5
|
+
require_relative "../namespaces"
|
6
6
|
|
7
7
|
module Calendav
|
8
8
|
module Requests
|
@@ -17,7 +17,7 @@ module Calendav
|
|
17
17
|
|
18
18
|
def call
|
19
19
|
Nokogiri::XML::Builder.new do |xml|
|
20
|
-
xml["caldav"].
|
20
|
+
xml["caldav"].mkcalendar(NAMESPACES) do
|
21
21
|
xml["dav"].set do
|
22
22
|
xml["dav"].prop do
|
23
23
|
xml["dav"].displayname display_name
|
@@ -32,6 +32,8 @@ module Calendav
|
|
32
32
|
xml["caldav"].public_send(:"calendar-timezone", time_zone)
|
33
33
|
end
|
34
34
|
|
35
|
+
xml["apple"].public_send(:"calendar-color", color) if color
|
36
|
+
|
35
37
|
xml["caldav"].public_send(
|
36
38
|
:"supported-calendar-component-set"
|
37
39
|
) do
|
@@ -47,6 +49,10 @@ module Calendav
|
|
47
49
|
|
48
50
|
attr_reader :attributes
|
49
51
|
|
52
|
+
def color
|
53
|
+
attributes[:color]
|
54
|
+
end
|
55
|
+
|
50
56
|
def display_name
|
51
57
|
attributes[:display_name]
|
52
58
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require_relative "../namespaces"
|
6
|
+
|
7
|
+
module Calendav
|
8
|
+
module Requests
|
9
|
+
class SyncCollection
|
10
|
+
def self.call(...)
|
11
|
+
new(...).call
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(token)
|
15
|
+
@token = token
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
Nokogiri::XML::Builder.new do |xml|
|
20
|
+
xml["dav"].public_send(:"sync-collection", NAMESPACES) do
|
21
|
+
xml["dav"].public_send(:"sync-token", token)
|
22
|
+
xml["dav"].public_send(:"sync-level", 1)
|
23
|
+
xml["dav"].prop do
|
24
|
+
xml["dav"].getetag
|
25
|
+
xml["caldav"].public_send(:"calendar-data")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :token
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require_relative "../namespaces"
|
6
|
+
|
7
|
+
module Calendav
|
8
|
+
module Requests
|
9
|
+
class UpdateCalendar
|
10
|
+
def self.call(...)
|
11
|
+
new(...).call
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(attributes)
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
Nokogiri::XML::Builder.new do |xml|
|
20
|
+
xml["dav"].propertyupdate(NAMESPACES) do
|
21
|
+
xml["dav"].set do
|
22
|
+
xml["dav"].prop do
|
23
|
+
xml["dav"].displayname display_name if display_name
|
24
|
+
|
25
|
+
if description
|
26
|
+
xml["caldav"].public_send(
|
27
|
+
:"calendar-description", description
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
if time_zone
|
32
|
+
xml["caldav"].public_send(:"calendar-timezone", time_zone)
|
33
|
+
end
|
34
|
+
|
35
|
+
xml["apple"].public_send(:"calendar-color", color) if color
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :attributes
|
45
|
+
|
46
|
+
def color
|
47
|
+
attributes[:color]
|
48
|
+
end
|
49
|
+
|
50
|
+
def display_name
|
51
|
+
attributes[:display_name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def description
|
55
|
+
attributes[:description]
|
56
|
+
end
|
57
|
+
|
58
|
+
def time_zone
|
59
|
+
attributes[:time_zone]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Calendav
|
4
|
+
class SyncCollection
|
5
|
+
attr_reader :changes, :deletions, :sync_token
|
6
|
+
|
7
|
+
def initialize(changes, deletions, sync_token)
|
8
|
+
@changes = changes
|
9
|
+
@deletions = deletions
|
10
|
+
@sync_token = sync_token
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "icalendar"
|
4
|
+
require "icalendar/tzinfo"
|
5
|
+
require "securerandom"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
RSpec.describe "Apple" do
|
9
|
+
let(:provider) { :apple }
|
10
|
+
let(:username) { ENV.fetch("APPLE_USERNAME") }
|
11
|
+
let(:password) { ENV.fetch("APPLE_PASSWORD") }
|
12
|
+
let(:credentials) { Calendav.credentials(provider, username, password) }
|
13
|
+
|
14
|
+
subject { Calendav.client(credentials) }
|
15
|
+
|
16
|
+
it "determines the user's principal URL" do
|
17
|
+
expect(subject.principal_url)
|
18
|
+
.to eq_encoded_url("https://caldav.icloud.com/20264203208/principal/")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "determines the user's calendar URL" do
|
22
|
+
expect(subject.calendars.home_url)
|
23
|
+
.to eq_encoded_url("https://p49-caldav.icloud.com/20264203208/calendars/")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "supports calendar creation" do
|
27
|
+
expect(subject.calendars.create?).to eq(true)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "can create, find, update and delete calendars" do
|
31
|
+
identifier = SecureRandom.uuid
|
32
|
+
time_zone = TZInfo::Timezone.get "UTC"
|
33
|
+
ical_time_zone = time_zone.ical_timezone Time.now.utc
|
34
|
+
|
35
|
+
url = subject.calendars.create(
|
36
|
+
identifier,
|
37
|
+
display_name: "Calendav Test",
|
38
|
+
description: "For test purposes only",
|
39
|
+
color: "#00FF00",
|
40
|
+
time_zone: ical_time_zone.to_ical
|
41
|
+
)
|
42
|
+
expect(url).to include(URI.decode_www_form_component(identifier))
|
43
|
+
expect(url).to start_with("https://")
|
44
|
+
|
45
|
+
calendars = subject.calendars.list
|
46
|
+
expect(calendars.collect(&:display_name)).to include("Calendav Test")
|
47
|
+
|
48
|
+
expect(
|
49
|
+
subject.calendars.update(url, display_name: "Calendav Update")
|
50
|
+
).to eq(true)
|
51
|
+
|
52
|
+
calendars = subject.calendars.list
|
53
|
+
expect(calendars.collect(&:display_name)).to include("Calendav Update")
|
54
|
+
|
55
|
+
subject.calendars.delete(url)
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with a calendar" do
|
59
|
+
let(:calendar_url) do
|
60
|
+
subject.calendars.create(SecureRandom.uuid, display_name: "Calendav Test")
|
61
|
+
end
|
62
|
+
let(:calendar) { subject.calendars.find(calendar_url) }
|
63
|
+
let(:identifier) { "#{SecureRandom.uuid}.ics" }
|
64
|
+
let(:start) { Time.new 2021, 6, 1, 10, 30 }
|
65
|
+
let(:finish) { Time.new 2021, 6, 1, 12, 30 }
|
66
|
+
|
67
|
+
after :each do
|
68
|
+
subject.calendars.delete(calendar_url)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "supports events" do
|
72
|
+
expect(calendar.components).to include("VEVENT")
|
73
|
+
end
|
74
|
+
|
75
|
+
it "supports WebDAV-Sync" do
|
76
|
+
expect(calendar.reports).to include("sync-collection")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "can create, find, update and delete events" do
|
80
|
+
ics = Icalendar::Calendar.new
|
81
|
+
ics.event do |event|
|
82
|
+
event.dtstart = start.utc
|
83
|
+
event.dtend = finish.utc
|
84
|
+
event.summary = "Brunch"
|
85
|
+
end
|
86
|
+
ics.publish
|
87
|
+
|
88
|
+
# Create an event
|
89
|
+
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
90
|
+
expect(event_url).to include(URI.decode_www_form_component(calendar.url))
|
91
|
+
|
92
|
+
# Search for the event
|
93
|
+
events = subject.events.list(
|
94
|
+
calendar.url, from: Time.new(2021, 6, 1), to: Time.new(2021, 6, 2)
|
95
|
+
)
|
96
|
+
expect(events.length).to eq(1)
|
97
|
+
expect(events.first.summary).to eq("Brunch")
|
98
|
+
expect(events.first.url).to eq_encoded_url(event_url)
|
99
|
+
|
100
|
+
# Update the event
|
101
|
+
ics.events.first.dtstart = Time.new(2021, 7, 1, 10, 30).utc
|
102
|
+
ics.events.first.dtend = Time.new(2021, 7, 1, 12, 30).utc
|
103
|
+
subject.events.update(event_url, ics.to_ical)
|
104
|
+
|
105
|
+
# Search again
|
106
|
+
events = subject.events.list(
|
107
|
+
calendar.url, from: Time.new(2021, 7, 1), to: Time.new(2021, 7, 2)
|
108
|
+
)
|
109
|
+
expect(events.length).to eq(1)
|
110
|
+
expect(events.first.summary).to eq("Brunch")
|
111
|
+
expect(events.first.url).to eq_encoded_url(event_url)
|
112
|
+
|
113
|
+
# Create another event
|
114
|
+
ics = Icalendar::Calendar.new
|
115
|
+
ics.event do |event|
|
116
|
+
event.dtstart = start.utc
|
117
|
+
event.dtend = finish.utc
|
118
|
+
event.summary = "Brunch"
|
119
|
+
end
|
120
|
+
ics.publish
|
121
|
+
|
122
|
+
another_url = subject.events.create(
|
123
|
+
calendar.url, "#{SecureRandom.uuid}.ics", ics.to_ical
|
124
|
+
)
|
125
|
+
|
126
|
+
# Search for all events
|
127
|
+
events = subject.events.list(calendar.url)
|
128
|
+
expect(events.length).to eq(2)
|
129
|
+
|
130
|
+
# Delete the events
|
131
|
+
expect(subject.events.delete(event_url)).to eq(true)
|
132
|
+
expect(subject.events.delete(another_url)).to eq(true)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "respects etag conditions with updates and deletions" do
|
136
|
+
ics = Icalendar::Calendar.new
|
137
|
+
ics.event do |event|
|
138
|
+
event.dtstart = start.utc
|
139
|
+
event.dtend = finish.utc
|
140
|
+
event.summary = "Brunch"
|
141
|
+
end
|
142
|
+
ics.publish
|
143
|
+
|
144
|
+
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
145
|
+
event = subject.events.find(event_url)
|
146
|
+
|
147
|
+
ics.events.first.summary = "Coffee"
|
148
|
+
expect(
|
149
|
+
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
150
|
+
).to eq(true)
|
151
|
+
|
152
|
+
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
153
|
+
|
154
|
+
# Updating with the old etag should fail
|
155
|
+
ics.events.first.summary = "Brunch"
|
156
|
+
expect(
|
157
|
+
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
158
|
+
).to eq(false)
|
159
|
+
|
160
|
+
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
161
|
+
|
162
|
+
expect(subject.events.delete(event_url, etag: event.etag)).to eq(false)
|
163
|
+
expect(subject.events.delete(event_url)).to eq(true)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "handles synchronisation requests" do
|
167
|
+
first = Icalendar::Calendar.new
|
168
|
+
first.event do |event|
|
169
|
+
event.dtstart = start.utc
|
170
|
+
event.dtend = finish.utc
|
171
|
+
event.summary = "Brunch"
|
172
|
+
end
|
173
|
+
first.publish
|
174
|
+
|
175
|
+
first_url = subject.events.create(calendar.url, identifier, first.to_ical)
|
176
|
+
token = subject.calendars.find(calendar.url, sync: true).sync_token
|
177
|
+
|
178
|
+
events = subject.events.list(calendar.url)
|
179
|
+
expect(events.length).to eq(1)
|
180
|
+
|
181
|
+
second = Icalendar::Calendar.new
|
182
|
+
second.event do |event|
|
183
|
+
event.dtstart = start.utc
|
184
|
+
event.dtend = finish.utc
|
185
|
+
event.summary = "Brunch Again"
|
186
|
+
end
|
187
|
+
second.publish
|
188
|
+
second_url = subject.events.create(
|
189
|
+
calendar.url, "#{SecureRandom.uuid}.ics", second.to_ical
|
190
|
+
)
|
191
|
+
|
192
|
+
first.events.first.summary = "Coffee"
|
193
|
+
subject.events.update(first_url, first.to_ical)
|
194
|
+
|
195
|
+
collection = subject.calendars.sync(calendar.url, token)
|
196
|
+
expect(collection.changes.collect(&:url))
|
197
|
+
.to match_encoded_urls([first_url, second_url])
|
198
|
+
|
199
|
+
expect(collection.deletions).to be_empty
|
200
|
+
|
201
|
+
first.events.first.summary = "Brunch"
|
202
|
+
subject.events.update(first_url, first.to_ical)
|
203
|
+
|
204
|
+
subject.events.delete(second_url)
|
205
|
+
|
206
|
+
collection = subject.calendars.sync(calendar.url, collection.sync_token)
|
207
|
+
urls = collection.changes.collect(&:url)
|
208
|
+
expect(urls.length).to eq(1)
|
209
|
+
expect(urls[0]).to eq_encoded_url(first_url)
|
210
|
+
|
211
|
+
expect(collection.deletions.length).to eq(1)
|
212
|
+
expect(collection.deletions.first).to eq_encoded_url(second_url)
|
213
|
+
|
214
|
+
subject.events.delete(first_url)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|