nexus_mods 0.1.0
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.
- 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
|