aliexpress 0.4
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/.coveralls.yml +1 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +38 -0
- data/.travis.yml +10 -0
- data/Gemfile +15 -0
- data/LICENSE.md +21 -0
- data/README.md +117 -0
- data/Rakefile +3 -0
- data/aliexpress.gemspec +29 -0
- data/bin/console +21 -0
- data/lib/aliexpress.rb +45 -0
- data/lib/aliexpress/affiliate.rb +141 -0
- data/lib/aliexpress/auth.rb +86 -0
- data/lib/aliexpress/base.rb +87 -0
- data/lib/aliexpress/product.rb +22 -0
- data/lib/aliexpress/response_error.rb +11 -0
- data/spec/affiliate_spec.rb +70 -0
- data/spec/aliexpress_spec.rb +94 -0
- data/spec/auth_spec.rb +41 -0
- data/spec/base_spec.rb +49 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/auth.rb +12 -0
- data/spec/support/webmock.rb +3 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 72fe66faf65e1f93edd912f2ba24148ca70c5ae1
|
|
4
|
+
data.tar.gz: 5ec3aa35f98e2deea15312fd5d243169d281dd43
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: db423a70fbb44d9063a2e7e5f809aaa3948a0bf7bbf7bcd5ea69bb7b53e53d75e3e77de01cf51d509eafcd193182d3f635b754b060fdc7b902f5f49b96344cec
|
|
7
|
+
data.tar.gz: 2caf268645d24586782369475cb5437e7ced287508746db1e4afa52616df55130638dd558bfb51b5f4ae1101f6046ca26e2b9ef5ec4a9fd524e14a0e6255c8a2
|
data/.coveralls.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.2
|
|
3
|
+
|
|
4
|
+
Metrics/AbcSize:
|
|
5
|
+
Enabled: false
|
|
6
|
+
Metrics/CyclomaticComplexity:
|
|
7
|
+
Enabled: false
|
|
8
|
+
Metrics/PerceivedComplexity:
|
|
9
|
+
Enabled: false
|
|
10
|
+
|
|
11
|
+
Metrics/LineLength:
|
|
12
|
+
Enabled: false
|
|
13
|
+
Metrics/ClassLength:
|
|
14
|
+
Enabled: false
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Enabled: false
|
|
17
|
+
Metrics/BlockLength:
|
|
18
|
+
Enabled: false
|
|
19
|
+
Metrics/ParameterLists:
|
|
20
|
+
Enabled: false
|
|
21
|
+
|
|
22
|
+
Lint/EndAlignment:
|
|
23
|
+
Enabled: false
|
|
24
|
+
|
|
25
|
+
Style/Documentation:
|
|
26
|
+
Enabled: false
|
|
27
|
+
Style/IndentationWidth:
|
|
28
|
+
Enabled: false
|
|
29
|
+
Style/ElseAlignment:
|
|
30
|
+
Enabled: false
|
|
31
|
+
Style/GuardClause:
|
|
32
|
+
Enabled: false
|
|
33
|
+
Style/NumericLiterals:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
# This is temporary - it can be remove after updating rubocop to the latest.
|
|
37
|
+
Style/PercentLiteralDelimiters:
|
|
38
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Tophatter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[](https://travis-ci.org/tophatter/aliexpress-api-ruby)
|
|
2
|
+
[](https://coveralls.io/github/tophatter/aliexpress-api-ruby?branch=master)
|
|
3
|
+
|
|
4
|
+
### AliExpress SDK for Ruby:
|
|
5
|
+
|
|
6
|
+
#### Authentication:
|
|
7
|
+
|
|
8
|
+
To use this SDK, you must have an app key and app secret (a.k.a. client id and client secret). These can be obtained [here](https://portals.aliexpress.com/adcenter/apiSetting.htm) if you're using the affiliate program, or [here](http://isvus.aliexpress.com/isv/index.htm) if you're using the ISV API.
|
|
9
|
+
|
|
10
|
+
In the affiliate program API settings, you'll see the following:
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
The "api key" is your client id, and the "digital signature" is your client secret.
|
|
15
|
+
|
|
16
|
+
The API uses OAuth for authentication. You need to first go through a manual authorization sequence to get a short-live authorization code. That code can be exchanged for an access token that last 10 hours, and a refresh token that lasts significantly longer.
|
|
17
|
+
|
|
18
|
+
Using this gem's console, you get obtain an access token as follows:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
bash $ bin/console
|
|
22
|
+
[1] pry(main)> AliExpress.client_id = [YOUR CLIENT ID]
|
|
23
|
+
[2] pry(main)> AliExpress.client_secret = [YOUR CLIENT SECRET]
|
|
24
|
+
[3] pry(main)> AliExpress::Auth.authorize
|
|
25
|
+
Open https://gw.api.alibaba.com/auth/authorize.htm?_aop_signature=REDACTED&client_id=REDACTED&redirect_uri=REDACTED&site=aliexpress&state=REDACTED in your browser, and then enter the code below.
|
|
26
|
+
authorization_code> [ENTER YOUR CODE HERE]
|
|
27
|
+
{
|
|
28
|
+
"refresh_token_timeout" => "20180402143144000-0700",
|
|
29
|
+
"aliId" => "[REDACTED]",
|
|
30
|
+
"resource_owner" => "[REDACTED]",
|
|
31
|
+
:access_token => "[REDACTED]",
|
|
32
|
+
:refresh_token => "[REDACTED]",
|
|
33
|
+
:expires_at => 1507194954
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Once your tokens are obtained, you can instruct this SDK to use them:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
AliExpress.access_token = [YOUR ACCESS TOKEN]
|
|
41
|
+
AliExpress.refresh_token = [YOUR REFRESH TOKEN]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If your access token expires, you can get a new one with the following:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
AliExpress::Auth.refresh
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Affiliate API:
|
|
51
|
+
|
|
52
|
+
##### api.listPromotionProduct
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
[1] pry(main)> AliExpress::Affiliate.list_promotion_product(keywords: 'drone')
|
|
56
|
+
D, [2017-10-05T11:23:03.971387 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&keywords=drone
|
|
57
|
+
[
|
|
58
|
+
[ 0] {
|
|
59
|
+
"lotNum" => 1,
|
|
60
|
+
"packageType" => "piece",
|
|
61
|
+
"imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
|
|
62
|
+
"allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-font-b-Drone-b-font-With-2MP-HD-Camera.jpg",
|
|
63
|
+
"productId" => 32817202058,
|
|
64
|
+
"discount" => "5%",
|
|
65
|
+
"originalPrice" => "US $27.99",
|
|
66
|
+
"productTitle" => "100% Original SYMA X5C (Upgrade Version) RC <font><b>Drone</b></font> With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
|
|
67
|
+
"validTime" => "2017-10-08",
|
|
68
|
+
"productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
|
|
69
|
+
"volume" => "14",
|
|
70
|
+
"salePrice" => "US $26.59"
|
|
71
|
+
},
|
|
72
|
+
<snip>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
##### api.getPromotionProductDetail
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
[1] pry(main)> AliExpress::Affiliate.get_promotion_product_detail(32817202058)
|
|
79
|
+
D, [2017-10-05T11:24:19.235970 #69079] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/[REDACTED]?fields=productId%2CproductTitle%2CproductUrl%2CimageUrl%2CallImageUrls%2CoriginalPrice%2CsalePrice%2ClocalPrice%2Cdiscount%2Cvolume%2CpackageType%2ClotNum%2CvalidTime&productId=32817202058
|
|
80
|
+
{
|
|
81
|
+
"lotNum" => 1,
|
|
82
|
+
"packageType" => "piece",
|
|
83
|
+
"imageUrl" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
|
|
84
|
+
"allImageUrls" => "https://ae01.alicdn.com/kf/HTB1nxEjSFXXXXcDXpXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1O6LUSFXXXXX5aXXXq6xXFXXXB/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1aA.DSFXXXXXgXXXXq6xXFXXX3/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1N.kySFXXXXaUXXXXq6xXFXXXF/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB1lc_1SFXXXXcgXVXXq6xXFXXX9/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg,https://ae01.alicdn.com/kf/HTB16lEkSFXXXXcpXpXXq6xXFXXXK/100-Original-SYMA-X5C-Upgrade-Version-RC-Drone-With-2MP-HD-Camera-6-Axis-RC-Quadcopter.jpg",
|
|
85
|
+
"productId" => 32817202058,
|
|
86
|
+
"discount" => "5%",
|
|
87
|
+
"originalPrice" => "US $27.99",
|
|
88
|
+
"productTitle" => "100% Original SYMA X5C (Upgrade Version) RC Drone With 2MP HD Camera 6-Axis RC Quadcopter Helicopter X5 Dron Without Camera",
|
|
89
|
+
"validTime" => "2017-10-08",
|
|
90
|
+
"productUrl" => "https://www.aliexpress.com/item/SYMA-X5C-1-Upgrade-Version-SYMA-X5C-Professional-RC-Drone-6-Axis-Helicopter-Quadcopter-With-2MP/32817202058.html",
|
|
91
|
+
"volume" => "14",
|
|
92
|
+
"salePrice" => "US $26.59"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
##### api.listHotProducts
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
[1] pry(main)> AliExpress::Affiliate.list_hot_products(3)
|
|
100
|
+
D, [2017-10-05T11:21:54.835675 #68982] DEBUG -- : GET https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/[REDACTED]?categoryId=3&language=en&localCurrency=USD
|
|
101
|
+
[
|
|
102
|
+
[ 0] {
|
|
103
|
+
"localPrice" => "21.00 $",
|
|
104
|
+
"imageUrl" => "https://ae01.alicdn.com/kf/HTB1YsQwQFXXXXcvXpXXq6xXFXXX0/Preself-Hoodies-Sweatshirt-Women-Casual-Outwear-Hoody-Loose-Long-Sleeve-Mantle-Hooded-Cover-Pullover-Clothes-2017.jpg",
|
|
105
|
+
"productUrl" => "https://www.aliexpress.com/item/New-Women-Loose-Fashion-Autumn-Winter-Hooded-Oversize-Sweatshirt-Coat-Outwear/32763830156.html",
|
|
106
|
+
"validTime" => "2017-10-18",
|
|
107
|
+
"productTitle" => "Preself Hoodies Sweatshirt Women Casual Outwear Hoody Loose Long Sleeve Mantle Hooded Cover Pullover Clothes 2017 New",
|
|
108
|
+
"volume" => "",
|
|
109
|
+
"salePrice" => "US $21.00",
|
|
110
|
+
"productId" => 32763830156
|
|
111
|
+
},
|
|
112
|
+
<snip>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### ISV API:
|
|
116
|
+
|
|
117
|
+
In progress.
|
data/Rakefile
ADDED
data/aliexpress.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# To publish the next version:
|
|
2
|
+
# gem build aliexpress.gemspec
|
|
3
|
+
# gem push aliexpress-{VERSION}.gem
|
|
4
|
+
Gem::Specification.new do |s|
|
|
5
|
+
s.name = 'aliexpress'
|
|
6
|
+
s.version = '0.4'
|
|
7
|
+
s.platform = Gem::Platform::RUBY
|
|
8
|
+
s.licenses = ['MIT']
|
|
9
|
+
s.authors = ['Chris Estreich']
|
|
10
|
+
s.email = ['chris@tophatter.com']
|
|
11
|
+
s.homepage = 'https://github.com/tophatter/aliexpress-api-ruby'
|
|
12
|
+
s.summary = 'AliExpress SDK for ruby.'
|
|
13
|
+
s.description = 'AliExpress SDK for ruby.'
|
|
14
|
+
|
|
15
|
+
s.extra_rdoc_files = ['README.md']
|
|
16
|
+
|
|
17
|
+
s.required_ruby_version = '~> 2.0'
|
|
18
|
+
|
|
19
|
+
s.add_dependency 'rest-client', '~> 1.6'
|
|
20
|
+
s.add_dependency 'addressable'
|
|
21
|
+
s.add_dependency 'oauth2'
|
|
22
|
+
|
|
23
|
+
s.post_install_message = 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
|
|
24
|
+
|
|
25
|
+
s.files = `git ls-files`.split("\n")
|
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
28
|
+
s.require_paths = ['lib']
|
|
29
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup' # Otherwise "require 'aliexpress'" won't work.
|
|
4
|
+
require 'aliexpress'
|
|
5
|
+
require 'awesome_print'
|
|
6
|
+
require 'pry'
|
|
7
|
+
|
|
8
|
+
%w(client_id client_secret access_token refresh_token).each do |method|
|
|
9
|
+
begin
|
|
10
|
+
AliExpress.send("#{method}=", File.read(".#{method}").chomp)
|
|
11
|
+
puts "AliExpress.#{method}: #{AliExpress.send(method)}"
|
|
12
|
+
rescue Errno::ENOENT
|
|
13
|
+
puts "No .#{method} file, using default: #{AliExpress.send(method)}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
AliExpress.logger = Logger.new(STDOUT)
|
|
18
|
+
AliExpress.logger.level = Logger::DEBUG
|
|
19
|
+
|
|
20
|
+
AwesomePrint.pry!
|
|
21
|
+
Pry.start
|
data/lib/aliexpress.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'rest-client'
|
|
3
|
+
require 'addressable'
|
|
4
|
+
require 'oauth2'
|
|
5
|
+
|
|
6
|
+
module AliExpress
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :client_id, :client_secret, :access_token, :refresh_token
|
|
9
|
+
|
|
10
|
+
attr_writer :protocol, :host, :base_uri
|
|
11
|
+
attr_writer :currency, :language
|
|
12
|
+
attr_writer :logger
|
|
13
|
+
|
|
14
|
+
def protocol
|
|
15
|
+
@protocol || 'https'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def host
|
|
19
|
+
@host || 'gw.api.alibaba.com'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_uri
|
|
23
|
+
@base_uri || "#{protocol}://#{host}/openapi"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def currency
|
|
27
|
+
@currency || 'USD'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def language
|
|
31
|
+
@language || 'en'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def logger
|
|
35
|
+
@logger ||= Logger.new('/dev/null')
|
|
36
|
+
@logger
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
require File.dirname(__FILE__) + '/aliexpress/base'
|
|
42
|
+
require File.dirname(__FILE__) + '/aliexpress/response_error'
|
|
43
|
+
require File.dirname(__FILE__) + '/aliexpress/auth'
|
|
44
|
+
require File.dirname(__FILE__) + '/aliexpress/product'
|
|
45
|
+
require File.dirname(__FILE__) + '/aliexpress/affiliate'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module AliExpress
|
|
2
|
+
class Affiliate < Base
|
|
3
|
+
CATEGORIES = {
|
|
4
|
+
'Apparel & Accessories' => 3,
|
|
5
|
+
'Automobiles & Motorcycles' => 34,
|
|
6
|
+
'Beauty & Health' => 66,
|
|
7
|
+
'Books for Local Russian' => 200004360,
|
|
8
|
+
'Computer & Office' => 7,
|
|
9
|
+
'Home Improvement' => 13,
|
|
10
|
+
'Consumer Electronics' => 44,
|
|
11
|
+
'Electrical Equipment & Supplies' => 5,
|
|
12
|
+
'Electronic Components & Supplies' => 502,
|
|
13
|
+
'Food' => 2,
|
|
14
|
+
'Furniture' => 1503,
|
|
15
|
+
'Hair & Accessories' => 200003655,
|
|
16
|
+
'Hardware' => 42,
|
|
17
|
+
'Home & Garden' => 15,
|
|
18
|
+
'Home Appliances' => 6,
|
|
19
|
+
'Industry & Business' => 200001996,
|
|
20
|
+
'Jewelry & Accessories' => 36,
|
|
21
|
+
'Lights & Lighting' => 39,
|
|
22
|
+
'Luggage & Bags' => 1524,
|
|
23
|
+
'Mother & Kids' => 1501,
|
|
24
|
+
'Office & School Supplies' => 21,
|
|
25
|
+
'Phones & Telecommunications' => 509,
|
|
26
|
+
'Security & Protection' => 30,
|
|
27
|
+
'Shoes' => 322,
|
|
28
|
+
'Special Category' => 200001075,
|
|
29
|
+
'Sports & Entertainment' => 18,
|
|
30
|
+
'Tools' => 1420,
|
|
31
|
+
'Toys & Hobbies' => 26,
|
|
32
|
+
'Travel and Coupon Services' => 200003498,
|
|
33
|
+
'Watches' => 1511,
|
|
34
|
+
'Weddings & Events' => 320
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
FILTERS = %w(category keywords min_price max_price high_quality).freeze
|
|
38
|
+
SORTS = %w(orignalPriceUp orignalPriceDown sellerRateUp sellerRateDown).freeze
|
|
39
|
+
FIELDS = %w(productId productTitle productUrl imageUrl allImageUrls originalPrice salePrice localPrice discount volume).freeze
|
|
40
|
+
|
|
41
|
+
SUCCESS = 20010000
|
|
42
|
+
ERRORS = {
|
|
43
|
+
20020000 => 'System error.',
|
|
44
|
+
20030000 => 'Unauthorized transfer request.',
|
|
45
|
+
20030010 => 'Required parameters.',
|
|
46
|
+
20030020 => 'Invalid protocol format.',
|
|
47
|
+
20030030 => 'API version input parameter error.',
|
|
48
|
+
20030040 => 'API namespace input parameter error.',
|
|
49
|
+
20030050 => 'API name input parameter error.',
|
|
50
|
+
20030060 => 'Fields input parameter error.',
|
|
51
|
+
20030070 => 'Keyword input parameter error.',
|
|
52
|
+
20030080 => 'Category ID input parameter error.',
|
|
53
|
+
20030140 => 'Page number input parameter error.',
|
|
54
|
+
20030150 => 'Page size input parameter error (default = 20, max = 40).',
|
|
55
|
+
20030160 => 'Sort input parameter error.'
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
def search(query:, filters: {}, sort: 'orignalPriceUp', page: nil, per_page: 40, fields: FIELDS)
|
|
60
|
+
response = get(
|
|
61
|
+
api_call: 'api.listPromotionProduct',
|
|
62
|
+
params: {
|
|
63
|
+
keywords: query,
|
|
64
|
+
# filters
|
|
65
|
+
categoryId: CATEGORIES[filters[:category]] || filters[:category],
|
|
66
|
+
originalPriceFrom: filters[:min_price],
|
|
67
|
+
originalPriceTo: filters[:max_price],
|
|
68
|
+
highQualityItems: filters[:high_quality],
|
|
69
|
+
# sorting
|
|
70
|
+
sort: sort,
|
|
71
|
+
# paging
|
|
72
|
+
pageNo: page,
|
|
73
|
+
pageSize: per_page,
|
|
74
|
+
# response format
|
|
75
|
+
fields: fields.join(',')
|
|
76
|
+
}.delete_if { |_k, v| v.nil? }
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
raise_response_error(response) unless response['errorCode'] == SUCCESS
|
|
80
|
+
response
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find(id, fields: FIELDS)
|
|
84
|
+
response = get(
|
|
85
|
+
api_call: 'api.getPromotionProductDetail',
|
|
86
|
+
params: { productId: id, fields: fields.join(',') }
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
raise_response_error(response) unless response['errorCode'] == SUCCESS
|
|
90
|
+
response
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def similar(id)
|
|
94
|
+
response = get(
|
|
95
|
+
api_call: 'api.listSimilarProducts',
|
|
96
|
+
params: { productId: id }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
raise_response_error(response) unless response['errorCode'] == SUCCESS
|
|
100
|
+
response
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def popular(category)
|
|
104
|
+
response = get(
|
|
105
|
+
api_call: 'api.listHotProducts',
|
|
106
|
+
params: { categoryId: CATEGORIES[category] || category }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
raise_response_error(response) unless response['errorCode'] == SUCCESS
|
|
110
|
+
response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def get(hash)
|
|
116
|
+
defaults = {
|
|
117
|
+
api_namespace: 'portals.open',
|
|
118
|
+
api_version: 2,
|
|
119
|
+
auth: false,
|
|
120
|
+
sign: false,
|
|
121
|
+
params: {
|
|
122
|
+
localCurrency: currency,
|
|
123
|
+
language: language
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
params = defaults[:params].merge(hash.delete(:params))
|
|
127
|
+
super(defaults.merge(hash).merge(params: params))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def raise_response_error(response)
|
|
131
|
+
exception = ResponseError.new(
|
|
132
|
+
code: response['error_code'] || response['errorCode'],
|
|
133
|
+
message: response['error_message'] || ERRORS[response['errorCode']] || response['errorCode'],
|
|
134
|
+
response: response
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
raise exception
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module AliExpress
|
|
2
|
+
class Auth < Base
|
|
3
|
+
class << self
|
|
4
|
+
def authorize(state = nil)
|
|
5
|
+
oauth_client = client
|
|
6
|
+
oauth_params = params(state || SecureRandom.hex(24))
|
|
7
|
+
oauth_params[:_aop_signature] = sign(oauth_params)
|
|
8
|
+
logger.debug oauth_params
|
|
9
|
+
url = oauth_client.authorize_url(oauth_params)
|
|
10
|
+
|
|
11
|
+
if RUBY_PLATFORM == 'x86_64-darwin16'
|
|
12
|
+
begin
|
|
13
|
+
system('open', url)
|
|
14
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
15
|
+
logger.debug "Call to open failed: #{e.message}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
$stdout.puts "Open #{url} in your browser, and then enter the code below."
|
|
20
|
+
$stdout.print 'authorization_code> '
|
|
21
|
+
authorization_code = $stdin.gets
|
|
22
|
+
|
|
23
|
+
token = oauth_client.get_token(
|
|
24
|
+
code: authorization_code,
|
|
25
|
+
grant_type: 'authorization_code',
|
|
26
|
+
need_refresh_token: 'true',
|
|
27
|
+
parse: :json,
|
|
28
|
+
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
|
|
29
|
+
).to_hash
|
|
30
|
+
|
|
31
|
+
token
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def refresh
|
|
35
|
+
if refresh_token
|
|
36
|
+
response = post(
|
|
37
|
+
api_call: 'getToken',
|
|
38
|
+
api_namespace: 'system.oauth2',
|
|
39
|
+
api_version: '1',
|
|
40
|
+
# protocol: 'http',
|
|
41
|
+
auth: false,
|
|
42
|
+
sign: false,
|
|
43
|
+
params: {
|
|
44
|
+
grant_type: 'refresh_token',
|
|
45
|
+
client_id: client_id,
|
|
46
|
+
client_secret: client_secret,
|
|
47
|
+
refresh_token: refresh_token
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
AliExpress.access_token = response['access_token']
|
|
51
|
+
else
|
|
52
|
+
raise Exception, 'You must have a refresh_token to refresh authorization. You need to call `AliExpress::Auth.authorize` and follow the instructions to generate one.'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def client
|
|
59
|
+
OAuth2::Client.new(
|
|
60
|
+
client_id,
|
|
61
|
+
client_secret,
|
|
62
|
+
site: 'https://gw.api.alibaba.com',
|
|
63
|
+
authorize_url: '/auth/authorize.htm',
|
|
64
|
+
token_url: "/openapi/http/1/system.oauth2/getToken/#{client_id}"
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sign(hash)
|
|
69
|
+
OpenSSL::HMAC.hexdigest(
|
|
70
|
+
OpenSSL::Digest.new('sha1'),
|
|
71
|
+
client_secret,
|
|
72
|
+
hash.sort.flatten.join.to_s
|
|
73
|
+
).upcase
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def params(state)
|
|
77
|
+
{
|
|
78
|
+
client_id: client_id,
|
|
79
|
+
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
|
80
|
+
site: 'aliexpress',
|
|
81
|
+
state: state
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module AliExpress
|
|
2
|
+
class Base
|
|
3
|
+
class << self
|
|
4
|
+
def client_id
|
|
5
|
+
AliExpress.client_id
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def client_secret
|
|
9
|
+
AliExpress.client_secret
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def access_token
|
|
13
|
+
AliExpress.access_token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def refresh_token
|
|
17
|
+
AliExpress.refresh_token
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def currency
|
|
21
|
+
AliExpress.currency
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def language
|
|
25
|
+
AliExpress.language
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def logger
|
|
29
|
+
AliExpress.logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(params)
|
|
33
|
+
request(params.merge(method: :get))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def post(params)
|
|
37
|
+
request(params.merge(method: :post))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def sign_payload(path, payload)
|
|
43
|
+
signature_factor = path + payload.map { |key, value| "#{key}#{value}" }.sort.join
|
|
44
|
+
payload[:_aop_signature] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), client_secret, signature_factor).upcase
|
|
45
|
+
logger.debug "signature_factor=#{signature_factor}, signature=#{payload[:_aop_signature]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def request(method:, protocol: 'param2', api_version: 1, api_namespace: 'aliexpress.open', api_call:, auth: true, sign: true, params: {})
|
|
49
|
+
path = [protocol, api_version, api_namespace, api_call, client_id].join('/')
|
|
50
|
+
|
|
51
|
+
payload = params
|
|
52
|
+
payload[:access_token] = access_token if auth
|
|
53
|
+
sign_payload(path, payload) if sign
|
|
54
|
+
url = [AliExpress.base_uri, path].join('/')
|
|
55
|
+
|
|
56
|
+
resp = if method == :get
|
|
57
|
+
unless params.length.zero?
|
|
58
|
+
uri = Addressable::URI.new
|
|
59
|
+
uri.query_values = params
|
|
60
|
+
url += "?#{uri.query}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
logger.debug "GET #{url}"
|
|
64
|
+
RestClient.get(url)
|
|
65
|
+
else
|
|
66
|
+
logger.debug "POST #{url} | #{payload}"
|
|
67
|
+
RestClient.post(url, payload)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
JSON.parse(resp.body)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
resp.body
|
|
74
|
+
end
|
|
75
|
+
rescue RestClient::Exception => exception
|
|
76
|
+
resp = begin
|
|
77
|
+
JSON.parse(exception.response)
|
|
78
|
+
rescue JSON::ParserError
|
|
79
|
+
exception.response
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
logger.debug "#{exception.class.name} (#{exception.response.code}): #{resp.inspect}"
|
|
83
|
+
resp
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module AliExpress
|
|
2
|
+
class Product < Base
|
|
3
|
+
class << self
|
|
4
|
+
def find(id)
|
|
5
|
+
post(api_call: 'api.findAeProductById', params: { productId: id })
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def get(params)
|
|
11
|
+
super(default_params.merge(params))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_params
|
|
15
|
+
{
|
|
16
|
+
api_namespace: 'aliexpress.open',
|
|
17
|
+
api_version: 1
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# rspec spec/affiliate_spec.rb
|
|
4
|
+
describe AliExpress::Affiliate do
|
|
5
|
+
let(:response) do
|
|
6
|
+
mock = double
|
|
7
|
+
allow(mock).to receive(:code).and_return(200)
|
|
8
|
+
allow(mock).to receive(:body).and_return(json)
|
|
9
|
+
mock
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:success) { 20010000 }
|
|
13
|
+
let(:hash) { { 'errorCode' => success } }
|
|
14
|
+
let(:json) { hash.to_json }
|
|
15
|
+
|
|
16
|
+
# before do
|
|
17
|
+
# AliExpress.logger = Logger.new(STDOUT)
|
|
18
|
+
# AliExpress.logger.level = Logger::DEBUG
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
describe '.search' do
|
|
22
|
+
let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listPromotionProduct/fake-client-id?categoryId=3&fields=productId&keywords=drone&language=en&localCurrency=USD&pageNo=2&pageSize=5&sort=orignalPriceDown' }
|
|
23
|
+
|
|
24
|
+
it 'returns a successful response' do
|
|
25
|
+
expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
|
|
26
|
+
response = AliExpress::Affiliate.search(query: 'drone', filters: { category: 'Apparel & Accessories' }, sort: 'orignalPriceDown', page: 2, per_page: 5, fields: %w(productId))
|
|
27
|
+
expect(response['errorCode']).to eq(success)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '.find' do
|
|
32
|
+
let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.getPromotionProductDetail/fake-client-id?fields=productId&language=en&localCurrency=USD&productId=12345' }
|
|
33
|
+
|
|
34
|
+
it 'returns a product' do
|
|
35
|
+
expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
|
|
36
|
+
response = AliExpress::Affiliate.find(12345, fields: %w(productId))
|
|
37
|
+
expect(response['errorCode']).to eq(success)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '.similar' do
|
|
42
|
+
let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listSimilarProducts/fake-client-id?language=en&localCurrency=USD&productId=12345' }
|
|
43
|
+
|
|
44
|
+
it 'returns a product' do
|
|
45
|
+
expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
|
|
46
|
+
response = AliExpress::Affiliate.similar(12345)
|
|
47
|
+
expect(response['errorCode']).to eq(success)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '.popular' do
|
|
52
|
+
let(:url) { 'https://gw.api.alibaba.com/openapi/param2/2/portals.open/api.listHotProducts/fake-client-id?categoryId=3&language=en&localCurrency=USD' }
|
|
53
|
+
|
|
54
|
+
context 'when a category id is used' do
|
|
55
|
+
it 'returns a successful response' do
|
|
56
|
+
expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
|
|
57
|
+
response = AliExpress::Affiliate.popular(3)
|
|
58
|
+
expect(response['errorCode']).to eq(success)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'when a category name is used' do
|
|
63
|
+
it 'returns a successful response' do
|
|
64
|
+
expect(RestClient).to receive(:get).exactly(1).times.with(url).and_return(response)
|
|
65
|
+
response = AliExpress::Affiliate.popular('Apparel & Accessories')
|
|
66
|
+
expect(response['errorCode']).to eq(success)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# rspec spec/aliexpress_spec.rb
|
|
4
|
+
describe AliExpress do
|
|
5
|
+
describe '.client_id' do
|
|
6
|
+
subject { AliExpress.client_id }
|
|
7
|
+
it { is_expected.to eq('fake-client-id') }
|
|
8
|
+
|
|
9
|
+
context('when assigned') do
|
|
10
|
+
before { AliExpress.client_id = 'foo' }
|
|
11
|
+
it { is_expected.to eq('foo') }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '.client_secret' do
|
|
16
|
+
subject { AliExpress.client_secret }
|
|
17
|
+
it { is_expected.to eq('fake-client-secret') }
|
|
18
|
+
|
|
19
|
+
context('when assigned') do
|
|
20
|
+
before { AliExpress.client_secret = 'foo' }
|
|
21
|
+
it { is_expected.to eq('foo') }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '.access_token' do
|
|
26
|
+
subject { AliExpress.access_token }
|
|
27
|
+
it { is_expected.to eq('fake-access-token') }
|
|
28
|
+
|
|
29
|
+
context('when assigned') do
|
|
30
|
+
before { AliExpress.access_token = 'foo' }
|
|
31
|
+
it { is_expected.to eq('foo') }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '.refresh_token' do
|
|
36
|
+
subject { AliExpress.refresh_token }
|
|
37
|
+
it { is_expected.to eq('fake-refresh-token') }
|
|
38
|
+
|
|
39
|
+
context('when assigned') do
|
|
40
|
+
before { AliExpress.refresh_token = 'foo' }
|
|
41
|
+
it { is_expected.to eq('foo') }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '.protocol' do
|
|
46
|
+
subject { AliExpress.protocol }
|
|
47
|
+
it { is_expected.to eq('https') }
|
|
48
|
+
|
|
49
|
+
context('when assigned') do
|
|
50
|
+
before { AliExpress.protocol = 'foo' }
|
|
51
|
+
it { is_expected.to eq('foo') }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '.host' do
|
|
56
|
+
subject { AliExpress.host }
|
|
57
|
+
it { is_expected.to eq('gw.api.alibaba.com') }
|
|
58
|
+
|
|
59
|
+
context('when assigned') do
|
|
60
|
+
before { AliExpress.host = 'foo' }
|
|
61
|
+
it { is_expected.to eq('foo') }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '.base_uri' do
|
|
66
|
+
subject { AliExpress.base_uri }
|
|
67
|
+
it { is_expected.to eq('https://gw.api.alibaba.com/openapi') }
|
|
68
|
+
|
|
69
|
+
context('when assigned') do
|
|
70
|
+
before { AliExpress.base_uri = 'foo' }
|
|
71
|
+
it { is_expected.to eq('foo') }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '.currency' do
|
|
76
|
+
subject { AliExpress.currency }
|
|
77
|
+
it { is_expected.to eq('USD') }
|
|
78
|
+
|
|
79
|
+
context('when assigned') do
|
|
80
|
+
before { AliExpress.currency = 'foo' }
|
|
81
|
+
it { is_expected.to eq('foo') }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '.language' do
|
|
86
|
+
subject { AliExpress.language }
|
|
87
|
+
it { is_expected.to eq('en') }
|
|
88
|
+
|
|
89
|
+
context('when assigned') do
|
|
90
|
+
before { AliExpress.language = 'foo' }
|
|
91
|
+
it { is_expected.to eq('foo') }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/spec/auth_spec.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# rspec spec/auth_spec.rb
|
|
4
|
+
describe AliExpress::Auth do
|
|
5
|
+
describe '.authorize' do
|
|
6
|
+
let(:authorization_code) { 'b5ce38e1-833c-4f15-bd29-65e85db9bd2e' }
|
|
7
|
+
let(:response) do
|
|
8
|
+
{
|
|
9
|
+
'refresh_token_timeout' => '20180402143144000-0700',
|
|
10
|
+
'aliId' => 'fake-ali-id',
|
|
11
|
+
'resource_owner' => 'fake-resource-owner',
|
|
12
|
+
access_token: 'new-fake-access-token',
|
|
13
|
+
refresh_token: 'new-fake-refresh-token',
|
|
14
|
+
expires_at: 1507266354
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'creates a new access token' do
|
|
19
|
+
stub_const('RUBY_PLATFORM', 'test')
|
|
20
|
+
allow($stdin).to receive(:gets).and_return(authorization_code)
|
|
21
|
+
allow($stdout).to receive(:puts).and_return(nil)
|
|
22
|
+
allow($stdout).to receive(:print).and_return(nil)
|
|
23
|
+
expect_any_instance_of(OAuth2::Client).to receive(:get_token).exactly(1).times.with(hash_including(code: authorization_code)).and_return(response)
|
|
24
|
+
expect(AliExpress::Auth.authorize).to eq(response)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '.refresh' do
|
|
29
|
+
let(:response) do
|
|
30
|
+
mock = double
|
|
31
|
+
allow(mock).to receive(:code).and_return(200)
|
|
32
|
+
allow(mock).to receive(:body).and_return('{"aliId":"fake-ali-id","resource_owner":"fake-resource-owner","expires_in":"36000","access_token":"jabber_wocky"}')
|
|
33
|
+
mock
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'exchanges the refresh token for a new access token' do
|
|
37
|
+
expect(RestClient).to receive(:post).exactly(1).times.and_return(response)
|
|
38
|
+
expect(AliExpress::Auth.refresh).to eq('jabber_wocky')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/spec/base_spec.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# rspec spec/base_spec.rb
|
|
4
|
+
describe AliExpress::Base do
|
|
5
|
+
let(:response) do
|
|
6
|
+
mock = double
|
|
7
|
+
allow(mock).to receive(:code).and_return(200)
|
|
8
|
+
allow(mock).to receive(:body).and_return('{}')
|
|
9
|
+
mock
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '.get' do
|
|
13
|
+
context 'when auth is true' do
|
|
14
|
+
it 'uses the access token' do
|
|
15
|
+
expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?access_token=fake-access-token').and_return(response)
|
|
16
|
+
AliExpress::Base.get(api_call: 'foo', sign: false, auth: true)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'when sign is true' do
|
|
21
|
+
it 'signs the request' do
|
|
22
|
+
expect(RestClient).to receive(:get).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id?_aop_signature=8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
|
|
23
|
+
AliExpress::Base.get(api_call: 'foo', sign: true, auth: false)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context 'when bogus params are used' do
|
|
28
|
+
it 'raises an error' do
|
|
29
|
+
expect { AliExpress::Base.get(api_call: 'foo', foo: 'bar') }.to raise_error(ArgumentError)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '.post' do
|
|
35
|
+
context 'when auth is true' do
|
|
36
|
+
it 'uses the access token' do
|
|
37
|
+
expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', access_token: 'fake-access-token').and_return(response)
|
|
38
|
+
AliExpress::Base.post(api_call: 'foo', sign: false, auth: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context 'when sign is true' do
|
|
43
|
+
it 'signs the request' do
|
|
44
|
+
expect(RestClient).to receive(:post).exactly(1).times.with('https://gw.api.alibaba.com/openapi/param2/1/aliexpress.open/foo/fake-client-id', _aop_signature: '8F19FBA00C4BF35E43FA9ED143639337047624C0').and_return(response)
|
|
45
|
+
AliExpress::Base.post(api_call: 'foo', sign: true, auth: false)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
RSpec.configure do |config|
|
|
2
|
+
config.before(:each) do
|
|
3
|
+
AliExpress.client_id = 'fake-client-id'
|
|
4
|
+
AliExpress.client_secret = 'fake-client-secret'
|
|
5
|
+
AliExpress.access_token = 'fake-access-token'
|
|
6
|
+
AliExpress.refresh_token = 'fake-refresh-token'
|
|
7
|
+
|
|
8
|
+
%i(protocol host base_uri currency language logger).each do |method|
|
|
9
|
+
AliExpress.send("#{method}=", nil)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aliexpress
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: '0.4'
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chris Estreich
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-10-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rest-client
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: addressable
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: oauth2
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: AliExpress SDK for ruby.
|
|
56
|
+
email:
|
|
57
|
+
- chris@tophatter.com
|
|
58
|
+
executables:
|
|
59
|
+
- console
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files:
|
|
62
|
+
- README.md
|
|
63
|
+
files:
|
|
64
|
+
- ".coveralls.yml"
|
|
65
|
+
- ".gitignore"
|
|
66
|
+
- ".rspec"
|
|
67
|
+
- ".rubocop.yml"
|
|
68
|
+
- ".travis.yml"
|
|
69
|
+
- Gemfile
|
|
70
|
+
- LICENSE.md
|
|
71
|
+
- README.md
|
|
72
|
+
- Rakefile
|
|
73
|
+
- aliexpress.gemspec
|
|
74
|
+
- bin/console
|
|
75
|
+
- lib/aliexpress.rb
|
|
76
|
+
- lib/aliexpress/affiliate.rb
|
|
77
|
+
- lib/aliexpress/auth.rb
|
|
78
|
+
- lib/aliexpress/base.rb
|
|
79
|
+
- lib/aliexpress/product.rb
|
|
80
|
+
- lib/aliexpress/response_error.rb
|
|
81
|
+
- spec/affiliate_spec.rb
|
|
82
|
+
- spec/aliexpress_spec.rb
|
|
83
|
+
- spec/auth_spec.rb
|
|
84
|
+
- spec/base_spec.rb
|
|
85
|
+
- spec/spec_helper.rb
|
|
86
|
+
- spec/support/auth.rb
|
|
87
|
+
- spec/support/webmock.rb
|
|
88
|
+
homepage: https://github.com/tophatter/aliexpress-api-ruby
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata: {}
|
|
92
|
+
post_install_message: 'Documentation is available at: http://isvus.aliexpress.com/isv/index.htm'
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '2.0'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubyforge_project:
|
|
108
|
+
rubygems_version: 2.6.12
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: AliExpress SDK for ruby.
|
|
112
|
+
test_files:
|
|
113
|
+
- spec/affiliate_spec.rb
|
|
114
|
+
- spec/aliexpress_spec.rb
|
|
115
|
+
- spec/auth_spec.rb
|
|
116
|
+
- spec/base_spec.rb
|
|
117
|
+
- spec/spec_helper.rb
|
|
118
|
+
- spec/support/auth.rb
|
|
119
|
+
- spec/support/webmock.rb
|