jira-ruby 3.1.0 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d81df9752f33a1c48332b6793d3b6596359b77b1a79575b57382553c1e04400
4
- data.tar.gz: 5d6d16a50ccd3293b893068c303405a67450006197bd1d5aec85d75a9d3179e4
3
+ metadata.gz: 4a043cafa57aad32acfda37495a8419b4511581786620049cb6264a05265aefb
4
+ data.tar.gz: 2c3b66dd69faaef351e043b10c62f3cfd7662e003905363db6bdbf458bfb10b1
5
5
  SHA512:
6
- metadata.gz: e4dc469b98fac2378bf647cd33e433a33ddc17ddf83d2e6fe835d21eda2239c6e3899fe9a838ebb4b1130356fa011302dd7f463ce9f4a0f660d4f66c5258e547
7
- data.tar.gz: 2a09541af9b0028a16338dd13d31014e1e919e730e86ff370150536f1a2e4a8911532283dea8f7d57571010fffa5c2986097fda8d4da7034b35a3afbc806fcfa
6
+ metadata.gz: 677a496c74235ea213ac92416b9dfb303e56bc04ea94b617134925ce744c25bd81583b2516a342c347a935959795c35813245499efcf2c058b075d308ef51454
7
+ data.tar.gz: ef42dd385a24ff73526e8e2d21fedb0af60832444f9d1e43c0c23a1cca58da61751a24901430abf0650cffacdd7b41ff28fa7620deb195ede7081af504eb4295
data/jira-ruby.gemspec CHANGED
@@ -23,8 +23,8 @@ Gem::Specification.new do |s|
23
23
  s.require_paths = ['lib']
24
24
 
25
25
  s.add_dependency 'activesupport'
26
- s.add_dependency 'atlassian-jwt'
27
26
  s.add_dependency 'cgi'
27
+ s.add_dependency 'jwt', '>= 2.1'
28
28
  s.add_dependency 'multipart-post'
29
29
  s.add_dependency 'oauth', '~> 1.0'
30
30
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2013 Atlassian Pty Ltd.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'jwt'
18
+ require 'uri'
19
+ require 'cgi'
20
+
21
+ module JIRA
22
+ module Atlassian
23
+ module Jwt
24
+ class << self
25
+ CANONICAL_QUERY_SEPARATOR = '&'
26
+ ESCAPED_CANONICAL_QUERY_SEPARATOR = '%26'
27
+
28
+ def create_canonical_request(uri, http_method, base_uri)
29
+ uri = URI.parse(uri) unless uri.is_a? URI
30
+ base_uri = URI.parse(base_uri) unless base_uri.is_a? URI
31
+
32
+ [
33
+ http_method.upcase,
34
+ canonicalize_uri(uri, base_uri),
35
+ canonicalize_query_string(uri.query)
36
+ ].join(CANONICAL_QUERY_SEPARATOR)
37
+ end
38
+
39
+ def build_claims(issuer, url, http_method, base_url = '', issued_at = nil, expires = nil, attributes = {}) # rubocop:disable Metrics/ParameterLists
40
+ issued_at ||= Time.now.to_i
41
+ expires ||= issued_at + 60
42
+ qsh = Digest::SHA256.hexdigest(create_canonical_request(url, http_method, base_url))
43
+
44
+ {
45
+ iss: issuer,
46
+ iat: issued_at,
47
+ exp: expires,
48
+ qsh: qsh
49
+ }.merge(attributes)
50
+ end
51
+
52
+ def canonicalize_uri(uri, base_uri)
53
+ path = uri.path.sub(/^#{base_uri.path}/, '')
54
+ path = '/' if path.nil? || path.empty?
55
+ path = "/#{path}" unless path.start_with? '/'
56
+ path.chomp!('/') if path.length > 1
57
+ path.gsub(CANONICAL_QUERY_SEPARATOR, ESCAPED_CANONICAL_QUERY_SEPARATOR)
58
+ end
59
+
60
+ def canonicalize_query_string(query)
61
+ return '' if query.nil? || query.empty?
62
+
63
+ query = CGI.parse(query)
64
+ query.delete('jwt')
65
+ query.each do |k, v|
66
+ query[k] = v.map { |a| CGI.escape a }.join(',') if v.is_a? Array
67
+ query[k].gsub!('+', '%20') # Use %20, not CGI.escape default of "+"
68
+ query[k].gsub!('%7E', '~') # Unescape "~" per JS tests
69
+ end
70
+ query = query.sort.to_h
71
+ query.map { |k, v| "#{CGI.escape k}=#{v}" }.join(CANONICAL_QUERY_SEPARATOR)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
data/lib/jira/client.rb CHANGED
@@ -302,6 +302,10 @@ module JIRA
302
302
  JIRA::Resource::AgileFactory.new(self)
303
303
  end
304
304
 
305
+ def Properties
306
+ JIRA::Resource::PropertiesFactory.new(self)
307
+ end
308
+
305
309
  # HTTP methods without a body
306
310
 
307
311
  # Make an HTTP DELETE request
@@ -351,7 +355,9 @@ module JIRA
351
355
  # @raise [JIRA::HTTPError] If the response is not an HTTP success code
352
356
  def post_multipart(path, file, headers = {})
353
357
  puts "post multipart: #{path} - [#{file}]" if @http_debug
354
- @request_client.request_multipart(path, file, merge_default_headers(headers))
358
+ res = @request_client.request_multipart(path, file, merge_default_headers(headers))
359
+ puts "response: #{res}" if @http_debug
360
+ res
355
361
  end
356
362
 
357
363
  # Make an HTTP PUT request
@@ -375,7 +381,9 @@ module JIRA
375
381
  # @raise [JIRA::HTTPError] If the response is not an HTTP success code
376
382
  def request(http_method, path, body = '', headers = {})
377
383
  puts "#{http_method}: #{path} - [#{body}]" if @http_debug
378
- @request_client.request(http_method, path, body, headers)
384
+ res = @request_client.request(http_method, path, body, headers)
385
+ puts "response: #{res}" if @http_debug
386
+ res
379
387
  end
380
388
 
381
389
  # @private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'atlassian/jwt'
3
+ require 'jira/atlassian/jwt'
4
4
 
5
5
  module JIRA
6
6
  class JwtClient < HttpClient
@@ -29,7 +29,7 @@ module JIRA
29
29
  end
30
30
 
31
31
  def build_jwt(url)
32
- claim = Atlassian::Jwt.build_claims \
32
+ claim = JIRA::Atlassian::Jwt.build_claims \
33
33
  @options[:issuer],
34
34
  url,
35
35
  http_method.to_s,
@@ -146,10 +146,14 @@ module JIRA
146
146
  # @raise [JIRA::HTTPError] if failed
147
147
  def save!(attrs, path = url)
148
148
  file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility
149
- mime_type = attrs[:mimeType] || 'application/binary'
149
+ # If :filename does not exist or is nil, that is fine as it will force
150
+ # Upload to determine the filename automatically from file.
151
+ # Breaking the filename out allows this to support any IO-based file parameter.
152
+ fname = attrs['filename'] || attrs[:filename]
153
+ mime_type = attrs['mimeType'] || attrs[:mimeType] || 'application/binary'
150
154
 
151
155
  headers = { 'X-Atlassian-Token' => 'nocheck' }
152
- data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, file) }
156
+ data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, fname) }
153
157
 
154
158
  response = client.post_multipart(path, data, headers)
155
159
 
@@ -159,6 +163,17 @@ module JIRA
159
163
  true
160
164
  end
161
165
 
166
+ def http_download
167
+ # Actually fetch the attachment
168
+ # Note: Jira handles attachment's weird!
169
+ # Typically, they respond with a redirect location that should not have the same authentication
170
+ client.get(attrs['content'])
171
+ rescue JIRA::HTTPError => e
172
+ raise e unless e.response.code =~ /\A3\d\d\z/ && e.response['location'].present?
173
+
174
+ Net::HTTP.get_response(URI(e.response['location']))
175
+ end
176
+
162
177
  private
163
178
 
164
179
  def set_attributes(attributes, response)
@@ -9,6 +9,18 @@ module JIRA
9
9
  belongs_to :issue
10
10
 
11
11
  nested_collections true
12
+
13
+ def self.all(client, options = {})
14
+ issue = options[:issue]
15
+ raise ArgumentError, 'parent issue is required' unless issue
16
+
17
+ response = client.get("#{issue.url}/#{endpoint_name}")
18
+ json = parse_json(response.body)
19
+ json = json[endpoint_name.pluralize]
20
+ json.map do |attrs|
21
+ new(client, { attrs: }.merge(options))
22
+ end
23
+ end
12
24
  end
13
25
  end
14
26
  end
@@ -12,22 +12,22 @@ module JIRA
12
12
 
13
13
  def self.all(client, params = {})
14
14
  if params.key?(:projectKeys)
15
- values = Array(params[:projectKeys]).map { |i| (i.is_a?(JIRA::Resource::Project) ? i.key : i) }
15
+ values = Array(params[:projectKeys]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.key : i }
16
16
  params[:projectKeys] = values.join(',')
17
17
  end
18
18
 
19
19
  if params.key?(:projectIds)
20
- values = Array(params[:projectIds]).map { |i| (i.is_a?(JIRA::Resource::Project) ? i.id : i) }
20
+ values = Array(params[:projectIds]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.id : i }
21
21
  params[:projectIds] = values.join(',')
22
22
  end
23
23
 
24
24
  if params.key?(:issuetypeNames)
25
- values = Array(params[:issuetypeNames]).map { |i| (i.is_a?(JIRA::Resource::Issuetype) ? i.name : i) }
25
+ values = Array(params[:issuetypeNames]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.name : i }
26
26
  params[:issuetypeNames] = values.join(',')
27
27
  end
28
28
 
29
29
  if params.key?(:issuetypeIds)
30
- values = Array(params[:issuetypeIds]).map { |i| (i.is_a?(JIRA::Resource::Issuetype) ? i.id : i) }
30
+ values = Array(params[:issuetypeIds]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.id : i }
31
31
  params[:issuetypeIds] = values.join(',')
32
32
  end
33
33
 
@@ -57,6 +57,7 @@ module JIRA
57
57
  has_many :issuelinks, nested_under: 'fields'
58
58
  has_many :remotelink, class: JIRA::Resource::Remotelink
59
59
  has_many :watchers, attribute_key: 'watches', nested_under: %w[fields watches]
60
+ has_many :properties, class: JIRA::Resource::Properties
60
61
 
61
62
  # Get collection of issues.
62
63
  # @param client [JIRA::Client]
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+
5
+ module JIRA
6
+ module Resource
7
+ class PropertiesFactory < JIRA::BaseFactory # :nodoc:
8
+ end
9
+
10
+ class Properties < JIRA::Base
11
+ belongs_to :issue
12
+
13
+ def self.key_attribute
14
+ :key
15
+ end
16
+
17
+ def self.all(client, options = {})
18
+ issue = options[:issue]
19
+ raise ArgumentError, 'parent issue is required' unless issue
20
+
21
+ response = client.get("#{issue.url}/#{endpoint_name}")
22
+ json = parse_json(response.body)
23
+ json[key_attribute.to_s.pluralize].map do |attrs|
24
+ ## Net get the individual property
25
+ self_response = client.get(attrs['self'])
26
+ property = parse_json(self_response.body)
27
+ ## Make sure to build the new resource via the issue.properties in order to support the has_many proxy
28
+ issue.properties.build(property)
29
+ end
30
+ end
31
+
32
+ ## Override save so we can handle the required attrs (and default 'value' when appropriate)
33
+ def save!(attrs = {}, path = nil)
34
+ if attrs.is_a?(Hash) && attrs.key?(self.class.key_attribute.to_s)
35
+ raise ArgumentError, "Use of 'value' is required when '#{self.class.key_attribute}' is provided" \
36
+ unless attrs.key?('value')
37
+
38
+ set_attrs(self.class.key_attribute.to_s => attrs[self.class.key_attribute.to_s])
39
+ end
40
+
41
+ raise ArgumentError, "'key' is required on a new record" if new_record?
42
+
43
+ path ||= patched_url
44
+ ## We can take either the 'value' element from the hash, OR use the entire attrs as the value
45
+ value = attrs.is_a?(Hash) && attrs.key?('value') ? attrs['value'] : attrs
46
+ value = '' if value.nil?
47
+ ## Note: this API endpoint requires a non-empty JSON body for the value of the property
48
+ ## Note2: this API endpoint does not return a body, so no need to call set_attrs_from_response
49
+ client.send(:put, path, value.to_json)
50
+ set_attrs({ 'value' => value }, false)
51
+ @expanded = false
52
+ true
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,6 +10,18 @@ module JIRA
10
10
  has_one :update_author, class: JIRA::Resource::User, attribute_key: 'updateAuthor'
11
11
  belongs_to :issue
12
12
  nested_collections true
13
+
14
+ def self.all(client, options = {})
15
+ issue = options[:issue]
16
+ raise ArgumentError, 'parent issue is required' unless issue
17
+
18
+ response = client.get("#{issue.url}/#{endpoint_name}")
19
+ json = parse_json(response.body)
20
+ json = json[endpoint_name.pluralize]
21
+ json.map do |attrs|
22
+ new(client, { attrs: }.merge(options))
23
+ end
24
+ end
13
25
  end
14
26
  end
15
27
  end
data/lib/jira/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JIRA
4
- VERSION = '3.1.0'
4
+ VERSION = '3.2.0'
5
5
  end
data/lib/jira-ruby.rb CHANGED
@@ -23,6 +23,7 @@ require 'jira/resource/status'
23
23
  require 'jira/resource/status_category'
24
24
  require 'jira/resource/transition'
25
25
  require 'jira/resource/project'
26
+ require 'jira/resource/properties'
26
27
  require 'jira/resource/priority'
27
28
  require 'jira/resource/comment'
28
29
  require 'jira/resource/worklog'
@@ -0,0 +1,317 @@
1
+ {
2
+ "secret": "7512836e-9137-48ac-baaf-a1935b96e17a",
3
+ "tests": [{
4
+ "name": "Simple",
5
+ "canonicalUrl": "GET\u0026/test\u0026param\u003dvalue",
6
+ "signedUrl": "https://example.com/test?param\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiZTE2OTEwODU4YTQxZmQxOWVhNWMxYjRlOWRlY2NhOWE3ODRkMTAyNGNiMDBiMjE1OGRlZmUyZjI5ZGM4NmRkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.ssbmLUBemh6nQW3MH5o2fSdnOFTIKf2w0KASnnH8aXo"
7
+ }, {
8
+ "name": "Simple (uri)",
9
+ "canonicalUrl": "GET\u0026/test\u0026param\u003dvalue",
10
+ "signedUrl": "https://example.com/test?param\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiZTE2OTEwODU4YTQxZmQxOWVhNWMxYjRlOWRlY2NhOWE3ODRkMTAyNGNiMDBiMjE1OGRlZmUyZjI5ZGM4NmRkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.ssbmLUBemh6nQW3MH5o2fSdnOFTIKf2w0KASnnH8aXo"
11
+ }, {
12
+ "name": "Spaces",
13
+ "canonicalUrl": "GET\u0026/test\u0026param\u003dsome%20spaces%20in%20this%20parameter",
14
+ "signedUrl": "https://example.com/test?param\u003dsome+spaces+in+this+parameter\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJlZjQwOTdkMDUzOTVlYWYxYTEzMzk1NmYxNDAxMTZhOWM0MTFhZmFkNzY4ZDRmMDZkYWFjOWUzMDU3NjQyNjVkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.JGTIXMHEtxAmLlTjIUFR21IEs9b8-3QIt3VwyRB816M"
15
+ }, {
16
+ "name": "Spaces (uri)",
17
+ "canonicalUrl": "GET\u0026/test\u0026param\u003dsome%20spaces%20in%20this%20parameter",
18
+ "signedUrl": "https://example.com/test?param\u003dsome+spaces+in+this+parameter\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJlZjQwOTdkMDUzOTVlYWYxYTEzMzk1NmYxNDAxMTZhOWM0MTFhZmFkNzY4ZDRmMDZkYWFjOWUzMDU3NjQyNjVkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.JGTIXMHEtxAmLlTjIUFR21IEs9b8-3QIt3VwyRB816M"
19
+ }, {
20
+ "name": "Asterisk",
21
+ "canonicalUrl": "GET\u0026/test\u0026query\u003dconnect%2A",
22
+ "signedUrl": "https://example.com/test?query\u003dconnect*\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZDI4YmMxMGQxOTcyZDljYWI1OWUxMjNjZTE3OTc2ZWIzMmFhZDY5NTdhMTNjZmI4Yjk3Y2VkNmQzZmM4YTI5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.Lj8eCYfC3mYcnkdGtvGPXT763nPI2XYyxlIJjSgIF5k"
23
+ }, {
24
+ "name": "Asterisk (uri)",
25
+ "canonicalUrl": "GET\u0026/test\u0026query\u003dconnect%2A",
26
+ "signedUrl": "https://example.com/test?query\u003dconnect*\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZDI4YmMxMGQxOTcyZDljYWI1OWUxMjNjZTE3OTc2ZWIzMmFhZDY5NTdhMTNjZmI4Yjk3Y2VkNmQzZmM4YTI5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.Lj8eCYfC3mYcnkdGtvGPXT763nPI2XYyxlIJjSgIF5k"
27
+ }, {
28
+ "name": "Unicode",
29
+ "canonicalUrl": "GET\u0026/test\u0026director\u003d%E5%AE%AE%E5%B4%8E%20%E9%A7%BF",
30
+ "signedUrl": "https://example.com/test?director\u003d%E5%AE%AE%E5%B4%8E+%E9%A7%BF\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0NGQ2MzU0ZmZlOGY5NjM5NzJhN2ZjNTllOThiMmMzZGZlNGI0OTMyOTVjNWI4OWVkZDk3NGEzMzllNjFhYzg5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.oc618kGGWy6ZP6ZqDsy2Hj61OS1kqFghcaQdvCBMLhc"
31
+ }, {
32
+ "name": "Unicode (uri)",
33
+ "canonicalUrl": "GET\u0026/test\u0026director\u003d%E5%AE%AE%E5%B4%8E%20%E9%A7%BF",
34
+ "signedUrl": "https://example.com/test?director\u003d%E5%AE%AE%E5%B4%8E+%E9%A7%BF\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0NGQ2MzU0ZmZlOGY5NjM5NzJhN2ZjNTllOThiMmMzZGZlNGI0OTMyOTVjNWI4OWVkZDk3NGEzMzllNjFhYzg5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.oc618kGGWy6ZP6ZqDsy2Hj61OS1kqFghcaQdvCBMLhc"
35
+ }, {
36
+ "name": "Comma-delimited",
37
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d10%2C2%2C20%2C1",
38
+ "signedUrl": "https://example.com/test?ids\u003d10%2C2%2C20%2C1\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyZTYxN2I2NWY5NjY2NTA3ODk5OTBlNWNhYzhmZjI5NWVlNWI0NTM4MzRkMDQ3OThmNDY2ZmY2MDM1YmQ4ODI5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.fdIooLlQ22kaE0FA15_cS_LvOtwBj2r5y586US4WJiw"
39
+ }, {
40
+ "name": "Comma-delimited (uri)",
41
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d10%2C2%2C20%2C1",
42
+ "signedUrl": "https://example.com/test?ids\u003d10%2C2%2C20%2C1\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyZTYxN2I2NWY5NjY2NTA3ODk5OTBlNWNhYzhmZjI5NWVlNWI0NTM4MzRkMDQ3OThmNDY2ZmY2MDM1YmQ4ODI5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.fdIooLlQ22kaE0FA15_cS_LvOtwBj2r5y586US4WJiw"
43
+ }, {
44
+ "name": "Multi-value Comma-delimited",
45
+ "canonicalUrl": "GET\u0026/test\u0026tuples\u003d1%2C2%2C3,6%2C5%2C4,7%2C9%2C8",
46
+ "signedUrl": "https://example.com/test?tuples\u003d1%2C2%2C3\u0026tuples\u003d6%2C5%2C4\u0026tuples\u003d7%2C9%2C8\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJmOWY2MmJiNjExOTFkZmYxYjIxOWU5YWRlOGFjMzhmY2ZhNDYyNzJlYTc2ZDk1NDU0MTBkYjY1NTlmZDhlN2JkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.PXOWzUEk0Be2zCEzO5W_yTvYWDOhuEQbm7t8XXctXrE"
47
+ }, {
48
+ "name": "Multi-value Comma-delimited (uri)",
49
+ "canonicalUrl": "GET\u0026/test\u0026tuples\u003d1%2C2%2C3,6%2C5%2C4,7%2C9%2C8",
50
+ "signedUrl": "https://example.com/test?tuples\u003d1%2C2%2C3\u0026tuples\u003d6%2C5%2C4\u0026tuples\u003d7%2C9%2C8\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJmOWY2MmJiNjExOTFkZmYxYjIxOWU5YWRlOGFjMzhmY2ZhNDYyNzJlYTc2ZDk1NDU0MTBkYjY1NTlmZDhlN2JkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.PXOWzUEk0Be2zCEzO5W_yTvYWDOhuEQbm7t8XXctXrE"
51
+ }, {
52
+ "name": "Plus",
53
+ "canonicalUrl": "GET\u0026/test\u0026title\u003d1%20%2B%201%20equals%203",
54
+ "signedUrl": "https://example.com/test?title\u003d1+%2B+1+equals+3\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJjZTBhODY2Y2UwYzM0Y2JkZTcwZGViNjNmNjkxNTE5MmY1ODUwMGQyYjQ5YjEyZTFmZmM4Y2RmOTFlZGFiYzkxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.NKD1M6kVQ3YFsQb-W2AUzYyntKSNvE15r4_hL9OO6A4"
55
+ }, {
56
+ "name": "Plus (uri)",
57
+ "canonicalUrl": "GET\u0026/test\u0026title\u003d1%20%2B%201%20equals%203",
58
+ "signedUrl": "https://example.com/test?title\u003d1+%2B+1+equals+3\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJjZTBhODY2Y2UwYzM0Y2JkZTcwZGViNjNmNjkxNTE5MmY1ODUwMGQyYjQ5YjEyZTFmZmM4Y2RmOTFlZGFiYzkxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.NKD1M6kVQ3YFsQb-W2AUzYyntKSNvE15r4_hL9OO6A4"
59
+ }, {
60
+ "name": "JSON Object",
61
+ "canonicalUrl": "GET\u0026/test\u0026json\u003d%7B%22key%22%3A%22value%22%7D",
62
+ "signedUrl": "https://example.com/test?json\u003d%7B%22key%22%3A%22value%22%7D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIxZDc0YzVhMWYwYTZjNzJhM2NlMjg1NTcwY2JmZDhmZTczNjkxMDEyM2U2YWRhMjAzZDRjN2JhOWE2MTI4YTZmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.AFKN_CHERMJa-VYAEVfRFw-Hi53VbJ_Y3clKB3RDAfE"
63
+ }, {
64
+ "name": "JSON Object (uri)",
65
+ "canonicalUrl": "GET\u0026/test\u0026json\u003d%7B%22key%22%3A%22value%22%7D",
66
+ "signedUrl": "https://example.com/test?json\u003d%7B%22key%22%3A%22value%22%7D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIxZDc0YzVhMWYwYTZjNzJhM2NlMjg1NTcwY2JmZDhmZTczNjkxMDEyM2U2YWRhMjAzZDRjN2JhOWE2MTI4YTZmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.AFKN_CHERMJa-VYAEVfRFw-Hi53VbJ_Y3clKB3RDAfE"
67
+ }, {
68
+ "name": "JSON Array",
69
+ "canonicalUrl": "GET\u0026/test\u0026json\u003d%5B%22val1%22%2C%22val2%22%5D",
70
+ "signedUrl": "https://example.com/test?json\u003d%5B%22val1%22%2C%22val2%22%5D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1YTY5ZDM1ZjE2NTM0MjkwYTRlZDJmOWU1M2E0NWU2ODcyY2YwYzNhMTE2MzI5NWM0Mjk0N2Q0MjkzMWExZjczIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xO5VqtU8GL8CTr_wn0tHJAJ-r63PU-bJBTeuKF759dA"
71
+ }, {
72
+ "name": "JSON Array (uri)",
73
+ "canonicalUrl": "GET\u0026/test\u0026json\u003d%5B%22val1%22%2C%22val2%22%5D",
74
+ "signedUrl": "https://example.com/test?json\u003d%5B%22val1%22%2C%22val2%22%5D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1YTY5ZDM1ZjE2NTM0MjkwYTRlZDJmOWU1M2E0NWU2ODcyY2YwYzNhMTE2MzI5NWM0Mjk0N2Q0MjkzMWExZjczIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xO5VqtU8GL8CTr_wn0tHJAJ-r63PU-bJBTeuKF759dA"
75
+ }, {
76
+ "name": "Single Quotes",
77
+ "canonicalUrl": "GET\u0026/test\u0026quote\u003d%27quoted%27",
78
+ "signedUrl": "https://example.com/test?quote\u003d%27quoted%27\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyMDdhMzAzNzZjNTk0ZDVjZGU3YjZlODczYjk0M2Y4NDc5ODM3MWQzMjkzYjdlYWMyN2UyYzJhMmE1ZDExMWQ1IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.XsfJjmLIyIuX7_xfuj1JlNBiNQ_oSoh8KQVfEJHNJ0A"
79
+ }, {
80
+ "name": "Single Quotes (uri)",
81
+ "canonicalUrl": "GET\u0026/test\u0026quote\u003d%27quoted%27",
82
+ "signedUrl": "https://example.com/test?quote\u003d%27quoted%27\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyMDdhMzAzNzZjNTk0ZDVjZGU3YjZlODczYjk0M2Y4NDc5ODM3MWQzMjkzYjdlYWMyN2UyYzJhMmE1ZDExMWQ1IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.XsfJjmLIyIuX7_xfuj1JlNBiNQ_oSoh8KQVfEJHNJ0A"
83
+ }, {
84
+ "name": "Brackets",
85
+ "canonicalUrl": "GET\u0026/test\u0026param\u003d%28%29",
86
+ "signedUrl": "https://example.com/test?param\u003d%28%29\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0NmI5Nzg3YTg5ODM1NjBmNDQ2ZjgyZDYxNDMxMjJkOTllMTUzYWZmZDU2ODhmYTQ1MzQzMDc0YTA5MTU3M2ViIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.E7tLaNwK3c9HmDEoptDGWh2p6DZ2pd1R4sNBAPJ0L_A"
87
+ }, {
88
+ "name": "Brackets (uri)",
89
+ "canonicalUrl": "GET\u0026/test\u0026param\u003d%28%29",
90
+ "signedUrl": "https://example.com/test?param\u003d%28%29\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0NmI5Nzg3YTg5ODM1NjBmNDQ2ZjgyZDYxNDMxMjJkOTllMTUzYWZmZDU2ODhmYTQ1MzQzMDc0YTA5MTU3M2ViIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.E7tLaNwK3c9HmDEoptDGWh2p6DZ2pd1R4sNBAPJ0L_A"
91
+ }, {
92
+ "name": "Tilde",
93
+ "canonicalUrl": "GET\u0026/test\u0026eta\u003din%20~3%20days",
94
+ "signedUrl": "https://example.com/test?eta\u003din+%7E3+days\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1YzVmOTdjZGZlM2FiZDhmZWI0OGY2ZjAyNDBhNzFlZjVjMTExMjg5YTViZjc3ZGRmNjQ1MjUwYjM5NTFlYzYzIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.kc_ugGzgCHSyCk4DgUFNVb5dhC2TdTlFVKyQx1oBcDQ"
95
+ }, {
96
+ "name": "Tilde (uri)",
97
+ "canonicalUrl": "GET\u0026/test\u0026eta\u003din%20~3%20days",
98
+ "signedUrl": "https://example.com/test?eta\u003din+%7E3+days\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1YzVmOTdjZGZlM2FiZDhmZWI0OGY2ZjAyNDBhNzFlZjVjMTExMjg5YTViZjc3ZGRmNjQ1MjUwYjM5NTFlYzYzIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.kc_ugGzgCHSyCk4DgUFNVb5dhC2TdTlFVKyQx1oBcDQ"
99
+ }, {
100
+ "name": "RFC-1738 Unsafe",
101
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%20%3C%3E%22%23%25%7B%7D%7C%5C%5E~%5B%5D%60",
102
+ "signedUrl": "https://example.com/test?rfc\u003d+%3C%3E%22%23%25%7B%7D%7C%5C%5E%7E%5B%5D%60\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIzNmJhMzljNTM3OWQ3NTJiYmVmMTM0OWJhNThkZjk4ZWNhYTQ1NmI3OGM3YzJlMzI0NWUyZjMzZWFjNzEyYTQ5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.AHIN8GC3aWL6o8ZOxZZBW_fxTI-6CY78ucWh_kaXBAs"
103
+ }, {
104
+ "name": "RFC-1738 Unsafe (uri)",
105
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%20%3C%3E%22%23%25%7B%7D%7C%5C%5E~%5B%5D%60",
106
+ "signedUrl": "https://example.com/test?rfc\u003d+%3C%3E%22%23%25%7B%7D%7C%5C%5E%7E%5B%5D%60\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIzNmJhMzljNTM3OWQ3NTJiYmVmMTM0OWJhNThkZjk4ZWNhYTQ1NmI3OGM3YzJlMzI0NWUyZjMzZWFjNzEyYTQ5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.AHIN8GC3aWL6o8ZOxZZBW_fxTI-6CY78ucWh_kaXBAs"
107
+ }, {
108
+ "name": "RFC-1738 Reserved",
109
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%3B%2F%3F%3A%40%3D%26",
110
+ "signedUrl": "https://example.com/test?rfc\u003d%3B%2F%3F%3A%40%3D%26\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI2MzkzMzBiYzkyZDc5MzczYjE5OGRhNjk3ODczYjJmMDNmMWEzYTBjNWFmNzhjZjhlZWE1MTdmMTQ5NjBiMTUwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.a7vQvXIFT-sSJiHFcQKf3o8pTFA__O0X4EJOD_91iK0"
111
+ }, {
112
+ "name": "RFC-1738 Reserved (uri)",
113
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%3B%2F%3F%3A%40%3D%26",
114
+ "signedUrl": "https://example.com/test?rfc\u003d%3B%2F%3F%3A%40%3D%26\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI2MzkzMzBiYzkyZDc5MzczYjE5OGRhNjk3ODczYjJmMDNmMWEzYTBjNWFmNzhjZjhlZWE1MTdmMTQ5NjBiMTUwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.a7vQvXIFT-sSJiHFcQKf3o8pTFA__O0X4EJOD_91iK0"
115
+ }, {
116
+ "name": "RFC-1738 Special",
117
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%24-_.%2B%21%2A%27%28%29%2C",
118
+ "signedUrl": "https://example.com/test?rfc\u003d%24-_.%2B%21*%27%28%29%2C\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZTM2MjI5Y2M0NTlmZjhjY2I0ODQ1YzExNDMzY2ZiYmUyNWJmYTQ1ZThjMWVhMTRkMmExMjQyMzFjYjBhMmU5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.vUERQg8SGBRFZpB8d0DFOcaFdDfJjsHZBUDujLJ7LhQ"
119
+ }, {
120
+ "name": "RFC-1738 Special (uri)",
121
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%24-_.%2B%21%2A%27%28%29%2C",
122
+ "signedUrl": "https://example.com/test?rfc\u003d%24-_.%2B%21*%27%28%29%2C\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZTM2MjI5Y2M0NTlmZjhjY2I0ODQ1YzExNDMzY2ZiYmUyNWJmYTQ1ZThjMWVhMTRkMmExMjQyMzFjYjBhMmU5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.vUERQg8SGBRFZpB8d0DFOcaFdDfJjsHZBUDujLJ7LhQ"
123
+ }, {
124
+ "name": "RFC-3986 Unreserved",
125
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d-._~",
126
+ "signedUrl": "https://example.com/test?rfc\u003d-._%7E\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZGRmN2U2MzFjM2IxMWU3OTc1ODE0ZWU4NGZlYzI2ODA0ZGM3MmIwYTBlOTk3ODcxZDg0MTVjZWU2Yjc4YjMxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.tqAbSA8_iIe-nfjtqBrHpxUe3HI7D9ILxpXIf0EqUfA"
127
+ }, {
128
+ "name": "RFC-3986 Unreserved (uri)",
129
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d-._~",
130
+ "signedUrl": "https://example.com/test?rfc\u003d-._%7E\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwZGRmN2U2MzFjM2IxMWU3OTc1ODE0ZWU4NGZlYzI2ODA0ZGM3MmIwYTBlOTk3ODcxZDg0MTVjZWU2Yjc4YjMxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.tqAbSA8_iIe-nfjtqBrHpxUe3HI7D9ILxpXIf0EqUfA"
131
+ }, {
132
+ "name": "RFC-3986 gen-delims",
133
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%3A%2F%3F%23%5B%5D%40",
134
+ "signedUrl": "https://example.com/test?rfc\u003d%3A%2F%3F%23%5B%5D%40\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI5NjA0MWVlNDVhZDRiOGVhMjliY2U0MmYzZTI4MzU0OTA1OTFmNTVlODRiZGRiYmFiODIxNTFkNGExYWMxYTExIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.ChCEYkRpoqkZsyZ2ZtGAAntcb0KYHjmqI6to9p1KKJQ"
135
+ }, {
136
+ "name": "RFC-3986 gen-delims (uri)",
137
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%3A%2F%3F%23%5B%5D%40",
138
+ "signedUrl": "https://example.com/test?rfc\u003d%3A%2F%3F%23%5B%5D%40\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI5NjA0MWVlNDVhZDRiOGVhMjliY2U0MmYzZTI4MzU0OTA1OTFmNTVlODRiZGRiYmFiODIxNTFkNGExYWMxYTExIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.ChCEYkRpoqkZsyZ2ZtGAAntcb0KYHjmqI6to9p1KKJQ"
139
+ }, {
140
+ "name": "RFC-3986 sub-delims",
141
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%21%24%26%27%28%29%2A%2B%2C%3B%3D",
142
+ "signedUrl": "https://example.com/test?rfc\u003d%21%24%26%27%28%29*%2B%2C%3B%3D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiOGQwNDk4NzRmMWZlNWFiZTI3NGQyMGJkZWNlYmVlOGRhYWRhNDUzMTkxYTgyNzczOGVlYmNkNzU5ZWZkNGUxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.rvu_onWEK0PWM-w6K7JEShKHVplx9oCTdeKyhpJmSx4"
143
+ }, {
144
+ "name": "RFC-3986 sub-delims (uri)",
145
+ "canonicalUrl": "GET\u0026/test\u0026rfc\u003d%21%24%26%27%28%29%2A%2B%2C%3B%3D",
146
+ "signedUrl": "https://example.com/test?rfc\u003d%21%24%26%27%28%29*%2B%2C%3B%3D\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiOGQwNDk4NzRmMWZlNWFiZTI3NGQyMGJkZWNlYmVlOGRhYWRhNDUzMTkxYTgyNzczOGVlYmNkNzU5ZWZkNGUxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.rvu_onWEK0PWM-w6K7JEShKHVplx9oCTdeKyhpJmSx4"
147
+ }, {
148
+ "name": "Empty",
149
+ "canonicalUrl": "GET\u0026/test\u0026notmuch\u003d",
150
+ "signedUrl": "https://example.com/test?notmuch\u003d\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyMGZlY2RlZDk5NTE1MTZhMzQ0N2IxYjU1NmQwZWIzNTBkZTI5NzcwNTA1MjUzYTMwZGM3Mzk1MDJlMDVkZGM3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.P8btoE2iwXDoTpWr9eL64OcumNeFtRVVuNAg_vb5EwE"
151
+ }, {
152
+ "name": "Empty (uri)",
153
+ "canonicalUrl": "GET\u0026/test\u0026notmuch\u003d",
154
+ "signedUrl": "https://example.com/test?notmuch\u003d\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIyMGZlY2RlZDk5NTE1MTZhMzQ0N2IxYjU1NmQwZWIzNTBkZTI5NzcwNTA1MjUzYTMwZGM3Mzk1MDJlMDVkZGM3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.P8btoE2iwXDoTpWr9eL64OcumNeFtRVVuNAg_vb5EwE"
155
+ }, {
156
+ "name": "Encoded",
157
+ "canonicalUrl": "GET\u0026/test\u0026referrer\u003dhttp%3A%2F%2Ffrom.net%2Fp%3Fx%3DA%2B%252B%2BB%26y%3D%2524-_.%252B%2521%2A%2527%2528%2529%252C",
158
+ "signedUrl": "https://example.com/test?referrer\u003dhttp%3A%2F%2Ffrom.net%2Fp%3Fx%3DA%2B%252B%2BB%26y%3D%2524-_.%252B%2521*%2527%2528%2529%252C\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3Y2E4MWI4ZTRlODNjMjM3NWVlYTdiOGI1MGJkMzc4NmJhOGI0MzI2MTE5M2EzYmQzY2NkZmNhMDYxMjBlZTMzIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.OTKND114j4id2cHr_AdHLAK9GqhbxFE7ad0eT0DmEV8"
159
+ }, {
160
+ "name": "Encoded (uri)",
161
+ "canonicalUrl": "GET\u0026/test\u0026referrer\u003dhttp%3A%2F%2Ffrom.net%2Fp%3Fx%3DA%2B%252B%2BB%26y%3D%2524-_.%252B%2521%2A%2527%2528%2529%252C",
162
+ "signedUrl": "https://example.com/test?referrer\u003dhttp%3A%2F%2Ffrom.net%2Fp%3Fx%3DA%2B%252B%2BB%26y%3D%2524-_.%252B%2521*%2527%2528%2529%252C\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3Y2E4MWI4ZTRlODNjMjM3NWVlYTdiOGI1MGJkMzc4NmJhOGI0MzI2MTE5M2EzYmQzY2NkZmNhMDYxMjBlZTMzIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.OTKND114j4id2cHr_AdHLAK9GqhbxFE7ad0eT0DmEV8"
163
+ }, {
164
+ "name": "Multi-value",
165
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d-1,1,10,2,20",
166
+ "signedUrl": "https://example.com/test?ids\u003d-1\u0026ids\u003d1\u0026ids\u003d10\u0026ids\u003d2\u0026ids\u003d20\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhNjQ2YzQ4NzQxZmFhZWI2NDlmOWNhNzg4OGFlMDI5ZWFkNDMyZTM4MDZmNTZjNGE1N2I4MGIzZTJhYzYyOGE5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.gUepTNdB4HtYcabEZSiuXZrf2vU18ZGft5cvAT2W6FI"
167
+ }, {
168
+ "name": "Multi-value (uri)",
169
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d-1,1,10,2,20",
170
+ "signedUrl": "https://example.com/test?ids\u003d-1\u0026ids\u003d1\u0026ids\u003d10\u0026ids\u003d2\u0026ids\u003d20\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhNjQ2YzQ4NzQxZmFhZWI2NDlmOWNhNzg4OGFlMDI5ZWFkNDMyZTM4MDZmNTZjNGE1N2I4MGIzZTJhYzYyOGE5IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.gUepTNdB4HtYcabEZSiuXZrf2vU18ZGft5cvAT2W6FI"
171
+ }, {
172
+ "name": "Multi-value II",
173
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d.1,.2,%3A1,%3A2",
174
+ "signedUrl": "https://example.com/test?ids\u003d.1\u0026ids\u003d.2\u0026ids\u003d%3A1\u0026ids\u003d%3A2\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJmMDUyNGQ0MmUwZjg2NjE4NjJlMWYxZTlhZGQ4NzU3OTcyYTAwNDNiYjcxMzZkNjQxNzM1ZDIxODc0YzVhMDU2IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.bbmy58zcb675U8eE1NaDvtd85EcVsGjZfIcSLbgOwas"
175
+ }, {
176
+ "name": "Multi-value II (uri)",
177
+ "canonicalUrl": "GET\u0026/test\u0026ids\u003d.1,.2,%3A1,%3A2",
178
+ "signedUrl": "https://example.com/test?ids\u003d.1\u0026ids\u003d.2\u0026ids\u003d%3A1\u0026ids\u003d%3A2\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJmMDUyNGQ0MmUwZjg2NjE4NjJlMWYxZTlhZGQ4NzU3OTcyYTAwNDNiYjcxMzZkNjQxNzM1ZDIxODc0YzVhMDU2IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.bbmy58zcb675U8eE1NaDvtd85EcVsGjZfIcSLbgOwas"
179
+ }, {
180
+ "name": "Multi-value Unicode",
181
+ "canonicalUrl": "GET\u0026/test\u0026chars\u003d%E5%AE%AE,%E5%B4%8E,%E9%A7%BF",
182
+ "signedUrl": "https://example.com/test?chars\u003d%E5%AE%AE\u0026chars\u003d%E5%B4%8E\u0026chars\u003d%E9%A7%BF\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3MmE3ZjdjZTlmOTMxNTdmMjk2Yjg2MzM4MzE1NDIzZDZmM2I2YWJhNTA4MWYwNGJiZGI2YmIxODIxOGI2NWVlIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.K8dOEK17rBCjHS_sNj58CO4_rB4IM-JeRoUYVv2osbc"
183
+ }, {
184
+ "name": "Multi-value Unicode (uri)",
185
+ "canonicalUrl": "GET\u0026/test\u0026chars\u003d%E5%AE%AE,%E5%B4%8E,%E9%A7%BF",
186
+ "signedUrl": "https://example.com/test?chars\u003d%E5%AE%AE\u0026chars\u003d%E5%B4%8E\u0026chars\u003d%E9%A7%BF\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3MmE3ZjdjZTlmOTMxNTdmMjk2Yjg2MzM4MzE1NDIzZDZmM2I2YWJhNTA4MWYwNGJiZGI2YmIxODIxOGI2NWVlIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.K8dOEK17rBCjHS_sNj58CO4_rB4IM-JeRoUYVv2osbc"
187
+ }, {
188
+ "name": "Multi-value Empty",
189
+ "canonicalUrl": "GET\u0026/test\u0026c\u003d,%20,%2520,%2B",
190
+ "signedUrl": "https://example.com/test?c\u003d\u0026c\u003d+\u0026c\u003d%2520\u0026c\u003d%2B\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJjYmJjYWM5YTZhMDJmM2FkOTZjNWFiNWJmODc2ZGQ5Zjc5YjJjNjFjZWVjNTY2MGExYzlkNzNhM2IxMDJlYzJjIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.R_DWbzp9IZbL9lutC4eRBf-NcN4sllakpx5H59F329A"
191
+ }, {
192
+ "name": "Multi-value Empty (uri)",
193
+ "canonicalUrl": "GET\u0026/test\u0026c\u003d,%20,%2520,%2B",
194
+ "signedUrl": "https://example.com/test?c\u003d\u0026c\u003d+\u0026c\u003d%2520\u0026c\u003d%2B\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJjYmJjYWM5YTZhMDJmM2FkOTZjNWFiNWJmODc2ZGQ5Zjc5YjJjNjFjZWVjNTY2MGExYzlkNzNhM2IxMDJlYzJjIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.R_DWbzp9IZbL9lutC4eRBf-NcN4sllakpx5H59F329A"
195
+ }, {
196
+ "name": "Key RFC-1738 Unsafe",
197
+ "canonicalUrl": "GET\u0026/test\u0026%231\u003dvalue",
198
+ "signedUrl": "https://example.com/test?%231\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJlY2NiNGEyMzJkNTBjYTQxYWM0MDM5ODY3NWI2NDg3YTcxMWQ5ZTk3MjkxN2Q4YTYxY2JjMTIwZjlmYmRhNTk3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.OLjm-tHtgDfOwTtBQ50-JGJM4zIP-yZcsDRAuJn0P7s"
199
+ }, {
200
+ "name": "Key RFC-1738 Unsafe (uri)",
201
+ "canonicalUrl": "GET\u0026/test\u0026%231\u003dvalue",
202
+ "signedUrl": "https://example.com/test?%231\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJlY2NiNGEyMzJkNTBjYTQxYWM0MDM5ODY3NWI2NDg3YTcxMWQ5ZTk3MjkxN2Q4YTYxY2JjMTIwZjlmYmRhNTk3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.OLjm-tHtgDfOwTtBQ50-JGJM4zIP-yZcsDRAuJn0P7s"
203
+ }, {
204
+ "name": "Key RFC-1738 Reserved",
205
+ "canonicalUrl": "GET\u0026/test\u0026%3A1\u003dvalue",
206
+ "signedUrl": "https://example.com/test?%3A1\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI2ZDgyMzE4MWE2OGE0OTUyYzc1ZTRiNTFhZDBiOGQ1OWU4Nzk4NTljZGY5NzdlNzI1NDU0NTIwMWJjYTg0NjVkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.lqRCsE131cmicAXf2BoCtLiV4g7D50piFnSzJoE0rCk"
207
+ }, {
208
+ "name": "Key RFC-1738 Reserved (uri)",
209
+ "canonicalUrl": "GET\u0026/test\u0026%3A1\u003dvalue",
210
+ "signedUrl": "https://example.com/test?%3A1\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI2ZDgyMzE4MWE2OGE0OTUyYzc1ZTRiNTFhZDBiOGQ1OWU4Nzk4NTljZGY5NzdlNzI1NDU0NTIwMWJjYTg0NjVkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.lqRCsE131cmicAXf2BoCtLiV4g7D50piFnSzJoE0rCk"
211
+ }, {
212
+ "name": "Key RFC-1738 Special",
213
+ "canonicalUrl": "GET\u0026/test\u0026%241\u003dvalue",
214
+ "signedUrl": "https://example.com/test?%241\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhOWY2MWYxYzgyNTBlYmZiOTIzNDY1NGU3MWRlYzIzM2M5M2MxNmE1NGZlMzBhNjQ5ZGZkNjViYWRmM2MwMTcxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xSvBJ10iVN7_Y97j1qApcYXUVq8JoXkCLnYf0Fwj0mY"
215
+ }, {
216
+ "name": "Key RFC-1738 Special (uri)",
217
+ "canonicalUrl": "GET\u0026/test\u0026%241\u003dvalue",
218
+ "signedUrl": "https://example.com/test?%241\u003dvalue\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhOWY2MWYxYzgyNTBlYmZiOTIzNDY1NGU3MWRlYzIzM2M5M2MxNmE1NGZlMzBhNjQ5ZGZkNjViYWRmM2MwMTcxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xSvBJ10iVN7_Y97j1qApcYXUVq8JoXkCLnYf0Fwj0mY"
219
+ }, {
220
+ "name": "Multiple Parameters Simple",
221
+ "canonicalUrl": "GET\u0026/test\u0026a\u003dx\u0026b\u003dy",
222
+ "signedUrl": "https://example.com/test?a\u003dx\u0026b\u003dy\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhYmQ1ZGU2MDRiOWQzY2RhNDRkM2M3YTlkZDY5OGEyMTI1OTY1NTM5NDY1OGE2NjQyZjg3YjlkODI1ODY5NDYwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.1jFYZh4FjFNsGo62Qj9E7xzGCVVunqj5vtXoz7a8bCo"
223
+ }, {
224
+ "name": "Multiple Parameters Simple (uri)",
225
+ "canonicalUrl": "GET\u0026/test\u0026a\u003dx\u0026b\u003dy",
226
+ "signedUrl": "https://example.com/test?a\u003dx\u0026b\u003dy\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJhYmQ1ZGU2MDRiOWQzY2RhNDRkM2M3YTlkZDY5OGEyMTI1OTY1NTM5NDY1OGE2NjQyZjg3YjlkODI1ODY5NDYwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.1jFYZh4FjFNsGo62Qj9E7xzGCVVunqj5vtXoz7a8bCo"
227
+ }, {
228
+ "name": "Multiple Multi-value Parameters",
229
+ "canonicalUrl": "GET\u0026/test\u0026a\u003dx1,x10\u0026b\u003dy1,y10",
230
+ "signedUrl": "https://example.com/test?a\u003dx1\u0026a\u003dx10\u0026b\u003dy1\u0026b\u003dy10\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIzMTBiYTNmZjdlMWU5YThjYWNkMzNkZDYxNzI1MDEzMDRiZjMyNTA5NmQ4MWNjYWQ0NDk0NjU0MjdlNThhOWM0IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.7OJXrJdQpPUni7Qm0IrFz9KHxTgCMeE1WbVC1unQJ3g"
231
+ }, {
232
+ "name": "Multiple Multi-value Parameters (uri)",
233
+ "canonicalUrl": "GET\u0026/test\u0026a\u003dx1,x10\u0026b\u003dy1,y10",
234
+ "signedUrl": "https://example.com/test?a\u003dx1\u0026a\u003dx10\u0026b\u003dy1\u0026b\u003dy10\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIzMTBiYTNmZjdlMWU5YThjYWNkMzNkZDYxNzI1MDEzMDRiZjMyNTA5NmQ4MWNjYWQ0NDk0NjU0MjdlNThhOWM0IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.7OJXrJdQpPUni7Qm0IrFz9KHxTgCMeE1WbVC1unQJ3g"
235
+ }, {
236
+ "name": "Multiple Parameters Spaces",
237
+ "canonicalUrl": "GET\u0026/test\u0026a\u003danother%20one,one%20string\u0026b\u003dand%20yet%20more,more%20here",
238
+ "signedUrl": "https://example.com/test?a\u003danother+one\u0026a\u003done+string\u0026b\u003dand+yet+more\u0026b\u003dmore+here\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3MDA3MjFlNjUzODU3ODFmYzFmYTI0ODIyMGM3NTQwMGYxZThhMTk3YzgzOGVmMTc3OWYwMGI2OWJmYTRhNmZmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.g5YMPR92qU3hw2uPmdWVztIIl25QKF5QlqsvCNZ7TDE"
239
+ }, {
240
+ "name": "Multiple Parameters Spaces (uri)",
241
+ "canonicalUrl": "GET\u0026/test\u0026a\u003danother%20one,one%20string\u0026b\u003dand%20yet%20more,more%20here",
242
+ "signedUrl": "https://example.com/test?a\u003danother+one\u0026a\u003done+string\u0026b\u003dand+yet+more\u0026b\u003dmore+here\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI3MDA3MjFlNjUzODU3ODFmYzFmYTI0ODIyMGM3NTQwMGYxZThhMTk3YzgzOGVmMTc3OWYwMGI2OWJmYTRhNmZmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.g5YMPR92qU3hw2uPmdWVztIIl25QKF5QlqsvCNZ7TDE"
243
+ }, {
244
+ "name": "Multiple Parameters Comma-delimited",
245
+ "canonicalUrl": "GET\u0026/test\u0026a\u003d1%2C2%2C3,4%2C5%2C6\u0026b\u003da%2Cb%2Cc,d%2Ce%2Cf",
246
+ "signedUrl": "https://example.com/test?a\u003d1%2C2%2C3\u0026a\u003d4%2C5%2C6\u0026b\u003da%2Cb%2Cc\u0026b\u003dd%2Ce%2Cf\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwNmI3NmViNjNhY2NjY2UyMzU2Y2RmYzY2ZjRhYmQxZTNhMDYxYzAzMDVjOWZjNzZiNjE2YWU3MWZlM2I2YWI1IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.6skGpsaWS3sR1ft8hdnJL5EvgFDlyUYmmjcziQrIK1s"
247
+ }, {
248
+ "name": "Multiple Parameters Comma-delimited (uri)",
249
+ "canonicalUrl": "GET\u0026/test\u0026a\u003d1%2C2%2C3,4%2C5%2C6\u0026b\u003da%2Cb%2Cc,d%2Ce%2Cf",
250
+ "signedUrl": "https://example.com/test?a\u003d1%2C2%2C3\u0026a\u003d4%2C5%2C6\u0026b\u003da%2Cb%2Cc\u0026b\u003dd%2Ce%2Cf\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwNmI3NmViNjNhY2NjY2UyMzU2Y2RmYzY2ZjRhYmQxZTNhMDYxYzAzMDVjOWZjNzZiNjE2YWU3MWZlM2I2YWI1IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.6skGpsaWS3sR1ft8hdnJL5EvgFDlyUYmmjcziQrIK1s"
251
+ }, {
252
+ "name": "Parameter Order",
253
+ "canonicalUrl": "GET\u0026/test\u0026a1\u003d2\u0026a10\u003d1\u0026b1\u003d3\u0026b10\u003d4",
254
+ "signedUrl": "https://example.com/test?a10\u003d1\u0026a1\u003d2\u0026b1\u003d3\u0026b10\u003d4\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIxNTljYzNkMzAwM2YwNjdjM2FlMjYzZDE5ZGVkNDJkZWRkMjgyMjFjY2U0ZDQ3NTdiMTNmYzc3MjJhMjQ0MDhmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.CEfY2ck1VpaOVlfNi9iXiF03lowoqm_WgHvtgf5Xfps"
255
+ }, {
256
+ "name": "Parameter Order (uri)",
257
+ "canonicalUrl": "GET\u0026/test\u0026a1\u003d2\u0026a10\u003d1\u0026b1\u003d3\u0026b10\u003d4",
258
+ "signedUrl": "https://example.com/test?a10\u003d1\u0026a1\u003d2\u0026b1\u003d3\u0026b10\u003d4\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIxNTljYzNkMzAwM2YwNjdjM2FlMjYzZDE5ZGVkNDJkZWRkMjgyMjFjY2U0ZDQ3NTdiMTNmYzc3MjJhMjQ0MDhmIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.CEfY2ck1VpaOVlfNi9iXiF03lowoqm_WgHvtgf5Xfps"
259
+ }, {
260
+ "name": "Upper- and Lower-case Parameters",
261
+ "canonicalUrl": "GET\u0026/test\u0026A\u003dA\u0026B\u003dB\u0026a\u003da\u0026b\u003db",
262
+ "signedUrl": "https://example.com/test?A\u003dA\u0026a\u003da\u0026b\u003db\u0026B\u003dB\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1MzlhZGMyYjBhZWYyZWM4NjU5ODI1ODkzYjhiOTJlNmE1M2M5NDgzZTIxNTBiODE4NWIwODQ5MGQ4YjZkZTYwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.wOC7mInLeAu_mQpoHlGW2DIlEeEfiqFvn32sea8vDvc"
263
+ }, {
264
+ "name": "Upper- and Lower-case Parameters (uri)",
265
+ "canonicalUrl": "GET\u0026/test\u0026A\u003dA\u0026B\u003dB\u0026a\u003da\u0026b\u003db",
266
+ "signedUrl": "https://example.com/test?A\u003dA\u0026a\u003da\u0026b\u003db\u0026B\u003dB\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1MzlhZGMyYjBhZWYyZWM4NjU5ODI1ODkzYjhiOTJlNmE1M2M5NDgzZTIxNTBiODE4NWIwODQ5MGQ4YjZkZTYwIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.wOC7mInLeAu_mQpoHlGW2DIlEeEfiqFvn32sea8vDvc"
267
+ }, {
268
+ "name": "Search Request View",
269
+ "canonicalUrl": "GET\u0026/search-view\u0026cp\u003djira\u0026endIssue\u003d2\u0026issues\u003dissues%3DTEST-2%2CTEST-1\u0026lic\u003dnone\u0026link\u003dhttp%3A%2F%2Fion%3A2990%2Fjira%2Fsecure%2FIssueNavigator.jspa%3Freset%3Dtrue%26jqlQuery%3Dissuetype%2B%253D%2BBug\u0026loc\u003den-US\u0026startIssue\u003d0\u0026totalIssues\u003d2\u0026tz\u003dAustralia%2FSydney\u0026user_id\u003dadmin\u0026user_key\u003dadmin\u0026xdm_c\u003dchannel-acmodule-1564427223927602208\u0026xdm_e\u003dhttp%3A%2F%2Fion.local%3A2990",
270
+ "signedUrl": "https://example.com/search-view?link\u003dhttp%3A%2F%2Fion%3A2990%2Fjira%2Fsecure%2FIssueNavigator.jspa%3Freset%3Dtrue%26jqlQuery%3Dissuetype%2B%253D%2BBug\u0026startIssue\u003d0\u0026totalIssues\u003d2\u0026endIssue\u003d2\u0026issues\u003dissues%3DTEST-2%2CTEST-1\u0026tz\u003dAustralia%2FSydney\u0026loc\u003den-US\u0026user_id\u003dadmin\u0026user_key\u003dadmin\u0026xdm_e\u003dhttp%3A%2F%2Fion.local%3A2990\u0026xdm_c\u003dchannel-acmodule-1564427223927602208\u0026cp\u003djira\u0026lic\u003dnone\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwY2I3NGI4MGRkN2Y3NzkzODQ5ODY3Mjk4Y2EyZDg4OWU0ZDhiNjYxMTFiNjg5NjIyN2JkYmI0OTBkYzQ0YWUxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.UXWVrjqrCcjX6Mt6gIap4nK7o3aTLRz0ab1fMwwSJqo"
271
+ }, {
272
+ "name": "Search Request View (uri)",
273
+ "canonicalUrl": "GET\u0026/search-view\u0026cp\u003djira\u0026endIssue\u003d2\u0026issues\u003dissues%3DTEST-2%2CTEST-1\u0026lic\u003dnone\u0026link\u003dhttp%3A%2F%2Fion%3A2990%2Fjira%2Fsecure%2FIssueNavigator.jspa%3Freset%3Dtrue%26jqlQuery%3Dissuetype%2B%253D%2BBug\u0026loc\u003den-US\u0026startIssue\u003d0\u0026totalIssues\u003d2\u0026tz\u003dAustralia%2FSydney\u0026user_id\u003dadmin\u0026user_key\u003dadmin\u0026xdm_c\u003dchannel-acmodule-1564427223927602208\u0026xdm_e\u003dhttp%3A%2F%2Fion.local%3A2990",
274
+ "signedUrl": "https://example.com/search-view?link\u003dhttp%3A%2F%2Fion%3A2990%2Fjira%2Fsecure%2FIssueNavigator.jspa%3Freset%3Dtrue%26jqlQuery%3Dissuetype%2B%253D%2BBug\u0026startIssue\u003d0\u0026totalIssues\u003d2\u0026endIssue\u003d2\u0026issues\u003dissues%3DTEST-2%2CTEST-1\u0026tz\u003dAustralia%2FSydney\u0026loc\u003den-US\u0026user_id\u003dadmin\u0026user_key\u003dadmin\u0026xdm_e\u003dhttp%3A%2F%2Fion.local%3A2990\u0026xdm_c\u003dchannel-acmodule-1564427223927602208\u0026cp\u003djira\u0026lic\u003dnone\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiIwY2I3NGI4MGRkN2Y3NzkzODQ5ODY3Mjk4Y2EyZDg4OWU0ZDhiNjYxMTFiNjg5NjIyN2JkYmI0OTBkYzQ0YWUxIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.UXWVrjqrCcjX6Mt6gIap4nK7o3aTLRz0ab1fMwwSJqo"
275
+ }, {
276
+ "name": "BasePath only",
277
+ "canonicalUrl": "GET\u0026/test\u0026",
278
+ "signedUrl": "https://example.com/test?jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiYWQzMjhmYWM5OTAzNDlhOGM4ODM5M2MxNzU1YmM0Zjk4NGE5YzM4NzIwMjIyOWI4ZWQ1MmUwNGZmN2U5ZmVjIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.1LHKcYMbSI-xGystYrcpniFDmOkA45g-aJ7NhfyB-wY"
279
+ }, {
280
+ "name": "BasePath only (uri)",
281
+ "canonicalUrl": "GET\u0026/test\u0026",
282
+ "signedUrl": "https://example.com/test?jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiJiYWQzMjhmYWM5OTAzNDlhOGM4ODM5M2MxNzU1YmM0Zjk4NGE5YzM4NzIwMjIyOWI4ZWQ1MmUwNGZmN2U5ZmVjIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.1LHKcYMbSI-xGystYrcpniFDmOkA45g-aJ7NhfyB-wY"
283
+ }, {
284
+ "name": "BasePath with Delimiter",
285
+ "canonicalUrl": "GET\u0026/endsWithDelimiter\u0026a\u003db",
286
+ "signedUrl": "https://example.com/endsWithDelimiter/?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI4Zjc3YzZhMGU2YmYyNTA0MTEyMWVlNDQ0ODBkM2M5N2FlOWNjYTRiN2EwMDdlYTZlMWQ2NDhkOWEyNmU1ZDFkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.Lssm_JCVXZ7Qh-FEMJZx-eblxAa13MlzkReNLvARJI8"
287
+ }, {
288
+ "name": "BasePath with Delimiter (uri)",
289
+ "canonicalUrl": "GET\u0026/endsWithDelimiter\u0026a\u003db",
290
+ "signedUrl": "https://example.com/endsWithDelimiter/?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI4Zjc3YzZhMGU2YmYyNTA0MTEyMWVlNDQ0ODBkM2M5N2FlOWNjYTRiN2EwMDdlYTZlMWQ2NDhkOWEyNmU1ZDFkIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.Lssm_JCVXZ7Qh-FEMJZx-eblxAa13MlzkReNLvARJI8"
291
+ }, {
292
+ "name": "BasePath with Delimiter Only",
293
+ "canonicalUrl": "GET\u0026/endsWithDelimiter\u0026",
294
+ "signedUrl": "https://example.com/endsWithDelimiter/?jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0Yzc5OWU5MWZjOGUxYmU5YjFmMDM0ZjIxOTFhZTgwY2IyMDgwYjU0YTg1ZTlhMjQ2NGFkMmYyMTE1ZjhkOTg3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.wqBRfpzUg79FTLQZiUqNIk9qGEBFXhrZmQ5GvicOFY8"
295
+ }, {
296
+ "name": "BasePath with Delimiter Only (uri)",
297
+ "canonicalUrl": "GET\u0026/endsWithDelimiter\u0026",
298
+ "signedUrl": "https://example.com/endsWithDelimiter/?jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI0Yzc5OWU5MWZjOGUxYmU5YjFmMDM0ZjIxOTFhZTgwY2IyMDgwYjU0YTg1ZTlhMjQ2NGFkMmYyMTE1ZjhkOTg3IiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.wqBRfpzUg79FTLQZiUqNIk9qGEBFXhrZmQ5GvicOFY8"
299
+ }, {
300
+ "name": "BasePath RFC3986 Unreserved",
301
+ "canonicalUrl": "GET\u0026/path-._~\u0026a\u003db",
302
+ "signedUrl": "https://example.com/path-._~?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1ZDE1NzRlYTBmZDg5NzRmYTQzMjZkODdiM2VmZWM3NDAwNGJmMTNkMzUzOTU1OTlkZTQyNTMyNzE3OGMxNzNiIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.oWGLh5OdI0tw1i6FL6CDOm-qGpGJQnru9rk5B37O2T8"
303
+ }, {
304
+ "name": "BasePath RFC3986 Unreserved (uri)",
305
+ "canonicalUrl": "GET\u0026/path-._~\u0026a\u003db",
306
+ "signedUrl": "https://example.com/path-._~?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI1ZDE1NzRlYTBmZDg5NzRmYTQzMjZkODdiM2VmZWM3NDAwNGJmMTNkMzUzOTU1OTlkZTQyNTMyNzE3OGMxNzNiIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.oWGLh5OdI0tw1i6FL6CDOm-qGpGJQnru9rk5B37O2T8"
307
+ }, {
308
+ "name": "BasePath RFC3986 Subdelimiters",
309
+ "canonicalUrl": "GET\u0026/path!$%26\u0027()*+,;\u003d\u0026a\u003db",
310
+ "signedUrl": "https://example.com/path!$\u0026\u0027()*+,;\u003d?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI5ZWY4MDNmOWNiOGRlYTRmN2Y4NzVjYWZmOGQzMWU5NTk2MmM2ZThiZDQ0ZDY0YTg0OGQ2ZWJiMDU1YjIxNDRiIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xT24PP9ialy7yso14IaFYO9c5dV2oJ1OMTrPon7yRF4"
311
+ }, {
312
+ "name": "BasePath RFC3986 Subdelimiters (uri)",
313
+ "canonicalUrl": "GET\u0026/path!$%26\u0027()*+,;\u003d\u0026a\u003db",
314
+ "signedUrl": "https://example.com/path!$\u0026\u0027()*+,;\u003d?a\u003db\u0026jwt\u003deyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxc2giOiI5ZWY4MDNmOWNiOGRlYTRmN2Y4NzVjYWZmOGQzMWU5NTk2MmM2ZThiZDQ0ZDY0YTg0OGQ2ZWJiMDU1YjIxNDRiIiwiY29udGV4dCI6e30sImlzcyI6ImppcmE6MTIzNC01Njc4LTkwMDAiLCJleHAiOjE0NjY3MzEyNTUsImlhdCI6MTQ2NjczMTA3NX0.xT24PP9ialy7yso14IaFYO9c5dV2oJ1OMTrPon7yRF4"
315
+ }],
316
+ "comment": "Generated by com.atlassian.plugin.connect.plugin.auth.jwt.JwtSigningInteroperabilityTest.SigningTests on Fri Jun 24 11:17:55 AEST 2016"
317
+ }
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::Properties do
4
+ with_each_client do |site_url, client|
5
+ context 'when accessing singular resource' do
6
+ let(:client) { client }
7
+ let(:site_url) { site_url }
8
+ let(:key) { 'xyz' }
9
+ let(:target) { described_class.new(client, attrs: { 'key' => 'xyz' }, issue_id: '10002') }
10
+ let(:belongs_to) { JIRA::Resource::Issue.new(client, attrs: { 'id' => '10002' }) }
11
+ let(:expected_attributes) { { 'key' => key, 'value' => 'supercalifragilistic' } }
12
+ let(:attributes_for_put) { { 'value' => 'expialidocious' } }
13
+ let(:expected_attributes_from_put) { { 'key' => key, 'value' => 'expialidocious' } }
14
+
15
+ it_behaves_like 'a resource'
16
+ it_behaves_like 'a resource with a singular GET endpoint'
17
+ it_behaves_like 'a resource with a DELETE endpoint'
18
+ it_behaves_like 'a resource with a PUT endpoint'
19
+ end
20
+
21
+ context 'when accessing collection' do
22
+ let(:client) { client }
23
+ let(:site_url) { site_url }
24
+ let(:key) { 'xyz' }
25
+ let(:belongs_to) { JIRA::Resource::Issue.new(client, attrs: { 'id' => '10002' }) }
26
+ let(:expected_collection_length) { 2 }
27
+ let(:expected_attributes) { { 'key' => key, 'value' => 'supercalifragilistic' } }
28
+
29
+ before do
30
+ ## Since properties collections do subsequent queries on each individual properties records,
31
+ ## we need to additionally define stub requests for each of the individual records
32
+ additional_targets = [
33
+ described_class.new(client, attrs: { 'key' => 'foo' }, issue_id: '10002'),
34
+ described_class.new(client, attrs: { 'key' => 'xyz' }, issue_id: '10002')
35
+ ]
36
+ additional_targets.each do |target|
37
+ req_url = site_url + target.url
38
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
39
+ end
40
+ end
41
+
42
+ it_behaves_like 'a resource with a collection GET endpoint'
43
+ end
44
+ end
45
+ end
@@ -38,9 +38,10 @@ describe JIRA::Resource::Transition do
38
38
 
39
39
  describe 'POST endpoint' do
40
40
  it 'saves a new resource' do
41
- stub_request(:post, /#{described_class.collection_path(client, prefix)}$/)
41
+ req_url = build_url
42
+ stub_request(:post, req_url)
42
43
  .with(body: attributes_for_post.to_json)
43
- .to_return(status: 200, body: get_mock_from_path(:post))
44
+ .to_return(status: 200, body: get_mock_from_url(:post, req_url))
44
45
  subject = build_receiver.build
45
46
  expect(subject.save(attributes_for_post)).to be_truthy
46
47
  end
@@ -1,7 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe JIRA::Resource::Webhook do
4
- with_each_client do |site_url, client|
4
+ ## This endpoint uses a different base path, so override this client's rest_base_path option
5
+ ## so this test can still use the SharedExampleGroups
6
+ with_each_client(rest_base_path: described_class.const_get(:REST_BASE_PATH)) do |site_url, client|
5
7
  let(:client) { client }
6
8
  let(:site_url) { site_url }
7
9
 
@@ -20,7 +22,7 @@ describe JIRA::Resource::Webhook do
20
22
 
21
23
  it 'returns a collection of components' do
22
24
  stub_request(:get, site_url + described_class.singular_path(client, key))
23
- .to_return(status: 200, body: get_mock_response('webhook/webhook.json'))
25
+ .to_return(status: 200, body: get_mock_response('webhook/2.json'))
24
26
 
25
27
  webhook = client.Webhook.find(key)
26
28
 
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ # rubocop:disable RSpec/LeakyLocalVariable
4
+ describe JIRA::Atlassian::Jwt do
5
+ let(:jwt_opts) do
6
+ {
7
+ algorithm: 'HS256',
8
+ leeway: (3600 * 24 * 365 * 10) # 10 years of leeway -- the JWT gem verifies the token expiry time
9
+ }
10
+ end
11
+ let(:base_url) { '' }
12
+
13
+ it 'generates claims' do
14
+ url = 'https://example.atlassian.com/jira/projects'
15
+ issuer = 'com.atlassian.test'
16
+
17
+ now = Time.now.to_i
18
+ qsh = Digest::SHA256.hexdigest(
19
+ described_class.create_canonical_request(url, 'get', base_url)
20
+ )
21
+
22
+ expected_claim = {
23
+ iss: 'com.atlassian.test',
24
+ iat: now,
25
+ exp: now + 60,
26
+ qsh: qsh
27
+ }
28
+
29
+ claim = described_class.build_claims(issuer, url, 'get', base_url, now, now + 60)
30
+ expect(claim).to eq expected_claim
31
+ end
32
+
33
+ # Offical Atlassian signed URL test data
34
+ json_tests = File.read(File.expand_path('../../data/files/jwt-signed-urls.json', File.dirname(__FILE__)))
35
+
36
+ test_data = JSON.parse(json_tests)
37
+ shared_secret = test_data['secret']
38
+
39
+ test_data['tests'].each do |test|
40
+ signed_url = test['signedUrl']
41
+ signed_uri = URI.parse(signed_url)
42
+ token = CGI.parse(signed_uri.query)['jwt'].first
43
+
44
+ it "#{test['name']} - Canonical URL" do
45
+ canonical_uri = described_class.create_canonical_request(signed_url, 'GET', base_url)
46
+
47
+ # Remote the jwt query param from the signed URL to get the original
48
+ expect(canonical_uri).to eq test['canonicalUrl']
49
+ end
50
+
51
+ it "#{test['name']} - QSH match" do
52
+ expected_qsh = Digest::SHA256.hexdigest(described_class.create_canonical_request(signed_url, 'GET', base_url))
53
+
54
+ decoded_token = JWT.decode(token, shared_secret, true, jwt_opts).first
55
+
56
+ expect(expected_qsh).to eq decoded_token['qsh']
57
+ end
58
+ end
59
+ end
60
+ # rubocop:enable RSpec/LeakyLocalVariable
@@ -244,7 +244,8 @@ describe JIRA::Resource::Issue do
244
244
  'comment' => { 'comments' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }] },
245
245
  'attachment' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }],
246
246
  'worklog' => { 'worklogs' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }] }
247
- }
247
+ },
248
+ 'properties' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }]
248
249
  })
249
250
  end
250
251
 
@@ -284,6 +285,9 @@ describe JIRA::Resource::Issue do
284
285
 
285
286
  expect(subject).to have_many(:worklogs, JIRA::Resource::Worklog)
286
287
  expect(subject.worklogs.length).to eq(2)
288
+
289
+ expect(subject).to have_many(:properties, JIRA::Resource::Properties)
290
+ expect(subject.properties.length).to eq(2)
287
291
  end
288
292
  end
289
293
  end
@@ -0,0 +1,4 @@
1
+ {
2
+ "key": "foo",
3
+ "value": "bar"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "key": "xyz",
3
+ "value": "supercalifragilistic"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "key": "xyz",
3
+ "value": "expialidocious"
4
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "keys": [
3
+ {
4
+ "key": "xyz",
5
+ "self": "http://localhost:2990/jira/rest/api/2/issue/10002/properties/xyz"
6
+ },
7
+ {
8
+ "key": "foo",
9
+ "self": "http://localhost:2990/jira/rest/api/2/issue/10002/properties/foo"
10
+ }
11
+ ]
12
+ }
@@ -1,12 +1,12 @@
1
1
  module ClientsHelper
2
- def with_each_client(&)
2
+ def with_each_client(opts = {}, &)
3
3
  clients = {}
4
4
 
5
- oauth_client = JIRA::Client.new(consumer_key: 'foo', consumer_secret: 'bar')
5
+ oauth_client = JIRA::Client.new({ consumer_key: 'foo', consumer_secret: 'bar' }.merge(opts))
6
6
  oauth_client.set_access_token('abc', '123')
7
7
  clients['http://localhost:2990'] = oauth_client
8
8
 
9
- basic_client = JIRA::Client.new(username: 'foo', password: 'bar', auth_type: :basic, use_ssl: false)
9
+ basic_client = JIRA::Client.new({ username: 'foo', password: 'bar', auth_type: :basic, use_ssl: false }.merge(opts))
10
10
  clients['http://localhost:2990'] = basic_client
11
11
 
12
12
  clients.each(&)
@@ -1,20 +1,18 @@
1
1
  require 'cgi'
2
2
 
3
- def get_mock_from_path(method, options = {})
4
- prefix = if defined? belongs_to
5
- "#{belongs_to.path_component}/"
6
- else
7
- ''
8
- end
9
-
10
- url = if options[:url]
11
- options[:url]
12
- elsif options[:key]
13
- described_class.singular_path(client, options[:key], prefix)
14
- else
15
- described_class.collection_path(client, prefix)
16
- end
17
- file_path = url.sub(client.options[:rest_base_path], '')
3
+ def build_url(options = {})
4
+ prefix = defined?(belongs_to) ? "#{belongs_to.path_component}/" : '/'
5
+ path = if options.key?(:key)
6
+ described_class.singular_path(client, options[:key], prefix)
7
+ else
8
+ described_class.collection_path(client, prefix)
9
+ end
10
+ site_url + path
11
+ end
12
+
13
+ def get_mock_from_url(method, url, options = {})
14
+ # Remove site_url and rest api portion of the url
15
+ file_path = url.sub(site_url + client.options[:rest_base_path], '')
18
16
  file_path = "#{file_path}.#{options[:suffix]}" if options[:suffix]
19
17
  file_path = "#{file_path}.#{method}" unless method == :get
20
18
  value_if_not_found = options.key?(:value_if_not_found) ? options[:value_if_not_found] : false
@@ -63,8 +61,8 @@ end
63
61
 
64
62
  shared_examples 'a resource with a collection GET endpoint' do
65
63
  it 'gets the collection' do
66
- stub_request(:get, site_url + described_class.collection_path(client))
67
- .to_return(status: 200, body: get_mock_from_path(:get))
64
+ req_url = build_url
65
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
68
66
  collection = build_receiver.all
69
67
 
70
68
  expect(collection.length).to eq(expected_collection_length)
@@ -74,10 +72,8 @@ end
74
72
 
75
73
  shared_examples 'a resource with JQL inputs and a collection GET endpoint' do
76
74
  it 'gets the collection' do
77
- stub_request(
78
- :get,
79
- "#{site_url}#{client.options[:rest_base_path]}/search/jql?jql=#{CGI.escape(jql_query_string)}"
80
- ).to_return(status: 200, body: get_mock_response('issue.json'))
75
+ req_url = "#{site_url}#{client.options[:rest_base_path]}/search/jql?jql=#{CGI.escape(jql_query_string)}"
76
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_response('issue.json'))
81
77
 
82
78
  collection = build_receiver.jql(jql_query_string)
83
79
 
@@ -90,8 +86,8 @@ shared_examples 'a resource with a singular GET endpoint' do
90
86
  it 'GETs a single resource' do
91
87
  # E.g., for JIRA::Resource::Project, we need to call
92
88
  # client.Project.find()
93
- stub_request(:get, site_url + described_class.singular_path(client, key, prefix))
94
- .to_return(status: 200, body: get_mock_from_path(:get, key:))
89
+ req_url = build_url(key:)
90
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
95
91
  subject = client.send(class_basename).find(key, options)
96
92
 
97
93
  expect(subject).to have_attributes(expected_attributes)
@@ -100,8 +96,8 @@ shared_examples 'a resource with a singular GET endpoint' do
100
96
  it 'builds and fetches a single resource' do
101
97
  # E.g., for JIRA::Resource::Project, we need to call
102
98
  # client.Project.build('key' => 'ABC123')
103
- stub_request(:get, site_url + described_class.singular_path(client, key, prefix))
104
- .to_return(status: 200, body: get_mock_from_path(:get, key:))
99
+ req_url = build_url(key:)
100
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
105
101
 
106
102
  subject = build_receiver.build(described_class.key_attribute.to_s => key)
107
103
  subject.fetch
@@ -110,7 +106,7 @@ shared_examples 'a resource with a singular GET endpoint' do
110
106
  end
111
107
 
112
108
  it 'handles a 404' do
113
- stub_request(:get, site_url + described_class.singular_path(client, '99999', prefix))
109
+ stub_request(:get, build_url(key: '99999'))
114
110
  .to_return(status: 404, body: "{\"errorMessages\":[\"#{class_basename} Does Not Exist\"],\"errors\": {}}")
115
111
  expect do
116
112
  client.send(class_basename).find('99999', options)
@@ -122,8 +118,8 @@ shared_examples 'a resource with a DELETE endpoint' do
122
118
  it 'deletes a resource' do
123
119
  # E.g., for JIRA::Resource::Project, we need to call
124
120
  # client.Project.delete()
125
- stub_request(:delete, site_url + described_class.singular_path(client, key, prefix))
126
- .to_return(status: 204, body: nil)
121
+ req_url = build_url(key:)
122
+ stub_request(:delete, req_url).to_return(status: 204, body: nil)
127
123
 
128
124
  subject = build_receiver.build(described_class.key_attribute.to_s => key)
129
125
  expect(subject.delete).to be_truthy
@@ -132,8 +128,8 @@ end
132
128
 
133
129
  shared_examples 'a resource with a POST endpoint' do
134
130
  it 'saves a new resource' do
135
- stub_request(:post, site_url + described_class.collection_path(client, prefix))
136
- .to_return(status: 201, body: get_mock_from_path(:post))
131
+ req_url = build_url
132
+ stub_request(:post, req_url).to_return(status: 201, body: get_mock_from_url(:post, req_url))
137
133
  subject = build_receiver.build
138
134
  expect(subject.save(attributes_for_post)).to be_truthy
139
135
  expected_attributes_from_post.each do |method_name, value|
@@ -144,10 +140,10 @@ end
144
140
 
145
141
  shared_examples 'a resource with a PUT endpoint' do
146
142
  it 'saves an existing component' do
147
- stub_request(:get, site_url + described_class.singular_path(client, key, prefix))
148
- .to_return(status: 200, body: get_mock_from_path(:get, key:))
149
- stub_request(:put, site_url + described_class.singular_path(client, key, prefix))
150
- .to_return(status: 200, body: get_mock_from_path(:put, key:, value_if_not_found: nil))
143
+ req_url = build_url(key:)
144
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
145
+ stub_request(:put, req_url)
146
+ .to_return(status: 200, body: get_mock_from_url(:put, req_url, value_if_not_found: nil))
151
147
  subject = build_receiver.build(described_class.key_attribute.to_s => key)
152
148
  subject.fetch
153
149
  expect(subject.save(attributes_for_put)).to be_truthy
@@ -159,10 +155,10 @@ end
159
155
 
160
156
  shared_examples 'a resource with a PUT endpoint that rejects invalid fields' do
161
157
  it 'fails to save with an invalid field' do
162
- stub_request(:get, site_url + described_class.singular_path(client, key))
163
- .to_return(status: 200, body: get_mock_from_path(:get, key:))
164
- stub_request(:put, site_url + described_class.singular_path(client, key))
165
- .to_return(status: 400, body: get_mock_from_path(:put, key:, suffix: 'invalid'))
158
+ req_url = build_url(key:)
159
+ stub_request(:get, req_url).to_return(status: 200, body: get_mock_from_url(:get, req_url))
160
+ stub_request(:put, req_url)
161
+ .to_return(status: 400, body: get_mock_from_url(:put, req_url, suffix: 'invalid'))
166
162
  subject = client.send(class_basename).build(described_class.key_attribute.to_s => key)
167
163
  subject.fetch
168
164
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jira-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SUMO Heavy Industries
8
8
  - test IO
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-26 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: atlassian-jwt
28
+ name: cgi
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: cgi
42
+ name: jwt
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '2.1'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '2.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: multipart-post
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -101,6 +101,7 @@ files:
101
101
  - Rakefile
102
102
  - jira-ruby.gemspec
103
103
  - lib/jira-ruby.rb
104
+ - lib/jira/atlassian/jwt.rb
104
105
  - lib/jira/base.rb
105
106
  - lib/jira/base_factory.rb
106
107
  - lib/jira/client.rb
@@ -129,6 +130,7 @@ files:
129
130
  - lib/jira/resource/issuetype.rb
130
131
  - lib/jira/resource/priority.rb
131
132
  - lib/jira/resource/project.rb
133
+ - lib/jira/resource/properties.rb
132
134
  - lib/jira/resource/rapidview.rb
133
135
  - lib/jira/resource/remotelink.rb
134
136
  - lib/jira/resource/resolution.rb
@@ -145,6 +147,7 @@ files:
145
147
  - lib/jira/resource/worklog.rb
146
148
  - lib/jira/version.rb
147
149
  - lib/tasks/generate.rake
150
+ - spec/data/files/jwt-signed-urls.json
148
151
  - spec/data/files/short.txt
149
152
  - spec/integration/attachment_spec.rb
150
153
  - spec/integration/comment_spec.rb
@@ -155,6 +158,7 @@ files:
155
158
  - spec/integration/issuetype_spec.rb
156
159
  - spec/integration/priority_spec.rb
157
160
  - spec/integration/project_spec.rb
161
+ - spec/integration/properties_spec.rb
158
162
  - spec/integration/rapidview_spec.rb
159
163
  - spec/integration/resolution_spec.rb
160
164
  - spec/integration/status_category_spec.rb
@@ -165,6 +169,7 @@ files:
165
169
  - spec/integration/watcher_spec.rb
166
170
  - spec/integration/webhook_spec.rb
167
171
  - spec/integration/worklog_spec.rb
172
+ - spec/jira/atlassian/jwt_spec.rb
168
173
  - spec/jira/base_factory_spec.rb
169
174
  - spec/jira/base_spec.rb
170
175
  - spec/jira/client_spec.rb
@@ -208,6 +213,10 @@ files:
208
213
  - spec/mock_responses/issue/10002/comment.post.json
209
214
  - spec/mock_responses/issue/10002/comment/10000.json
210
215
  - spec/mock_responses/issue/10002/comment/10000.put.json
216
+ - spec/mock_responses/issue/10002/properties.json
217
+ - spec/mock_responses/issue/10002/properties/foo.json
218
+ - spec/mock_responses/issue/10002/properties/xyz.json
219
+ - spec/mock_responses/issue/10002/properties/xyz.put.json
211
220
  - spec/mock_responses/issue/10002/transitions.json
212
221
  - spec/mock_responses/issue/10002/transitions.post.json
213
222
  - spec/mock_responses/issue/10002/watchers.json
@@ -219,8 +228,6 @@ files:
219
228
  - spec/mock_responses/issueLinkType/10000.json
220
229
  - spec/mock_responses/issuetype.json
221
230
  - spec/mock_responses/issuetype/5.json
222
- - spec/mock_responses/jira/rest/webhooks/1.0/webhook.json
223
- - spec/mock_responses/jira/rest/webhooks/1.0/webhook/2.json
224
231
  - spec/mock_responses/priority.json
225
232
  - spec/mock_responses/priority/1.json
226
233
  - spec/mock_responses/project.json
@@ -244,7 +251,7 @@ files:
244
251
  - spec/mock_responses/version/10000.json
245
252
  - spec/mock_responses/version/10000.put.json
246
253
  - spec/mock_responses/webhook.json
247
- - spec/mock_responses/webhook/webhook.json
254
+ - spec/mock_responses/webhook/2.json
248
255
  - spec/spec_helper.rb
249
256
  - spec/support/clients_helper.rb
250
257
  - spec/support/matchers/have_attributes.rb
@@ -1,11 +0,0 @@
1
- [{"name":"from API",
2
- "url":"http://localhost:3000/webhooks/1",
3
- "excludeBody":false,
4
- "filters":{"issue-related-events-section":""},
5
- "events":[],
6
- "enabled":true,
7
- "self":"http://localhost:2990/jira/rest/webhooks/1.0/webhook/2",
8
- "lastUpdatedUser":"admin",
9
- "lastUpdatedDisplayName":"admin",
10
- "lastUpdated":1453306520188
11
- }]
@@ -1,11 +0,0 @@
1
- {"name":"from API",
2
- "url":"http://localhost:3000/webhooks/1",
3
- "excludeBody":false,
4
- "filters":{"issue-related-events-section":""},
5
- "events":[],
6
- "enabled":true,
7
- "self":"http://localhost:2990/jira/rest/webhooks/1.0/webhook/2",
8
- "lastUpdatedUser":"admin",
9
- "lastUpdatedDisplayName":"admin",
10
- "lastUpdated":1453306520188
11
- }