sclemmer-jira-ruby 0.1.12

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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +46 -0
  6. data/README.rdoc +309 -0
  7. data/Rakefile +28 -0
  8. data/example.rb +119 -0
  9. data/http-basic-example.rb +112 -0
  10. data/lib/jira.rb +33 -0
  11. data/lib/jira/base.rb +497 -0
  12. data/lib/jira/base_factory.rb +49 -0
  13. data/lib/jira/client.rb +165 -0
  14. data/lib/jira/has_many_proxy.rb +43 -0
  15. data/lib/jira/http_client.rb +69 -0
  16. data/lib/jira/http_error.rb +16 -0
  17. data/lib/jira/oauth_client.rb +84 -0
  18. data/lib/jira/railtie.rb +10 -0
  19. data/lib/jira/request_client.rb +18 -0
  20. data/lib/jira/resource/attachment.rb +12 -0
  21. data/lib/jira/resource/comment.rb +14 -0
  22. data/lib/jira/resource/component.rb +10 -0
  23. data/lib/jira/resource/field.rb +10 -0
  24. data/lib/jira/resource/filter.rb +15 -0
  25. data/lib/jira/resource/issue.rb +80 -0
  26. data/lib/jira/resource/issuetype.rb +10 -0
  27. data/lib/jira/resource/priority.rb +10 -0
  28. data/lib/jira/resource/project.rb +31 -0
  29. data/lib/jira/resource/status.rb +10 -0
  30. data/lib/jira/resource/transition.rb +33 -0
  31. data/lib/jira/resource/user.rb +14 -0
  32. data/lib/jira/resource/version.rb +10 -0
  33. data/lib/jira/resource/worklog.rb +16 -0
  34. data/lib/jira/tasks.rb +0 -0
  35. data/lib/jira/version.rb +3 -0
  36. data/lib/tasks/generate.rake +18 -0
  37. data/sclemmer-jira-ruby.gemspec +28 -0
  38. data/spec/integration/attachment_spec.rb +23 -0
  39. data/spec/integration/comment_spec.rb +55 -0
  40. data/spec/integration/component_spec.rb +42 -0
  41. data/spec/integration/field_spec.rb +35 -0
  42. data/spec/integration/issue_spec.rb +94 -0
  43. data/spec/integration/issuetype_spec.rb +26 -0
  44. data/spec/integration/priority_spec.rb +27 -0
  45. data/spec/integration/project_spec.rb +56 -0
  46. data/spec/integration/status_spec.rb +27 -0
  47. data/spec/integration/transition_spec.rb +52 -0
  48. data/spec/integration/user_spec.rb +25 -0
  49. data/spec/integration/version_spec.rb +43 -0
  50. data/spec/integration/worklog_spec.rb +55 -0
  51. data/spec/jira/base_factory_spec.rb +46 -0
  52. data/spec/jira/base_spec.rb +583 -0
  53. data/spec/jira/client_spec.rb +188 -0
  54. data/spec/jira/has_many_proxy_spec.rb +47 -0
  55. data/spec/jira/http_client_spec.rb +109 -0
  56. data/spec/jira/http_error_spec.rb +26 -0
  57. data/spec/jira/oauth_client_spec.rb +111 -0
  58. data/spec/jira/request_client_spec.rb +14 -0
  59. data/spec/jira/resource/attachment_spec.rb +20 -0
  60. data/spec/jira/resource/filter_spec.rb +97 -0
  61. data/spec/jira/resource/issue_spec.rb +123 -0
  62. data/spec/jira/resource/project_factory_spec.rb +13 -0
  63. data/spec/jira/resource/project_spec.rb +70 -0
  64. data/spec/jira/resource/worklog_spec.rb +24 -0
  65. data/spec/mock_responses/attachment/10000.json +20 -0
  66. data/spec/mock_responses/component.post.json +28 -0
  67. data/spec/mock_responses/component/10000.invalid.put.json +5 -0
  68. data/spec/mock_responses/component/10000.json +39 -0
  69. data/spec/mock_responses/component/10000.put.json +39 -0
  70. data/spec/mock_responses/field.json +32 -0
  71. data/spec/mock_responses/field/1.json +15 -0
  72. data/spec/mock_responses/issue.json +1108 -0
  73. data/spec/mock_responses/issue.post.json +5 -0
  74. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  75. data/spec/mock_responses/issue/10002.json +126 -0
  76. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  77. data/spec/mock_responses/issue/10002/comment.json +65 -0
  78. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  79. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  80. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  81. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  82. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  83. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  84. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  85. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  86. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  87. data/spec/mock_responses/issuetype.json +42 -0
  88. data/spec/mock_responses/issuetype/5.json +8 -0
  89. data/spec/mock_responses/priority.json +42 -0
  90. data/spec/mock_responses/priority/1.json +8 -0
  91. data/spec/mock_responses/project.json +12 -0
  92. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  93. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  94. data/spec/mock_responses/status.json +37 -0
  95. data/spec/mock_responses/status/1.json +7 -0
  96. data/spec/mock_responses/user_username=admin.json +17 -0
  97. data/spec/mock_responses/version.post.json +7 -0
  98. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  99. data/spec/mock_responses/version/10000.json +11 -0
  100. data/spec/mock_responses/version/10000.put.json +7 -0
  101. data/spec/spec_helper.rb +22 -0
  102. data/spec/support/clients_helper.rb +16 -0
  103. data/spec/support/matchers/have_attributes.rb +11 -0
  104. data/spec/support/matchers/have_many.rb +9 -0
  105. data/spec/support/matchers/have_one.rb +5 -0
  106. data/spec/support/shared_examples/integration.rb +194 -0
  107. metadata +302 -0
@@ -0,0 +1,84 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/project/SAMPLEPROJECT",
3
+ "id": "10001",
4
+ "key": "SAMPLEPROJECT",
5
+ "lead": {
6
+ "self": "http://localhost:2990/jira/rest/api/2/user?username=admin",
7
+ "name": "admin",
8
+ "avatarUrls": {
9
+ "16x16": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122",
10
+ "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122"
11
+ },
12
+ "displayName": "admin",
13
+ "active": true
14
+ },
15
+ "components": [
16
+ {
17
+ "self": "http://localhost:2990/jira/rest/api/2/component/10001",
18
+ "id": "10001",
19
+ "name": "Jamflam",
20
+ "isAssigneeTypeValid": false
21
+ },
22
+ {
23
+ "self": "http://localhost:2990/jira/rest/api/2/component/10000",
24
+ "id": "10000",
25
+ "name": "Jammy",
26
+ "description": "Description!",
27
+ "isAssigneeTypeValid": false
28
+ }
29
+ ],
30
+ "issueTypes": [
31
+ {
32
+ "self": "http://localhost:2990/jira/rest/api/2/issuetype/1",
33
+ "id": "1",
34
+ "description": "A problem which impairs or prevents the functions of the product.",
35
+ "iconUrl": "http://localhost:2990/jira/images/icons/bug.gif",
36
+ "name": "Bug",
37
+ "subtask": false
38
+ },
39
+ {
40
+ "self": "http://localhost:2990/jira/rest/api/2/issuetype/2",
41
+ "id": "2",
42
+ "description": "A new feature of the product, which has yet to be developed.",
43
+ "iconUrl": "http://localhost:2990/jira/images/icons/newfeature.gif",
44
+ "name": "New Feature",
45
+ "subtask": false
46
+ },
47
+ {
48
+ "self": "http://localhost:2990/jira/rest/api/2/issuetype/3",
49
+ "id": "3",
50
+ "description": "A task that needs to be done.",
51
+ "iconUrl": "http://localhost:2990/jira/images/icons/task.gif",
52
+ "name": "Task",
53
+ "subtask": false
54
+ },
55
+ {
56
+ "self": "http://localhost:2990/jira/rest/api/2/issuetype/4",
57
+ "id": "4",
58
+ "description": "An improvement or enhancement to an existing feature or task.",
59
+ "iconUrl": "http://localhost:2990/jira/images/icons/improvement.gif",
60
+ "name": "Improvement",
61
+ "subtask": false
62
+ },
63
+ {
64
+ "self": "http://localhost:2990/jira/rest/api/2/issuetype/5",
65
+ "id": "5",
66
+ "description": "The sub-task of the issue",
67
+ "iconUrl": "http://localhost:2990/jira/images/icons/issue_subtask.gif",
68
+ "name": "Sub-task",
69
+ "subtask": true
70
+ }
71
+ ],
72
+ "assigneeType": "PROJECT_LEAD",
73
+ "versions": [],
74
+ "name": "Sample Project for Developing RoR RESTful API",
75
+ "roles": {
76
+ "Users": "http://localhost:2990/jira/rest/api/2/project/SAMPLEPROJECT/role/10000",
77
+ "Administrators": "http://localhost:2990/jira/rest/api/2/project/SAMPLEPROJECT/role/10002",
78
+ "Developers": "http://localhost:2990/jira/rest/api/2/project/SAMPLEPROJECT/role/10001"
79
+ },
80
+ "avatarUrls": {
81
+ "16x16": "http://localhost:2990/jira/secure/projectavatar?size=small&pid=10001&avatarId=10011",
82
+ "48x48": "http://localhost:2990/jira/secure/projectavatar?pid=10001&avatarId=10011"
83
+ }
84
+ }
@@ -0,0 +1,37 @@
1
+ [
2
+ {
3
+ "self": "http://localhost:2990/jira/rest/api/2/status/1",
4
+ "description": "The issue is open and ready for the assignee to start work on it.",
5
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_open.gif",
6
+ "name": "Open",
7
+ "id": "1"
8
+ },
9
+ {
10
+ "self": "http://localhost:2990/jira/rest/api/2/status/3",
11
+ "description": "This issue is being actively worked on at the moment by the assignee.",
12
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_inprogress.gif",
13
+ "name": "In Progress",
14
+ "id": "3"
15
+ },
16
+ {
17
+ "self": "http://localhost:2990/jira/rest/api/2/status/4",
18
+ "description": "This issue was once resolved, but the resolution was deemed incorrect. From here issues are either marked assigned or resolved.",
19
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_reopened.gif",
20
+ "name": "Reopened",
21
+ "id": "4"
22
+ },
23
+ {
24
+ "self": "http://localhost:2990/jira/rest/api/2/status/5",
25
+ "description": "A resolution has been taken, and it is awaiting verification by reporter. From here issues are either reopened, or are closed.",
26
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_resolved.gif",
27
+ "name": "Resolved",
28
+ "id": "5"
29
+ },
30
+ {
31
+ "self": "http://localhost:2990/jira/rest/api/2/status/6",
32
+ "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.",
33
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_closed.gif",
34
+ "name": "Closed",
35
+ "id": "6"
36
+ }
37
+ ]
@@ -0,0 +1,7 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/status/1",
3
+ "description": "The issue is open and ready for the assignee to start work on it.",
4
+ "iconUrl": "http://localhost:2990/jira/images/icons/status_open.gif",
5
+ "name": "Open",
6
+ "id": "1"
7
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/user?username=admin",
3
+ "name": "admin",
4
+ "emailAddress": "admin@example.com",
5
+ "avatarUrls": {
6
+ "16x16": "http://localhost:2990/jira/secure/useravatar?size=small&avatarId=10122",
7
+ "48x48": "http://localhost:2990/jira/secure/useravatar?avatarId=10122"
8
+ },
9
+ "displayName": "admin",
10
+ "active": true,
11
+ "timeZone": "Pacific/Auckland",
12
+ "groups": {
13
+ "size": 3,
14
+ "items": []
15
+ },
16
+ "expand": "groups"
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/version/10001",
3
+ "id": "10001",
4
+ "name": "2.0",
5
+ "archived": false,
6
+ "released": false
7
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "errorMessages": [
3
+ "Unrecognized field \"chump\" (Class com.atlassian.jira.rest.v2.issue.version.VersionBean), not marked as ignorable\n at [Source: org.apache.catalina.connector.CoyoteInputStream@4264a42e; line: 1, column: 2]"
4
+ ]
5
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/version/10000",
3
+ "id": "10000",
4
+ "description": "Initial version",
5
+ "name": "1.0",
6
+ "overdue": false,
7
+ "userReleaseDate": "12/Jan/12",
8
+ "archived": false,
9
+ "releaseDate": "2012-01-12",
10
+ "released": false
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "self": "http://localhost:2990/jira/rest/api/2/version/10000",
3
+ "id": "10000",
4
+ "name": "2.0.0",
5
+ "archived": false,
6
+ "released": false
7
+ }
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'webmock/rspec'
5
+ Dir["./spec/support/**/*.rb"].each {|f| require f}
6
+
7
+ require 'jira'
8
+
9
+ RSpec.configure do |config|
10
+ config.extend ClientsHelper
11
+ end
12
+
13
+
14
+ def get_mock_response(file, value_if_file_not_found = false)
15
+ begin
16
+ file.sub!('?', '_') # we have to replace this character on Windows machine
17
+ File.read(File.join(File.dirname(__FILE__), 'mock_responses/', file))
18
+ rescue Errno::ENOENT => e
19
+ raise e if value_if_file_not_found == false
20
+ value_if_file_not_found
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module ClientsHelper
2
+ def with_each_client
3
+ clients = {}
4
+
5
+ oauth_client = JIRA::Client.new({ :consumer_key => 'foo', :consumer_secret => 'bar' })
6
+ oauth_client.set_access_token('abc', '123')
7
+ clients["http://localhost:2990"] = oauth_client
8
+
9
+ basic_client = JIRA::Client.new({ :username => 'foo', :password => 'bar', :auth_type => :basic, :use_ssl => false })
10
+ clients["http://foo:bar@localhost:2990"] = basic_client
11
+
12
+ clients.each do |site_url, client|
13
+ yield site_url, client
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ RSpec::Matchers.define :have_attributes do |expected|
2
+ match do |actual|
3
+ expected.each do |key, value|
4
+ expect(actual.attrs[key]).to eq(value)
5
+ end
6
+ end
7
+
8
+ failure_message do |actual|
9
+ "expected #{actual.attrs} to match #{expected}"
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ RSpec::Matchers.define :have_many do |collection, klass|
2
+ match do |actual|
3
+ expect(actual.send(collection).class).to eq(JIRA::HasManyProxy)
4
+ expect(actual.send(collection).length).to be > 0
5
+ actual.send(collection).each do |member|
6
+ expect(member.class).to eq(klass)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :have_one do |resource, klass|
2
+ match do |actual|
3
+ expect(actual.send(resource).class).to eq(klass)
4
+ end
5
+ end
@@ -0,0 +1,194 @@
1
+ require 'cgi'
2
+
3
+ def get_mock_from_path(method, options = {})
4
+ if defined? belongs_to
5
+ prefix = belongs_to.path_component + '/'
6
+ else
7
+ prefix = ''
8
+ end
9
+
10
+ if options[:url]
11
+ url = options[:url]
12
+ elsif options[:key]
13
+ url = described_class.singular_path(client, options[:key], prefix)
14
+ else
15
+ url = described_class.collection_path(client, prefix)
16
+ end
17
+ file_path = url.sub(client.options[:rest_base_path], '')
18
+ file_path = file_path + '.' + options[:suffix] if options[:suffix]
19
+ file_path = file_path + '.' + method.to_s unless method == :get
20
+ value_if_not_found = options.keys.include?(:value_if_not_found) ? options[:value_if_not_found] : false
21
+ get_mock_response("#{file_path}.json", value_if_not_found)
22
+ end
23
+
24
+ def class_basename
25
+ described_class.name.split('::').last
26
+ end
27
+
28
+ def options
29
+ options = {}
30
+ if defined? belongs_to
31
+ options[belongs_to.to_sym] = belongs_to
32
+ end
33
+ options
34
+ end
35
+
36
+ def prefix
37
+ prefix = '/'
38
+ if defined? belongs_to
39
+ prefix = belongs_to.path_component + '/'
40
+ end
41
+ prefix
42
+ end
43
+
44
+ def build_receiver
45
+ if defined?(belongs_to)
46
+ belongs_to.send(described_class.endpoint_name.pluralize.to_sym)
47
+ else
48
+ client.send(class_basename)
49
+ end
50
+ end
51
+
52
+ shared_examples "a resource" do
53
+
54
+ it "gracefully handles non-json responses" do
55
+ if defined? target
56
+ subject = target
57
+ else
58
+ subject = client.send(class_basename).build(described_class.key_attribute.to_s => '99999')
59
+ end
60
+ stub_request(:put, site_url + subject.url).
61
+ to_return(:status => 405, :body => "<html><body>Some HTML</body></html>")
62
+ expect(subject.save('foo' => 'bar')).to be_falsey
63
+ expect(lambda do
64
+ expect(subject.save!('foo' => 'bar')).to be_falsey
65
+ end).to raise_error(JIRA::HTTPError)
66
+ end
67
+
68
+ end
69
+
70
+ shared_examples "a resource with a collection GET endpoint" do
71
+
72
+ it "should get the collection" do
73
+ stub_request(:get, site_url + described_class.collection_path(client)).
74
+ to_return(:status => 200, :body => get_mock_from_path(:get))
75
+ collection = build_receiver.all
76
+
77
+ expect(collection.length).to eq(expected_collection_length)
78
+ expect(collection.first).to have_attributes(expected_attributes)
79
+ end
80
+
81
+ end
82
+
83
+ shared_examples "a resource with JQL inputs and a collection GET endpoint" do
84
+
85
+ it "should get the collection" do
86
+ stub_request(
87
+ :get,
88
+ site_url +
89
+ client.options[:rest_base_path] +
90
+ '/search?jql=' +
91
+ CGI.escape(jql_query_string)
92
+ ).to_return(:status => 200, :body => get_mock_response('issue.json'))
93
+
94
+ collection = build_receiver.jql(jql_query_string)
95
+
96
+ expect(collection.length).to eq(expected_collection_length)
97
+ expect(collection.first).to have_attributes(expected_attributes)
98
+ end
99
+
100
+ end
101
+
102
+ shared_examples "a resource with a singular GET endpoint" do
103
+
104
+ it "GETs a single resource" do
105
+ # E.g., for JIRA::Resource::Project, we need to call
106
+ # client.Project.find()
107
+ stub_request(:get, site_url + described_class.singular_path(client, key, prefix)).
108
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key => key))
109
+ subject = client.send(class_basename).find(key, options)
110
+
111
+ expect(subject).to have_attributes(expected_attributes)
112
+ end
113
+
114
+ it "builds and fetches a single resource" do
115
+ # E.g., for JIRA::Resource::Project, we need to call
116
+ # client.Project.build('key' => 'ABC123')
117
+ stub_request(:get, site_url + described_class.singular_path(client, key, prefix)).
118
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key => key))
119
+
120
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
121
+ subject.fetch
122
+
123
+ expect(subject).to have_attributes(expected_attributes)
124
+ end
125
+
126
+ it "handles a 404" do
127
+ stub_request(:get, site_url + described_class.singular_path(client, '99999', prefix)).
128
+ to_return(:status => 404, :body => '{"errorMessages":["'+class_basename+' Does Not Exist"],"errors": {}}')
129
+ expect( lambda do
130
+ client.send(class_basename).find('99999', options)
131
+ end).to raise_exception(JIRA::HTTPError)
132
+ end
133
+ end
134
+
135
+ shared_examples "a resource with a DELETE endpoint" do
136
+ it "deletes a resource" do
137
+ # E.g., for JIRA::Resource::Project, we need to call
138
+ # client.Project.delete()
139
+ stub_request(:delete, site_url + described_class.singular_path(client, key, prefix)).
140
+ to_return(:status => 204, :body => nil)
141
+
142
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
143
+ expect(subject.delete).to be_truthy
144
+ end
145
+ end
146
+
147
+ shared_examples "a resource with a POST endpoint" do
148
+
149
+ it "saves a new resource" do
150
+ stub_request(:post, site_url + described_class.collection_path(client, prefix)).
151
+ to_return(:status => 201, :body => get_mock_from_path(:post))
152
+ subject = build_receiver.build
153
+ expect(subject.save(attributes_for_post)).to be_truthy
154
+ expected_attributes_from_post.each do |method_name, value|
155
+ expect(subject.send(method_name)).to eq(value)
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ shared_examples "a resource with a PUT endpoint" do
162
+
163
+ it "saves an existing component" do
164
+ stub_request(:get, site_url + described_class.singular_path(client, key, prefix)).
165
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key =>key))
166
+ stub_request(:put, site_url + described_class.singular_path(client, key, prefix)).
167
+ to_return(:status => 200, :body => get_mock_from_path(:put, :key => key, :value_if_not_found => nil))
168
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
169
+ subject.fetch
170
+ expect(subject.save(attributes_for_put)).to be_truthy
171
+ expected_attributes_from_put.each do |method_name, value|
172
+ expect(subject.send(method_name)).to eq(value)
173
+ end
174
+ end
175
+
176
+ end
177
+
178
+ shared_examples 'a resource with a PUT endpoint that rejects invalid fields' do
179
+
180
+ it "fails to save with an invalid field" do
181
+ stub_request(:get, site_url + described_class.singular_path(client, key)).
182
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key => key))
183
+ stub_request(:put, site_url + described_class.singular_path(client, key)).
184
+ to_return(:status => 400, :body => get_mock_from_path(:put, :key => key, :suffix => "invalid"))
185
+ subject = client.send(class_basename).build(described_class.key_attribute.to_s => key)
186
+ subject.fetch
187
+
188
+ expect(subject.save('fields'=> {'invalid' => 'field'})).to be_falsey
189
+ expect(lambda do
190
+ subject.save!('fields'=> {'invalid' => 'field'})
191
+ end).to raise_error(JIRA::HTTPError)
192
+ end
193
+
194
+ end