keybase-core 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +23 -0
- data/README.md +137 -0
- data/Rakefile +20 -0
- data/keybase.gemspec +25 -0
- data/lib/keybase.rb +49 -0
- data/lib/keybase/crypto.rb +20 -0
- data/lib/keybase/error.rb +37 -0
- data/lib/keybase/models/dump.rb +33 -0
- data/lib/keybase/models/user.rb +168 -0
- data/lib/keybase/request/base.rb +32 -0
- data/lib/keybase/request/dump/all.rb +10 -0
- data/lib/keybase/request/dump/latest.rb +10 -0
- data/lib/keybase/request/key/add.rb +13 -0
- data/lib/keybase/request/key/revoke.rb +14 -0
- data/lib/keybase/request/root/get_salt_and_login_session.rb +12 -0
- data/lib/keybase/request/root/login.rb +14 -0
- data/lib/keybase/request/sig/post_auth.rb +13 -0
- data/lib/keybase/request/user/lookup.rb +10 -0
- data/lib/keybase/response.rb +14 -0
- data/lib/keybase/token_store.rb +32 -0
- data/test/all.rb +6 -0
- data/test/fixtures/example_dump.json +9 -0
- data/test/fixtures/example_user.json +50 -0
- data/test/integration/dumps_test.rb +23 -0
- data/test/integration/keys_test.rb +26 -0
- data/test/integration/login_users_test.rb +18 -0
- data/test/integration/lookup_users_test.rb +42 -0
- data/test/integration_test_helper.rb +6 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/lib/keybase/crypto_test.rb +45 -0
- data/test/unit/lib/keybase/error_test.rb +32 -0
- data/test/unit/lib/keybase/models/dump_test.rb +31 -0
- data/test/unit/lib/keybase/models/user_test.rb +136 -0
- data/test/unit/lib/keybase/requests/base_test.rb +60 -0
- data/test/unit/lib/keybase/requests/dump/all_test.rb +17 -0
- data/test/unit/lib/keybase/requests/dump/latest_test.rb +17 -0
- data/test/unit/lib/keybase/requests/key/add_test.rb +19 -0
- data/test/unit/lib/keybase/requests/key/revoke_test.rb +19 -0
- data/test/unit/lib/keybase/requests/root/get_salt_test.rb +23 -0
- data/test/unit/lib/keybase/requests/root/login_test.rb +19 -0
- data/test/unit/lib/keybase/requests/sig/post_auth_test.rb +19 -0
- data/test/unit/lib/keybase/requests/user/lookup_test.rb +17 -0
- data/test/unit/lib/keybase/response_test.rb +23 -0
- data/test/unit/lib/keybase/token_store_test.rb +25 -0
- data/test/unit/lib/keybase_test.rb +25 -0
- data/test/vcr_cassettes/dumps.yml +49418 -0
- data/test/vcr_cassettes/keys.yml +349 -0
- data/test/vcr_cassettes/user_login.yml +176 -0
- data/test/vcr_cassettes/user_lookup_foo.yml +89 -0
- data/test/vcr_cassettes/user_lookup_invalid.yml +52 -0
- data/test/vcr_cassettes/user_lookup_missing.yml +52 -0
- data/test/vcr_cassettes/user_lookup_not_found.yml +55 -0
- metadata +200 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
module Keybase
|
2
|
+
# A Keybase user containing all attributes you have permission to see
|
3
|
+
class User
|
4
|
+
|
5
|
+
attr_reader :id, :basics, :invitation_stats, :profile, :emails,
|
6
|
+
:public_keys, :private_keys
|
7
|
+
|
8
|
+
def initialize(params)
|
9
|
+
@id = params['id']
|
10
|
+
@invitation_stats = OpenStruct.new(params['invitation_stats'])
|
11
|
+
|
12
|
+
set_basics(params['basics']) if params['basics']
|
13
|
+
set_profile(params['profile']) if params['profile']
|
14
|
+
set_emails(params['emails']) if params['emails']
|
15
|
+
set_public_keys(params['public_keys']) if params['public_keys']
|
16
|
+
set_private_keys(params['private_keys']) if params['private_keys']
|
17
|
+
end
|
18
|
+
|
19
|
+
# Lookup a user on Keybase
|
20
|
+
#
|
21
|
+
# @param [String] username the username of the user you are searching for
|
22
|
+
# @raise [Keybase::UserNotFoundError] if the user is not found
|
23
|
+
# @raise [Keybase::InputError] if the username is empty or invalid
|
24
|
+
# @return [Keybase::Model::User] the user, if they exist
|
25
|
+
def self.lookup(username)
|
26
|
+
new(Request::User.lookup(username))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Login to Keybase
|
30
|
+
#
|
31
|
+
# @param [String] email_or_username the email or username of the account
|
32
|
+
# @param [String] passphrase the passphrase for the account
|
33
|
+
# @raise [Keybase::UserNotFoundError] if the user is not found
|
34
|
+
# @raise [Keybase::InputError] if the submitted parameters are empty or invalid
|
35
|
+
# @raise [Keybase::BadPasswordError] if the submitted passphrase is incorrect
|
36
|
+
# @return [Keybase::Model::User] the user, if login is successful
|
37
|
+
def self.login(email_or_username, passphrase)
|
38
|
+
salt, login_session = Request::Root.get_salt_and_login_session(email_or_username)
|
39
|
+
pwh = Crypto.scrypt(passphrase, salt)
|
40
|
+
hmac_pwh = Crypto.hmac_sha512(pwh, login_session)
|
41
|
+
response = Request::Root.login(email_or_username, hmac_pwh, login_session)
|
42
|
+
return new(response['me'])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Post a self-signed authentication certificate to Keybase
|
46
|
+
#
|
47
|
+
# This requires login first.
|
48
|
+
#
|
49
|
+
# The payload of the signature should take the form of other keybase signatures,
|
50
|
+
# like self-signing keys, or proving ownership of remote accounts.
|
51
|
+
#
|
52
|
+
# An example looks like:
|
53
|
+
#
|
54
|
+
# {
|
55
|
+
# "body": {
|
56
|
+
# "key": {
|
57
|
+
# "fingerprint": "da99a6ebeca98b14d944cb6e1ca9bfeab344f0fc",
|
58
|
+
# "host": "keybase.io",
|
59
|
+
# "key_id": "1ca9bfeab344f0fc",
|
60
|
+
# "uid": "15a9e2826313eaf005291a1ae00c3f00",
|
61
|
+
# "username": "taco422107"
|
62
|
+
# },
|
63
|
+
# "nonce": null,
|
64
|
+
# "type": "auth",
|
65
|
+
# "version": 1
|
66
|
+
# },
|
67
|
+
# "ctime": 1386537779,
|
68
|
+
# "expire_in": 86400,
|
69
|
+
# "tag": "signature"
|
70
|
+
# }
|
71
|
+
#
|
72
|
+
# The client can provide an optional nonce to randomize the signatures. The server
|
73
|
+
# will check the signature for validatity, and on success, will return an auth_token,
|
74
|
+
# which is the SHA-256 hash of the full signature body, from the "---- BEGIN" all the
|
75
|
+
# way through to the ---- END PGP MESSAGE ----.
|
76
|
+
#
|
77
|
+
# @param [String] sig the whole certificate contents
|
78
|
+
# @raise [Keybase::InputError] if the certificate is empty or invalid
|
79
|
+
# @raise [Keybase::BadSessionError] if the session is not valid
|
80
|
+
# @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
|
81
|
+
# @return [String] the authentication token
|
82
|
+
def post_auth(sig)
|
83
|
+
Request::Sig.post_auth(basics.username, sig)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add a new public key to Keybase
|
87
|
+
#
|
88
|
+
# This requires login first.
|
89
|
+
#
|
90
|
+
# @param [String] key the public key
|
91
|
+
# @raise [Keybase::InputError] if the key is empty or invalid
|
92
|
+
# @raise [Keybase::BadSessionError] if the session is not valid
|
93
|
+
# @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
|
94
|
+
# @return [String] The Key ID for the uploaded key
|
95
|
+
def add_public_key(key)
|
96
|
+
Request::Key.add(public_key: key)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a new private key to Keybase
|
100
|
+
#
|
101
|
+
# This requires login first.
|
102
|
+
#
|
103
|
+
# @param [String] key the private key
|
104
|
+
# @raise [Keybase::InputError] if the key is empty or invalid
|
105
|
+
# @raise [Keybase::BadSessionError] if the session is not valid
|
106
|
+
# @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
|
107
|
+
# @return [String] The Key ID for the uploaded key
|
108
|
+
def add_private_key(key)
|
109
|
+
Request::Key.add(private_key: key)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Revoke a key from Keybase
|
113
|
+
#
|
114
|
+
# This requires login first.
|
115
|
+
#
|
116
|
+
# Currently the key is simply deleted - full revokation is due in later
|
117
|
+
# revisions of the API.
|
118
|
+
#
|
119
|
+
# @param [String] kid the key id to be revoked
|
120
|
+
# @raise [Keybase::InputError] if the key id is empty or invalid
|
121
|
+
# @raise [Keybase::BadSessionError] if the session is not valid
|
122
|
+
# @raise [Keybase::CSRFVerificationError] if the CSRF token is not valid
|
123
|
+
# @return [Boolean] success
|
124
|
+
def revoke_key(kid)
|
125
|
+
Request::Key.revoke(kid)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def set_basics(params)
|
131
|
+
@basics = OpenStruct.new(params.merge(created_at: nil, updated_at: nil))
|
132
|
+
@basics.created_at = Time.at(@basics.ctime).to_datetime if @basics.ctime
|
133
|
+
@basics.updated_at = Time.at(@basics.mtime).to_datetime if @basics.mtime
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_profile(params)
|
137
|
+
@profile = OpenStruct.new(params.merge(updated_at: nil))
|
138
|
+
@profile.updated_at = Time.at(@profile.mtime).to_datetime if @profile.mtime
|
139
|
+
end
|
140
|
+
|
141
|
+
def set_emails(params)
|
142
|
+
@emails = OpenStruct.new(params)
|
143
|
+
params.each do |k,v|
|
144
|
+
v.merge!('is_verified?' => (v['is_verified'] == 1 ? true : false))
|
145
|
+
@emails.send("#{k}=".to_sym, OpenStruct.new(v))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def set_public_keys(params)
|
150
|
+
@public_keys = OpenStruct.new(params)
|
151
|
+
params.each do |k,v|
|
152
|
+
v.merge!('created_at' => Time.at(v['ctime']).to_datetime)
|
153
|
+
v.merge!('updated_at' => Time.at(v['mtime']).to_datetime)
|
154
|
+
@public_keys.send("#{k}=".to_sym, OpenStruct.new(v))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def set_private_keys(params)
|
159
|
+
@private_keys = OpenStruct.new(params)
|
160
|
+
params.each do |k,v|
|
161
|
+
v.merge!('created_at' => Time.at(v['ctime']).to_datetime)
|
162
|
+
v.merge!('updated_at' => Time.at(v['mtime']).to_datetime)
|
163
|
+
@private_keys.send("#{k}=".to_sym, OpenStruct.new(v))
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Keybase
|
2
|
+
# @private
|
3
|
+
module Request
|
4
|
+
API_BASE_URL = 'https://keybase.io'
|
5
|
+
class Base
|
6
|
+
|
7
|
+
def self.get(url, params={})
|
8
|
+
Keybase::Response.new(conn.get(url, params)).body
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.post(url, params={})
|
12
|
+
response = Keybase::Response.new(conn.post(url, params))
|
13
|
+
TokenStore.cookie = response.cookie if response.cookie
|
14
|
+
response.body
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def self.conn
|
20
|
+
Faraday.new(:url => API_BASE_URL) do |faraday|
|
21
|
+
faraday.path_prefix = "/_/api/1.0"
|
22
|
+
faraday.request :url_encoded
|
23
|
+
faraday.headers['Cookie'] = TokenStore.cookie if TokenStore.cookie
|
24
|
+
faraday.headers['X-CSRF-Token'] = TokenStore.csrf if TokenStore.csrf
|
25
|
+
# faraday.response :logger
|
26
|
+
faraday.adapter Faraday.default_adapter
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# POST /key/add.json
|
2
|
+
# public_key: "-----BEGIN PGP PUBLIC..."
|
3
|
+
# private_key: "hKRib2R5gqRwcml2gqRkY..."
|
4
|
+
# is_primary: true
|
5
|
+
module Keybase
|
6
|
+
module Request
|
7
|
+
class Key < Base
|
8
|
+
def self.add(params)
|
9
|
+
post('key/add.json', params.merge(is_primary: true))['kid']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# POST /key/revoke.json
|
2
|
+
# revocation_type: 0
|
3
|
+
# kid: "a140c70404a13370f7..."
|
4
|
+
module Keybase
|
5
|
+
module Request
|
6
|
+
class Key < Base
|
7
|
+
def self.revoke(kid)
|
8
|
+
post('key/revoke.json', revocation_type: 0,
|
9
|
+
csrf_token: TokenStore.csrf, kid: kid)
|
10
|
+
true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# GET /getsalt.json?email_or_username=EMAIL_OR_USERNAME
|
2
|
+
module Keybase
|
3
|
+
module Request
|
4
|
+
class Root < Base
|
5
|
+
def self.get_salt_and_login_session(email_or_username)
|
6
|
+
result = get('getsalt.json', email_or_username: email_or_username)
|
7
|
+
TokenStore.csrf = result['csrf_token']
|
8
|
+
return [result['salt'], result['login_session']]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# POST /login.json
|
2
|
+
# email_or_username: "chris"
|
3
|
+
# hmac_pwh: "38902fe098f..."
|
4
|
+
# login_session: "lgHZIwfee99..."
|
5
|
+
module Keybase
|
6
|
+
module Request
|
7
|
+
class Root < Base
|
8
|
+
def self.login(email_or_username, hmac_pwh, login_session)
|
9
|
+
post('login.json', email_or_username: email_or_username,
|
10
|
+
hmac_pwh: hmac_pwh, login_session: login_session)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# POST sig/post_auth.json
|
2
|
+
# sig : "----- BEGIN PGP MESSAGE ----- ..."
|
3
|
+
# email_or_username : "maxtaco"
|
4
|
+
module Keybase
|
5
|
+
module Request
|
6
|
+
class Sig < Base
|
7
|
+
def self.post_auth(email_or_username, sig)
|
8
|
+
post('sig/post_auth.json', email_or_username: email_or_username,
|
9
|
+
sig: sig)['auth_token']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Keybase
|
2
|
+
# @private
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_reader :body, :cookie
|
6
|
+
|
7
|
+
def initialize(server_response)
|
8
|
+
@body = JSON.parse(server_response.body)
|
9
|
+
Error.raise_unless_successful(@body['status'])
|
10
|
+
@cookie = server_response.headers['Set-Cookie'] ? server_response.headers['Set-Cookie'] : nil
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Keybase
|
2
|
+
# @private
|
3
|
+
class TokenStore
|
4
|
+
|
5
|
+
attr_accessor :csrf, :cookie
|
6
|
+
|
7
|
+
def self.csrf
|
8
|
+
instance.csrf
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.csrf=(csrf)
|
12
|
+
instance.csrf = csrf
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.cookie
|
16
|
+
instance.cookie
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.cookie=(cookie)
|
20
|
+
instance.cookie = cookie
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.instance
|
24
|
+
@@instance ||= new
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def initialize ; end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/test/all.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
{
|
2
|
+
"dump_id": "472eba4586993583b395b00e",
|
3
|
+
"full_data": "https://s3.amazonaws.com/keybase_data_dumps/2014-02-24-13-46-31-...json",
|
4
|
+
"full_data_sha256": "bea8863f1b17d35d7db4480197886e2f2d9f8deb0633c5f2adcc427541b79bd7",
|
5
|
+
"parent_dump_id": "c56698ea60027c2e9f98620e",
|
6
|
+
"changes_from_parent": "https://s3.amazonaws.com/keybase_data_dumps/2014-02-...json",
|
7
|
+
"changes_sha256": "88a248688299eb2fd25d1031d35abca8243ad5100fc614a213d300512fd2b634",
|
8
|
+
"ctime": 1393267591
|
9
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
{
|
2
|
+
"id": "bf5f1650fc1361a",
|
3
|
+
"basics": {
|
4
|
+
"uid": "55c079bf5f1650fc1361a1c3a4709900",
|
5
|
+
"salt": "533a6842479b5e925856d1ce2a444...",
|
6
|
+
"username": "chris",
|
7
|
+
"ctime": 1383161061,
|
8
|
+
"mtime": 1383162061
|
9
|
+
},
|
10
|
+
"invitation_stats": {
|
11
|
+
"available": 3,
|
12
|
+
"used": 2,
|
13
|
+
"power": 1,
|
14
|
+
"open": 0
|
15
|
+
},
|
16
|
+
"profile": {
|
17
|
+
"full_name": "Chris Coyne",
|
18
|
+
"location": null,
|
19
|
+
"bio": "I am the terror that flaps in the night.",
|
20
|
+
"mtime": 1383247339
|
21
|
+
},
|
22
|
+
"emails": {
|
23
|
+
"primary": {
|
24
|
+
"email": "chris@okcupid.com",
|
25
|
+
"is_verified": 1
|
26
|
+
},
|
27
|
+
"secondary": {
|
28
|
+
"email": "chris@theinter.net",
|
29
|
+
"is_verified": 0
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"public_keys": {
|
33
|
+
"primary": {
|
34
|
+
"kid": "d028ac1542b24a5f77f123ba467873e3ce96a992570a",
|
35
|
+
"key_type": 1,
|
36
|
+
"bundle": "-----BEGIN PGP PUBLIC KEY BLOCK-----\\nVersion => Keybase OpenPGP JS 0.0.1\\nComment => https => //keybase.io\\n\\nxo0EUnlbBAEEALU9OLTEIA0h2o8NZc+0b9f8RaCa8nLfqm3cKNZ3aAV9iYLkeHnd\\nqhP99c+VTiB3Rtjeqf9hZAqKCIJlVrURPgN/mPRzRk5u4OmI/vw/HMn+3lBLwTJZ\\n+FQGEGvLvzBz9wEpcM3IKrXl70BJbq/uM3SP0fsu60/fIy4twEvYOS9JABEBAAHN\\nI2tleWJhc2UuaW8vY2hyaXMgPGNocmlzQGtleWJhc2UuaW8+wrMEEAEKAB0FAlJ5\\nWwQCGy8FCRLMAwADCwkHAxUKCAIeAQIXgAAKCRAb5d5e+VB/cndDA/0Rgsu84CpW\\nN2lwvjCr/MJM9MV8Y75NAl5SFArKksg1d4DYH40M0boJuHETk3AR60bHDRlrhS5/\\nUPm9+A3PAa4IRXKw967e/8nhH3o8mhstDRCMJUrNfo2so5JzFSgWQaflDta0kE/E\\nNlUEOBqmt9Y2f/elhuxiGa0GkoBpJja2ks6NBFJ5WwQBBACva+Om+XqIrBMyDO54\\nNptSiNnjEEJoFJ6hMCqR3S88Kj/Jzu3jTiY+sqOsCDqMOv83AiKWBEL7U28eqhb7\\nwUEOL9nbKOWsQVvCqtla8vgQBLPZ3kYxs1CfJCnaC/bT6abh8+OHECSTENmurSV0\\na22IlMNJrU+3DZ/A3nrca8GGcQARAQABwsCDBBgBCgAPBQJSeVsEBQkB4TOAAhsu\\nAKgJEBvl3l75UH9ynSAEGQEKAAYFAlJ5WwQACgkQl6sBE/Y073xyYwQAryX0fcCA\\nO/ZCZi3pC26CWUtu6Vr6uSl9VSIRQFqcNoZ7PIcjQwGif/iHbBSap1wDkRBlMKkd\\n76vJi67z3etGZ5bJUb4iG/IJUOx6Yn2RGFtyV6N3t1w84dafqaR3cuOsT+UPYSIB\\njxgUPqfN68hqhW0ZBjXCva/P3dPhLulm6zY1AgQAqNDxGPui5OfNBzczq2raoF8E\\nyVJ1gRtP1eGJv6LZ01ABCSJg4h+tgKZuVHIX+i+rcqOY3ABEYWrhfjJBuVNPkM2I\\nFoMllSH7/ywNv8k8PvNBtybqrMiIUJyGrtLvjKhxmD+yxxvPcHWbTkearCycY4QW\\nVlLXYoBhF0AWKph5v/M=\\n=3FfV\\n-----END PGP PUBLIC KEY BLOCK-----\\n",
|
37
|
+
"mtime": 1383685212,
|
38
|
+
"ctime": 1383685212
|
39
|
+
}
|
40
|
+
},
|
41
|
+
"private_keys": {
|
42
|
+
"primary": {
|
43
|
+
"kid": "d028ac1542b24a5f77f123ba467873e3ce96a992570a",
|
44
|
+
"key_type": 2,
|
45
|
+
"bundle": "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",
|
46
|
+
"mtime": 1383685212,
|
47
|
+
"ctime": 1383685212
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require_relative '../integration_test_helper'
|
3
|
+
|
4
|
+
module Keybase
|
5
|
+
class DumpsIntegrationTest < Minitest::Test
|
6
|
+
|
7
|
+
def setup
|
8
|
+
VCR.use_cassette('dumps') do
|
9
|
+
@all = Keybase.dump_all
|
10
|
+
@latest = Keybase.dump_latest
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_latest_dump_object_has_expected_fields
|
15
|
+
assert @latest.respond_to? :id
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_first_dump_object_has_expected_fields
|
19
|
+
assert @all[0].respond_to? :id
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|