calendlyr 0.7.5 → 1.0.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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -5
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +98 -0
  5. data/README.md +209 -73
  6. data/calendlyr.gemspec +8 -6
  7. data/docs/resources/activity_log/list_activity_log_entries.md +2 -1
  8. data/docs/resources/availabilities/user_availability_schedule.md +3 -2
  9. data/docs/resources/availabilities/user_busy_time.md +3 -2
  10. data/docs/resources/data_compliance.md +1 -1
  11. data/docs/resources/event_types/availability_schedule.md +28 -0
  12. data/docs/resources/event_types/available_time.md +15 -11
  13. data/docs/resources/event_types/event_type.md +21 -9
  14. data/docs/resources/event_types/membership.md +15 -16
  15. data/docs/resources/events/cancellation.md +1 -1
  16. data/docs/resources/events/event.md +6 -5
  17. data/docs/resources/events/invitee.md +18 -0
  18. data/docs/resources/events/invitee_no_show.md +2 -1
  19. data/docs/resources/groups/group.md +2 -1
  20. data/docs/resources/locations/location.md +16 -0
  21. data/docs/resources/organizations/membership.md +3 -2
  22. data/docs/resources/organizations/organization.md +13 -9
  23. data/docs/resources/outgoing_communications/outgoing_communication.md +28 -0
  24. data/docs/resources/routing_forms/routing_form.md +2 -1
  25. data/docs/resources/routing_forms/submission.md +2 -1
  26. data/docs/resources/scheduling_links/scheduling_link.md +26 -0
  27. data/docs/resources/share.md +4 -10
  28. data/docs/resources/webhooks/invitee_payload.md +10 -16
  29. data/docs/resources/webhooks/payload.md +32 -3
  30. data/docs/resources/webhooks/sample.md +23 -0
  31. data/docs/resources/webhooks/subscription.md +10 -6
  32. data/lib/calendlyr/client.rb +7 -2
  33. data/lib/calendlyr/collection.rb +42 -9
  34. data/lib/calendlyr/configuration.rb +24 -0
  35. data/lib/calendlyr/error.rb +65 -27
  36. data/lib/calendlyr/object.rb +85 -13
  37. data/lib/calendlyr/objects/event_type.rb +0 -4
  38. data/lib/calendlyr/objects/event_types/availability_schedule.rb +8 -0
  39. data/lib/calendlyr/objects/event_types/available_time.rb +5 -1
  40. data/lib/calendlyr/objects/event_types/membership.rb +4 -7
  41. data/lib/calendlyr/objects/events/invitee.rb +1 -1
  42. data/lib/calendlyr/objects/location.rb +4 -0
  43. data/lib/calendlyr/objects/organization.rb +0 -4
  44. data/lib/calendlyr/objects/outgoing_communication.rb +6 -0
  45. data/lib/calendlyr/objects/scheduling_link.rb +2 -5
  46. data/lib/calendlyr/objects/share.rb +0 -5
  47. data/lib/calendlyr/objects/webhooks/payload.rb +30 -0
  48. data/lib/calendlyr/resource.rb +84 -12
  49. data/lib/calendlyr/resources/availability.rb +14 -2
  50. data/lib/calendlyr/resources/event_types.rb +45 -5
  51. data/lib/calendlyr/resources/events.rb +20 -2
  52. data/lib/calendlyr/resources/groups.rb +14 -2
  53. data/lib/calendlyr/resources/locations.rb +13 -0
  54. data/lib/calendlyr/resources/organizations.rb +25 -3
  55. data/lib/calendlyr/resources/outgoing_communications.rb +11 -3
  56. data/lib/calendlyr/resources/routing_forms.rb +14 -2
  57. data/lib/calendlyr/resources/scheduling_links.rb +5 -2
  58. data/lib/calendlyr/resources/shares.rb +1 -0
  59. data/lib/calendlyr/resources/webhooks.rb +11 -3
  60. data/lib/calendlyr/version.rb +1 -1
  61. data/lib/calendlyr/webhook.rb +105 -0
  62. data/lib/calendlyr.rb +50 -0
  63. data/logos/calendlyr.png +0 -0
  64. data/logos/calendlyr_bg_white.png +0 -0
  65. data/test/calendlyr/client_test.rb +29 -0
  66. data/test/calendlyr/collection_test.rb +168 -0
  67. data/test/calendlyr/configuration_test.rb +157 -0
  68. data/test/calendlyr/object_test.rb +82 -1
  69. data/test/calendlyr/objects/event_type_test.rb +0 -15
  70. data/test/calendlyr/objects/event_types/availability_schedule_test.rb +20 -0
  71. data/test/calendlyr/objects/events/cancellation_test.rb +1 -1
  72. data/test/calendlyr/objects/events/guest_test.rb +1 -1
  73. data/test/calendlyr/objects/events/invitee_no_show_test.rb +1 -1
  74. data/test/calendlyr/objects/events/invitee_test.rb +10 -3
  75. data/test/calendlyr/objects/location_test.rb +22 -0
  76. data/test/calendlyr/objects/organization_test.rb +0 -8
  77. data/test/calendlyr/objects/organizations/invitation_test.rb +1 -1
  78. data/test/calendlyr/objects/share_test.rb +3 -9
  79. data/test/calendlyr/objects/webhooks/payload_test.rb +15 -0
  80. data/test/calendlyr/resource_test.rb +456 -2
  81. data/test/calendlyr/resources/availabilities/user_busy_times_test.rb +26 -0
  82. data/test/calendlyr/resources/availabilities/user_schedules_test.rb +25 -0
  83. data/test/calendlyr/resources/data_compliance_test.rb +1 -4
  84. data/test/calendlyr/resources/event_types_test.rb +132 -0
  85. data/test/calendlyr/resources/events_test.rb +87 -0
  86. data/test/calendlyr/resources/groups_test.rb +54 -0
  87. data/test/calendlyr/resources/locations_test.rb +30 -0
  88. data/test/calendlyr/resources/organizations_test.rb +96 -2
  89. data/test/calendlyr/resources/outgoing_communications_test.rb +34 -8
  90. data/test/calendlyr/resources/routing_forms_test.rb +57 -0
  91. data/test/calendlyr/resources/scheduling_links_test.rb +31 -6
  92. data/test/calendlyr/resources/shares_test.rb +15 -0
  93. data/test/calendlyr/resources/webhooks_test.rb +63 -5
  94. data/test/calendlyr/webhook_test.rb +292 -0
  95. data/test/fixtures/activity_log/list_page2.json +30 -0
  96. data/test/fixtures/event_invitees/list_page2.json +35 -0
  97. data/test/fixtures/event_invitees/retrieve.json +11 -1
  98. data/test/fixtures/event_type_availability_schedules/list.json +17 -0
  99. data/test/fixtures/event_type_availability_schedules/update.json +3 -0
  100. data/test/fixtures/event_type_available_times/list.json +0 -12
  101. data/test/fixtures/event_type_memberships/list.json +43 -0
  102. data/test/fixtures/event_type_memberships/list_page2.json +33 -0
  103. data/test/fixtures/event_types/create.json +30 -0
  104. data/test/fixtures/event_types/list_page2.json +37 -0
  105. data/test/fixtures/event_types/update.json +30 -0
  106. data/test/fixtures/events/create_invitee.json +37 -0
  107. data/test/fixtures/events/list_page2.json +29 -0
  108. data/test/fixtures/events/retrieve.json +12 -2
  109. data/test/fixtures/group_relationships/list_page2.json +35 -0
  110. data/test/fixtures/groups/list_page2.json +16 -0
  111. data/test/fixtures/locations/list.json +16 -0
  112. data/test/fixtures/locations/list_page2.json +16 -0
  113. data/test/fixtures/objects/event.json +10 -2
  114. data/test/fixtures/objects/event_types/availability_schedule.json +6 -0
  115. data/test/fixtures/objects/location.json +5 -0
  116. data/test/fixtures/organizations/list_invitations_page2.json +18 -0
  117. data/test/fixtures/organizations/list_memberships_page2.json +26 -0
  118. data/test/fixtures/organizations/retrieve.json +11 -0
  119. data/test/fixtures/outgoing_communications/list.json +4 -6
  120. data/test/fixtures/outgoing_communications/list_page2.json +21 -0
  121. data/test/fixtures/routing_forms/list_page2.json +17 -0
  122. data/test/fixtures/routing_forms/list_routing_form_submission_page2.json +29 -0
  123. data/test/fixtures/user_availability_schedules/list_page1.json +16 -0
  124. data/test/fixtures/user_availability_schedules/list_page2.json +16 -0
  125. data/test/fixtures/user_busy_times/list_page1.json +18 -0
  126. data/test/fixtures/user_busy_times/list_page2.json +13 -0
  127. data/test/fixtures/webhooks/list_page2.json +23 -0
  128. data/test/fixtures/webhooks/sample.json +55 -94
  129. data/test/test_helper.rb +19 -7
  130. metadata +70 -27
  131. data/docs/resources/scheduling_link.md +0 -26
  132. data/test/calendlyr/objects/event_types/available_time_test.rb +0 -20
  133. data/test/calendlyr/objects/event_types/membership_test.rb +0 -32
  134. data/test/calendlyr/objects/scheduling_link_test.rb +0 -17
  135. data/test/calendlyr/resources/event_types/membership_test.rb +0 -22
  136. data/test/fixtures/objects/event_types/available_time.json +0 -6
  137. data/test/fixtures/objects/event_types/membership.json +0 -65
  138. data/test/fixtures/objects/scheduling_links/event_type.json +0 -5
@@ -1,8 +1,14 @@
1
1
  module Calendlyr
2
2
  class GroupsResource < Resource
3
3
  def list(organization:, **params)
4
+ next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
5
+ organization = expand_uri(organization, "organizations")
4
6
  response = get_request("groups", params: params.merge(organization: organization))
5
- Collection.from_response(response, type: Group, client: client)
7
+ Collection.from_response(response, type: Group, client: client, next_page_caller: next_page_caller)
8
+ end
9
+
10
+ def list_all(organization:, **params)
11
+ list(organization: organization, **params).auto_paginate.to_a
6
12
  end
7
13
 
8
14
  def retrieve(uuid:)
@@ -11,8 +17,14 @@ module Calendlyr
11
17
 
12
18
  # Relationships
13
19
  def list_relationships(**params)
20
+ next_page_caller = ->(page_token:) { list_relationships(**params, page_token: page_token) }
21
+ params[:organization] = expand_uri(params[:organization], "organizations") if params[:organization]
14
22
  response = get_request("group_relationships", params: params)
15
- Collection.from_response(response, type: Groups::Relationship, client: client)
23
+ Collection.from_response(response, type: Groups::Relationship, client: client, next_page_caller: next_page_caller)
24
+ end
25
+
26
+ def list_all_relationships(**params)
27
+ list_relationships(**params).auto_paginate.to_a
16
28
  end
17
29
 
18
30
  def retrieve_relationship(uuid:)
@@ -0,0 +1,13 @@
1
+ module Calendlyr
2
+ class LocationsResource < Resource
3
+ def list(**params)
4
+ next_page_caller = ->(page_token:) { list(**params, page_token: page_token) }
5
+ response = get_request("locations", params: params)
6
+ Collection.from_response(response, type: Location, client: client, next_page_caller: next_page_caller)
7
+ end
8
+
9
+ def list_all(**params)
10
+ list(**params).auto_paginate.to_a
11
+ end
12
+ end
13
+ end
@@ -1,14 +1,31 @@
1
1
  module Calendlyr
2
2
  class OrganizationsResource < Resource
3
+ def retrieve(uuid:)
4
+ Organization.new get_request("organizations/#{uuid}").dig("resource").merge(client: client)
5
+ end
6
+
3
7
  def activity_log(organization: nil, **params)
8
+ next_page_caller = ->(page_token:) { activity_log(organization: organization, **params, page_token: page_token) }
9
+ organization = expand_uri(organization, "organizations")
4
10
  response = get_request("activity_log_entries", params: {organization: organization}.merge(params).compact)
5
- Collection.from_response(response, type: ActivityLog, client: client)
11
+ Collection.from_response(response, type: ActivityLog, client: client, next_page_caller: next_page_caller)
12
+ end
13
+
14
+ def list_all_activity_log(organization: nil, **params)
15
+ activity_log(organization: organization, **params).auto_paginate.to_a
6
16
  end
7
17
 
8
18
  # Memberships
9
19
  def list_memberships(**params)
20
+ next_page_caller = ->(page_token:) { list_memberships(**params, page_token: page_token) }
21
+ params[:organization] = expand_uri(params[:organization], "organizations") if params[:organization]
22
+ params[:user] = expand_uri(params[:user], "users") if params[:user]
10
23
  response = get_request("organization_memberships", params: params)
11
- Collection.from_response(response, type: Organizations::Membership, client: client)
24
+ Collection.from_response(response, type: Organizations::Membership, client: client, next_page_caller: next_page_caller)
25
+ end
26
+
27
+ def list_all_memberships(**params)
28
+ list_memberships(**params).auto_paginate.to_a
12
29
  end
13
30
 
14
31
  def retrieve_membership(uuid:)
@@ -21,8 +38,13 @@ module Calendlyr
21
38
 
22
39
  # Invitations
23
40
  def list_invitations(uuid:, **params)
41
+ next_page_caller = ->(page_token:) { list_invitations(uuid: uuid, **params, page_token: page_token) }
24
42
  response = get_request("organizations/#{uuid}/invitations", params: params)
25
- Collection.from_response(response, type: Organizations::Invitation, client: client)
43
+ Collection.from_response(response, type: Organizations::Invitation, client: client, next_page_caller: next_page_caller)
44
+ end
45
+
46
+ def list_all_invitations(uuid:, **params)
47
+ list_invitations(uuid: uuid, **params).auto_paginate.to_a
26
48
  end
27
49
 
28
50
  def retrieve_invitation(org_uuid:, uuid:)
@@ -1,8 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Calendlyr
2
4
  class OutgoingCommunicationsResource < Resource
3
- def list(**params)
4
- response = get_request("outgoing_communications", params: params)
5
- Collection.from_response(response, type: Object, client: client)
5
+ def list(organization:, **params)
6
+ next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
7
+ organization = expand_uri(organization, "organizations")
8
+ response = get_request("outgoing_communications", params: {organization: organization}.merge(params))
9
+ Collection.from_response(response, type: OutgoingCommunication, client: client, next_page_caller: next_page_caller)
10
+ end
11
+
12
+ def list_all(organization:, **params)
13
+ list(organization: organization, **params).auto_paginate.to_a
6
14
  end
7
15
  end
8
16
  end
@@ -1,8 +1,14 @@
1
1
  module Calendlyr
2
2
  class RoutingFormsResource < Resource
3
3
  def list(organization:, **params)
4
+ next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
5
+ organization = expand_uri(organization, "organizations")
4
6
  response = get_request("routing_forms", params: {organization: organization}.merge(params))
5
- Collection.from_response(response, type: RoutingForm, client: client)
7
+ Collection.from_response(response, type: RoutingForm, client: client, next_page_caller: next_page_caller)
8
+ end
9
+
10
+ def list_all(organization:, **params)
11
+ list(organization: organization, **params).auto_paginate.to_a
6
12
  end
7
13
 
8
14
  def retrieve(uuid:)
@@ -11,8 +17,14 @@ module Calendlyr
11
17
 
12
18
  # Routing Form Submission
13
19
  def list_submissions(form:, **params)
20
+ next_page_caller = ->(page_token:) { list_submissions(form: form, **params, page_token: page_token) }
21
+ form = expand_uri(form, "routing_forms")
14
22
  response = get_request("routing_form_submissions", params: {form: form}.merge(params))
15
- Collection.from_response(response, type: RoutingForms::Submission, client: client)
23
+ Collection.from_response(response, type: RoutingForms::Submission, client: client, next_page_caller: next_page_caller)
24
+ end
25
+
26
+ def list_all_submissions(form:, **params)
27
+ list_submissions(form: form, **params).auto_paginate.to_a
16
28
  end
17
29
 
18
30
  def retrieve_submission(uuid:)
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Calendlyr
2
4
  class SchedulingLinksResource < Resource
3
- def create(owner:, max_event_count:, owner_type: "EventType")
4
- body = {owner: owner, max_event_count: max_event_count, owner_type: owner_type}
5
+ def create(owner:, owner_type: "EventType", max_event_count: 1)
6
+ owner = expand_uri(owner, "event_types")
7
+ body = {max_event_count: max_event_count, owner: owner, owner_type: owner_type}
5
8
  SchedulingLink.new post_request("scheduling_links", body: body).dig("resource").merge(client: client)
6
9
  end
7
10
  end
@@ -1,6 +1,7 @@
1
1
  module Calendlyr
2
2
  class SharesResource < Resource
3
3
  def create(event_type:, **params)
4
+ event_type = expand_uri(event_type, "event_types")
4
5
  body = {event_type: event_type}.merge(params)
5
6
  Share.new post_request("shares", body: body).dig("resource").merge(client: client)
6
7
  end
@@ -1,11 +1,18 @@
1
1
  module Calendlyr
2
2
  class WebhooksResource < Resource
3
3
  def list(organization:, scope:, **params)
4
+ next_page_caller = ->(page_token:) { list(organization: organization, scope: scope, **params, page_token: page_token) }
5
+ organization = expand_uri(organization, "organizations")
4
6
  response = get_request("webhook_subscriptions", params: params.merge(organization: organization, scope: scope).compact)
5
- Collection.from_response(response, type: Webhooks::Subscription, client: client)
7
+ Collection.from_response(response, type: Webhooks::Subscription, client: client, next_page_caller: next_page_caller)
8
+ end
9
+
10
+ def list_all(organization:, scope:, **params)
11
+ list(organization: organization, scope: scope, **params).auto_paginate.to_a
6
12
  end
7
13
 
8
14
  def create(url:, events:, organization:, scope:, **params)
15
+ organization = expand_uri(organization, "organizations")
9
16
  body = params.merge(url: url, events: events, organization: organization, scope: scope)
10
17
  Webhooks::Subscription.new post_request("webhook_subscriptions", body: body).dig("resource").merge(client: client)
11
18
  end
@@ -18,8 +25,9 @@ module Calendlyr
18
25
  delete_request("webhook_subscriptions/#{webhook_uuid}")
19
26
  end
20
27
 
21
- def sample_webhook_data(event:, organization:, scope:, **params)
22
- Object.new get_request("sample_webhook_data", params: params.merge(event: event, organization: organization, scope: scope)).merge(client: client)
28
+ def sample(event:, organization:, scope:, **params)
29
+ organization = expand_uri(organization, "organizations")
30
+ get_request("sample_webhook_data", params: {event: event, organization: organization, scope: scope}.merge(params))
23
31
  end
24
32
  end
25
33
  end
@@ -1,3 +1,3 @@
1
1
  module Calendlyr
2
- VERSION = "0.7.5"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module Calendlyr
7
+ module Webhook
8
+ DEFAULT_TOLERANCE = 300
9
+
10
+ module_function
11
+
12
+ def verify!(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
13
+ raise ArgumentError, "signing_key is required" if signing_key.nil? || signing_key.empty?
14
+
15
+ resolved_header = resolve_signature_header(header: header, signature_header: signature_header)
16
+ timestamp, provided_signature = parse_header(resolved_header)
17
+ verify_timestamp!(timestamp, tolerance)
18
+
19
+ expected_signature = compute_signature(payload, timestamp, signing_key)
20
+ return true if secure_compare?(expected_signature, provided_signature)
21
+
22
+ raise WebhookSignatureError, "Invalid webhook signature"
23
+ end
24
+
25
+ def valid?(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
26
+ verify!(payload: payload, header: header, signature_header: signature_header, signing_key: signing_key, tolerance: tolerance)
27
+ rescue WebhookSignatureError, WebhookTimestampError
28
+ false
29
+ end
30
+
31
+ def parse(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
32
+ verify!(payload: payload, header: header, signature_header: signature_header, signing_key: signing_key, tolerance: tolerance)
33
+ parsed_payload = JSON.parse(payload)
34
+ Webhooks::Payload.new(parsed_payload.merge(client: nil))
35
+ end
36
+
37
+ def resolve_signature_header(header:, signature_header:)
38
+ if header && signature_header && header != signature_header
39
+ raise ArgumentError, "Provide either header or signature_header"
40
+ end
41
+
42
+ header || signature_header
43
+ end
44
+ private_class_method :resolve_signature_header
45
+
46
+ def parse_header(header)
47
+ raise WebhookSignatureError, "Missing webhook signature header" if header.nil? || header.strip.empty?
48
+
49
+ pairs = header.split(",").map(&:strip)
50
+ parsed = Hash.new { |hash, key| hash[key] = [] }
51
+
52
+ pairs.each do |pair|
53
+ key, value = pair.split("=", 2)
54
+ raise WebhookSignatureError, "Malformed webhook signature header" if key.nil? || value.nil?
55
+
56
+ parsed[key] << value.strip
57
+ end
58
+
59
+ timestamp_value = extract_single!(parsed, "t")
60
+ signature_value = extract_single!(parsed, "v1")
61
+
62
+ raise WebhookSignatureError, "Malformed webhook timestamp" unless timestamp_value.match?(/\A\d+\z/)
63
+
64
+ normalized_signature = signature_value.downcase
65
+ raise WebhookSignatureError, "Malformed webhook signature" unless normalized_signature.match?(/\A\h{64}\z/)
66
+
67
+ [timestamp_value.to_i, normalized_signature]
68
+ end
69
+ private_class_method :parse_header
70
+
71
+ def extract_single!(parsed, key)
72
+ values = parsed[key]
73
+ raise WebhookSignatureError, "Missing #{key} in webhook signature header" if values.empty?
74
+ raise WebhookSignatureError, "Duplicate #{key} in webhook signature header" if values.size > 1
75
+
76
+ value = values.first
77
+ raise WebhookSignatureError, "Blank #{key} in webhook signature header" if value.nil? || value.empty?
78
+
79
+ value
80
+ end
81
+ private_class_method :extract_single!
82
+
83
+ def verify_timestamp!(timestamp, tolerance)
84
+ return if tolerance.nil?
85
+
86
+ age = Time.now.to_i - timestamp
87
+ return if age.abs <= tolerance
88
+
89
+ raise WebhookTimestampError, "Webhook timestamp outside tolerance"
90
+ end
91
+ private_class_method :verify_timestamp!
92
+
93
+ def compute_signature(payload, timestamp, signing_key)
94
+ OpenSSL::HMAC.hexdigest("SHA256", signing_key, "#{timestamp}.#{payload}")
95
+ end
96
+ private_class_method :compute_signature
97
+
98
+ def secure_compare?(expected_signature, provided_signature)
99
+ return false unless expected_signature.bytesize == provided_signature.bytesize
100
+
101
+ OpenSSL.fixed_length_secure_compare(expected_signature, provided_signature)
102
+ end
103
+ private_class_method :secure_compare?
104
+ end
105
+ end
data/lib/calendlyr.rb CHANGED
@@ -2,11 +2,54 @@ require "calendlyr/version"
2
2
 
3
3
  module Calendlyr
4
4
  autoload :Client, "calendlyr/client"
5
+ autoload :Configuration, "calendlyr/configuration"
5
6
  autoload :Collection, "calendlyr/collection"
6
7
  autoload :Object, "calendlyr/object"
7
8
  autoload :Resource, "calendlyr/resource"
8
9
 
10
+ class << self
11
+ def configure
12
+ yield(configuration)
13
+ end
14
+
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def client
20
+ raise ArgumentError, "Missing Calendly token. Configure it with Calendlyr.configure { |c| c.token = \"...\" }" if configuration.token.nil?
21
+
22
+ signature = configuration_signature
23
+ @client = nil if @client_signature != signature
24
+ @client ||= Client.new(**client_attributes)
25
+ @client_signature = signature
26
+ @client
27
+ end
28
+
29
+ def reset!
30
+ @configuration = nil
31
+ @client = nil
32
+ @client_signature = nil
33
+ end
34
+
35
+ private
36
+
37
+ def configuration_signature
38
+ [configuration.token, configuration.open_timeout, configuration.read_timeout, configuration.logger]
39
+ end
40
+
41
+ def client_attributes
42
+ {
43
+ token: configuration.token,
44
+ open_timeout: configuration.open_timeout,
45
+ read_timeout: configuration.read_timeout,
46
+ logger: configuration.logger
47
+ }
48
+ end
49
+ end
50
+
9
51
  # Errors
52
+ autoload :ERROR_TYPES, "calendlyr/error"
10
53
  autoload :BadRequest, "calendlyr/error"
11
54
  autoload :Error, "calendlyr/error"
12
55
  autoload :ExternalCalendarError, "calendlyr/error"
@@ -17,6 +60,9 @@ module Calendlyr
17
60
  autoload :Unauthenticated, "calendlyr/error"
18
61
  autoload :TooManyRequests, "calendlyr/error"
19
62
  autoload :ResponseErrorHandler, "calendlyr/error"
63
+ autoload :WebhookSignatureError, "calendlyr/error"
64
+ autoload :WebhookTimestampError, "calendlyr/error"
65
+ autoload :Webhook, "calendlyr/webhook"
20
66
 
21
67
  # High-level categories of Calendly API calls
22
68
  autoload :AvailabilityResource, "calendlyr/resources/availability"
@@ -25,6 +71,7 @@ module Calendlyr
25
71
  autoload :EventTypesResource, "calendlyr/resources/event_types"
26
72
  autoload :GroupsResource, "calendlyr/resources/groups"
27
73
  autoload :OrganizationsResource, "calendlyr/resources/organizations"
74
+ autoload :LocationsResource, "calendlyr/resources/locations"
28
75
  autoload :OutgoingCommunicationsResource, "calendlyr/resources/outgoing_communications"
29
76
  autoload :RoutingFormsResource, "calendlyr/resources/routing_forms"
30
77
  autoload :SchedulingLinksResource, "calendlyr/resources/scheduling_links"
@@ -38,6 +85,8 @@ module Calendlyr
38
85
  autoload :EventType, "calendlyr/objects/event_type"
39
86
  autoload :Group, "calendlyr/objects/group"
40
87
  autoload :Organization, "calendlyr/objects/organization"
88
+ autoload :Location, "calendlyr/objects/location"
89
+ autoload :OutgoingCommunication, "calendlyr/objects/outgoing_communication"
41
90
  autoload :RoutingForm, "calendlyr/objects/routing_form"
42
91
  autoload :SchedulingLink, "calendlyr/objects/scheduling_link"
43
92
  autoload :Share, "calendlyr/objects/share"
@@ -57,6 +106,7 @@ module Calendlyr
57
106
  end
58
107
 
59
108
  module EventTypes
109
+ autoload :AvailabilitySchedule, "calendlyr/objects/event_types/availability_schedule"
60
110
  autoload :AvailableTime, "calendlyr/objects/event_types/available_time"
61
111
  autoload :Membership, "calendlyr/objects/event_types/membership"
62
112
  autoload :Profile, "calendlyr/objects/event_types/profile"
Binary file
Binary file
@@ -1,4 +1,6 @@
1
1
  require "test_helper"
2
+ require "logger"
3
+ require "stringio"
2
4
 
3
5
  class ClientTest < Minitest::Test
4
6
  def test_token
@@ -19,4 +21,31 @@ class ClientTest < Minitest::Test
19
21
  assert client.respond_to?(:users)
20
22
  refute client.respond_to?(:useers)
21
23
  end
24
+
25
+ def test_default_timeouts
26
+ new_client = Calendlyr::Client.new(token: "fake")
27
+
28
+ assert_equal 30, new_client.open_timeout
29
+ assert_equal 30, new_client.read_timeout
30
+ end
31
+
32
+ def test_custom_timeouts
33
+ new_client = Calendlyr::Client.new(token: "fake", open_timeout: 10, read_timeout: 15)
34
+
35
+ assert_equal 10, new_client.open_timeout
36
+ assert_equal 15, new_client.read_timeout
37
+ end
38
+
39
+ def test_default_logger_is_nil
40
+ new_client = Calendlyr::Client.new(token: "fake")
41
+
42
+ assert_nil new_client.logger
43
+ end
44
+
45
+ def test_custom_logger
46
+ logger = Logger.new(StringIO.new)
47
+ new_client = Calendlyr::Client.new(token: "fake", logger: logger)
48
+
49
+ assert_equal logger, new_client.logger
50
+ end
22
51
  end
@@ -0,0 +1,168 @@
1
+ require "test_helper"
2
+
3
+ class CollectionTest < Minitest::Test
4
+ def build_collection
5
+ data = [
6
+ Calendlyr::Object.new(name: "Alice"),
7
+ Calendlyr::Object.new(name: "Bob")
8
+ ]
9
+
10
+ Calendlyr::Collection.new(data: data, count: 2, next_page_url: nil, client: client)
11
+ end
12
+
13
+ def test_each_yields_items
14
+ collection = build_collection
15
+ names = []
16
+
17
+ collection.each do |item|
18
+ names << item.name
19
+ end
20
+
21
+ assert_equal %w[Alice Bob], names
22
+ end
23
+
24
+ def test_map_is_available_from_enumerable
25
+ collection = build_collection
26
+
27
+ assert_equal %w[Alice Bob], collection.map(&:name)
28
+ end
29
+
30
+ def test_collection_responds_to_select
31
+ collection = build_collection
32
+
33
+ assert collection.respond_to?(:select)
34
+ end
35
+
36
+ def test_count_without_block_returns_metadata_count
37
+ collection = Calendlyr::Collection.new(
38
+ data: [Calendlyr::Object.new(name: "Alice")],
39
+ count: 10,
40
+ next_page_url: nil,
41
+ client: client
42
+ )
43
+
44
+ assert_equal 10, collection.count
45
+ end
46
+
47
+ def test_count_with_block_uses_enumerable_count
48
+ collection = build_collection
49
+
50
+ assert_equal(1, collection.count { |item| item.name == "Alice" })
51
+ end
52
+
53
+ def test_count_with_argument_uses_enumerable_count
54
+ alice = Calendlyr::Object.new(name: "Alice")
55
+ collection = Calendlyr::Collection.new(
56
+ data: [alice, Calendlyr::Object.new(name: "Bob")],
57
+ count: 10,
58
+ next_page_url: nil,
59
+ client: client
60
+ )
61
+
62
+ assert_equal 1, collection.count(alice)
63
+ end
64
+
65
+ def test_next_page_returns_next_collection
66
+ next_data = [Calendlyr::Object.new(name: "Charlie")]
67
+ next_collection = Calendlyr::Collection.new(
68
+ data: next_data, count: 1, next_page_url: nil, client: client
69
+ )
70
+ caller_lambda = ->(page_token:) { next_collection }
71
+
72
+ collection = Calendlyr::Collection.new(
73
+ data: [Calendlyr::Object.new(name: "Alice")],
74
+ count: 1,
75
+ next_page_url: "https://api.calendly.com/scheduled_events?page_token=TOKEN123",
76
+ client: client,
77
+ next_page_caller: caller_lambda
78
+ )
79
+
80
+ result = collection.next_page
81
+
82
+ assert_equal next_collection, result
83
+ assert_equal Calendlyr::Collection, result.class
84
+ end
85
+
86
+ def test_next_page_returns_nil_when_no_token
87
+ collection = Calendlyr::Collection.new(
88
+ data: [Calendlyr::Object.new(name: "Alice")],
89
+ count: 1,
90
+ next_page_url: nil,
91
+ client: client,
92
+ next_page_caller: ->(page_token:) { raise "should not be called" }
93
+ )
94
+
95
+ assert_nil collection.next_page
96
+ end
97
+
98
+ def test_next_page_returns_nil_when_no_caller
99
+ collection = Calendlyr::Collection.new(
100
+ data: [Calendlyr::Object.new(name: "Alice")],
101
+ count: 1,
102
+ next_page_url: "https://api.calendly.com/scheduled_events?page_token=TOKEN123",
103
+ client: client
104
+ )
105
+
106
+ assert_nil collection.next_page
107
+ end
108
+
109
+ def test_auto_paginate_yields_all_items_across_pages
110
+ page3_data = [Calendlyr::Object.new(name: "Charlie")]
111
+ page3 = Calendlyr::Collection.new(
112
+ data: page3_data, count: 1, next_page_url: nil, client: client
113
+ )
114
+
115
+ page2_data = [Calendlyr::Object.new(name: "Bob")]
116
+ page2 = Calendlyr::Collection.new(
117
+ data: page2_data, count: 1,
118
+ next_page_url: "https://api.calendly.com/events?page_token=P3",
119
+ client: client,
120
+ next_page_caller: ->(page_token:) { page3 }
121
+ )
122
+
123
+ page1_data = [Calendlyr::Object.new(name: "Alice")]
124
+ page1 = Calendlyr::Collection.new(
125
+ data: page1_data, count: 1,
126
+ next_page_url: "https://api.calendly.com/events?page_token=P2",
127
+ client: client,
128
+ next_page_caller: ->(page_token:) { page2 }
129
+ )
130
+
131
+ result = page1.auto_paginate.to_a
132
+
133
+ assert_equal %w[Alice Bob Charlie], result.map(&:name)
134
+ end
135
+
136
+ def test_auto_paginate_empty_collection
137
+ collection = Calendlyr::Collection.new(
138
+ data: [], count: 0, next_page_url: nil, client: client
139
+ )
140
+
141
+ assert_equal [], collection.auto_paginate.to_a
142
+ end
143
+
144
+ def test_auto_paginate_lazy_does_not_prefetch
145
+ page2_called = false
146
+
147
+ page2_data = [Calendlyr::Object.new(name: "Bob")]
148
+ page2 = Calendlyr::Collection.new(
149
+ data: page2_data, count: 1, next_page_url: nil, client: client
150
+ )
151
+
152
+ page1_data = [Calendlyr::Object.new(name: "Alice")]
153
+ page1 = Calendlyr::Collection.new(
154
+ data: page1_data, count: 1,
155
+ next_page_url: "https://api.calendly.com/events?page_token=P2",
156
+ client: client,
157
+ next_page_caller: lambda { |page_token:|
158
+ page2_called = true
159
+ page2
160
+ }
161
+ )
162
+
163
+ result = page1.auto_paginate.first(1)
164
+
165
+ assert_equal ["Alice"], result.map(&:name)
166
+ refute page2_called, "Page 2 lambda should NOT have been called"
167
+ end
168
+ end