calendav 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|