hallon 0.8.0 → 0.9.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 (59) hide show
  1. data/.travis.yml +2 -0
  2. data/CHANGELOG +43 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +21 -13
  5. data/Rakefile +84 -23
  6. data/dev/login.rb +16 -0
  7. data/examples/adding_tracks_to_playlist.rb +49 -0
  8. data/examples/logging_in.rb +1 -6
  9. data/examples/show_published_playlists_of_user.rb +9 -19
  10. data/hallon.gemspec +1 -1
  11. data/lib/hallon.rb +3 -2
  12. data/lib/hallon/album.rb +55 -41
  13. data/lib/hallon/album_browse.rb +41 -37
  14. data/lib/hallon/artist.rb +30 -21
  15. data/lib/hallon/artist_browse.rb +59 -41
  16. data/lib/hallon/base.rb +68 -5
  17. data/lib/hallon/enumerator.rb +1 -0
  18. data/lib/hallon/error.rb +3 -0
  19. data/lib/hallon/ext/spotify.rb +169 -36
  20. data/lib/hallon/image.rb +30 -44
  21. data/lib/hallon/link.rb +29 -43
  22. data/lib/hallon/linkable.rb +68 -20
  23. data/lib/hallon/observable.rb +0 -1
  24. data/lib/hallon/player.rb +21 -7
  25. data/lib/hallon/playlist.rb +291 -0
  26. data/lib/hallon/playlist_container.rb +27 -0
  27. data/lib/hallon/search.rb +52 -45
  28. data/lib/hallon/session.rb +129 -81
  29. data/lib/hallon/toplist.rb +37 -19
  30. data/lib/hallon/track.rb +68 -45
  31. data/lib/hallon/user.rb +69 -33
  32. data/lib/hallon/version.rb +1 -1
  33. data/spec/hallon/album_browse_spec.rb +15 -9
  34. data/spec/hallon/album_spec.rb +15 -15
  35. data/spec/hallon/artist_browse_spec.rb +28 -9
  36. data/spec/hallon/artist_spec.rb +30 -14
  37. data/spec/hallon/enumerator_spec.rb +0 -1
  38. data/spec/hallon/hallon_spec.rb +20 -1
  39. data/spec/hallon/image_spec.rb +18 -41
  40. data/spec/hallon/link_spec.rb +10 -12
  41. data/spec/hallon/linkable_spec.rb +37 -18
  42. data/spec/hallon/player_spec.rb +8 -0
  43. data/spec/hallon/playlist_container_spec.rb +75 -0
  44. data/spec/hallon/playlist_spec.rb +204 -0
  45. data/spec/hallon/search_spec.rb +19 -16
  46. data/spec/hallon/session_spec.rb +61 -29
  47. data/spec/hallon/spotify_spec.rb +30 -0
  48. data/spec/hallon/toplist_spec.rb +22 -14
  49. data/spec/hallon/track_spec.rb +62 -21
  50. data/spec/hallon/user_spec.rb +47 -36
  51. data/spec/mockspotify.rb +35 -10
  52. data/spec/mockspotify/mockspotify_spec.rb +22 -0
  53. data/spec/spec_helper.rb +7 -3
  54. data/spec/support/common_objects.rb +91 -16
  55. data/spec/support/shared_for_linkable_objects.rb +39 -0
  56. metadata +30 -20
  57. data/Termfile +0 -7
  58. data/lib/hallon/synchronizable.rb +0 -32
  59. data/spec/hallon/synchronizable_spec.rb +0 -19
@@ -0,0 +1,30 @@
1
+ describe Spotify do
2
+ describe "a wrapped function" do
3
+ let(:null_pointer) { FFI::Pointer.new(0) }
4
+ subject do
5
+ Spotify.should_receive(:session_user).and_return(null_pointer)
6
+ Spotify.session_user!(session)
7
+ end
8
+
9
+ it "should return a Spotify::Pointer" do
10
+ subject.should be_a Spotify::Pointer
11
+ end
12
+
13
+ it "should not add ref when the result is nil" do
14
+ Spotify.should_not_receive(:user_add_ref)
15
+ subject.should be_null
16
+ end
17
+ end
18
+
19
+ describe "garbage collection" do
20
+ let(:my_pointer) { FFI::Pointer.new(1) }
21
+
22
+ it "should work" do
23
+ # GC tests are a bit funky, but as long as we garbage_release at least once, then
24
+ # we can assume our GC works properly, but up the stakes just for the sake of it
25
+ Spotify.should_receive(:garbage_release).with(my_pointer).at_least(3).times
26
+ 5.times { Spotify::Pointer.new(my_pointer, :garbage, false) }
27
+ 5.times { GC.start; sleep 0.1 }
28
+ end
29
+ end
30
+ end
@@ -1,14 +1,14 @@
1
1
  describe Hallon::Toplist do
2
- subject do
3
- mock_session do
4
- Spotify.should_receive(:toplistbrowse_create).and_return(mock_toplistbrowse)
5
- Hallon::Toplist.new(:artists)
6
- end
2
+ let(:toplist) do
3
+ Spotify.registry_add 'spotify:toplist:artists:everywhere', mock_toplistbrowse
4
+ mock_session { Hallon::Toplist.new(:artists) }
7
5
  end
8
6
 
7
+ subject { toplist }
8
+
9
9
  it { should be_a Hallon::Observable }
10
10
  it { should be_loaded }
11
- its(:error) { should eq :ok }
11
+ its(:status) { should eq :ok }
12
12
 
13
13
  its('artists.size') { should eq 2 }
14
14
  its('artists.to_a') { should eq instantiate(Hallon::Artist, mock_artist, mock_artist_two) }
@@ -19,22 +19,30 @@ describe Hallon::Toplist do
19
19
  its('tracks.size') { should eq 2 }
20
20
  its('tracks.to_a') { should eq instantiate(Hallon::Track, mock_track, mock_track_two) }
21
21
 
22
- describe ".new" do
23
- around { |test| mock_session(&test) }
22
+ describe "#request_duration" do
23
+ it "should return the request duration in seconds" do
24
+ toplist.request_duration.should eq 2.751
25
+ end
24
26
 
27
+ it "should be nil if the request was fetched from local cache" do
28
+ Spotify.should_receive(:toplistbrowse_backend_request_duration).and_return(-1)
29
+ toplist.request_duration.should be_nil
30
+ end
31
+ end
32
+
33
+ describe ".new" do
25
34
  it "should fail given an invalid type" do
26
- expect { Hallon::Toplist.new(:invalid_type) }.to raise_error(ArgumentError, /invalid enum value/)
35
+ expect { mock_session { Hallon::Toplist.new(:invalid_type) } }.to raise_error(ArgumentError, /invalid enum value/)
27
36
  end
28
37
 
29
38
  it "should pass the username given a string to libspotify" do
30
- Spotify.should_receive(:toplistbrowse_create).with(anything, anything, :user, "Kim", anything, nil).and_return(null_pointer)
31
- Hallon::Toplist.new(:artists, "Kim")
39
+ Spotify.registry_add 'spotify:toplist:user:Kim', mock_toplistbrowse
40
+ mock_session { Hallon::Toplist.new(:tracks, "Kim").should be_loaded }
32
41
  end
33
42
 
34
43
  it "should pass the correct region to libspotify" do
35
- sweden = 21317
36
- Spotify.should_receive(:toplistbrowse_create).with(anything, anything, sweden, anything, anything, nil).and_return(null_pointer)
37
- Hallon::Toplist.new(:artists, :se)
44
+ Spotify.registry_add 'spotify:toplist:tracks:SE', mock_toplistbrowse
45
+ mock_session { Hallon::Toplist.new(:tracks, :se).should be_loaded }
38
46
  end
39
47
  end
40
48
  end
@@ -1,6 +1,11 @@
1
1
  # coding: utf-8
2
2
  describe Hallon::Track do
3
- subject { Hallon::Track.new(mock_track) }
3
+ it_should_behave_like "a Linkable object" do
4
+ let(:spotify_uri) { "spotify:track:7N2Vc8u56VGA4KUrGbikC2#01:00" }
5
+ end
6
+
7
+ let(:track) { Hallon::Track.new(mock_track) }
8
+ subject { track }
4
9
 
5
10
  it { should be_loaded }
6
11
  its(:name) { should eq "They" }
@@ -18,20 +23,19 @@ describe Hallon::Track do
18
23
  around { |test| mock_session(&test) }
19
24
 
20
25
  it "should delegate to session to unstar" do
21
- session.should_receive(:unstar).with(subject)
22
- subject.starred = false
26
+ session.should_receive(:unstar).with(track)
27
+ track.starred = false
23
28
  end
24
29
 
25
-
26
30
  it "should delegate to session to star" do
27
- session.should_receive(:star).with(subject)
28
- subject.starred = true
31
+ session.should_receive(:star).with(track)
32
+ track.starred = true
29
33
  end
30
34
 
31
35
  it "should change starred status of track" do
32
- subject.should be_starred
33
- subject.starred = false
34
- subject.should_not be_starred
36
+ track.should be_starred
37
+ track.starred = false
38
+ track.should_not be_starred
35
39
  end
36
40
  end
37
41
 
@@ -43,34 +47,67 @@ describe Hallon::Track do
43
47
  it { should_not be_local }
44
48
  it { should be_autolinked }
45
49
  it { should be_starred }
50
+
51
+ its(:availability) { should eq :available }
46
52
  end
47
53
 
48
54
  describe "album" do
49
55
  it "should be an album when there is one" do
50
- subject.album.should eq Hallon::Album.new(mock_album)
56
+ track.album.should eq Hallon::Album.new(mock_album)
51
57
  end
52
58
 
53
59
  it "should be nil when there isn’t one" do
54
- Spotify.should_receive(:track_album).and_return(FFI::Pointer.new(0))
55
- subject.album.should be_nil
60
+ Spotify.should_receive(:track_album).and_return(null_pointer)
61
+ track.album.should be_nil
56
62
  end
57
63
  end
58
64
 
59
65
  describe "to_link" do
60
- before(:each) { Hallon::Link.stub(:new) }
61
-
62
66
  it "should pass the current offset by default" do
63
- Spotify.should_receive(:link_create_from_track).with(subject.pointer, 10_000)
64
- subject.should_receive(:offset).and_return(10)
65
-
66
- subject.to_link
67
+ track.should_receive(:offset).and_return(10)
68
+ track.to_link.to_str.should match(/#00:10/)
67
69
  end
68
70
 
69
71
  it "should accept offset as parameter" do
70
- Spotify.should_receive(:link_create_from_track).with(subject.pointer, 1_337_000)
71
- subject.should_not_receive(:offset)
72
+ track.should_not_receive(:offset)
73
+ track.to_link(1337).to_str.should match(/#22:17/)
74
+ end
75
+ end
76
+
77
+ describe "#placeholder?" do
78
+ let(:yes) { Hallon::Track.new(mock_track_two) }
79
+
80
+ it "should return the placeholder status of the track" do
81
+ yes.should be_placeholder
82
+ track.should_not be_placeholder
83
+ end
84
+ end
85
+
86
+ describe "#unwrap" do
87
+ let(:track) { Hallon::Track.new(mock_track_two) }
88
+ let(:playlist) { Spotify.link_create_from_string!('spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi') }
89
+ let(:artist) { Spotify.link_create_from_string!('spotify:artist:3bftcFwl4vqRNNORRsqm1G') }
90
+ let(:album) { Spotify.link_create_from_string!('spotify:album:1xvnWMz2PNFf7mXOSRuLws') }
91
+
92
+ it "should unwrap a playlist placeholder into a playlist" do
93
+ Spotify.should_receive(:link_create_from_track!).and_return(playlist)
94
+ mock_session { track.unwrap.should eq Hallon::Playlist.new(playlist) }
95
+ end
96
+
97
+ it "should unwrap an album placeholder into an album" do
98
+ Spotify.should_receive(:link_create_from_track!).and_return(album)
99
+ track.unwrap.should eq Hallon::Album.new(album)
100
+ end
72
101
 
73
- subject.to_link(1337)
102
+ it "should unwrap an artist placeholder into an artist" do
103
+ Spotify.should_receive(:link_create_from_track!).and_return(artist)
104
+ track.unwrap.should eq Hallon::Artist.new(artist)
105
+ end
106
+ end
107
+
108
+ describe "#offline_status" do
109
+ it "should return the tracks’ offline status" do
110
+ track.offline_status.should eq :done
74
111
  end
75
112
  end
76
113
 
@@ -85,6 +122,10 @@ describe Hallon::Track do
85
122
  specify "without offset" do
86
123
  Hallon::Track.new(without_offset).offset.should eq 0
87
124
  end
125
+
126
+ specify "when instantiated from a pointer" do
127
+ Hallon::Track.new(mock_track).offset.should eq 0
128
+ end
88
129
  end
89
130
 
90
131
  describe "a local track" do
@@ -1,34 +1,18 @@
1
1
  # coding: utf-8
2
2
  describe Hallon::User do
3
- describe ".new" do
4
- it "should raise ArgumentError when given an invalid link" do
5
- expect { Hallon::User.new("invalid link") }.to raise_error ArgumentError
6
- end
7
-
8
- it "should raise ArgumentError when given a non-user link" do
9
- expect { Hallon::User.new("spotify:search:moo") }.to raise_error ArgumentError
10
- end
3
+ it_should_behave_like "a Linkable object" do
4
+ let(:spotify_uri) { "spotify:user:burgestrand" }
5
+ let(:custom_object) { "burgestrand" }
11
6
  end
12
7
 
13
- describe "creating a User", :logged_in => true do
14
- context ".new", "from a Spotify URI" do
15
- subject { Hallon::User.new("spotify:user:burgestrand") }
16
- it_should_behave_like "a loadable object"
17
- end
18
-
19
- context ".new", "from a Link" do
20
- subject { Hallon::User.new Hallon::Link.new("spotify:user:burgestrand") }
21
- it_should_behave_like "a loadable object"
22
- end
8
+ describe "an instance" do
9
+ let(:user) { Hallon::User.new(mock_user) }
23
10
 
24
- context "from Session#user" do
25
- subject { session.user }
26
- it_should_behave_like "a loadable object"
11
+ describe "#loaded?" do
12
+ it "should return true as the user is loaded" do
13
+ user.should be_loaded
14
+ end
27
15
  end
28
- end
29
-
30
- describe "an instance", :logged_in => true do
31
- let(:user) { Hallon::User.new(mock_user) }
32
16
 
33
17
  describe "#to_link" do
34
18
  it "should return a Link for this user" do
@@ -37,26 +21,53 @@ describe Hallon::User do
37
21
  end
38
22
 
39
23
  describe "#name" do
40
- it "should be able to get the display name" do
41
- user.name(:display).should eq "Burgestrand"
24
+ it "should be the canonical name" do
25
+ user.name.should eq "burgestrand"
42
26
  end
27
+ end
43
28
 
44
- it "should be able to get the full name" do
45
- user.name(:full).should eq "Kim Burgestrand"
29
+ describe "#display_name" do
30
+ it "should be the users’ display name" do
31
+ user.display_name.should eq "Burgestrand"
46
32
  end
33
+ end
47
34
 
48
- it "should get the canonical name when unspecified" do
49
- user.name.should eq "burgestrand"
35
+ describe "#post" do
36
+ let(:post) { mock_session { user.post(tracks) } }
37
+ let(:tracks) { instantiate(Hallon::Track, mock_track, mock_track_two) }
38
+
39
+ it "should have an error status" do
40
+ post.status.should eq :ok
41
+ end
42
+
43
+ it "should post to the correct user" do
44
+ Spotify.should_receive(:inbox_post_tracks).with(any_args, user.name, any_args, any_args, any_args, any_args, any_args).and_return(null_pointer)
45
+ mock_session { user.post(tracks) }
50
46
  end
51
47
 
52
- it "should fail on invalid name types" do
53
- expect { user.name(:i_am_invalid) }.to raise_error
48
+ it "should use given message if available" do
49
+ Spotify.should_receive(:inbox_post_tracks).with(any_args, any_args, any_args, any_args, "Hello there", any_args, any_args).and_return(null_pointer)
50
+ mock_session { user.post("Hello there", tracks) }
51
+ end
52
+
53
+ it "should return nil on failure" do
54
+ Spotify.should_receive(:inbox_post_tracks).and_return(null_pointer)
55
+ mock_session { user.post([]).should be_nil }
54
56
  end
55
57
  end
56
58
 
57
- describe "#picture" do
58
- it "should retrieve the user picture" do
59
- user.picture.should eq "https://secure.gravatar.com/avatar/b67b73b5b1fd84119ec788b1c3df02ad"
59
+ describe "#starred" do
60
+ let(:starred) { Hallon::Playlist.new("spotify:user:%s:starred" % user.name) }
61
+
62
+ it "should return the users’ starred playlist" do
63
+ session.login 'Kim', 'pass'
64
+ session.should be_logged_in
65
+ mock_session { user.starred.should eq starred }
66
+ end
67
+
68
+ it "should return nil if not logged in" do
69
+ session.should_not be_logged_in
70
+ mock_session { user.starred.should be_nil }
60
71
  end
61
72
  end
62
73
  end
data/spec/mockspotify.rb CHANGED
@@ -5,7 +5,7 @@ module Spotify
5
5
  module Mock
6
6
  # @return [String] path to the libmockspotify C extension binary.
7
7
  def self.path
8
- File.expand_path('../mockspotify/libmockspotify.', __FILE__) << Config::MAKEFILE_CONFIG['DLEXT']
8
+ File.expand_path('../mockspotify/libmockspotify.', __FILE__) << RbConfig::MAKEFILE_CONFIG['DLEXT']
9
9
  end
10
10
 
11
11
  # Overridden to always ffi_lib the mock path.
@@ -25,23 +25,48 @@ module Spotify
25
25
  extend Mock
26
26
  require 'spotify'
27
27
 
28
+ module Mock
29
+ class PlaylistTrack < FFI::Struct
30
+ layout :track, :pointer,
31
+ :create_time, :int,
32
+ :creator, :pointer,
33
+ :message, :pointer,
34
+ :seen, :bool
35
+ end
36
+ end
37
+
28
38
  old_verbose, $VERBOSE = $VERBOSE, true
29
39
 
40
+ def self.attach_mock_function(name, cname, params, returns, options = {})
41
+ attach_function(name, cname, params, returns, options)
42
+ old_method = method(name)
43
+ define_singleton_method(name) do |*args|
44
+ Spotify::Pointer.new(old_method[*args], returns, false)
45
+ end
46
+ end
47
+
30
48
  attach_function :registry_find, [:string], :pointer
31
49
  attach_function :registry_add, [:string, :pointer], :void
50
+ attach_function :registry_clean, [], :void
32
51
 
33
- attach_function :mock_session, :mocksp_session_create, [:pointer, :connectionstate, :int, :array, :int, Spotify::OfflineSyncStatus, :int, :int], :session
34
- attach_function :mock_user, :mocksp_user_create, [:string, :string, :string, :string, :relation_type, :bool], :user
35
- attach_function :mock_track, :mocksp_track_create, [:string, :int, :array, :album, :int, :int, :int, :int, :error, :bool, :bool, :bool, :bool, :bool], :track
36
- attach_function :mock_image, :mocksp_image_create, [:image_id, :imageformat, :size_t, :buffer_in, :error], :image
37
- attach_function :mock_artist, :mocksp_artist_create, [:string, :bool], :artist
38
- attach_function :mock_album, :mocksp_album_create, [:string, :artist, :int, :image_id, :albumtype, :bool, :bool], :album
52
+ attach_function :mock_session, :mocksp_session_create, [:pointer, :connectionstate, :int, Spotify::OfflineSyncStatus, :int, :int, :playlist], :session
53
+ attach_mock_function :mock_user, :mocksp_user_create, [:string, :string, :bool], :user
54
+ attach_mock_function :mock_track, :mocksp_track_create, [:string, :int, :array, :album, :int, :int, :int, :int, :error, :bool, :availability, :track_offline_status, :bool, :bool, :bool, :bool], :track
55
+ attach_mock_function :mock_image, :mocksp_image_create, [:image_id, :imageformat, :size_t, :buffer_in, :error], :image
56
+ attach_mock_function :mock_artist, :mocksp_artist_create, [:string, :image_id, :bool], :artist
57
+ attach_mock_function :mock_album, :mocksp_album_create, [:string, :artist, :int, :image_id, :albumtype, :bool, :bool], :album
39
58
 
40
- attach_function :mock_albumbrowse, :mocksp_albumbrowse_create, [:error, :album, :artist, :int, :array, :int, :array, :string, :albumbrowse_complete_cb, :pointer], :albumbrowse
41
- attach_function :mock_artistbrowse, :mocksp_artistbrowse_create, [:error, :artist, :int, :array, :int, :array, :int, :array, :int, :array, :string, :artistbrowse_complete_cb, :pointer], :artistbrowse
42
- attach_function :mock_toplistbrowse, :mocksp_toplistbrowse_create, [:error, :int, :array, :int, :array, :int, :array], :toplistbrowse
59
+ attach_function :mock_albumbrowse, :mocksp_albumbrowse_create, [:error, :int, :album, :artist, :int, :array, :int, :array, :string, :albumbrowse_complete_cb, :pointer], :albumbrowse
60
+ attach_function :mock_artistbrowse, :mocksp_artistbrowse_create, [:error, :int, :artist, :int, :array, :int, :array, :int, :array, :int, :array, :string, :artistbrowse_type, :artistbrowse_complete_cb, :pointer], :artistbrowse
61
+ attach_function :mock_toplistbrowse, :mocksp_toplistbrowse_create, [:error, :int, :int, :array, :int, :array, :int, :array], :toplistbrowse
43
62
 
63
+ attach_mock_function :mock_playlist, :mocksp_playlist_create, [:string, :bool, :user, :bool, :string, :image_id, :bool, :uint, Spotify::Subscribers, :bool, :playlist_offline_status, :int, :int, :array], :playlist
64
+ attach_mock_function :mock_playlistcontainer, :mocksp_playlistcontainer_create, [:user, :bool], :playlistcontainer
44
65
  attach_function :mock_search, :mocksp_search_create, [:error, :string, :string, :int, :int, :array, :int, :int, :array, :int, :int, :array, :search_complete_cb, :pointer], :search
66
+ attach_function :mock_subscribers, :mocksp_subscribers, [:int, :array], Spotify::Subscribers
67
+
68
+ # mocked accessors
69
+ attach_function :mocksp_playlist_get_autolink_tracks, [:playlist], :bool
45
70
 
46
71
  $VERBOSE = old_verbose
47
72
  end
@@ -25,6 +25,15 @@ describe Spotify::Mock do
25
25
  end
26
26
  end
27
27
 
28
+ describe "unregion" do
29
+ it "should convert an integer to the correct region" do
30
+ Spotify.attach_function :unregion, [ :int ], :string
31
+
32
+ sweden = 21317
33
+ Spotify.unregion(sweden).should eq "SE"
34
+ end
35
+ end
36
+
28
37
  describe "the registry" do
29
38
  it "should find previously added entries" do
30
39
  Spotify.registry_add("i_exist", FFI::Pointer.new(1))
@@ -37,5 +46,18 @@ describe Spotify::Mock do
37
46
  it "should return nil for entries not in the registry" do
38
47
  Spotify.registry_find("i_do_not_exist").should be_null
39
48
  end
49
+
50
+ it "should be cleanable" do
51
+ pointer = FFI::MemoryPointer.new(:uint)
52
+
53
+ Spotify.registry_add("i_exist", pointer)
54
+ Spotify.registry_find("i_exist").should_not be_null
55
+
56
+ Spotify.registry_clean
57
+ Spotify.registry_find("i_exist").should be_null
58
+
59
+ Spotify.registry_add("i_exist", pointer)
60
+ Spotify.registry_find("i_exist").should_not be_null
61
+ end
40
62
  end
41
63
  end
data/spec/spec_helper.rb CHANGED
@@ -16,6 +16,10 @@ Thread.abort_on_exception = true
16
16
  RSpec.configure do |config|
17
17
  config.treat_symbols_as_metadata_keys_with_true_values = true
18
18
 
19
+ config.before do
20
+ Hallon::Session.stub(:instance?).and_return(true)
21
+ end
22
+
19
23
  def fixture_image_path
20
24
  File.expand_path('../fixtures/pink_cover.jpg', __FILE__)
21
25
  end
@@ -26,11 +30,11 @@ RSpec.configure do |config|
26
30
  end
27
31
 
28
32
  def instantiate(klass, *pointers)
29
- pointers.map { |x| klass.new(x) }
33
+ pointers.map { |x| klass.new(*x) }
30
34
  end
31
35
 
32
- def mock_session
33
- Hallon::Session.should_receive(:instance).at_least(1).times.and_return(session)
36
+ def mock_session(times = 1)
37
+ Hallon::Session.should_receive(:instance).at_least(times).times.and_return(session)
34
38
  yield
35
39
  end
36
40