trajectory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +57 -0
  9. data/Rakefile +18 -0
  10. data/config/env.yml +3 -0
  11. data/lib/trajectory/client.rb +24 -0
  12. data/lib/trajectory/core_ext/hash.rb +8 -0
  13. data/lib/trajectory/core_ext.rb +1 -0
  14. data/lib/trajectory/data_access/api.rb +54 -0
  15. data/lib/trajectory/data_access/data_store.rb +70 -0
  16. data/lib/trajectory/data_access.rb +2 -0
  17. data/lib/trajectory/domain/idea.rb +70 -0
  18. data/lib/trajectory/domain/ideas.rb +23 -0
  19. data/lib/trajectory/domain/iteration.rb +85 -0
  20. data/lib/trajectory/domain/iterations.rb +46 -0
  21. data/lib/trajectory/domain/project.rb +168 -0
  22. data/lib/trajectory/domain/projects.rb +55 -0
  23. data/lib/trajectory/domain/stories.rb +56 -0
  24. data/lib/trajectory/domain/story.rb +119 -0
  25. data/lib/trajectory/domain/update.rb +17 -0
  26. data/lib/trajectory/domain/user.rb +28 -0
  27. data/lib/trajectory/domain/users.rb +30 -0
  28. data/lib/trajectory/domain.rb +11 -0
  29. data/lib/trajectory/exceptions/bad_environment_error.rb +7 -0
  30. data/lib/trajectory/exceptions/missing_attribute_error.rb +12 -0
  31. data/lib/trajectory/exceptions/velocity_equal_to_zero_error.rb +11 -0
  32. data/lib/trajectory/exceptions.rb +3 -0
  33. data/lib/trajectory/version.rb +3 -0
  34. data/lib/trajectory.rb +15 -0
  35. data/spec/fabricators/iteration_fabricator.rb +18 -0
  36. data/spec/fabricators/project_fabricator.rb +11 -0
  37. data/spec/fabricators/story_fabricator.rb +18 -0
  38. data/spec/fixtures/vcr_cassettes/client.yml +853 -0
  39. data/spec/fixtures/vcr_cassettes/projects_and_ideas.yml +98 -0
  40. data/spec/fixtures/vcr_cassettes/projects_and_ideas_association.yml +52 -0
  41. data/spec/fixtures/vcr_cassettes/projects_and_iterations.yml +49 -0
  42. data/spec/fixtures/vcr_cassettes/projects_and_iterations_association.yml +49 -0
  43. data/spec/fixtures/vcr_cassettes/projects_and_stories.yml +101 -0
  44. data/spec/fixtures/vcr_cassettes/projects_and_stories_association.yml +147 -0
  45. data/spec/integration/client_spec.rb +96 -0
  46. data/spec/spec_helper.rb +13 -0
  47. data/spec/support/env.rb +11 -0
  48. data/spec/support/fabrication.rb +1 -0
  49. data/spec/support/simple_cov.rb +11 -0
  50. data/spec/support/timecop.rb +1 -0
  51. data/spec/support/vcr.rb +8 -0
  52. data/spec/unit/client_spec.rb +27 -0
  53. data/spec/unit/core_ext/hash_spec.rb +11 -0
  54. data/spec/unit/data_access/api_spec.rb +100 -0
  55. data/spec/unit/data_access/data_store_spec.rb +86 -0
  56. data/spec/unit/domain/idea_spec.rb +46 -0
  57. data/spec/unit/domain/ideas_spec.rb +22 -0
  58. data/spec/unit/domain/iteration_spec.rb +51 -0
  59. data/spec/unit/domain/iterations_spec.rb +47 -0
  60. data/spec/unit/domain/project_spec.rb +163 -0
  61. data/spec/unit/domain/projects_spec.rb +84 -0
  62. data/spec/unit/domain/stories_spec.rb +84 -0
  63. data/spec/unit/domain/story_spec.rb +71 -0
  64. data/spec/unit/domain/user_spec.rb +29 -0
  65. data/spec/unit/domain/users_spec.rb +36 -0
  66. data/spec/unit/exceptions/bad_environment_error_spec.rb +9 -0
  67. data/spec/unit/exceptions/missing_attribute_error_spec.rb +10 -0
  68. data/spec/unit/exceptions/velocity_equal_to_zero_error_spec.rb +10 -0
  69. data/trajectory.gemspec +21 -0
  70. metadata +174 -0
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Client do
5
+ it 'raises an exceptions when environment variables are not set' do
6
+ begin
7
+ original_api_key = ENV['TRAJECTORY_API_KEY']
8
+ original_account_keyword = ENV['TRAJECTORY_ACCOUNT_KEYWORD']
9
+ ENV['TRAJECTORY_API_KEY'] = nil
10
+ ENV['TRAJECTORY_ACCOUNT_KEYWORD'] = nil
11
+
12
+ expect do
13
+ Client.new
14
+ end.to raise_error(BadEvnrionmentError)
15
+ ensure
16
+ ENV['TRAJECTORY_API_KEY'] = original_api_key
17
+ ENV['TRAJECTORY_ACCOUNT_KEYWORD'] = original_account_keyword
18
+ end
19
+ end
20
+
21
+ it 'delegate fetching of projects to the data store' do
22
+ DataStore.should_receive(:projects)
23
+
24
+ Client.new.projects
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hash do
4
+ it 'can symbolize hash keys' do
5
+ hash = {'foo' => 'bar', 'baz' => 'qux'}
6
+
7
+ hash.symbolize_keys!
8
+
9
+ hash.should == {:foo => 'bar', :baz => 'qux'}
10
+ end
11
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module Trajectory
5
+ describe Api do
6
+ describe '#projects' do
7
+ it 'make an API call to retrieve all trajectory projects' do
8
+ query = '/projects.json'
9
+ body = [{'id' => '1234'}]
10
+ headers = {:headers => {'Content-Type' => 'application/json'}}
11
+
12
+ Api.should_receive(:get).
13
+ with(query, headers).
14
+ and_return(OpenStruct.new :headers => headers, :body => body.to_json)
15
+
16
+ Api.projects.should == body
17
+ end
18
+ end
19
+
20
+ let(:project) { double(:project, :keyword => 'project-keyword') }
21
+
22
+ describe '#users_for_project' do
23
+ it 'make an API call to retrieve all users of a given project' do
24
+
25
+ query = "/projects/#{project.keyword}/users.json"
26
+ body = [{'id' => '1234'}]
27
+ headers = {:headers => {'Content-Type' => 'application/json'}}
28
+
29
+ Api.should_receive(:get).
30
+ with(query, headers).
31
+ and_return(OpenStruct.new :headers => headers, :body => body.to_json)
32
+
33
+ Api.users_for_project(project).should == body
34
+ end
35
+ end
36
+
37
+ describe '#stories_for_project' do
38
+ it 'make an API call to retrieve all users of a given project' do
39
+ not_completed_stories_query = "/projects/#{project.keyword}/stories.json"
40
+ completed_stories_query = "/projects/#{project.keyword}/stories/completed.json"
41
+ not_completed_stories_body = {'stories' => [{'id' => '1234'}]}
42
+ completed_stories_body = {'stories' => [{'id' => '1234'}]}
43
+ headers = {:headers => {'Content-Type' => 'application/json'}}
44
+
45
+ Api.should_receive(:get).
46
+ with(not_completed_stories_query, headers).
47
+ and_return(OpenStruct.new :headers => headers, :body => not_completed_stories_body.to_json)
48
+ Api.should_receive(:get).
49
+ with(completed_stories_query, headers).
50
+ and_return(OpenStruct.new :headers => headers, :body => completed_stories_body.to_json)
51
+
52
+ Api.stories_for_project(project).should == completed_stories_body['stories'] + not_completed_stories_body['stories']
53
+ end
54
+ end
55
+
56
+ describe '#iterations_for_project' do
57
+ it 'make an API call to retrieve all users of a given project' do
58
+ query = "/projects/#{project.keyword}/iterations.json"
59
+ body = [{'id' => '1234'}]
60
+ headers = {:headers => {'Content-Type' => 'application/json'}}
61
+
62
+ Api.should_receive(:get).
63
+ with(query, headers).
64
+ and_return(OpenStruct.new :headers => headers, :body => body.to_json)
65
+
66
+ Api.iterations_for_project(project).should == body
67
+ end
68
+ end
69
+
70
+ describe '#ideas_for_project' do
71
+ it 'make an API call to retrieve all users of a given project' do
72
+ query = "/projects/#{project.keyword}/ideas.json"
73
+ body = [{'id' => '1234'}]
74
+ headers = {:headers => {'Content-Type' => 'application/json'}}
75
+
76
+ Api.should_receive(:get).
77
+ with(query, headers).
78
+ and_return(OpenStruct.new :headers => headers, :body => body.to_json)
79
+
80
+ Api.ideas_for_project(project).should == body
81
+ end
82
+ end
83
+
84
+ describe '#updates_for_project' do
85
+ it 'make an API call to retrieve all users of a given project' do
86
+ since = DateTime.now
87
+
88
+ query = "/projects/#{project.keyword}/updates.json?since=#{since}"
89
+ body = [{'id' => '1234'}]
90
+ headers = {:headers => {'Content-Type' => 'application/json'}}
91
+
92
+ Api.should_receive(:get).
93
+ with(query, headers).
94
+ and_return(OpenStruct.new :headers => headers, :body => body.to_json)
95
+
96
+ Api.updates_for_project(project, since).should == body
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe DataStore do
5
+ before(:each) do
6
+ DataStore.instance_variable_set(:@projects, nil)
7
+ end
8
+
9
+ it 'maps JSON projects to actual collection of projects' do
10
+ json_projects_collection = double
11
+ Api.stub(:projects).and_return(json_projects_collection)
12
+ Projects.should_receive(:from_json).with(json_projects_collection)
13
+
14
+ DataStore.projects
15
+ end
16
+
17
+ let(:project) { double(:project, :id => 4567) }
18
+
19
+ it 'maps JSON users to actual collection of users' do
20
+ json_users_collection = double
21
+ Api.stub(:users_for_project).and_return(json_users_collection)
22
+ Users.should_receive(:from_json).with(json_users_collection)
23
+
24
+ DataStore.users_for_project(project)
25
+ end
26
+
27
+ it 'maps JSON stories to actual collection of stories' do
28
+ json_stories_collection = double
29
+ Api.stub(:stories_for_project).and_return(json_stories_collection)
30
+ Stories.should_receive(:from_json).with(project, json_stories_collection)
31
+
32
+ DataStore.stories_for_project(project)
33
+ end
34
+
35
+ it 'maps JSON iterations to actual collection of iterations' do
36
+ json_iterations_collection = double
37
+ Api.stub(:iterations_for_project).and_return(json_iterations_collection)
38
+ Iterations.should_receive(:from_json).with(project, json_iterations_collection)
39
+
40
+ DataStore.iterations_for_project(project)
41
+ end
42
+
43
+ it 'maps JSON ideas to actual collection of ideas' do
44
+ json_ideas_collection = double
45
+ Api.stub(:ideas_for_project).and_return(json_ideas_collection)
46
+ Ideas.should_receive(:from_json).with(project, json_ideas_collection)
47
+
48
+ DataStore.ideas_for_project(project)
49
+ end
50
+
51
+ it 'maps JSON updates to acutal array of updates with properly updated stories and iterations' do
52
+ updated_stories = double
53
+ updated_iterations = double
54
+
55
+ json_stories_collection = double
56
+ json_iterations_collection = double
57
+
58
+ Api.stub(:updates_for_project).and_return({'stories' => json_stories_collection, 'iterations' => json_iterations_collection})
59
+
60
+ Stories.should_receive(:from_json).with(project, json_stories_collection).and_return(updated_stories)
61
+ Iterations.should_receive(:from_json).with(project, json_iterations_collection).and_return(updated_iterations)
62
+
63
+ updates = DataStore.updates_for_project(project)
64
+
65
+ updates.stories.should == updated_stories
66
+ updates.iterations.should == updated_iterations
67
+ end
68
+
69
+ it 'delegates fetching of project by id to the project collection' do
70
+ projects_collection = double
71
+ DataStore.stub(:projects).and_return(projects_collection)
72
+
73
+ id = 12
74
+ projects_collection.should_receive(:find_by_id).with(id)
75
+
76
+ DataStore.find_project_by_id(id)
77
+ end
78
+
79
+ it 'delegates fetching of user of a project to the project' do
80
+ id = 12
81
+ project.should_receive(:find_user_by_id).with(id)
82
+
83
+ DataStore.find_user_of_project_with_id(project, id)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Idea do
5
+ let(:idea) { Idea.new(id: 42, subject: 'foo') }
6
+
7
+ it 'can be initialized with named parameters' do
8
+ idea.id.should == 42
9
+ idea.subject.should == 'foo'
10
+ end
11
+
12
+ it 'requires an id attribute' do
13
+ expect do
14
+ Idea.new.id
15
+ end.to raise_error(MissingAttributeError)
16
+ end
17
+
18
+ it 'is the same idea when ids are the same' do
19
+ idea.should == Idea.new(id: 42, subject: 'bar')
20
+ end
21
+
22
+ context 'it has attributes accessors' do
23
+ %w(id subject stories_count comments_count last_comment_user_name last_comment_created_at editable_by_current_user user_id subscribed_user_ids project_id).each do |attribute|
24
+ it "'#{attribute}' accessor" do
25
+ Idea.new.should respond_to(attribute.to_sym)
26
+ end
27
+ end
28
+ end
29
+
30
+ it 'delegates project fetching to data store' do
31
+ DataStore.should_receive(:find_project_by_id)
32
+
33
+ Idea.new.project
34
+ end
35
+
36
+ it 'delegates user fetching to data store' do
37
+ project = double
38
+ idea = Idea.new(user_id: 42)
39
+ idea.stub(:project).and_return(project)
40
+
41
+ DataStore.should_receive(:find_user_of_project_with_id).with(project, 42)
42
+
43
+ idea.user
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ module Trajectory
4
+ describe Ideas do
5
+ it 'can be initialized from json array of components attributes' do
6
+ project = double(:project, :id => 4567)
7
+ json_ideas_collection = [{'id' => 1234, 'subject' => 'idea-subject'}, {'id' => 42, 'subject' => 'other-idea-subject'}]
8
+
9
+ ideas = Ideas.from_json(project, json_ideas_collection)
10
+
11
+ ideas.should be_kind_of(Ideas)
12
+ ideas.first.id.should == 1234
13
+ ideas.first.subject.should == 'idea-subject'
14
+
15
+ ideas[1].id.should == 42
16
+ ideas[1].subject.should == 'other-idea-subject'
17
+
18
+ ideas.first.project_id.should == 4567
19
+ ideas[1].project_id.should == 4567
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Iteration do
5
+ let(:iteration) { Iteration.new(id: 42) }
6
+
7
+ it 'can be initialized with named parameters' do
8
+ iteration.id.should == 42
9
+ end
10
+
11
+ it 'requires an id attribute' do
12
+ expect do
13
+ Iteration.new.id
14
+ end.to raise_error(MissingAttributeError)
15
+ end
16
+
17
+ it 'is the same story when ids are the same' do
18
+ iteration.should == Iteration.new(id: 42)
19
+ end
20
+
21
+ it 'knows when the iteration is a past one' do
22
+ Iteration.new(:complete => true).past?.should == true
23
+ Iteration.new(:complete => false).past?.should == false
24
+ end
25
+
26
+ it 'knows when the iteration is a future one' do
27
+ Iteration.new(:complete => true, :current => true).future?.should == false
28
+ Iteration.new(:complete => true, :current => false).future?.should == false
29
+ Iteration.new(:complete => false, :current => true).future?.should == false
30
+ Iteration.new(:complete => false, :current => false).future?.should == true
31
+ end
32
+
33
+ context 'it has attributes accessors' do
34
+ %w(percent_complete started_stories_count created_at estimated_velocity delivered_stories_count updated_at unstarted_stories_count starts_on current stories_count id complete accepted_points estimated_points comments_count accepted_stories_count).each do |attribute|
35
+ it "'#{attribute}' accessor" do
36
+ Iteration.new.should respond_to(attribute.to_sym)
37
+ end
38
+ end
39
+ end
40
+
41
+ it 'delegates fetching of its stories to its project' do
42
+ project = double
43
+ iteration = Iteration.new(:id => 42)
44
+ iteration.project = project
45
+
46
+ project.should_receive(:stories_in_iteration).with(iteration)
47
+
48
+ iteration.stories
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Iterations do
5
+ it 'can be initializes from a json array of components attributes' do
6
+ project = double(:project, :id => 4567)
7
+ json_iterations_collection = [{'id' => 1234, 'estimated_points' => 12}, {'id' => 42, 'estimated_points' => 10}]
8
+
9
+ iterations = Iterations.from_json project, json_iterations_collection
10
+
11
+ iterations.should be_kind_of(Iterations)
12
+ iterations.first.id.should == 1234
13
+ iterations.first.estimated_points.should == 12
14
+
15
+ iterations[1].id.should == 42
16
+ iterations[1].estimated_points.should == 10
17
+
18
+ iterations.first.project_id.should == 4567
19
+ iterations[1].project_id.should == 4567
20
+ end
21
+
22
+ it 'can retrieve current iteration' do
23
+ current_iteration = double(:current? => true)
24
+ iterations = Iterations.new(double(:current? => false),
25
+ current_iteration,
26
+ double(:current? => false))
27
+
28
+ iterations.current.should == current_iteration
29
+ end
30
+
31
+ it 'can retrieve pasts iterations' do
32
+ past_iteration_1 = double(:past? => true)
33
+ past_iteration_2 = double(:past? => true)
34
+ iterations = Iterations.new(past_iteration_1, double(:past? => false), double(:past? => false), past_iteration_2)
35
+
36
+ iterations.past.should == Iterations.new(past_iteration_1, past_iteration_2)
37
+ end
38
+
39
+ it 'can retrieve future iterations' do
40
+ future_iteration_1 = double(:future? => true)
41
+ future_iteration_2 = double(:future? => true)
42
+ iterations = Iterations.new(future_iteration_1, double(:future? => false), double(:future? => false), future_iteration_2)
43
+
44
+ iterations.future.should == Iterations.new(future_iteration_1, future_iteration_2)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Project do
5
+ let(:project) { Project.new(id: 42, name: 'foo') }
6
+
7
+ it 'can be initialized with named parameters' do
8
+ project.id.should == 42
9
+ project.name.should == 'foo'
10
+ end
11
+
12
+ it 'requires an id attribute' do
13
+ expect do
14
+ Project.new.id
15
+ end.to raise_error(MissingAttributeError)
16
+ end
17
+
18
+ it 'is the same project when ids are the same' do
19
+ project.should == Project.new(id: 42, name: 'bar')
20
+ end
21
+
22
+ context 'it has attributes accessors' do
23
+ %w(archived? created_at estimated_velocity historic_velocity id keyword updated_at completed_iterations_count completed_stories_count).each do |attribute|
24
+ it "'#{attribute}' accessor" do
25
+ Project.new.should respond_to(attribute.to_sym)
26
+ end
27
+ end
28
+ end
29
+
30
+ it 'can sum the points of all its stories' do
31
+ stories = Stories.new(double(:story, :points => 2),
32
+ double(:story, :points => 10),
33
+ double(:story, :points => 8))
34
+ DataStore.stub(:stories_for_project).and_return(stories)
35
+
36
+ Project.new.total_points.should == 20
37
+ end
38
+
39
+ it 'can evaluate remaining points' do
40
+ stories = Stories.new(double(:story, :points => 2, :completed? => false),
41
+ double(:story, :points => 10, :completed? => false),
42
+ double(:story, :points => 8, :completed? => true))
43
+ DataStore.stub(:stories_for_project).and_return(stories)
44
+
45
+ Project.new.remaining_points.should == 12
46
+ end
47
+
48
+ it 'can evaluate the number of days to the end' do
49
+ stories = Stories.new(double(:story, :points => 2, :completed? => false),
50
+ double(:story, :points => 10, :completed? => false),
51
+ double(:story, :points => 8, :completed? => false))
52
+ DataStore.stub(:stories_for_project).and_return(stories)
53
+
54
+ project = Project.new(:estimated_velocity => 20)
55
+
56
+ project.remaining_days.should == 7
57
+ end
58
+
59
+ it 'can evaluate project end date' do
60
+ stories = Stories.new(double(:story, :points => 20, :completed? => false),
61
+ double(:story, :points => 10, :completed? => false),
62
+ double(:story, :points => 10, :completed? => false))
63
+ DataStore.stub(:stories_for_project).and_return(stories)
64
+
65
+ project = Project.new(:estimated_velocity => 20)
66
+
67
+ project.estimated_end_date.should == Date.today + 14
68
+ end
69
+
70
+ it 'can evaluate velocity per day' do
71
+ Project.new(:estimated_velocity => 20).estimated_velocity_per_day.should == 20 / 7.0
72
+ Project.new(:estimated_velocity => 21).estimated_velocity_per_day.should == 21 / 7.0
73
+ Project.new(:estimated_velocity => 7).estimated_velocity_per_day.should == 7 / 7.0
74
+ end
75
+
76
+ it 'can count the number of remaining working days' do
77
+ stories = Stories.new(double(:story, :points => 2, :completed? => false),
78
+ double(:story, :points => 10, :completed? => false),
79
+ double(:story, :points => 8, :completed? => false))
80
+ DataStore.stub(:stories_for_project).and_return(stories)
81
+
82
+ project = Project.new(:estimated_velocity => 20)
83
+
84
+ project.remaining_working_days.should == 5
85
+ end
86
+
87
+ it 'can estimate velocity per working day' do
88
+ Project.new(:estimated_velocity => 20).estimated_velocity_per_working_day.should == 20 / 5.0
89
+ Project.new(:estimated_velocity => 21).estimated_velocity_per_working_day.should == 21 / 5.0
90
+ Project.new(:estimated_velocity => 7).estimated_velocity_per_working_day.should == 7 / 5.0
91
+ end
92
+
93
+ it 'can estimate the percentage of completion' do
94
+ stories = Stories.new(double(:story, :points => 1, :completed? => false),
95
+ double(:story, :points => 10, :completed? => false),
96
+ double(:story, :points => 9, :completed? => true))
97
+ DataStore.stub(:stories_for_project).and_return(stories)
98
+
99
+ Project.new.percent_complete.should == 45.0
100
+ end
101
+
102
+ it 'can return the sum of completed stories points' do
103
+ stories = Stories.new(double(:story, :points => 1, :completed? => false),
104
+ double(:story, :points => 10, :completed? => true),
105
+ double(:story, :points => 9, :completed? => true))
106
+ DataStore.stub(:stories_for_project).and_return(stories)
107
+
108
+ Project.new.accepted_points.should == 19
109
+ end
110
+
111
+ it 'delegates fetching of iterations of a project to the data store' do
112
+ DataStore.should_receive(:iterations_for_project).with(project)
113
+
114
+ project.iterations
115
+ end
116
+
117
+ it 'delegates fetching of updates of a project to the data store' do
118
+ since = double
119
+ DataStore.should_receive(:updates_for_project).with(project, since)
120
+
121
+ project.updates(since)
122
+ end
123
+
124
+ it 'delegates fetching of ideas of a project to the data store' do
125
+ DataStore.should_receive(:ideas_for_project).with(project)
126
+
127
+ project.ideas
128
+ end
129
+
130
+ it 'can get stories of a given iteration' do
131
+ stories = double
132
+ iteration = double
133
+
134
+ project.stories = stories
135
+ stories.should_receive(:in_iteration).with(iteration)
136
+
137
+ project.stories_in_iteration(iteration)
138
+ end
139
+
140
+ it 'can get one of its user by id' do
141
+ users = double
142
+
143
+ project.users_collection = users
144
+ users.should_receive(:find_by_id).with(1234)
145
+
146
+ project.find_user_by_id(1234)
147
+ end
148
+
149
+ context 'when estimated velocity is zero' do
150
+ it 'cannot estimate remaining days until the project end' do
151
+ expect do
152
+ Project.new(:estimated_velocity => 0).remaining_days
153
+ end.to raise_error(VelocityEqualToZeroError)
154
+ end
155
+
156
+ it 'cannot estimate remaining working days until the project end' do
157
+ expect do
158
+ Project.new(:estimated_velocity => 0).remaining_working_days
159
+ end.to raise_error(VelocityEqualToZeroError)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ module Trajectory
4
+ describe Projects do
5
+ it 'can be initialized from an array of json attributes of its components' do
6
+ json_projects_collection = [{'id' => 1234, 'name' => 'foo'}, {'id' => 42, 'name' => 'bar'}]
7
+
8
+ projects = Projects.from_json(json_projects_collection)
9
+
10
+ projects.should be_kind_of(Projects)
11
+ projects.first.id.should == 1234
12
+ projects.first.name.should == 'foo'
13
+
14
+ projects[1].id.should == 42
15
+ projects[1].name.should == 'bar'
16
+ end
17
+
18
+ it 'can find a project by id' do
19
+ project = double(:project, id: 1234)
20
+ projects = Projects.new(double(:project, id: 1),
21
+ double(:project, id: 2),
22
+ project,
23
+ double(:project, id: 3))
24
+
25
+ projects.find_by_id(1234).should == project
26
+ end
27
+
28
+ it "returns false when it can't find a project by id" do
29
+ projects = Projects.new(double(:project, id: 1),
30
+ double(:project, id: 2),
31
+ double(:project, id: 3))
32
+
33
+ projects.find_by_id(1234).should == false
34
+ end
35
+
36
+ it 'can find a project by keyword' do
37
+ project = double(:project, keyword: 'a_keyword')
38
+ projects = Projects.new(double(:project, keyword: 'some_identifier'),
39
+ double(:project, keyword: 'another_identifier'),
40
+ project,
41
+ double(:project, keyword: 'the_last_identifier'))
42
+
43
+ projects.find_by_keyword('a_keyword').should == project
44
+ end
45
+
46
+ it "returns false when it can't find a project by keyword" do
47
+ projects = Projects.new(double(:project, keyword: 'some_identifier'),
48
+ double(:project, keyword: 'another_identifier'),
49
+ double(:project, keyword: 'the_last_identifier'))
50
+
51
+ projects.find_by_keyword('a_keyword').should == false
52
+ end
53
+
54
+ it 'can filter active projects' do
55
+ project_1 = double(:archived? => true)
56
+ project_2 = double(:archived? => true)
57
+ project_3 = double(:archived? => true)
58
+
59
+ projects = Projects.new(double(:archived? => false),
60
+ project_1,
61
+ double(:archived? => false),
62
+ project_2,
63
+ project_3,
64
+ double(:archived? => false))
65
+
66
+ projects.archived.should include(project_1, project_2, project_3)
67
+ end
68
+
69
+ it 'can filter archived projects' do
70
+ project_1 = double(:archived? => false)
71
+ project_2 = double(:archived? => false)
72
+ project_3 = double(:archived? => false)
73
+
74
+ projects = Projects.new(double(:archived? => true),
75
+ project_1,
76
+ double(:archived? => true),
77
+ project_2,
78
+ project_3,
79
+ double(:archived? => true))
80
+
81
+ projects.active.should include(project_1, project_2, project_3)
82
+ end
83
+ end
84
+ end