analysand 1.0.1
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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +41 -0
- data/Rakefile +8 -0
- data/analysand.gemspec +30 -0
- data/bin/analysand +28 -0
- data/lib/analysand.rb +5 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +276 -0
- data/lib/analysand/connection_testing.rb +46 -0
- data/lib/analysand/database.rb +492 -0
- data/lib/analysand/errors.rb +26 -0
- data/lib/analysand/instance.rb +156 -0
- data/lib/analysand/response.rb +45 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_session_grantor.rb +40 -0
- data/spec/analysand/change_watcher_spec.rb +84 -0
- data/spec/analysand/database_spec.rb +228 -0
- data/spec/analysand/database_writing_spec.rb +478 -0
- data/spec/analysand/instance_spec.rb +86 -0
- data/spec/analysand/response_spec.rb +22 -0
- data/spec/analysand/view_response_spec.rb +33 -0
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +73 -0
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +75 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- metadata +276 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module Analysand
|
2
|
+
class InvalidURIError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class DatabaseError < StandardError
|
6
|
+
attr_accessor :response
|
7
|
+
end
|
8
|
+
|
9
|
+
class DocumentNotSaved < DatabaseError
|
10
|
+
end
|
11
|
+
|
12
|
+
class DocumentNotDeleted < DatabaseError
|
13
|
+
end
|
14
|
+
|
15
|
+
class CannotAccessDocument < DatabaseError
|
16
|
+
end
|
17
|
+
|
18
|
+
class CannotAccessView < DatabaseError
|
19
|
+
end
|
20
|
+
|
21
|
+
class CannotDropDatabase < DatabaseError
|
22
|
+
end
|
23
|
+
|
24
|
+
class BulkOperationFailed < DatabaseError
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'analysand/errors'
|
2
|
+
require 'base64'
|
3
|
+
require 'json/ext'
|
4
|
+
require 'net/http/persistent'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module Analysand
|
8
|
+
##
|
9
|
+
# Wraps a CouchDB instance.
|
10
|
+
#
|
11
|
+
# This class is meant to be used for interacting with parts of CouchDB that
|
12
|
+
# aren't associated with any particular database: session management, for
|
13
|
+
# example. If you're looking to do database operations,
|
14
|
+
# Analysand::Database is where you want to be.
|
15
|
+
#
|
16
|
+
# Instances MUST be identified by an absolute URI; instantiating this class
|
17
|
+
# with a relative URI will raise an exception.
|
18
|
+
#
|
19
|
+
# Common tasks
|
20
|
+
# ============
|
21
|
+
#
|
22
|
+
# Opening an instance
|
23
|
+
# -------------------
|
24
|
+
#
|
25
|
+
# instance = Analysand::Instance(URI('http://localhost:5984'))
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# Pinging an instance
|
29
|
+
# -------------------
|
30
|
+
#
|
31
|
+
# instance.ping # => #<Response code=200 ...>
|
32
|
+
#
|
33
|
+
#
|
34
|
+
# Establishing a session
|
35
|
+
# ----------------------
|
36
|
+
#
|
37
|
+
# session, resp = instance.establish_session('username', 'password')
|
38
|
+
# # for correct credentials:
|
39
|
+
# # => [ {
|
40
|
+
# # :issued_at => (a UNIX timestamp),
|
41
|
+
# # :roles => [...roles...],
|
42
|
+
# # :token => 'AuthSession ...',
|
43
|
+
# # :username => (the supplied username)
|
44
|
+
# # },
|
45
|
+
# # the response
|
46
|
+
# # ]
|
47
|
+
# #
|
48
|
+
# # for incorrect credentials:
|
49
|
+
# # => [nil, the response]
|
50
|
+
#
|
51
|
+
# The value in :token should be supplied as a cookie on subsequent requests,
|
52
|
+
# and can be passed as a credential when using Analysand::Database
|
53
|
+
# methods, e.g.
|
54
|
+
#
|
55
|
+
# db = Analysand::Database.new(...)
|
56
|
+
# session, resp = instance.establish_session(username, password)
|
57
|
+
#
|
58
|
+
# db.put(doc, session[:token])
|
59
|
+
#
|
60
|
+
#
|
61
|
+
# Renewing a session
|
62
|
+
# ------------------
|
63
|
+
#
|
64
|
+
# auth, _ = instance.establish_session('username', 'password')
|
65
|
+
# # ...time passes...
|
66
|
+
# session, resp = instance.renew_session(auth)
|
67
|
+
#
|
68
|
+
# Note: CouchDB doesn't always renew a session when asked; see the
|
69
|
+
# documentation for #renew_session for more details.
|
70
|
+
#
|
71
|
+
class Instance
|
72
|
+
attr_reader :http
|
73
|
+
attr_reader :uri
|
74
|
+
|
75
|
+
def initialize(uri)
|
76
|
+
raise InvalidURIError, 'You must supply an absolute URI' unless uri.absolute?
|
77
|
+
|
78
|
+
@http = Net::HTTP::Persistent.new('catalog_database')
|
79
|
+
@uri = uri
|
80
|
+
end
|
81
|
+
|
82
|
+
def establish_session(username, password)
|
83
|
+
path = uri + "/_session"
|
84
|
+
|
85
|
+
req = Net::HTTP::Post.new(path.to_s)
|
86
|
+
req.add_field('Content-Type', 'application/x-www-form-urlencoded')
|
87
|
+
req.body =
|
88
|
+
"name=#{URI.encode(username)}&password=#{URI.encode(password)}"
|
89
|
+
|
90
|
+
resp = http.request path, req
|
91
|
+
|
92
|
+
if Net::HTTPSuccess === resp
|
93
|
+
[session(resp), resp]
|
94
|
+
else
|
95
|
+
[nil, resp]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Attempts to renew a session.
|
101
|
+
#
|
102
|
+
# If the session was renewed, returns a session information hash identical
|
103
|
+
# in form to the hash returned by #establish_session. If the session was
|
104
|
+
# not renewed, returns the passed-in hash.
|
105
|
+
#
|
106
|
+
#
|
107
|
+
# Renewal behavior
|
108
|
+
# ================
|
109
|
+
#
|
110
|
+
# CouchDB will only send a new session cookie if the current time is
|
111
|
+
# close enough to the session timeout. For CouchDB, that means that the
|
112
|
+
# current time must be within a 10% timeout window (i.e. time left before
|
113
|
+
# timeout < timeout * 0.9).
|
114
|
+
def renew_session(old_session)
|
115
|
+
path = uri + "/_session"
|
116
|
+
|
117
|
+
req = Net::HTTP::Get.new(path.to_s)
|
118
|
+
req.add_field('Cookie', old_session[:token])
|
119
|
+
|
120
|
+
resp = http.request path, req
|
121
|
+
|
122
|
+
if Net::HTTPSuccess === resp
|
123
|
+
if !resp.get_fields('Set-Cookie')
|
124
|
+
[old_session, resp]
|
125
|
+
else
|
126
|
+
[session(resp), resp]
|
127
|
+
end
|
128
|
+
else
|
129
|
+
[nil, resp]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def session(resp)
|
136
|
+
token = resp.get_fields('Set-Cookie').
|
137
|
+
detect { |c| c =~ /^AuthSession=([^;]+)/i }
|
138
|
+
|
139
|
+
fields = Base64.decode64($1).split(':')
|
140
|
+
username = fields[0]
|
141
|
+
time = fields[1].to_i(16)
|
142
|
+
|
143
|
+
body = JSON.parse(resp.body)
|
144
|
+
roles = body.has_key?('userCtx') ?
|
145
|
+
body['userCtx']['roles'] : body['roles']
|
146
|
+
|
147
|
+
{ :issued_at => time,
|
148
|
+
:roles => roles,
|
149
|
+
:token => token,
|
150
|
+
:username => username
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'json/ext'
|
3
|
+
|
4
|
+
module Analysand
|
5
|
+
##
|
6
|
+
# The response object is a wrapper around Net::HTTPResponse that provides a
|
7
|
+
# few amenities:
|
8
|
+
#
|
9
|
+
# 1. A #success? method. It returns true if the response code is between
|
10
|
+
# (200..299) and false otherwise.
|
11
|
+
# 2. Automatic JSON deserialization of all response bodies.
|
12
|
+
# 3. Delegates the [] property accessor to the body.
|
13
|
+
class Response
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
attr_reader :response
|
17
|
+
attr_reader :body
|
18
|
+
|
19
|
+
def_delegators :body, :[]
|
20
|
+
|
21
|
+
def initialize(response)
|
22
|
+
@response = response
|
23
|
+
|
24
|
+
if !@response.body.nil? && !@response.body.empty?
|
25
|
+
@body = JSON.parse(@response.body)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def etag
|
30
|
+
response.get_fields('ETag').first.gsub('"', '')
|
31
|
+
end
|
32
|
+
|
33
|
+
def success?
|
34
|
+
c = code.to_i
|
35
|
+
|
36
|
+
c >= 200 && c <= 299
|
37
|
+
end
|
38
|
+
|
39
|
+
def code
|
40
|
+
response.code
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'analysand/response'
|
2
|
+
|
3
|
+
module Analysand
|
4
|
+
##
|
5
|
+
# A subclass of Response with additional view-specific accessors: total_rows,
|
6
|
+
# offset, and rows.
|
7
|
+
class ViewResponse < Response
|
8
|
+
def total_rows
|
9
|
+
body['total_rows']
|
10
|
+
end
|
11
|
+
|
12
|
+
def offset
|
13
|
+
body['offset']
|
14
|
+
end
|
15
|
+
|
16
|
+
def rows
|
17
|
+
body['rows']
|
18
|
+
end
|
19
|
+
|
20
|
+
def docs
|
21
|
+
rows.map { |r| r['doc'] }.compact
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('../../spec/support/test_parameters', __FILE__)
|
4
|
+
require 'digest'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
include TestParameters
|
8
|
+
|
9
|
+
admin_command = ['curl',
|
10
|
+
'-X PUT',
|
11
|
+
%Q{--data-binary '"#{admin_password}"'},
|
12
|
+
"#{instance_uri}/_config/admins/#{admin_username}"
|
13
|
+
].join(' ')
|
14
|
+
|
15
|
+
salt = "ff518dabd59b04b527de7c55179059a46ac54976"
|
16
|
+
|
17
|
+
member_doc = {
|
18
|
+
"name" => member1_username,
|
19
|
+
"salt" => salt,
|
20
|
+
"password_sha" => Digest::SHA1.hexdigest("#{member1_password}#{salt}"),
|
21
|
+
"type" => "user",
|
22
|
+
"roles" => []
|
23
|
+
}
|
24
|
+
|
25
|
+
member_command = ['curl',
|
26
|
+
'-X PUT',
|
27
|
+
"--data-binary '#{member_doc.to_json}'",
|
28
|
+
"-u #{admin_username}:#{admin_password}",
|
29
|
+
"#{instance_uri}/_users/org.couchdb.user:#{member1_username}"
|
30
|
+
].join(' ')
|
31
|
+
|
32
|
+
db_command = ['curl',
|
33
|
+
'-X PUT',
|
34
|
+
"-u #{admin_username}:#{admin_password}",
|
35
|
+
database_uri.to_s
|
36
|
+
].join(' ')
|
37
|
+
|
38
|
+
[ admin_command,
|
39
|
+
member_command,
|
40
|
+
db_command
|
41
|
+
].each do |cmd|
|
42
|
+
puts cmd
|
43
|
+
system cmd
|
44
|
+
puts
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
shared_examples_for 'a session grantor' do
|
4
|
+
let(:result) { instance.establish_session(credentials[:username], credentials[:password]) }
|
5
|
+
let(:role_locator) do
|
6
|
+
lambda { |resp| resp['roles'] }
|
7
|
+
end
|
8
|
+
|
9
|
+
before do
|
10
|
+
@session, @resp = result
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns a session and the original response' do
|
14
|
+
@session.should_not be_nil
|
15
|
+
@resp.should_not be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'the session object' do
|
19
|
+
it 'has the username' do
|
20
|
+
@session[:username].should == credentials[:username]
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'has the token issuance time' do
|
24
|
+
@session[:token] =~ /AuthSession=([^;]+)/
|
25
|
+
|
26
|
+
time_as_hex = Base64.decode64($1).split(':')[1]
|
27
|
+
issuance_time = time_as_hex.to_i(16)
|
28
|
+
|
29
|
+
@session[:issued_at].should == issuance_time
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'has the user roles' do
|
33
|
+
@session[:roles].should == role_locator[JSON.parse(@resp.body)]
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'has a session token' do
|
37
|
+
@session[:token].should =~ /AuthSession.+/
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'analysand/change_watcher'
|
4
|
+
require 'analysand/database'
|
5
|
+
require 'celluloid'
|
6
|
+
|
7
|
+
module Analysand
|
8
|
+
describe ChangeWatcher do
|
9
|
+
class TestWatcher < Analysand::ChangeWatcher
|
10
|
+
attr_accessor :changes
|
11
|
+
|
12
|
+
def initialize(database, credentials)
|
13
|
+
super(database)
|
14
|
+
|
15
|
+
self.changes = []
|
16
|
+
|
17
|
+
@credentials = credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
def customize_request(req)
|
21
|
+
req.basic_auth(@credentials[:username], @credentials[:password])
|
22
|
+
end
|
23
|
+
|
24
|
+
def process(change)
|
25
|
+
changes << change
|
26
|
+
change_processed(change)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:db) { Database.new(database_uri) }
|
31
|
+
|
32
|
+
before do
|
33
|
+
create_databases!
|
34
|
+
end
|
35
|
+
|
36
|
+
after do
|
37
|
+
Celluloid.shutdown
|
38
|
+
|
39
|
+
drop_databases!
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#changes_feed_uri' do
|
43
|
+
let!(:watcher) { TestWatcher.new(db, admin_credentials) }
|
44
|
+
|
45
|
+
describe 'when invoked multiple times' do
|
46
|
+
it 'returns what it returned the first time' do
|
47
|
+
uri1 = watcher.changes_feed_uri
|
48
|
+
uri2 = watcher.changes_feed_uri
|
49
|
+
|
50
|
+
uri1.should == uri2
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#waiter_for' do
|
56
|
+
let!(:watcher) { TestWatcher.new(db, admin_credentials) }
|
57
|
+
|
58
|
+
describe 'if the given document has not been processed' do
|
59
|
+
it 'blocks until the document has been processed' do
|
60
|
+
waiter = watcher.waiter_for('bar')
|
61
|
+
|
62
|
+
Thread.new do
|
63
|
+
db.put('foo', { 'foo' => 'bar' }, admin_credentials)
|
64
|
+
db.put('bar', { 'foo' => 'bar' }, admin_credentials)
|
65
|
+
end
|
66
|
+
|
67
|
+
waiter.wait
|
68
|
+
|
69
|
+
watcher.changes.detect { |r| r['id'] == 'foo' }.should_not be_nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'receives changes' do
|
75
|
+
watcher = TestWatcher.new(db, admin_credentials)
|
76
|
+
|
77
|
+
waiter = watcher.waiter_for('foo')
|
78
|
+
db.put('foo', { 'foo' => 'bar' }, admin_credentials)
|
79
|
+
waiter.wait
|
80
|
+
|
81
|
+
watcher.changes.select { |r| r['id'] == 'foo' }.length.should == 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'analysand/database'
|
4
|
+
require 'analysand/errors'
|
5
|
+
require 'thread'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Analysand
|
9
|
+
describe Database do
|
10
|
+
let(:db) { Database.new(database_uri) }
|
11
|
+
|
12
|
+
describe '#initialize' do
|
13
|
+
it 'requires an absolute URI' do
|
14
|
+
lambda { Database.new(URI("/abc")) }.should raise_error(InvalidURIError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.create!' do
|
19
|
+
before do
|
20
|
+
drop_databases!
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'creates the database at the given URI' do
|
24
|
+
db = Database.create!(database_uri, admin_credentials)
|
25
|
+
|
26
|
+
resp = db.ping
|
27
|
+
resp.body['db_name'].should == database_name
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'raises an exception if the database cannot be created' do
|
31
|
+
Database.create!(database_uri, admin_credentials)
|
32
|
+
|
33
|
+
lambda { Database.create!(database_uri, admin_credentials) }.should raise_error(DatabaseError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '.drop' do
|
38
|
+
before do
|
39
|
+
create_databases!
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'drops the database at the given URI' do
|
43
|
+
resp = Database.drop(database_uri, admin_credentials)
|
44
|
+
|
45
|
+
resp.should be_success
|
46
|
+
Database.new(database_uri).ping.code.should == '404'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#close' do
|
51
|
+
it "shuts down the current thread's connection" do
|
52
|
+
pending 'a good way to test this'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#head' do
|
57
|
+
before do
|
58
|
+
clean_databases!
|
59
|
+
|
60
|
+
db.put!('foo', { 'foo' => 'bar' })
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'retrieves the rev of a document' do
|
64
|
+
resp = db.head('foo')
|
65
|
+
|
66
|
+
resp.etag.should_not be_empty
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#get!' do
|
71
|
+
before do
|
72
|
+
clean_databases!
|
73
|
+
|
74
|
+
db.put!('foo', { 'foo' => 'bar' })
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'if the response code is 200' do
|
78
|
+
it 'returns the document' do
|
79
|
+
db.get!('foo').body['foo'].should == 'bar'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'if the response code is 404' do
|
84
|
+
it 'raises Analysand::CannotAccessDocument' do
|
85
|
+
lambda { db.get!('bar') }.should raise_error(Analysand::CannotAccessDocument)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'includes the response in the exception' do
|
89
|
+
code = nil
|
90
|
+
|
91
|
+
begin
|
92
|
+
db.get!('bar')
|
93
|
+
rescue Analysand::CannotAccessDocument => e
|
94
|
+
code = e.response.code
|
95
|
+
end
|
96
|
+
|
97
|
+
code.should == '404'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#get_attachment' do
|
103
|
+
let(:io) { StringIO.new('an attachment') }
|
104
|
+
|
105
|
+
before do
|
106
|
+
clean_databases!
|
107
|
+
|
108
|
+
db.put_attachment('doc_id/a', io, {}, admin_credentials)
|
109
|
+
end
|
110
|
+
|
111
|
+
xit 'permits streaming' do
|
112
|
+
resp = db.get_attachment('doc_id/a')
|
113
|
+
|
114
|
+
lambda { resp.read_body { } }.should_not raise_error
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#view' do
|
119
|
+
before do
|
120
|
+
clean_databases!
|
121
|
+
|
122
|
+
doc = {
|
123
|
+
'views' => {
|
124
|
+
'a_view' => {
|
125
|
+
'map' => %q{function (doc) { emit(doc['_id'], 1); }}
|
126
|
+
},
|
127
|
+
'composite_key' => {
|
128
|
+
'map' => %q{function (doc) { emit([1, doc['_id']], 1); }}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
db.put('_design/doc', doc, admin_credentials)
|
134
|
+
db.put('abc123', {}, admin_credentials)
|
135
|
+
db.put('abc456', {}, admin_credentials)
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'retrieves _all_docs' do
|
139
|
+
resp = db.view('_all_docs')
|
140
|
+
|
141
|
+
resp.code.should == '200'
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'retrieves a view' do
|
145
|
+
resp = db.view('doc/a_view')
|
146
|
+
|
147
|
+
resp.code.should == '200'
|
148
|
+
resp.total_rows.should == resp.body['total_rows']
|
149
|
+
resp.offset.should == resp.body['offset']
|
150
|
+
resp.rows.should == resp.body['rows']
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'passes through view parameters' do
|
154
|
+
resp = db.view('doc/a_view', :skip => 1)
|
155
|
+
|
156
|
+
resp.offset.should == 1
|
157
|
+
resp.rows.length.should == 1
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'JSON-encodes the key parameter' do
|
161
|
+
resp = db.view('doc/composite_key', :key => [1, 'abc123'])
|
162
|
+
|
163
|
+
resp.code.should == '200'
|
164
|
+
resp.rows.length.should == 1
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'JSON-encodes the keys parameter' do
|
168
|
+
resp = db.view('doc/composite_key', :keys => [[1, 'abc123'], [1, 'abc456']])
|
169
|
+
|
170
|
+
resp.code.should == '200'
|
171
|
+
resp.rows.length.should == 2
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'JSON-encodes the startkey parameter' do
|
175
|
+
resp = db.view('doc/composite_key', :startkey => [1, 'abc123'])
|
176
|
+
|
177
|
+
resp.code.should == '200'
|
178
|
+
resp.rows.length.should == 2
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'JSON-encodes the endkey parameter' do
|
182
|
+
resp = db.view('doc/composite_key', :endkey => [1, 'abc456'], :skip => 1)
|
183
|
+
|
184
|
+
resp.code.should == '200'
|
185
|
+
resp.rows.length.should == 1
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'passes credentials' do
|
189
|
+
security = {
|
190
|
+
'members' => {
|
191
|
+
'names' => [member1_username],
|
192
|
+
'roles' => []
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
db.put!('_security', security, admin_credentials)
|
197
|
+
|
198
|
+
resp = db.view('doc/a_view', { :skip => 1 }, member1_credentials)
|
199
|
+
|
200
|
+
resp.code.should == '200'
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe '#view!' do
|
205
|
+
before do
|
206
|
+
clean_databases!
|
207
|
+
end
|
208
|
+
|
209
|
+
describe 'if the response code is 404' do
|
210
|
+
it 'raises Analysand::CannotAccessView' do
|
211
|
+
lambda { db.view!('unknown/view') }.should raise_error(Analysand::CannotAccessView)
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'includes the response in the exception' do
|
215
|
+
code = nil
|
216
|
+
|
217
|
+
begin
|
218
|
+
db.view!('unknown/view')
|
219
|
+
rescue Analysand::CannotAccessView => e
|
220
|
+
code = e.response.code
|
221
|
+
end
|
222
|
+
|
223
|
+
code.should == '404'
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|