jira-ruby-dmg 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
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/jira-ruby-dmg.gemspec +28 -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 +76 -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/jira.rb +33 -0
  37. data/lib/tasks/generate.rake +18 -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 +586 -0
  53. data/spec/jira/client_spec.rb +188 -0
  54. data/spec/jira/has_many_proxy_spec.rb +45 -0
  55. data/spec/jira/http_client_spec.rb +109 -0
  56. data/spec/jira/http_error_spec.rb +25 -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 +107 -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/10000.invalid.put.json +5 -0
  67. data/spec/mock_responses/component/10000.json +39 -0
  68. data/spec/mock_responses/component/10000.put.json +39 -0
  69. data/spec/mock_responses/component.post.json +28 -0
  70. data/spec/mock_responses/field/1.json +15 -0
  71. data/spec/mock_responses/field.json +32 -0
  72. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  73. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  74. data/spec/mock_responses/issue/10002/comment.json +65 -0
  75. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  76. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  77. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  78. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  79. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  80. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  81. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  82. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  83. data/spec/mock_responses/issue/10002.json +126 -0
  84. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  85. data/spec/mock_responses/issue.json +1108 -0
  86. data/spec/mock_responses/issue.post.json +5 -0
  87. data/spec/mock_responses/issuetype/5.json +8 -0
  88. data/spec/mock_responses/issuetype.json +42 -0
  89. data/spec/mock_responses/priority/1.json +8 -0
  90. data/spec/mock_responses/priority.json +42 -0
  91. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  92. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  93. data/spec/mock_responses/project.json +12 -0
  94. data/spec/mock_responses/status/1.json +7 -0
  95. data/spec/mock_responses/status.json +37 -0
  96. data/spec/mock_responses/user_username=admin.json +17 -0
  97. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  98. data/spec/mock_responses/version/10000.json +11 -0
  99. data/spec/mock_responses/version/10000.put.json +7 -0
  100. data/spec/mock_responses/version.post.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 +190 -0
  107. metadata +301 -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,12 @@
1
+ [
2
+ {
3
+ "self": "http://localhost:2990/jira/rest/api/2/project/SAMPLEPROJECT",
4
+ "id": "10001",
5
+ "key": "SAMPLEPROJECT",
6
+ "name": "Sample Project for Developing RoR RESTful API",
7
+ "avatarUrls": {
8
+ "16x16": "http://localhost:2990/jira/secure/projectavatar?size=small&pid=10001&avatarId=10011",
9
+ "48x48": "http://localhost:2990/jira/secure/projectavatar?pid=10001&avatarId=10011"
10
+ }
11
+ }
12
+ ]
@@ -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,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,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,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,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,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
+ actual.attrs[key].should == value
5
+ end
6
+ end
7
+
8
+ failure_message_for_should 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
+ actual.send(collection).class.should == JIRA::HasManyProxy
4
+ actual.send(collection).length.should > 0
5
+ actual.send(collection).each do |member|
6
+ member.class.should == 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
+ actual.send(resource).class.should == klass
4
+ end
5
+ end
@@ -0,0 +1,190 @@
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
+ subject.save('foo' => 'bar').should be_false
63
+ lambda do
64
+ subject.save!('foo' => 'bar').should be_false
65
+ end.should 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
+ collection.length.should == expected_collection_length
77
+
78
+ first = collection.first
79
+ first.should have_attributes(expected_attributes)
80
+ end
81
+
82
+ end
83
+
84
+ shared_examples "a resource with JQL inputs and a collection GET endpoint" do
85
+
86
+ it "should get the collection" do
87
+ stub_request(:get, site_url + client.options[:rest_base_path] + '/search?jql=' + CGI.escape(jql_query_string)).
88
+ to_return(:status => 200, :body => get_mock_response('issue.json'))
89
+ collection = build_receiver.jql(jql_query_string)
90
+ collection.length.should == expected_collection_length
91
+
92
+ first = collection.first
93
+ first.should have_attributes(expected_attributes)
94
+ end
95
+
96
+ end
97
+
98
+ shared_examples "a resource with a singular GET endpoint" do
99
+
100
+ it "GETs a single resource" do
101
+ # E.g., for JIRA::Resource::Project, we need to call
102
+ # client.Project.find()
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 => key))
105
+ subject = client.send(class_basename).find(key, options)
106
+
107
+ subject.should have_attributes(expected_attributes)
108
+ end
109
+
110
+ it "builds and fetches a single resource" do
111
+ # E.g., for JIRA::Resource::Project, we need to call
112
+ # client.Project.build('key' => 'ABC123')
113
+ stub_request(:get, site_url + described_class.singular_path(client, key, prefix)).
114
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key => key))
115
+
116
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
117
+ subject.fetch
118
+
119
+ subject.should have_attributes(expected_attributes)
120
+ end
121
+
122
+ it "handles a 404" do
123
+ stub_request(:get, site_url + described_class.singular_path(client, '99999', prefix)).
124
+ to_return(:status => 404, :body => '{"errorMessages":["'+class_basename+' Does Not Exist"],"errors": {}}')
125
+ lambda do
126
+ client.send(class_basename).find('99999', options)
127
+ end.should raise_exception(JIRA::HTTPError)
128
+ end
129
+ end
130
+
131
+ shared_examples "a resource with a DELETE endpoint" do
132
+ it "deletes a resource" do
133
+ # E.g., for JIRA::Resource::Project, we need to call
134
+ # client.Project.delete()
135
+ stub_request(:delete, site_url + described_class.singular_path(client, key, prefix)).
136
+ to_return(:status => 204, :body => nil)
137
+
138
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
139
+ subject.delete.should be_true
140
+ end
141
+ end
142
+
143
+ shared_examples "a resource with a POST endpoint" do
144
+
145
+ it "saves a new resource" do
146
+ stub_request(:post, site_url + described_class.collection_path(client, prefix)).
147
+ to_return(:status => 201, :body => get_mock_from_path(:post))
148
+ subject = build_receiver.build
149
+ subject.save(attributes_for_post).should be_true
150
+ expected_attributes_from_post.each do |method_name, value|
151
+ subject.send(method_name).should == value
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ shared_examples "a resource with a PUT endpoint" do
158
+
159
+ it "saves an existing component" do
160
+ stub_request(:get, site_url + described_class.singular_path(client, key, prefix)).
161
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key =>key))
162
+ stub_request(:put, site_url + described_class.singular_path(client, key, prefix)).
163
+ to_return(:status => 200, :body => get_mock_from_path(:put, :key => key, :value_if_not_found => nil))
164
+ subject = build_receiver.build(described_class.key_attribute.to_s => key)
165
+ subject.fetch
166
+ subject.save(attributes_for_put).should be_true
167
+ expected_attributes_from_put.each do |method_name, value|
168
+ subject.send(method_name).should == value
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+ shared_examples 'a resource with a PUT endpoint that rejects invalid fields' do
175
+
176
+ it "fails to save with an invalid field" do
177
+ stub_request(:get, site_url + described_class.singular_path(client, key)).
178
+ to_return(:status => 200, :body => get_mock_from_path(:get, :key => key))
179
+ stub_request(:put, site_url + described_class.singular_path(client, key)).
180
+ to_return(:status => 400, :body => get_mock_from_path(:put, :key => key, :suffix => "invalid"))
181
+ subject = client.send(class_basename).build(described_class.key_attribute.to_s => key)
182
+ subject.fetch
183
+
184
+ subject.save('fields'=> {'invalid' => 'field'}).should be_false
185
+ lambda do
186
+ subject.save!('fields'=> {'invalid' => 'field'})
187
+ end.should raise_error(JIRA::HTTPError)
188
+ end
189
+
190
+ end