pod4 0.6.2

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.
@@ -0,0 +1,452 @@
1
+ require 'pod4/pg_interface'
2
+
3
+ require_relative 'shared_examples_for_interface'
4
+ require_relative 'fixtures/database'
5
+
6
+
7
+ class TestPgInterface < PgInterface
8
+ set_table :customer
9
+ set_id_fld :id
10
+ end
11
+
12
+ class SchemaPgInterface < PgInterface
13
+ set_schema :public
14
+ set_table :customer
15
+ set_id_fld :id
16
+ end
17
+
18
+ class BadPgInterface1 < PgInterface
19
+ set_table :customer
20
+ end
21
+
22
+ class BadPgInterface2 < PgInterface
23
+ set_id_fld :id
24
+ end
25
+
26
+
27
+ describe TestPgInterface do
28
+
29
+ def db_setup(connect)
30
+ client = PG.connect(connect)
31
+
32
+ client.exec(%Q|drop table if exists customer;|)
33
+
34
+ client.exec(%Q|
35
+ create table customer (
36
+ id serial,
37
+ name text,
38
+ level real null,
39
+ day date null,
40
+ timestamp timestamp null,
41
+ price money null,
42
+ qty numeric null );| )
43
+
44
+ ensure
45
+ client.finish if client
46
+ end
47
+
48
+
49
+ def fill_data(ifce)
50
+ @data.each{|r| ifce.create(r) }
51
+ end
52
+
53
+
54
+ before(:all) do
55
+ @connect_hash = DB[:pg]
56
+ db_setup(@connect_hash)
57
+
58
+ @data = []
59
+ @data << { name: 'Barney',
60
+ level: 1.23,
61
+ day: Date.parse("2016-01-01"),
62
+ timestamp: Time.parse('2015-01-01 12:11'),
63
+ price: BigDecimal.new("1.24"),
64
+ qty: BigDecimal.new("1.25") }
65
+
66
+ @data << { name: 'Fred',
67
+ level: 2.34,
68
+ day: Date.parse("2016-02-02"),
69
+ timestamp: Time.parse('2015-01-02 12:22'),
70
+ price: BigDecimal.new("2.35"),
71
+ qty: BigDecimal.new("2.36") }
72
+
73
+ @data << { name: 'Betty',
74
+ level: 3.45,
75
+ day: Date.parse("2016-03-03"),
76
+ timestamp: Time.parse('2015-01-03 12:33'),
77
+ price: BigDecimal.new("3.46"),
78
+ qty: BigDecimal.new("3.47") }
79
+
80
+ end
81
+
82
+
83
+ before do
84
+ # TRUNCATE TABLE also resets the identity counter
85
+ interface.execute(%Q|truncate table customer restart identity;|)
86
+ end
87
+
88
+
89
+ let(:interface) do
90
+ TestPgInterface.new(@connect_hash)
91
+ end
92
+
93
+ #####
94
+
95
+
96
+ it_behaves_like 'an interface' do
97
+
98
+ let(:interface) do
99
+ TestPgInterface.new(@connect_hash)
100
+ end
101
+
102
+ let(:record) { {name: 'Barney'} }
103
+
104
+ end
105
+ ##
106
+
107
+
108
+ describe 'PgInterface.set_schema' do
109
+ it 'takes one argument' do
110
+ expect( PgInterface ).to respond_to(:set_schema).with(1).argument
111
+ end
112
+ end
113
+ ##
114
+
115
+
116
+ describe 'PgInterface.schema' do
117
+ it 'returns the schema' do
118
+ expect( SchemaPgInterface.schema ).to eq :public
119
+ end
120
+
121
+ it 'is optional' do
122
+ expect{ TestPgInterface.schema }.not_to raise_exception
123
+ expect( TestPgInterface.schema ).to eq nil
124
+ end
125
+ end
126
+ ##
127
+
128
+
129
+ describe 'PgInterface.set_table' do
130
+ it 'takes one argument' do
131
+ expect( PgInterface ).to respond_to(:set_table).with(1).argument
132
+ end
133
+ end
134
+ ##
135
+
136
+
137
+ describe 'PgInterface.table' do
138
+ it 'returns the table' do
139
+ expect( TestPgInterface.table ).to eq :customer
140
+ end
141
+ end
142
+ ##
143
+
144
+
145
+ describe 'PgInterface.set_id_fld' do
146
+ it 'takes one argument' do
147
+ expect( PgInterface ).to respond_to(:set_id_fld).with(1).argument
148
+ end
149
+ end
150
+ ##
151
+
152
+
153
+ describe 'PgInterface.id_fld' do
154
+ it 'returns the ID field name' do
155
+ expect( TestPgInterface.id_fld ).to eq :id
156
+ end
157
+ end
158
+ ##
159
+
160
+
161
+ describe '#new' do
162
+
163
+ it 'requires a TinyTds connection string' do
164
+ expect{ TestPgInterface.new }.to raise_exception ArgumentError
165
+ expect{ TestPgInterface.new(nil) }.to raise_exception ArgumentError
166
+ expect{ TestPgInterface.new('foo') }.to raise_exception ArgumentError
167
+
168
+ expect{ TestPgInterface.new(@connect_hash) }.not_to raise_exception
169
+ end
170
+
171
+ end
172
+ ##
173
+
174
+
175
+ describe '#quoted_table' do
176
+
177
+ it 'returns just the table when the schema is not set' do
178
+ expect( interface.quoted_table ).to eq( %Q|"customer"| )
179
+ end
180
+
181
+ it 'returns the schema plus table when the schema is set' do
182
+ ifce = SchemaPgInterface.new(@connect_hash)
183
+ expect( ifce.quoted_table ).to eq( %|"public"."customer"| )
184
+ end
185
+
186
+ end
187
+ ##
188
+
189
+
190
+ describe '#create' do
191
+
192
+ let(:hash) { {name: 'Bam-Bam', price: 4.44} }
193
+ let(:ot) { Octothorpe.new(name: 'Wilma', price: 5.55) }
194
+
195
+ it 'raises a Pod4::DatabaseError if anything goes wrong' do
196
+ expect{ interface.create(one: 'two') }.to raise_exception DatabaseError
197
+ end
198
+
199
+ it 'creates the record when given a hash' do
200
+ # kinda impossible to seperate these two tests
201
+ id = interface.create(hash)
202
+
203
+ expect{ interface.read(id) }.not_to raise_exception
204
+ expect( interface.read(id).to_h ).to include hash
205
+ end
206
+
207
+ it 'creates the record when given an Octothorpe' do
208
+ id = interface.create(ot)
209
+
210
+ expect{ interface.read(id) }.not_to raise_exception
211
+ expect( interface.read(id).to_h ).to include ot.to_h
212
+ end
213
+
214
+ it 'shouldnt have a problem with record values of nil' do
215
+ record = {name: 'Ranger', price: nil}
216
+ expect{ interface.create(record) }.not_to raise_exception
217
+ id = interface.create(record)
218
+ expect( interface.read(id).to_h ).to include(record)
219
+ end
220
+
221
+ end
222
+ ##
223
+
224
+
225
+ describe '#read' do
226
+ before { fill_data(interface) }
227
+
228
+ it 'returns the record for the id as an Octothorpe' do
229
+ rec = interface.read(2)
230
+ expect( rec ).to be_a_kind_of Octothorpe
231
+ expect( rec.>>.name ).to eq 'Fred'
232
+ end
233
+
234
+ it 'raises a Pod4::CantContinue if the ID is bad' do
235
+ expect{ interface.read(:foo) }.to raise_exception CantContinue
236
+ end
237
+
238
+ it 'returns an empty Octothorpe if no record matches the ID' do
239
+ expect{ interface.read(99) }.not_to raise_exception
240
+ expect( interface.read(99) ).to be_a_kind_of Octothorpe
241
+ expect( interface.read(99) ).to be_empty
242
+ end
243
+
244
+ it 'returns real fields as Float' do
245
+ level = interface.read(1).>>.level
246
+
247
+ expect( level ).to be_a_kind_of Float
248
+ expect( level ).to be_within(0.001).of( @data.first[:level] )
249
+ end
250
+
251
+ it 'returns date fields as Date' do
252
+ date = interface.read(1).>>.day
253
+
254
+ expect( date ).to be_a_kind_of Date
255
+ expect( date ).to eq @data.first[:day]
256
+ end
257
+
258
+ it 'returns datetime fields as Time' do
259
+ timestamp = interface.read(1).>>.timestamp
260
+
261
+ expect( timestamp ).to be_a_kind_of Time
262
+ expect( timestamp ).to eq @data.first[:timestamp]
263
+ end
264
+
265
+ it 'returns numeric fields as BigDecimal' do
266
+ qty = interface.read(1).>>.qty
267
+
268
+ expect( qty ).to be_a_kind_of BigDecimal
269
+ expect( qty ).to eq @data.first[:qty]
270
+ end
271
+
272
+ it 'returns money fields as BigDecimal' do
273
+ price = interface.read(1).>>.price
274
+
275
+ expect( price ).to be_a_kind_of BigDecimal
276
+ expect( price ).to eq @data.first[:price]
277
+ end
278
+
279
+ end
280
+ ##
281
+
282
+
283
+ describe '#list' do
284
+ before { fill_data(interface) }
285
+
286
+ it 'has an optional selection parameter, a hash' do
287
+ # Actually it does not have to be a hash, but FTTB we only support that.
288
+ expect{ interface.list(name: 'Barney') }.not_to raise_exception
289
+ end
290
+
291
+ it 'returns an array of Octothorpes that match the records' do
292
+ # convert each OT to a hash and remove the ID key
293
+ arr = interface.list.map {|ot| x = ot.to_h; x.delete(:id); x }
294
+
295
+ expect( arr ).to match_array @data
296
+ end
297
+
298
+ it 'returns a subset of records based on the selection parameter' do
299
+ expect( interface.list(name: 'Fred').size ).to eq 1
300
+
301
+ expect( interface.list(name: 'Betty').first.to_h ).
302
+ to include(name: 'Betty')
303
+
304
+ end
305
+
306
+ it 'returns an empty Array if nothing matches' do
307
+ expect( interface.list(name: 'Yogi') ).to eq([])
308
+ end
309
+
310
+ it 'raises ArgumentError if the selection criteria is nonsensical' do
311
+ expect{ interface.list('foo') }.to raise_exception ArgumentError
312
+ end
313
+
314
+ end
315
+ ##
316
+
317
+
318
+ describe '#update' do
319
+ before { fill_data(interface) }
320
+
321
+ let(:id) { interface.list.first[:id] }
322
+
323
+ def float_price(row)
324
+ row[:price] = row[:price].to_f
325
+ row
326
+ end
327
+
328
+ it 'updates the record at ID with record parameter' do
329
+ record = {name: 'Booboo', price: 99.99}
330
+ interface.update(id, record)
331
+
332
+ # It so happens that TinyTds returns money as BigDecimal --
333
+ # this is a really good thing, even though it screws with our test.
334
+ expect( float_price( interface.read(id).to_h ) ).to include(record)
335
+ end
336
+
337
+ it 'raises a CantContinue if anything weird happens with the ID' do
338
+ expect{ interface.update(99, name: 'Booboo') }.
339
+ to raise_exception CantContinue
340
+
341
+ end
342
+
343
+ it 'raises a DatabaseError if anything weird happens with the record' do
344
+ expect{ interface.update(id, smarts: 'more') }.
345
+ to raise_exception DatabaseError
346
+
347
+ end
348
+
349
+ it 'shouldnt have a problem with record values of nil' do
350
+ record = {name: 'Ranger', price: nil}
351
+ expect{ interface.update(id, record) }.not_to raise_exception
352
+ expect( interface.read(id).to_h ).to include(record)
353
+ end
354
+
355
+ end
356
+ ##
357
+
358
+
359
+ describe '#delete' do
360
+
361
+ def list_contains(id)
362
+ interface.list.find {|x| x[interface.id_fld] == id }
363
+ end
364
+
365
+ let(:id) { interface.list.first[:id] }
366
+
367
+ before { fill_data(interface) }
368
+
369
+ it 'raises CantContinue if anything hinky happens with the id' do
370
+ expect{ interface.delete(:foo) }.to raise_exception CantContinue
371
+ expect{ interface.delete(99) }.to raise_exception CantContinue
372
+ end
373
+
374
+ it 'makes the record at ID go away' do
375
+ expect( list_contains(id) ).to be_truthy
376
+ interface.delete(id)
377
+ expect( list_contains(id) ).to be_falsy
378
+ end
379
+
380
+ end
381
+ ##
382
+
383
+
384
+ describe '#execute' do
385
+
386
+ let(:sql) { 'delete from customer where cast(price as numeric) < 2.0;' }
387
+
388
+ before { fill_data(interface) }
389
+
390
+ it 'requires an SQL string' do
391
+ expect{ interface.execute }.to raise_exception ArgumentError
392
+ expect{ interface.execute(nil) }.to raise_exception ArgumentError
393
+ expect{ interface.execute(14) }.to raise_exception ArgumentError
394
+ end
395
+
396
+ it 'raises some sort of Pod4 error if it runs into problems' do
397
+ expect{ interface.execute('delete from not_a_table') }.
398
+ to raise_exception Pod4Error
399
+
400
+ end
401
+
402
+ it 'executes the string' do
403
+ expect{ interface.execute(sql) }.not_to raise_exception
404
+ expect( interface.list.size ).to eq(@data.size - 1)
405
+ expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
406
+ end
407
+
408
+ end
409
+ ##
410
+
411
+
412
+ describe '#select' do
413
+
414
+ before { fill_data(interface) }
415
+
416
+ it 'requires an SQL string' do
417
+ expect{ interface.select }.to raise_exception ArgumentError
418
+ expect{ interface.select(nil) }.to raise_exception ArgumentError
419
+ expect{ interface.select(14) }.to raise_exception ArgumentError
420
+ end
421
+
422
+ it 'raises some sort of Pod4 error if it runs into problems' do
423
+ expect{ interface.select('select * from not_a_table') }.
424
+ to raise_exception Pod4Error
425
+
426
+ end
427
+
428
+ it 'returns the result of the sql' do
429
+ sql1 = 'select name from customer where cast(price as numeric) < 2.0;'
430
+ sql2 = 'select name from customer where cast(price as numeric) < 0.0;'
431
+
432
+ expect{ interface.select(sql1) }.not_to raise_exception
433
+ expect( interface.select(sql1) ).to eq( [{name: 'Barney'}] )
434
+ expect( interface.select(sql2) ).to eq( [] )
435
+ end
436
+
437
+ it 'works if you pass a non-select' do
438
+ # By which I mean: still executes the SQL; returns []
439
+ sql = 'delete from customer where cast(price as numeric) < 2.0;'
440
+ ret = interface.select(sql)
441
+
442
+ expect( interface.list.size ).to eq(@data.size - 1)
443
+ expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
444
+ expect( ret ).to eq( [] )
445
+ end
446
+
447
+ end
448
+ ##
449
+
450
+
451
+ end
452
+
data/spec/pod4_spec.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'logger'
2
+
3
+ require 'pod4'
4
+ require 'pod4/param'
5
+ require 'pod4/errors'
6
+
7
+
8
+ describe Pod4 do
9
+
10
+ # Magically replaces the real Param module
11
+ let(:param) { class_double(Pod4::Param).as_stubbed_const }
12
+
13
+ after(:all) { Param.reset }
14
+
15
+
16
+ it 'has a version' do
17
+ expect( Pod4::VERSION ).not_to be_nil
18
+ end
19
+ ##
20
+
21
+
22
+ describe "Pod4.set_logger" do
23
+
24
+ it "calls Param.set" do
25
+ l = Logger.new(STDOUT)
26
+ expect(param).to receive(:set).with(:logger, l)
27
+ Pod4.set_logger(l)
28
+ end
29
+
30
+ end
31
+ ##
32
+
33
+
34
+ describe 'Pod4.logger' do
35
+
36
+ it 'returns the logger as set' do
37
+ l = Logger.new(STDOUT)
38
+ Pod4.set_logger(l)
39
+
40
+ expect( Pod4.logger ).to eq l
41
+ end
42
+
43
+ it 'still works if no-one set the logger' do
44
+ expect{ Pod4.logger }.not_to raise_exception
45
+ expect( Pod4.logger ).to be_a_kind_of Logger
46
+ end
47
+
48
+ end
49
+ ##
50
+
51
+
52
+ describe 'Pod4::NotImplemented' do
53
+ it 'is an exception' do
54
+ expect( Pod4::NotImplemented ).not_to be_nil
55
+ expect( Pod4::NotImplemented.ancestors ).to include Exception
56
+ end
57
+ end
58
+ ##
59
+
60
+
61
+ describe 'Pod4::Pod4Error' do
62
+ it 'is an exception' do
63
+ expect( Pod4::Pod4Error ).not_to be_nil
64
+ expect( Pod4::Pod4Error.ancestors ).to include Exception
65
+ end
66
+ end
67
+ ##
68
+
69
+
70
+ describe 'Pod4::DatabaseError' do
71
+ it 'is an exception based on Pod4Error' do
72
+ expect( Pod4::DatabaseError ).not_to be_nil
73
+ expect( Pod4::DatabaseError.ancestors ).to include Pod4::Pod4Error
74
+ end
75
+ end
76
+ ##
77
+
78
+
79
+ describe 'Pod4::ValidationError' do
80
+ it 'is an exception based on Pod4Error' do
81
+ expect( Pod4::ValidationError ).not_to be_nil
82
+ expect( Pod4::ValidationError.ancestors ).to include Pod4::Pod4Error
83
+ end
84
+ end
85
+ ##
86
+
87
+
88
+ end