nexus_mods 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.md +31 -0
- data/README.md +61 -0
- data/lib/nexus_mods/api_limits.rb +44 -0
- data/lib/nexus_mods/category.rb +37 -0
- data/lib/nexus_mods/game.rb +78 -0
- data/lib/nexus_mods/mod.rb +106 -0
- data/lib/nexus_mods/mod_file.rb +86 -0
- data/lib/nexus_mods/user.rb +37 -0
- data/lib/nexus_mods/version.rb +5 -0
- data/lib/nexus_mods.rb +274 -0
- data/spec/nexus_mods_test/helpers.rb +140 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods/api_limits_spec.rb +19 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods/game_spec.rb +122 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods/mod_file_spec.rb +140 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods/mod_spec.rb +185 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods_access_spec.rb +37 -0
- data/spec/nexus_mods_test/scenarios/rubocop_spec.rb +31 -0
- data/spec/spec_helper.rb +103 -0
- metadata +165 -0
data/lib/nexus_mods.rb
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'json'
|
3
|
+
require 'time'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'faraday'
|
6
|
+
require 'nexus_mods/api_limits'
|
7
|
+
require 'nexus_mods/category'
|
8
|
+
require 'nexus_mods/game'
|
9
|
+
require 'nexus_mods/user'
|
10
|
+
require 'nexus_mods/mod'
|
11
|
+
require 'nexus_mods/mod_file'
|
12
|
+
|
13
|
+
# Ruby API to access NexusMods REST API
|
14
|
+
class NexusMods
|
15
|
+
|
16
|
+
# Error raised by the API calls
|
17
|
+
class ApiError < RuntimeError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Error raised when the API calls have exceed their usage limits
|
21
|
+
class LimitsExceededError < ApiError
|
22
|
+
end
|
23
|
+
|
24
|
+
# Error raised when the API key is invalid
|
25
|
+
class InvalidApiKeyError < ApiError
|
26
|
+
end
|
27
|
+
|
28
|
+
# The default game domain name to be queried
|
29
|
+
# String
|
30
|
+
attr_accessor :game_domain_name
|
31
|
+
|
32
|
+
# The default mod id to be queried
|
33
|
+
# Integer
|
34
|
+
attr_accessor :mod_id
|
35
|
+
|
36
|
+
# Constructor
|
37
|
+
#
|
38
|
+
# Parameters::
|
39
|
+
# * *api_key* (String or nil): The API key to be used, or nil for another authentication [default: nil]
|
40
|
+
# * *game_domain_name* (String): Game domain name to query by default [default: 'skyrimspecialedition']
|
41
|
+
# * *mod_id* (Integer): Mod to query by default [default: 1]
|
42
|
+
# * *file_id* (Integer): File to query by default [default: 1]
|
43
|
+
# * *logger* (Logger): The logger to be used for log messages [default: Logger.new(STDOUT)]
|
44
|
+
def initialize(
|
45
|
+
api_key: nil,
|
46
|
+
game_domain_name: 'skyrimspecialedition',
|
47
|
+
mod_id: 1,
|
48
|
+
file_id: 1,
|
49
|
+
logger: Logger.new($stdout)
|
50
|
+
)
|
51
|
+
@api_key = api_key
|
52
|
+
@game_domain_name = game_domain_name
|
53
|
+
@mod_id = mod_id
|
54
|
+
@file_id = file_id
|
55
|
+
@logger = logger
|
56
|
+
@premium = false
|
57
|
+
# Initialize our HTTP client
|
58
|
+
@http_client = Faraday.new do |builder|
|
59
|
+
builder.adapter Faraday.default_adapter
|
60
|
+
end
|
61
|
+
# Check that the key is correct and know if the user is premium
|
62
|
+
begin
|
63
|
+
@premium = api('users/validate')['is_premium?']
|
64
|
+
rescue LimitsExceededError
|
65
|
+
raise
|
66
|
+
rescue ApiError
|
67
|
+
raise InvalidApiKeyError, 'Invalid API key'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get limits of API calls.
|
72
|
+
# This call does not count in the limits.
|
73
|
+
#
|
74
|
+
# Result::
|
75
|
+
# * ApiLimits: API calls limits
|
76
|
+
def api_limits
|
77
|
+
api_limits_headers = http('users/validate').headers
|
78
|
+
ApiLimits.new(
|
79
|
+
daily_limit: Integer(api_limits_headers['x-rl-daily-limit']),
|
80
|
+
daily_remaining: Integer(api_limits_headers['x-rl-daily-remaining']),
|
81
|
+
daily_reset: Time.parse(api_limits_headers['x-rl-daily-reset']).utc,
|
82
|
+
hourly_limit: Integer(api_limits_headers['x-rl-hourly-limit']),
|
83
|
+
hourly_remaining: Integer(api_limits_headers['x-rl-hourly-remaining']),
|
84
|
+
hourly_reset: Time.parse(api_limits_headers['x-rl-hourly-reset']).utc
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the list of games
|
89
|
+
#
|
90
|
+
# Result::
|
91
|
+
# * Array<Game>: List of games
|
92
|
+
def games
|
93
|
+
api('games').map do |game_json|
|
94
|
+
# First create categories tree
|
95
|
+
# Hash<Integer, [Category, Integer]>: Category and its parent category id, per category id
|
96
|
+
categories = game_json['categories'].to_h do |category_json|
|
97
|
+
category_id = category_json['category_id']
|
98
|
+
[
|
99
|
+
category_id,
|
100
|
+
[
|
101
|
+
Category.new(
|
102
|
+
id: category_id,
|
103
|
+
name: category_json['name']
|
104
|
+
),
|
105
|
+
category_json['parent_category']
|
106
|
+
]
|
107
|
+
]
|
108
|
+
end
|
109
|
+
categories.each_value do |(category, parent_category_id)|
|
110
|
+
category.parent_category = categories[parent_category_id].first if parent_category_id
|
111
|
+
end
|
112
|
+
Game.new(
|
113
|
+
id: game_json['id'],
|
114
|
+
name: game_json['name'],
|
115
|
+
forum_url: game_json['forum_url'],
|
116
|
+
nexusmods_url: game_json['nexusmods_url'],
|
117
|
+
genre: game_json['genre'],
|
118
|
+
domain_name: game_json['domain_name'],
|
119
|
+
approved_date: Time.at(game_json['approved_date']),
|
120
|
+
files_count: game_json['file_count'],
|
121
|
+
files_views: game_json['file_views'],
|
122
|
+
files_endorsements: game_json['file_endorsements'],
|
123
|
+
downloads_count: game_json['downloads'],
|
124
|
+
authors_count: game_json['authors'],
|
125
|
+
mods_count: game_json['mods'],
|
126
|
+
categories: categories.values.map { |(category, _parent_category_id)| category }
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get information about a mod
|
132
|
+
#
|
133
|
+
# Parameters::
|
134
|
+
# * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
|
135
|
+
# * *mod_id* (Integer): The mod ID [default: @mod_id]
|
136
|
+
# Result::
|
137
|
+
# * Mod: Mod information
|
138
|
+
def mod(game_domain_name: @game_domain_name, mod_id: @mod_id)
|
139
|
+
mod_json = api "games/#{game_domain_name}/mods/#{mod_id}"
|
140
|
+
Mod.new(
|
141
|
+
uid: mod_json['uid'],
|
142
|
+
mod_id: mod_json['mod_id'],
|
143
|
+
game_id: mod_json['game_id'],
|
144
|
+
allow_rating: mod_json['allow_rating'],
|
145
|
+
domain_name: mod_json['domain_name'],
|
146
|
+
category_id: mod_json['category_id'],
|
147
|
+
version: mod_json['version'],
|
148
|
+
created_time: Time.parse(mod_json['created_time']),
|
149
|
+
updated_time: Time.parse(mod_json['updated_time']),
|
150
|
+
author: mod_json['author'],
|
151
|
+
contains_adult_content: mod_json['contains_adult_content'],
|
152
|
+
status: mod_json['status'],
|
153
|
+
available: mod_json['available'],
|
154
|
+
uploader: User.new(
|
155
|
+
member_id: mod_json['user']['member_id'],
|
156
|
+
member_group_id: mod_json['user']['member_group_id'],
|
157
|
+
name: mod_json['user']['name'],
|
158
|
+
profile_url: mod_json['uploaded_users_profile_url']
|
159
|
+
),
|
160
|
+
name: mod_json['name'],
|
161
|
+
summary: mod_json['summary'],
|
162
|
+
description: mod_json['description'],
|
163
|
+
picture_url: mod_json['picture_url'],
|
164
|
+
downloads_count: mod_json['mod_downloads'],
|
165
|
+
unique_downloads_count: mod_json['mod_unique_downloads'],
|
166
|
+
endorsements_count: mod_json['endorsement_count']
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Enum of file categories from the API
|
171
|
+
FILE_CATEGORIES = {
|
172
|
+
1 => :main,
|
173
|
+
2 => :patch,
|
174
|
+
3 => :optional,
|
175
|
+
4 => :old,
|
176
|
+
6 => :deleted
|
177
|
+
}
|
178
|
+
|
179
|
+
# Get files belonging to a mod
|
180
|
+
#
|
181
|
+
# Parameters::
|
182
|
+
# * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
|
183
|
+
# * *mod_id* (Integer): The mod ID [default: @mod_id]
|
184
|
+
# Result::
|
185
|
+
# * Array<ModFile>: List of mod's files
|
186
|
+
def mod_files(game_domain_name: @game_domain_name, mod_id: @mod_id)
|
187
|
+
api("games/#{game_domain_name}/mods/#{mod_id}/files")['files'].map do |file_json|
|
188
|
+
category_id = FILE_CATEGORIES[file_json['category_id']]
|
189
|
+
raise "Unknown file category: #{file_json['category_id']}" if category_id.nil?
|
190
|
+
|
191
|
+
ModFile.new(
|
192
|
+
ids: file_json['id'],
|
193
|
+
uid: file_json['uid'],
|
194
|
+
id: file_json['file_id'],
|
195
|
+
name: file_json['name'],
|
196
|
+
version: file_json['version'],
|
197
|
+
category_id:,
|
198
|
+
category_name: file_json['category_name'],
|
199
|
+
is_primary: file_json['is_primary'],
|
200
|
+
size: file_json['size_in_bytes'],
|
201
|
+
file_name: file_json['file_name'],
|
202
|
+
uploaded_time: Time.parse(file_json['uploaded_time']),
|
203
|
+
mod_version: file_json['mod_version'],
|
204
|
+
external_virus_scan_url: file_json['external_virus_scan_url'],
|
205
|
+
description: file_json['description'],
|
206
|
+
changelog_html: file_json['changelog_html'],
|
207
|
+
content_preview_url: file_json['content_preview_link']
|
208
|
+
)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
# Send an HTTP request to the API and get back the answer as a JSON
|
215
|
+
#
|
216
|
+
# Parameters::
|
217
|
+
# * *path* (String): API path to contact (from v1/ and without .json)
|
218
|
+
# * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
|
219
|
+
# Result::
|
220
|
+
# * Object: The JSON response
|
221
|
+
def api(path, verb: :get)
|
222
|
+
res = http(path, verb:)
|
223
|
+
json = JSON.parse(res.body)
|
224
|
+
uri = api_uri(path)
|
225
|
+
@logger.debug "[API call] - #{verb} #{uri} => #{res.status}\n#{
|
226
|
+
JSON.
|
227
|
+
pretty_generate(json).
|
228
|
+
split("\n").
|
229
|
+
map { |line| " #{line}" }.
|
230
|
+
join("\n")
|
231
|
+
}\n#{
|
232
|
+
res.
|
233
|
+
headers.
|
234
|
+
map { |header, value| " #{header}: #{value}" }.
|
235
|
+
join("\n")
|
236
|
+
}"
|
237
|
+
case res.status
|
238
|
+
when 200
|
239
|
+
# Happy
|
240
|
+
when 429
|
241
|
+
# Some limits of the API have been reached
|
242
|
+
raise LimitsExceededError, "Exceeding limits of API calls: #{res.headers.select { |header, _value| header =~ /^x-rl-.+$/ }}"
|
243
|
+
else
|
244
|
+
raise ApiError, "API #{uri} returned error code #{res.status}" unless res.status == '200'
|
245
|
+
end
|
246
|
+
json
|
247
|
+
end
|
248
|
+
|
249
|
+
# Send an HTTP request to the API and get back the HTTP response
|
250
|
+
#
|
251
|
+
# Parameters::
|
252
|
+
# * *path* (String): API path to contact (from v1/ and without .json)
|
253
|
+
# * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
|
254
|
+
# Result::
|
255
|
+
# * Faraday::Response: The HTTP response
|
256
|
+
def http(path, verb: :get)
|
257
|
+
@http_client.send(verb) do |req|
|
258
|
+
req.url api_uri(path)
|
259
|
+
req.headers['apikey'] = @api_key
|
260
|
+
req.headers['User-Agent'] = "nexus_mods (#{RUBY_PLATFORM}) Ruby/#{RUBY_VERSION}"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Get the real URI to query for a given API path
|
265
|
+
#
|
266
|
+
# Parameters::
|
267
|
+
# * *path* (String): API path to contact (from v1/ and without .json)
|
268
|
+
# Result::
|
269
|
+
# * String: The URI
|
270
|
+
def api_uri(path)
|
271
|
+
"https://api.nexusmods.com/v1/#{path}.json"
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'webmock/rspec'
|
3
|
+
require 'rspec/support/object_formatter'
|
4
|
+
require 'nexus_mods'
|
5
|
+
|
6
|
+
module NexusModsTests
|
7
|
+
|
8
|
+
module Helpers
|
9
|
+
|
10
|
+
# Integer: Mocked user ID
|
11
|
+
MOCKED_USER_ID = 1_234_567
|
12
|
+
|
13
|
+
# String: Mocked API key
|
14
|
+
MOCKED_API_KEY = '1234567891234566546543123546879s8df46s5df4sd5f4sd6f87wer9f846sf54sd65v16x5v48r796rwe84f654f35sd1v5df6v54687rUGZWcG0rdz09--62dcd41bb308d2d660548a3cd1ef4094162c4379'
|
15
|
+
|
16
|
+
# String: Mocked user name
|
17
|
+
MOCKED_USER_NAME = 'NexusModsUser'
|
18
|
+
|
19
|
+
# String: Mocked user email
|
20
|
+
MOCKED_USER_EMAIL = 'nexus_mods_user@test_mail.com'
|
21
|
+
|
22
|
+
# Hash<String, String>: Default HTTP headers returned by the HTTP responses
|
23
|
+
DEFAULT_API_HEADERS = {
|
24
|
+
'date' => 'Wed, 02 Oct 2019 15:08:43 GMT',
|
25
|
+
'content-type' => 'application/json; charset=utf-8',
|
26
|
+
'transfer-encoding' => 'chunked',
|
27
|
+
'connection' => 'close',
|
28
|
+
'set-cookie' => '__cfduid=1234561234561234561234561234561234560028923; expires=Thu, 01-Oct-20 15:08:43 GMT; path=/; domain=.nexusmods.com; HttpOnly; Secure',
|
29
|
+
'vary' => 'Accept-Encoding, Origin',
|
30
|
+
'userid' => MOCKED_USER_ID.to_s,
|
31
|
+
'x-rl-hourly-limit' => '100',
|
32
|
+
'x-rl-hourly-remaining' => '100',
|
33
|
+
'x-rl-hourly-reset' => '2019-10-02T16:00:00+00:00',
|
34
|
+
'x-rl-daily-limit' => '2500',
|
35
|
+
'x-rl-daily-remaining' => '2500',
|
36
|
+
'x-rl-daily-reset' => '2019-10-03 00:00:00 +0000',
|
37
|
+
'cache-control' => 'max-age=0, private, must-revalidate',
|
38
|
+
'x-request-id' => '1234561234561234561daf88a8134abc',
|
39
|
+
'x-runtime' => '0.022357',
|
40
|
+
'strict-transport-security' => 'max-age=15724800; includeSubDomains',
|
41
|
+
'expect-ct' => 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"',
|
42
|
+
'server' => 'cloudflare',
|
43
|
+
'cf-ray' => '1234561234561234-MAD'
|
44
|
+
}
|
45
|
+
|
46
|
+
# Return the NexusMods instance to be tested.
|
47
|
+
# Handle the API key.
|
48
|
+
# Cache it for the scope of a test case.
|
49
|
+
#
|
50
|
+
# Parameters::
|
51
|
+
# * *args* (Hash): List of named arguments to give the constructor for the first time usage
|
52
|
+
def nexus_mods(**args)
|
53
|
+
if @nexus_mods.nil?
|
54
|
+
args[:api_key] = MOCKED_API_KEY unless args.key?(:api_key)
|
55
|
+
# Redirect any log into a string so that they don't pollute the tests output and they could be asserted.
|
56
|
+
nexus_mods_logger = StringIO.new
|
57
|
+
args[:logger] = Logger.new(nexus_mods_logger)
|
58
|
+
@nexus_mods = NexusMods.new(**args)
|
59
|
+
end
|
60
|
+
@nexus_mods
|
61
|
+
end
|
62
|
+
|
63
|
+
# Expect an HTTP API call to be made, and mock the corresponding HTTP response.
|
64
|
+
# Handle the API key and user agent.
|
65
|
+
#
|
66
|
+
# Parameters::
|
67
|
+
# * *host* (String): Expected host being targeted [default: 'api.nexusmodstoto.com']
|
68
|
+
# * *http_method* (Symbol): Expected requested HTTP method [default: :get]
|
69
|
+
# * *path* (String): Expected requested path [default: '/']
|
70
|
+
# * *api_key* (String): Expected API key to be used in request headers [default: MOCKED_API_KEY]
|
71
|
+
# * *code* (Integer): Mocked return code [default: 200]
|
72
|
+
# * *message* (String): Mocked returned message [default: 'OK']
|
73
|
+
# * *json* (Object): Mocked JSON body [default: {}]
|
74
|
+
# * *headers* (Hash<String,String>): Mocked additional HTTP headers [default: {}]
|
75
|
+
def expect_http_call_to(
|
76
|
+
host: 'api.nexusmods.com',
|
77
|
+
http_method: :get,
|
78
|
+
path: '/',
|
79
|
+
api_key: MOCKED_API_KEY,
|
80
|
+
code: 200,
|
81
|
+
message: 'OK',
|
82
|
+
json: {},
|
83
|
+
headers: {}
|
84
|
+
)
|
85
|
+
json_as_str = json.to_json
|
86
|
+
mocked_etag = "W/\"#{Digest::MD5.hexdigest("#{path}|#{json_as_str}")}\""
|
87
|
+
expected_request_headers = {
|
88
|
+
'User-Agent' => "nexus_mods (#{RUBY_PLATFORM}) Ruby/#{RUBY_VERSION}",
|
89
|
+
'apikey' => api_key
|
90
|
+
}
|
91
|
+
if @expected_returned_etags.include? mocked_etag
|
92
|
+
expected_request_headers['If-None-Match'] = mocked_etag
|
93
|
+
else
|
94
|
+
@expected_returned_etags << mocked_etag
|
95
|
+
end
|
96
|
+
stub_request(http_method, "https://#{host}#{path}").with(headers: expected_request_headers).to_return(
|
97
|
+
status: [code, message],
|
98
|
+
body: json_as_str,
|
99
|
+
headers: DEFAULT_API_HEADERS.
|
100
|
+
merge(
|
101
|
+
'etag' => mocked_etag
|
102
|
+
).
|
103
|
+
merge(headers)
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Expect a successfull call made to validate the user
|
108
|
+
def expect_validate_user
|
109
|
+
expect_http_call_to(
|
110
|
+
path: '/v1/users/validate.json',
|
111
|
+
json: {
|
112
|
+
user_id: MOCKED_USER_ID,
|
113
|
+
key: MOCKED_API_KEY,
|
114
|
+
name: MOCKED_USER_NAME,
|
115
|
+
email: MOCKED_USER_EMAIL,
|
116
|
+
is_premium?: false,
|
117
|
+
is_supporter?: false,
|
118
|
+
profile_url: 'https://www.nexusmods.com/Contents/Images/noavatar.gif',
|
119
|
+
is_supporter: false,
|
120
|
+
is_premium: false
|
121
|
+
}
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
RSpec.configure do |config|
|
130
|
+
config.include NexusModsTests::Helpers
|
131
|
+
config.before do
|
132
|
+
@nexus_mods = nil
|
133
|
+
# Keep a list of the etags we should have returned, so that we know when queries should contain them
|
134
|
+
# Array<String>
|
135
|
+
@expected_returned_etags = []
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Set a bigger output length for expectation errors, as most error messages include API keys and headers which can be lengthy
|
140
|
+
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 16_384
|
@@ -0,0 +1,19 @@
|
|
1
|
+
describe NexusMods::ApiLimits do
|
2
|
+
|
3
|
+
context 'when testing API limits' do
|
4
|
+
|
5
|
+
it 'gets api limits' do
|
6
|
+
expect_validate_user
|
7
|
+
expect_validate_user
|
8
|
+
api_limits = nexus_mods.api_limits
|
9
|
+
expect(api_limits.daily_limit).to eq 2500
|
10
|
+
expect(api_limits.daily_remaining).to eq 2500
|
11
|
+
expect(api_limits.daily_reset).to eq Time.parse('2019-10-03 00:00:00 +0000')
|
12
|
+
expect(api_limits.hourly_limit).to eq 100
|
13
|
+
expect(api_limits.hourly_remaining).to eq 100
|
14
|
+
expect(api_limits.hourly_reset).to eq Time.parse('2019-10-02T16:00:00+00:00')
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
describe NexusMods::Game do
|
2
|
+
|
3
|
+
context 'when testing games' do
|
4
|
+
|
5
|
+
it 'returns the games list' do
|
6
|
+
expect_validate_user
|
7
|
+
expect_http_call_to(
|
8
|
+
path: '/v1/games.json',
|
9
|
+
json: [
|
10
|
+
{
|
11
|
+
'id' => 100,
|
12
|
+
'name' => 'Morrowind',
|
13
|
+
'forum_url' => 'https://forums.nexusmods.com/index.php?/forum/111-morrowind/',
|
14
|
+
'nexusmods_url' => 'http://www.nexusmods.com/morrowind',
|
15
|
+
'genre' => 'RPG',
|
16
|
+
'file_count' => 14_143,
|
17
|
+
'downloads' => 20_414_985,
|
18
|
+
'domain_name' => 'morrowind',
|
19
|
+
'approved_date' => 1,
|
20
|
+
'file_views' => 100_014_750,
|
21
|
+
'authors' => 2062,
|
22
|
+
'file_endorsements' => 719_262,
|
23
|
+
'mods' => 6080,
|
24
|
+
'categories' => [
|
25
|
+
{
|
26
|
+
'category_id' => 1,
|
27
|
+
'name' => 'Morrowind',
|
28
|
+
'parent_category' => false
|
29
|
+
},
|
30
|
+
{
|
31
|
+
'category_id' => 2,
|
32
|
+
'name' => 'Buildings',
|
33
|
+
'parent_category' => 1
|
34
|
+
}
|
35
|
+
]
|
36
|
+
},
|
37
|
+
{
|
38
|
+
'id' => 101,
|
39
|
+
'name' => 'Oblivion',
|
40
|
+
'forum_url' => 'https://forums.nexusmods.com/index.php?/forum/131-oblivion/',
|
41
|
+
'nexusmods_url' => 'http://www.nexusmods.com/oblivion',
|
42
|
+
'genre' => 'RPG',
|
43
|
+
'file_count' => 52_775,
|
44
|
+
'downloads' => 187_758_634,
|
45
|
+
'domain_name' => 'oblivion',
|
46
|
+
'approved_date' => 1,
|
47
|
+
'file_views' => 880_508_188,
|
48
|
+
'authors' => 10_673,
|
49
|
+
'file_endorsements' => 4_104_067,
|
50
|
+
'mods' => 29_220,
|
51
|
+
'categories' => [
|
52
|
+
{
|
53
|
+
'category_id' => 20,
|
54
|
+
'name' => 'Oblivion',
|
55
|
+
'parent_category' => false
|
56
|
+
},
|
57
|
+
{
|
58
|
+
'category_id' => 22,
|
59
|
+
'name' => 'New structures - Buildings',
|
60
|
+
'parent_category' => 20
|
61
|
+
}
|
62
|
+
]
|
63
|
+
}
|
64
|
+
]
|
65
|
+
)
|
66
|
+
games = nexus_mods.games.sort_by(&:id)
|
67
|
+
expect(games.size).to eq 2
|
68
|
+
first_game = games.first
|
69
|
+
expect(first_game.id).to eq 100
|
70
|
+
expect(first_game.name).to eq 'Morrowind'
|
71
|
+
expect(first_game.forum_url).to eq 'https://forums.nexusmods.com/index.php?/forum/111-morrowind/'
|
72
|
+
expect(first_game.nexusmods_url).to eq 'http://www.nexusmods.com/morrowind'
|
73
|
+
expect(first_game.genre).to eq 'RPG'
|
74
|
+
expect(first_game.files_count).to eq 14_143
|
75
|
+
expect(first_game.downloads_count).to eq 20_414_985
|
76
|
+
expect(first_game.domain_name).to eq 'morrowind'
|
77
|
+
expect(first_game.approved_date).to eq Time.parse('1970-01-01 00:00:01 +0000')
|
78
|
+
expect(first_game.files_views).to eq 100_014_750
|
79
|
+
expect(first_game.authors_count).to eq 2062
|
80
|
+
expect(first_game.files_endorsements).to eq 719_262
|
81
|
+
expect(first_game.mods_count).to eq 6080
|
82
|
+
first_game_categories = first_game.categories
|
83
|
+
expect(first_game_categories.size).to eq 2
|
84
|
+
expect(first_game_categories.first.id).to eq 1
|
85
|
+
expect(first_game_categories.first.name).to eq 'Morrowind'
|
86
|
+
expect(first_game_categories.first.parent_category).to be_nil
|
87
|
+
expect(first_game_categories[1].id).to eq 2
|
88
|
+
expect(first_game_categories[1].name).to eq 'Buildings'
|
89
|
+
expect(first_game_categories[1].parent_category).not_to be_nil
|
90
|
+
expect(first_game_categories[1].parent_category.id).to eq 1
|
91
|
+
expect(first_game_categories[1].parent_category.name).to eq 'Morrowind'
|
92
|
+
expect(first_game_categories[1].parent_category.parent_category).to be_nil
|
93
|
+
second_game = games[1]
|
94
|
+
expect(second_game.id).to eq 101
|
95
|
+
expect(second_game.name).to eq 'Oblivion'
|
96
|
+
expect(second_game.forum_url).to eq 'https://forums.nexusmods.com/index.php?/forum/131-oblivion/'
|
97
|
+
expect(second_game.nexusmods_url).to eq 'http://www.nexusmods.com/oblivion'
|
98
|
+
expect(second_game.genre).to eq 'RPG'
|
99
|
+
expect(second_game.files_count).to eq 52_775
|
100
|
+
expect(second_game.downloads_count).to eq 187_758_634
|
101
|
+
expect(second_game.domain_name).to eq 'oblivion'
|
102
|
+
expect(second_game.approved_date).to eq Time.parse('1970-01-01 00:00:01 +0000')
|
103
|
+
expect(second_game.files_views).to eq 880_508_188
|
104
|
+
expect(second_game.authors_count).to eq 10_673
|
105
|
+
expect(second_game.files_endorsements).to eq 4_104_067
|
106
|
+
expect(second_game.mods_count).to eq 29_220
|
107
|
+
second_game_categories = second_game.categories
|
108
|
+
expect(second_game_categories.size).to eq 2
|
109
|
+
expect(second_game_categories.first.id).to eq 20
|
110
|
+
expect(second_game_categories.first.name).to eq 'Oblivion'
|
111
|
+
expect(second_game_categories.first.parent_category).to be_nil
|
112
|
+
expect(second_game_categories[1].id).to eq 22
|
113
|
+
expect(second_game_categories[1].name).to eq 'New structures - Buildings'
|
114
|
+
expect(second_game_categories[1].parent_category).not_to be_nil
|
115
|
+
expect(second_game_categories[1].parent_category.id).to eq 20
|
116
|
+
expect(second_game_categories[1].parent_category.name).to eq 'Oblivion'
|
117
|
+
expect(second_game_categories[1].parent_category.parent_category).to be_nil
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|