openlogic-couchrest_model 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,6 @@
1
+ class Client < CouchRest::Model::Base
2
+ use_database DB
3
+
4
+ property :name
5
+ property :tax_code
6
+ end
@@ -0,0 +1,27 @@
1
+ require 'question'
2
+ require 'person'
3
+
4
+ class Course < CouchRest::Model::Base
5
+ use_database TEST_SERVER.default_database
6
+
7
+ property :title, String
8
+ property :questions, [Question]
9
+ property :professor, Person
10
+ property :participants, [Object]
11
+ property :ends_at, Time
12
+ property :estimate, Float
13
+ property :hours, Integer
14
+ property :profit, BigDecimal
15
+ property :started_on, :type => Date
16
+ property :updated_at, DateTime
17
+ property :active, :type => TrueClass
18
+ property :very_active, :type => TrueClass
19
+ property :klass, :type => Class
20
+
21
+ view_by :title
22
+ view_by :title, :active
23
+ view_by :dept, :ducktype => true
24
+
25
+ view_by :active, :map => "function(d) { if (d['#{model_type_key}'] == 'Course' && d['active']) { emit(d['updated_at'], 1); }}", :reduce => "function(k,v,r) { return sum(v); }"
26
+
27
+ end
@@ -0,0 +1,8 @@
1
+ class Event < CouchRest::Model::Base
2
+ use_database DB
3
+
4
+ property :subject
5
+ property :occurs_at, Time, :init_method => 'parse'
6
+ property :end_date, Date, :init_method => 'parse'
7
+
8
+ end
@@ -0,0 +1,14 @@
1
+ class Invoice < CouchRest::Model::Base
2
+ # Set the default database to use
3
+ use_database DB
4
+
5
+ # Official Schema
6
+ property :client_name
7
+ property :employee_name
8
+ property :location
9
+
10
+ # Validation
11
+ validates_presence_of :client_name, :employee_name
12
+ validates_presence_of :location, :message => "Hey stupid!, you forgot the location"
13
+
14
+ end
@@ -0,0 +1,5 @@
1
+ class KeyChain < CouchRest::Model::Base
2
+ use_database(DB)
3
+
4
+ property(:keys, Hash)
5
+ end
@@ -0,0 +1,4 @@
1
+ class Membership
2
+ include CouchRest::Model::Embeddable
3
+
4
+ end
@@ -0,0 +1,11 @@
1
+ require 'cat'
2
+
3
+ class Person
4
+ include ::CouchRest::Model::Embeddable
5
+ property :pet, Cat
6
+ property :name, [String]
7
+
8
+ def last_name
9
+ name.last
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ class Project < CouchRest::Model::Base
2
+ use_database DB
3
+ property :name, String
4
+ timestamps!
5
+ view_by :name
6
+ end
@@ -0,0 +1,7 @@
1
+ class Question
2
+ include ::CouchRest::Model::Embeddable
3
+
4
+ property :q
5
+ property :a
6
+
7
+ end
@@ -0,0 +1,9 @@
1
+ class SaleEntry < CouchRest::Model::Base
2
+ use_database DB
3
+
4
+ property :description
5
+ property :price
6
+
7
+ view_by :description
8
+
9
+ end
@@ -0,0 +1,14 @@
1
+ require 'client'
2
+ require 'sale_entry'
3
+
4
+ class SaleInvoice < CouchRest::Model::Base
5
+ use_database DB
6
+
7
+ belongs_to :client
8
+ belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id'
9
+
10
+ collection_of :entries, :class_name => 'SaleEntry'
11
+
12
+ property :date, Date
13
+ property :price, Integer
14
+ end
@@ -0,0 +1,10 @@
1
+ class Service < CouchRest::Model::Base
2
+ # Set the default database to use
3
+ use_database DB
4
+
5
+ # Official Schema
6
+ property :name
7
+ property :price, Integer
8
+
9
+ validates_length_of :name, :minimum => 4, :maximum => 20
10
+ end
@@ -0,0 +1,22 @@
1
+ class User < CouchRest::Model::Base
2
+ # Set the default database to use
3
+ use_database DB
4
+ property :name, :accessible => true
5
+ property :admin # this will be automatically protected
6
+ end
7
+
8
+ class SpecialUser < CouchRest::Model::Base
9
+ # Set the default database to use
10
+ use_database DB
11
+ property :name # this will not be protected
12
+ property :admin, :protected => true
13
+ end
14
+
15
+ # There are two modes of protection
16
+ # 1) Declare accessible poperties, assume all the rest are protected
17
+ # property :name, :accessible => true
18
+ # property :admin # this will be automatically protected
19
+ #
20
+ # 2) Declare protected properties, assume all the rest are accessible
21
+ # property :name # this will not be protected
22
+ # property :admin, :protected => true
@@ -0,0 +1,3 @@
1
+ function globalLib() {
2
+ return "fixture";
3
+ };
@@ -0,0 +1,3 @@
1
+ function justThisView() {
2
+ return "fixture";
3
+ };
@@ -0,0 +1,4 @@
1
+ function(doc) {
2
+ //include-lib
3
+ emit(null, null);
4
+ };
@@ -0,0 +1,3 @@
1
+ function(doc) {
2
+ emit(null, null);
3
+ };
@@ -0,0 +1,3 @@
1
+ function(ks,vs,co) {
2
+ return vs.length;
3
+ };
@@ -0,0 +1,8 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe CouchRest::Model::Validations do
4
+
5
+ let(:invoice) do
6
+ Invoice.new()
7
+ end
8
+ end
@@ -0,0 +1,60 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
3
+
4
+ require "bundler/setup"
5
+ require "rubygems"
6
+ require "rspec"
7
+
8
+ require 'couchrest_model'
9
+
10
+ unless defined?(FIXTURE_PATH)
11
+ MODEL_PATH = File.join(File.dirname(__FILE__), "fixtures", "models")
12
+ $LOAD_PATH.unshift(MODEL_PATH)
13
+
14
+ FIXTURE_PATH = File.join(File.dirname(__FILE__), '/fixtures')
15
+ SCRATCH_PATH = File.join(File.dirname(__FILE__), '/tmp')
16
+
17
+ COUCHHOST = "http://127.0.0.1:5984"
18
+ TESTDB = 'couchrest-model-test'
19
+ TEST_SERVER = CouchRest.new COUCHHOST
20
+ TEST_SERVER.default_database = TESTDB
21
+ DB = TEST_SERVER.database(TESTDB)
22
+ end
23
+
24
+ RSpec.configure do |config|
25
+ config.before(:all) { reset_test_db! }
26
+
27
+ config.after(:all) do
28
+ cr = TEST_SERVER
29
+ test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ }
30
+ test_dbs.each do |db|
31
+ cr.database(db).delete! rescue nil
32
+ end
33
+ end
34
+ end
35
+
36
+ # Require each of the fixture models
37
+ Dir[ File.join(MODEL_PATH, "*.rb") ].sort.each { |file| require File.basename(file) }
38
+
39
+ class Basic < CouchRest::Model::Base
40
+ use_database TEST_SERVER.default_database
41
+ end
42
+
43
+ def reset_test_db!
44
+ DB.recreate! rescue nil
45
+ # Reset the Design Cache
46
+ Thread.current[:couchrest_design_cache] = {}
47
+ DB
48
+ end
49
+
50
+
51
+ def couchdb_lucene_available?
52
+ lucene_path = "http://localhost:5985/"
53
+ url = URI.parse(lucene_path)
54
+ req = Net::HTTP::Get.new(url.path)
55
+ res = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
56
+ true
57
+ rescue Exception => e
58
+ false
59
+ end
60
+
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+ require 'test/unit/assertions'
4
+ require 'active_model/lint'
5
+
6
+ class CompliantModel < CouchRest::Model::Base
7
+ end
8
+
9
+
10
+ describe CouchRest::Model::Base do
11
+ include Test::Unit::Assertions
12
+ include ActiveModel::Lint::Tests
13
+
14
+ before :each do
15
+ @model = CompliantModel.new
16
+ end
17
+
18
+ describe "active model lint tests" do
19
+ ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
20
+ example m.gsub('_',' ') do
21
+ send m
22
+ end
23
+ end
24
+ end
25
+
26
+ def model
27
+ @model
28
+ end
29
+
30
+ end
@@ -0,0 +1,242 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe "Assocations" do
5
+
6
+ describe ".merge_belongs_to_association_options" do
7
+ before :all do
8
+ def SaleInvoice.merge_assoc_opts(*args)
9
+ merge_belongs_to_association_options(*args)
10
+ end
11
+ end
12
+
13
+ it "should return a default set of options" do
14
+ o = SaleInvoice.merge_assoc_opts(:cat)
15
+ o[:foreign_key].should eql('cat_id')
16
+ o[:class_name].should eql('Cat')
17
+ o[:proxy_name].should eql('cats')
18
+ o[:proxy].should eql('Cat') # same as class name
19
+ end
20
+
21
+ it "should merge with provided options" do
22
+ o = SaleInvoice.merge_assoc_opts(:cat, :foreign_key => 'somecat_id', :proxy => 'some_cats')
23
+ o[:foreign_key].should eql('somecat_id')
24
+ o[:proxy].should eql('some_cats')
25
+ end
26
+
27
+ it "should generate a proxy string if proxied" do
28
+ SaleInvoice.stub!(:proxy_owner_method).twice.and_return('company')
29
+ o = SaleInvoice.merge_assoc_opts(:cat)
30
+ o[:proxy].should eql('self.company.cats')
31
+ end
32
+
33
+ end
34
+
35
+ describe "of type belongs to" do
36
+
37
+ before :each do
38
+ @invoice = SaleInvoice.create(:price => 2000)
39
+ @client = Client.create(:name => "Sam Lown")
40
+ end
41
+
42
+ it "should create a foreign key property with setter and getter" do
43
+ @invoice.properties.find{|p| p.name == 'client_id'}.should_not be_nil
44
+ @invoice.respond_to?(:client_id).should be_true
45
+ @invoice.respond_to?("client_id=").should be_true
46
+ end
47
+
48
+ it "should set the property and provide object when set" do
49
+ @invoice.client = @client
50
+ @invoice.client_id.should eql(@client.id)
51
+ @invoice.client.should eql(@client)
52
+ end
53
+
54
+ it "should set the attribute, save and return" do
55
+ @invoice.client = @client
56
+ @invoice.save
57
+ @invoice = SaleInvoice.get(@invoice.id)
58
+ @invoice.client.id.should eql(@client.id)
59
+ end
60
+
61
+ it "should remove the association if nil is provided" do
62
+ @invoice.client = @client
63
+ @invoice.client = nil
64
+ @invoice.client_id.should be_nil
65
+ end
66
+
67
+ it "should not try to search for association if foreign_key is nil" do
68
+ @invoice.client_id = nil
69
+ Client.should_not_receive(:get)
70
+ @invoice.client
71
+ end
72
+
73
+ it "should allow override of foreign key" do
74
+ @invoice.respond_to?(:alternate_client).should be_true
75
+ @invoice.respond_to?("alternate_client=").should be_true
76
+ @invoice.properties.find{|p| p.name == 'alt_client_id'}.should_not be_nil
77
+ end
78
+
79
+ it "should allow override of foreign key and save" do
80
+ @invoice.alternate_client = @client
81
+ @invoice.save
82
+ @invoice = SaleInvoice.get(@invoice.id)
83
+ @invoice.alternate_client.id.should eql(@client.id)
84
+ end
85
+
86
+ end
87
+
88
+ describe "of type collection_of" do
89
+
90
+ before(:each) do
91
+ @invoice = SaleInvoice.create(:price => 2000)
92
+ @entries = [
93
+ SaleEntry.create(:description => 'test line 1', :price => 500),
94
+ SaleEntry.create(:description => 'test line 2', :price => 500),
95
+ SaleEntry.create(:description => 'test line 3', :price => 1000)
96
+ ]
97
+ end
98
+
99
+ it "should create an associated property and collection proxy" do
100
+ @invoice.respond_to?('entry_ids').should be_true
101
+ @invoice.respond_to?('entry_ids=').should be_true
102
+ @invoice.entries.class.should eql(::CouchRest::Model::CollectionOfProxy)
103
+ end
104
+
105
+ it "should allow replacement of objects" do
106
+ @invoice.entries = @entries
107
+ @invoice.entries.length.should eql(3)
108
+ @invoice.entry_ids.length.should eql(3)
109
+ @invoice.entries.first.should eql(@entries.first)
110
+ @invoice.entry_ids.first.should eql(@entries.first.id)
111
+ end
112
+
113
+ it "should allow ids to be set directly and load entries" do
114
+ @invoice.entry_ids = @entries.collect{|i| i.id}
115
+ @invoice.entries.length.should eql(3)
116
+ @invoice.entries.first.should eql(@entries.first)
117
+ end
118
+
119
+ it "should replace collection if ids replaced" do
120
+ @invoice.entry_ids = @entries.collect{|i| i.id}
121
+ @invoice.entries.length.should eql(3) # load once
122
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
123
+ @invoice.entries.length.should eql(2)
124
+ end
125
+
126
+ it "should allow forced collection update if ids changed" do
127
+ @invoice.entry_ids = @entries[0..1].collect{|i| i.id}
128
+ @invoice.entries.length.should eql(2) # load once
129
+ @invoice.entry_ids << @entries[2].id
130
+ @invoice.entry_ids.length.should eql(3)
131
+ @invoice.entries.length.should eql(2) # cached!
132
+ @invoice.entries(true).length.should eql(3)
133
+ end
134
+
135
+ it "should empty arrays when nil collection provided" do
136
+ @invoice.entries = @entries
137
+ @invoice.entries = nil
138
+ @invoice.entry_ids.should be_empty
139
+ @invoice.entries.should be_empty
140
+ end
141
+
142
+ it "should empty arrays when nil ids array provided" do
143
+ @invoice.entries = @entries
144
+ @invoice.entry_ids = nil
145
+ @invoice.entry_ids.should be_empty
146
+ @invoice.entries.should be_empty
147
+ end
148
+
149
+ it "should ignore nil entries" do
150
+ @invoice.entries = [ nil ]
151
+ @invoice.entry_ids.should be_empty
152
+ @invoice.entries.should be_empty
153
+ end
154
+
155
+ # Account for dirty tracking
156
+ describe "dirty tracking" do
157
+ it "should register changes on push" do
158
+ @invoice.changed?.should be_false
159
+ @invoice.entries << @entries[0]
160
+ @invoice.changed?.should be_true
161
+ end
162
+ it "should register changes on pop" do
163
+ @invoice.entries << @entries[0]
164
+ @invoice.save
165
+ @invoice.changed?.should be_false
166
+ @invoice.entries.pop
167
+ @invoice.changed?.should be_true
168
+ end
169
+ it "should register id changes on push" do
170
+ @invoice.entry_ids << @entries[0].id
171
+ @invoice.changed?.should be_true
172
+ end
173
+ it "should register id changes on pop" do
174
+ @invoice.entry_ids << @entries[0].id
175
+ @invoice.save
176
+ @invoice.changed?.should be_false
177
+ @invoice.entry_ids.pop
178
+ @invoice.changed?.should be_true
179
+ end
180
+ end
181
+
182
+ describe "proxy" do
183
+
184
+ it "should ensure new entries to proxy are matched" do
185
+ @invoice.entries << @entries.first
186
+ @invoice.entry_ids.first.should eql(@entries.first.id)
187
+ @invoice.entries.first.should eql(@entries.first)
188
+ @invoice.entries << @entries[1]
189
+ @invoice.entries.count.should eql(2)
190
+ @invoice.entry_ids.count.should eql(2)
191
+ @invoice.entry_ids.last.should eql(@entries[1].id)
192
+ @invoice.entries.last.should eql(@entries[1])
193
+ end
194
+
195
+ it "should support push method" do
196
+ @invoice.entries.push(@entries.first)
197
+ @invoice.entry_ids.first.should eql(@entries.first.id)
198
+ end
199
+
200
+ it "should support []= method" do
201
+ @invoice.entries[0] = @entries.first
202
+ @invoice.entry_ids.first.should eql(@entries.first.id)
203
+ end
204
+
205
+ it "should support unshift method" do
206
+ @invoice.entries.unshift(@entries.first)
207
+ @invoice.entry_ids.first.should eql(@entries.first.id)
208
+ @invoice.entries.unshift(@entries[1])
209
+ @invoice.entry_ids.first.should eql(@entries[1].id)
210
+ end
211
+
212
+ it "should support pop method" do
213
+ @invoice.entries.push(@entries.first)
214
+ @invoice.entries.pop.should eql(@entries.first)
215
+ @invoice.entries.empty?.should be_true
216
+ @invoice.entry_ids.empty?.should be_true
217
+ end
218
+
219
+ it "should support shift method" do
220
+ @invoice.entries.push(@entries[0])
221
+ @invoice.entries.push(@entries[1])
222
+ @invoice.entries.shift.should eql(@entries[0])
223
+ @invoice.entries.first.should eql(@entries[1])
224
+ @invoice.entry_ids.first.should eql(@entries[1].id)
225
+ end
226
+
227
+ it "should raise error when adding un-persisted entries" do
228
+ SaleEntry.find_by_description('test entry').should be_nil
229
+ entry = SaleEntry.new(:description => 'test entry', :price => 500)
230
+ lambda {
231
+ @invoice.entries << entry
232
+ }.should raise_error
233
+ # In the future maybe?
234
+ # @invoice.save.should be_true
235
+ # SaleEntry.find_by_description('test entry').should_not be_nil
236
+ end
237
+
238
+ end
239
+
240
+ end
241
+
242
+ end