hallon 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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