├── .babelrc
├── .eslintrc
├── .gitignore
├── .npm
└── scripts
│ ├── build
│ ├── postinstall
│ └── test
├── .npmignore
├── .vimrc
├── LICENSE
├── README.md
├── package.json
├── src
├── exceptions.js
├── index.js
├── lang.js
├── models.js
├── querystring.js
├── rest.js
├── sort.js
├── string.js
└── xml.js
└── tests
├── bootstrap.js
├── fixtures
├── combination-47.xml
├── manufacturer-1.xml
├── manufacturers.xml
├── product-10.xml
├── product-8-images.xml
├── product-8.xml
├── product-9.xml
├── product-option-values-25.xml
├── products.xml
├── stock-available-80.xml
├── stock-availables.xml
└── valid.xml
├── functional
└── .gitignore
└── unit
├── exceptions.spec.js
├── lang.spec.js
├── models
├── Model.spec.js
└── Product.spec.js
├── querystring.spec.js
├── rest
├── Client.spec.js
└── resources
│ ├── Images.spec.js
│ ├── Manufacturers.spec.js
│ ├── Products.spec.js
│ └── Resource.spec.js
├── sort.spec.js
└── xml.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"],
3 | "plugins": [
4 | "add-module-exports"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "strict": 0
5 | },
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "allowImportExportEverywhere": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 |
4 | /babel
5 | /eslint
6 | /istanbul
7 | /mocha
8 | /nodemon
9 |
10 | npm-debug.log
11 |
12 | *.swp
13 | *.swo
14 | nohup.*
15 |
--------------------------------------------------------------------------------
/.npm/scripts/build:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
5 | PROJECT_ROOT="$SCRIPT_DIR/../.."
6 |
7 | cd $PROJECT_ROOT && ./babel src --out-dir dist
8 |
--------------------------------------------------------------------------------
/.npm/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
5 | PROJECT_ROOT="$SCRIPT_DIR/../.."
6 |
7 | BINARIES=(babel eslint istanbul mocha nodemon)
8 |
9 | for binary in ${BINARIES[@]}; do
10 | if [ ! -L "$PROJECT_ROOT/$binary" ]; then
11 | $(cd "$PROJECT_ROOT" && ln -s "node_modules/.bin/$binary")
12 | fi
13 | done
14 |
--------------------------------------------------------------------------------
/.npm/scripts/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
5 | PROJECT_ROOT="$SCRIPT_DIR/../.."
6 |
7 | cd "$PROJECT_ROOT"
8 |
9 | NODE_ENV=test ./node_modules/.bin/_mocha \
10 | --compilers js:babel-core/register \
11 | --timeout 15000 \
12 | -r tests/bootstrap.js \
13 | --recursive \
14 | tests/unit \
15 | tests/functional
16 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itconsultis/prestashop-api-client/787ff125506600bb719c239f46de16ac974e3967/.npmignore
--------------------------------------------------------------------------------
/.vimrc:
--------------------------------------------------------------------------------
1 | sy on
2 | set modeline
3 | set backspace=indent,eol,start
4 | set ts=2
5 | set sw=2
6 | set sts=2
7 | set et
8 | set nowrap
9 | set ruler
10 | set smartindent
11 | set wildmode=longest,list
12 | set wildmenu
13 | set incsearch
14 | set cursorline
15 |
16 | hi CursorLine term=bold cterm=bold guibg=Grey40
17 |
18 | au FileType javascript setlocal ts=2 sw=2 sts=2 et
19 | au FileType yaml setlocal ts=2 sw=2 sts=2 et
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 by IT Consultis
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PrestaShop API Client
2 |
3 | This is a server-side library that exposes the PrestaShop web service
4 | as resource and model abstractions. It allows web service consumers to be
5 | unaware of the web service's HTTP interface, or how to deal with XML payloads.
6 |
7 | ### Basic usage
8 |
9 | Create a client instance
10 |
11 | ```javascript
12 | import { rest } from 'prestashop-api-client';
13 |
14 | const client = new rest.Client({
15 | language: 'en',
16 | languages: {
17 | 'en': 1,
18 | 'es': 2,
19 | },
20 | webservice: {
21 | key: 'YOUR_PRESTASHOP_API_KEY',
22 | scheme: 'https',
23 | host: 'your-prestashop-domain',
24 | root: '/api',
25 | },
26 | });
27 | ```
28 |
29 | Change the current client language
30 | ```javascript
31 | client.setLanguageIso('en');
32 | ```
33 |
34 | Access a resource
35 | ```javascript
36 | const resource = client.resource('products');
37 | ```
38 |
39 | Retrieve all models exposed by a resource
40 |
41 | ```javascript
42 | resource.list().then((models) => {
43 | // models is an Array containing Model instances
44 | });
45 | ```
46 |
47 | Retrieve a single model from a resource by its ID
48 | ```javascript
49 | resource.get(id).then((model) => {
50 | // model is a Model instance
51 | });
52 | ```
53 |
54 |
55 | ### Resources
56 |
57 | - products
58 | - images
59 | - manufacturers
60 | - combinations
61 | - stock_availables
62 | - product_option_values
63 |
64 | ### Models
65 |
66 | - Product
67 | - Image
68 | - Manufacturer
69 | - Combination
70 | - StockAvailable
71 | - ProductOptionValue
72 |
73 |
74 | ### Model relations
75 |
76 | Some concrete Models implement methods that return a Resource. When a Model
77 | returns a Resource this way, that resource acts on objects that are
78 | *related to that model*.
79 |
80 |
81 | `Product`
82 |
83 | ```javascript
84 | // get an Images resource that exposes Image models related to the Product
85 | product.images()
86 |
87 | // get Image models related to the Product
88 | product.images().list()
89 |
90 | // get a Manufacturers resource
91 | product.manufacturer()
92 |
93 | // get the related Manufacturer model
94 | product.manufacturer().first()
95 |
96 | // get a Combinations resource
97 | product.combinations()
98 |
99 | // get Combination models related to the Product
100 | product.combinations().list()
101 | ```
102 |
103 | ### Client options
104 |
105 | `languages`
106 |
107 | A dictionary that maps ISO-639-1 language codes to PrestaShop language ids.
108 |
109 | `language`
110 |
111 | The language to select when parsing translatable attributes.
112 |
113 | `webservice`
114 |
115 | HTTP request parameters
116 |
117 | key HTTP Basic Auth username
118 | scheme defaults to "https"
119 | host your PrestaShop host
120 | root the api root path; defaults to "/api"
121 |
122 | `logger`
123 |
124 | A logger instance. Defaults to dummy logger that doesn't log anything.
125 |
126 |
127 | ### Contributing
128 |
129 | 1. Fork `develop` branch
130 | 2. Push changes to your fork.
131 | 3. Submit a pull request.
132 |
133 |
134 | ### License
135 |
136 | MIT
137 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prestashop-api-client",
3 | "version": "0.3.11",
4 | "description": "PrestaShop API client",
5 | "homepage": "https://github.com/itconsultis/prestashop-api-client",
6 | "bugs": "https://github.com/itconsultis/prestashop-api-client/issues",
7 | "keywords": [
8 | "prestashop"
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "git@github.com:itconsultis/prestashop-api-client.git"
13 | },
14 | "main": "./dist/index.js",
15 | "scripts": {
16 | "postinstall": ".npm/scripts/postinstall",
17 | "test": ".npm/scripts/test",
18 | "coverage": "./istanbul cover npm test",
19 | "start": "./nodemon --ignore ./dist -x 'npm test || true'",
20 | "prepublish": "npm config set registry 'https://registry.npmjs.org' && npm test && .npm/scripts/build",
21 | "postpublish": "git checkout develop"
22 | },
23 | "author": "IT Consultis parseInt(v, 10);
8 | coerce.string = String;
9 | coerce.bool = (v) => !empty(coerce.integer(v));
10 | coerce.number = Number;
11 |
12 |
13 | /**
14 | * Given an Object or Map, return a list of two-element tuples. Each tuple is
15 | * an Array that contains a key at index 0, and the corresponding value at
16 | * index 1.
17 | * @param {Object|Map} input
18 | * @return {Array}
19 | */
20 | export const tuples = lang.tuples = (input) => {
21 | let output = [];
22 |
23 | if (input instanceof Map) {
24 | for (let [k, v] of input.entries()) {
25 | output.push([k, v]);
26 | }
27 | }
28 | else {
29 | for (let prop in input) {
30 | if (input.hasOwnProperty(prop)) {
31 | output.push([prop, input[prop]]);
32 | }
33 | }
34 | }
35 |
36 | return output;
37 | };
38 |
39 |
40 | /**
41 | * This works like PHP's empty() function. Behaviors:
42 | * - return true if value is falsy
43 | * - return true if the value is the integer zero
44 | * - return true if the value has a length property that is zero
45 | * - return true if the value is an object that does not own any properties
46 | * - return false for everything else
47 | * @param mixed value
48 | * @return {Boolean}
49 | */
50 | export const empty = lang.empty = (value) => {
51 | if (value === undefined) {
52 | return true;
53 | }
54 |
55 | if (typeof value !== 'object' && isNaN(value)) {
56 | return false;
57 | }
58 |
59 | // falsy value is always empty
60 | if (!value) {
61 | return true;
62 | }
63 |
64 | // number zero is empty
65 | if (typeof value === 'number') {
66 | return value === 0;
67 | }
68 |
69 | // zero-length anything is empty
70 | if (typeof value.length === 'number') {
71 | return !value.length;
72 | }
73 |
74 | // zero-size anything is empty
75 | if (typeof value.size === 'number') {
76 | return !value.size;
77 | }
78 |
79 | // an object that does not own any properties is empty
80 | if (typeof value === 'object') {
81 | return Object.getOwnPropertyNames(value).length === 0;
82 | }
83 |
84 | // default to false
85 | return false;
86 | };
87 |
88 |
--------------------------------------------------------------------------------
/src/models.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import lang from './lang';
3 | import querystring from './querystring';
4 | import { resources } from './rest';
5 | import { each, merge } from 'lodash';
6 | import { NotImplemented } from './exceptions';
7 | import sort from './sort';
8 |
9 | const P = Promise;
10 | const { bool, integer, number, string } = lang.coerce;
11 |
12 | ////////////////////////////////////////////////////////////////////////////////
13 |
14 | const models = {};
15 |
16 | export default models;
17 |
18 | export const Model = models.Model = class {
19 |
20 | /**
21 | * Return default model attributes
22 | * @param void
23 | * @return {Object}
24 | */
25 | defaults () {
26 | return {
27 | // the resource that created the model
28 | resource: null,
29 |
30 | // rest.Client instance
31 | client: null,
32 |
33 | // model attributes
34 | attrs: {
35 | related: {},
36 | },
37 | };
38 | }
39 |
40 | /**
41 | * Define attribute mutators
42 | * @param void
43 | * @return {Object}
44 | */
45 | mutators () {
46 | return {
47 | // attribute: [set-mutator, get-mutator],
48 | id: [integer],
49 | quantity: [integer],
50 | position: [integer],
51 | };
52 | }
53 |
54 | /**
55 | * @param {Object} attrs - initial model attributes
56 | */
57 | constructor (options={}) {
58 | this.options = merge(this.defaults(), options);
59 | this.attrs = {};
60 | this.set(this.options.attrs);
61 | delete this.options.attrs;
62 | }
63 |
64 | /**
65 | * Assign a single attribute or mass-assign multiple attribute. Map
66 | * attribute values through the dictionary returned by mutators().
67 | * @param ...mixed args
68 | * @return void
69 | */
70 | set (...args) {
71 | let arity = args.length;
72 | let mutators = this.mutators();
73 | let defaults = [v=>v];
74 |
75 | let assign = (value, attr) => {
76 | let [set] = mutators[attr] || defaults;
77 | this.attrs[attr] = set(value);
78 | };
79 |
80 | if (arity < 1) {
81 | throw new Error('expected at least one argument');
82 | }
83 | if (arity > 2) {
84 | throw new Error('expected no more than two arguments');
85 | }
86 |
87 | if (arity === 1) {
88 | each(args[0], assign);
89 | return;
90 | }
91 |
92 | let [attr, value] = args;
93 | assign(value, attr);
94 | }
95 |
96 | /**
97 | * @param void
98 | * @return {Object}
99 | */
100 | attributes () {
101 | return {...this.attrs};
102 | }
103 |
104 | /**
105 | * @param void
106 | * @return {Object}
107 | */
108 | toJSON () {
109 | return this.attributes();
110 | }
111 |
112 | }
113 |
114 | export const Language = models.Language = class extends Model {
115 | // implement me
116 | }
117 |
118 | export const Product = models.Product = class extends Model {
119 |
120 | /**
121 | * @inheritdoc
122 | */
123 | mutators () {
124 | return {
125 | ...super.mutators(),
126 | price: [number],
127 | available_for_order: [integer, bool],
128 | };
129 | }
130 |
131 | /**
132 | * Return a rest.Resource that provides access to the related Manufacturer
133 | * @param void
134 | * @return {rest.resources.Combinations}
135 | */
136 | manufacturer () {
137 | let related = this.attrs.related;
138 |
139 | return new resources.Manufacturers({
140 | client: this.options.client,
141 | filter: (manufacturer) => related.manufacturer == manufacturer.attrs.id,
142 | });
143 | }
144 |
145 | /**
146 | * Return a rest.Resource that provides access to related Images
147 | * @param void
148 | * @return {rest.resources.Images}
149 | */
150 | images () {
151 | return new resources.Images({
152 | client: this.options.client,
153 | root: `/images/products/${this.attrs.id}`,
154 | });
155 | }
156 |
157 | /**
158 | * Return a rest.Resource that provides access to related Combinations
159 | * @param void
160 | * @return {rest.resources.Combinations}
161 | */
162 | combinations () {
163 | let related = this.attrs.related;
164 |
165 | return new resources.Combinations({
166 | client: this.options.client,
167 | filter: (combo) => related.combinations.indexOf(combo.attrs.id) > -1,
168 | });
169 | }
170 | }
171 |
172 | export const Image = models.Image = class extends Model {
173 |
174 | /**
175 | * Return a Buffer that contains the raw image
176 | * @async Promise
177 | * @param void
178 | * @return {Buffer}
179 | */
180 | load () {
181 | if (this.loading) {
182 | return P.resolve(this.loading);
183 | }
184 |
185 | let {client} = this.options;
186 | let attrs = this.attrs;
187 |
188 | this.loading = client.get(attrs.src)
189 |
190 | .then((response) => {
191 | response = response.clone();
192 | return P.all([P.resolve(response), response.clone().buffer()]);
193 | })
194 |
195 | .then((result) => {
196 | let [response, buffer] = result;
197 | // TODO inspect the Content-Type response header and set "type" attribute
198 | return buffer;
199 | })
200 |
201 | return this.loading;
202 | }
203 |
204 | }
205 |
206 | export const Combination = models.Combination = class extends Model {
207 |
208 | /**
209 | * Define property mutators
210 | * @param void
211 | * @return {Object}
212 | */
213 | mutators () {
214 | return {
215 | // property: [set-mutator, get-mutator],
216 | ...super.mutators(),
217 | id_product: [integer],
218 | quantity: [integer],
219 | price: [number],
220 | ecotax: [number],
221 | weight: [number],
222 | unit_price_impact: [number],
223 | minimal_quantity: [number],
224 | };
225 | }
226 |
227 | /**
228 | * Return a rest.Resource that provides access to the parent Product
229 | * models.
230 | * @param void
231 | * @return {rest.resources.Products}
232 | */
233 | product () {
234 | return new resources.Product({
235 | client: this.options.client,
236 | filter: (product) => this.attrs.related.product == product.attrs.id,
237 | });
238 | }
239 |
240 | /**
241 | * Return a rest.Resource that provides access to related ProductOptionValue
242 | * models.
243 | * @param void
244 | * @return {rest.resources.ProductOptionvalues}
245 | */
246 | product_option_values () {
247 | return new resources.ProductOptionValues({
248 | client: this.options.client,
249 | filter: (pov) => {
250 | return this.attrs.related.product_option_values == pov.attrs.id;
251 | },
252 | });
253 | }
254 |
255 | /**
256 | * Return a rest.Resource that provides access to related StockAvailable
257 | * models.
258 | * @param void
259 | * @return {rest.resources.ProductOptionvalues}
260 | */
261 | stock_availables () {
262 | return new resources.StockAvailables({
263 | client: this.options.client,
264 | filter: (stock) => {
265 | return this.attrs.id == stock.attrs.id_product_attribute;
266 | },
267 | });
268 | }
269 | }
270 |
271 | export const StockAvailable = models.StockAvailable = class extends Model {
272 |
273 | /**
274 | * @inheritdoc
275 | */
276 | mutators () {
277 | return {
278 | ...super.mutators(),
279 | id_product: [integer],
280 | id_product_attribute: [integer],
281 | id_shop: [integer],
282 | id_shop_group: [integer],
283 | quantity: [integer],
284 | depends_on_stock: [integer],
285 | out_of_stock: [integer],
286 | };
287 | }
288 | }
289 |
290 | export const Manufacturer = models.Manufacturer = class extends Model {
291 | // implement me
292 | }
293 |
294 | export const ProductOptionValue = models.ProductOptionValue = class extends Model {
295 | // implement me
296 | }
297 |
298 |
--------------------------------------------------------------------------------
/src/querystring.js:
--------------------------------------------------------------------------------
1 | import { map } from 'lodash';
2 |
3 | ////////////////////////////////////////////////////////////////////////////////
4 | const querystring = {};
5 |
6 | export default querystring;
7 |
8 | /**
9 | * Stringify a query per RFC 3986
10 | * @usage
11 | *
12 | * // serialize a dictionary
13 | * stringify({foo: 1, bar: 2})
14 | * >>> foo=1&bar=2
15 | *
16 | * // serialize an array of key-value tuples
17 | * stringify([['foo', 1], ['bar', 2]])
18 | * >>> foo=1&bar=2
19 | *
20 | * @param {Object|Array} query - a dictionary or an array of key-value tuples
21 | * @return {String}
22 | */
23 | export const stringify = querystring.stringify = (query) => {
24 | let encode = encodeURIComponent;
25 | let serialize = (k, v) => `${encode(k)}=${encode(v)}`;
26 |
27 | if (Array.isArray(query)) {
28 | return map(query, (tuple) => serialize(...tuple)).join('&');
29 | }
30 |
31 | return map(query, (v, k) => serialize(k, v)).join('&');
32 | };
33 |
--------------------------------------------------------------------------------
/src/rest.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import querystring from './querystring';
3 | import { each, merge } from 'lodash';
4 | import { NotImplemented, InvalidArgument, UnexpectedValue } from './exceptions';
5 | import { parse } from './xml';
6 | import { empty, tuples, coerce } from './lang';
7 | import models from './models';
8 | import sort from './sort';
9 | import string from './string';
10 | import fetch from 'node-fetch';
11 |
12 | const { integer } = coerce;
13 | const P = Promise;
14 | const noop = () => {};
15 | const dummylogger = {log: noop, info: noop};
16 |
17 | ////////////////////////////////////////////////////////////////////////////////
18 |
19 | /**
20 | * HTTP client
21 | */
22 | export const Client = class {
23 |
24 | /**
25 | * Return instance configuration defaults
26 | * @param void
27 | * @return {Object}
28 | */
29 | defaults () {
30 | let location = global.location || {
31 | protocol: 'https:',
32 | host: 'localhost',
33 | };
34 |
35 | return {
36 | language: 'en',
37 |
38 | // these must match your PrestaShop backend languages
39 | languages: {
40 | // ISO code => PrestaShop language id
41 | 'en': 1,
42 | },
43 |
44 | // PrestaShop web service parameters
45 | webservice: {
46 | key: 'your-prestashop-key',
47 | scheme: 'https',
48 | host: 'your-prestashop-host',
49 | root: '/api',
50 | },
51 |
52 | // logger
53 | logger: dummylogger,
54 |
55 | // Fetch-related options
56 | fetch: {
57 |
58 | // the actual fetch function; facilitates testing
59 | algo: fetch,
60 | },
61 | };
62 | }
63 |
64 | /**
65 | * @param {Object} options
66 | */
67 | constructor (options={}) {
68 | this.options = merge(this.defaults(), options);
69 | this.fetch = this.options.fetch.algo;
70 | this.logger = this.options.logger;
71 | this.funnel = {};
72 | }
73 |
74 | /**
75 | * Return a Prestashop API language id
76 | * @property
77 | * @type {Number}
78 | */
79 | get language () {
80 | let {languages, language: isocode} = this.options;
81 | return languages[isocode];
82 | }
83 |
84 | /**
85 | * Set the current client language ISO code
86 | * @param {String} iso
87 | * @return void
88 | */
89 | setLanguageIso (iso) {
90 | let {languages} = this.options;
91 |
92 | if (!languages.hasOwnProperty(iso)) {
93 | throw new Error(`language "${iso}" is not configured`);
94 | }
95 |
96 | this.options.language = iso;
97 | }
98 |
99 | /**
100 | * Return the current client language ISO code
101 | * @param void
102 | * @return {String}
103 | */
104 | getLanguageIso () {
105 | return this.options.language;
106 | }
107 |
108 | /**
109 | * Send a GET request
110 | * @async Promise
111 | * @param {String} url
112 | * @param {Object} query
113 | * @return {Response}
114 | */
115 | get (uri, options={}) {
116 | let url = this.url(uri, options.query);
117 | let key = `${this.options.language}:GET:${url}`;
118 | let funnel = this.funnel;
119 |
120 | // concurrent requests on the same url converge on a single promise
121 | if (funnel[key]) {
122 | return funnel[key];
123 | }
124 |
125 | let fopts = this.createFetchOptions({...options.fetch, method: 'GET'});
126 |
127 | funnel[key] = this.fetch(url, fopts)
128 |
129 | .then((response) => {
130 | delete funnel[key];
131 | this.validateResponse(response);
132 | return response;
133 | })
134 |
135 | .catch((e) => {
136 | delete funnel[key];
137 | throw e;
138 | })
139 |
140 | return funnel[key];
141 | }
142 |
143 | /**
144 | * Return a fully qualified API url
145 | * @param {String} uri
146 | * @param {String}
147 | */
148 | url (uri, query={}) {
149 | let qs = '';
150 |
151 | if (!empty(query)) {
152 | query = tuples(query).sort();
153 | qs = '?' + querystring.stringify(query);
154 | }
155 |
156 | if (uri.indexOf('http') === 0) {
157 | return uri + qs;
158 | }
159 |
160 | let {webservice} = this.options;
161 | let fullpath = path.join(webservice.root, uri);
162 |
163 | return `${webservice.scheme}://${webservice.host}${fullpath}${qs}`;
164 | }
165 |
166 | /**
167 | * Return node-fetch request options
168 | * @param {Object} augments
169 | * @return {Object}
170 | */
171 | createFetchOptions (augments={}) {
172 | return {
173 | ...this.options.fetch.defaults,
174 | headers: this.createHeaders(),
175 | ...augments,
176 | };
177 | }
178 |
179 | /**
180 | * Return a dictionary containing request headers
181 | * @param {Object} augments
182 | * @return {Object}
183 | */
184 | createHeaders (augments={}) {
185 | let {key} = {...this.options.webservice};
186 | let Authorization = this.createAuthorizationHeader(key);
187 |
188 | return {...augments, Authorization};
189 | }
190 |
191 | /**
192 | * @param {Response} response
193 | * @return void
194 | * @throws Error
195 | */
196 | validateResponse (response) {
197 | if (!response.ok) {
198 | throw new UnexpectedValue('got non-2XX HTTP response');
199 | }
200 | }
201 |
202 | /**
203 | * Return an Authorization header value given a web service key
204 | * @param {String} key
205 | * @return {String}
206 | */
207 | createAuthorizationHeader (key) {
208 | let username = key;
209 | let password = '';
210 | let precursor = `${username}:${password}`;
211 | let credentials = Buffer.from(precursor).toString('base64');
212 |
213 | return `Basic ${credentials}`;
214 | }
215 |
216 | /**
217 | * @param {String} api - snake case form of a resource class name
218 | * @param {Object} options
219 | * @return {rest.Resource}
220 | */
221 | resource (api, options={}) {
222 | let classname = string.studly(api);
223 | let constructor = resources[classname];
224 |
225 | if (!constructor) {
226 | throw new InvalidArgument(`invalid root resource: "${api}"`);
227 | }
228 |
229 | return new constructor({
230 | ...options,
231 | client: this,
232 | logger: this.logger,
233 | });
234 | }
235 |
236 | }
237 |
238 | ////////////////////////////////////////////////////////////////////////////////
239 | export const resources = {};
240 |
241 | /**
242 | * Resource is an HTTP-aware context that uses a Client to fetch XML payloads
243 | * from the PrestaShop API. It converts XML payloads into Model instances.
244 | */
245 | export const Resource = resources.Resource = class {
246 |
247 | /**
248 | * Return the canonical constructor name. This is a workaround to a side
249 | * effect of Babel transpilation, which is that Babel mangles class names.
250 | * @property
251 | * @type {String}
252 | */
253 | static get name () {
254 | return 'Resource';
255 | }
256 |
257 | /**
258 | * Return instance configuration defaults
259 | * @param void
260 | * @return {Object}
261 | */
262 | defaults () {
263 | let classname = this.constructor.name;
264 | let api = string.snake(classname);
265 | let nodetype = api.slice(0, -1);
266 | let root = `/${api}`;
267 | let modelname = classname.slice(0, -1);
268 |
269 | return {
270 | client: null,
271 | logger: dummylogger,
272 | model: models[modelname],
273 | root: root,
274 | api: api,
275 | nodetype: nodetype,
276 |
277 | // model list filter function
278 | filter: null,
279 |
280 | // model list sort function
281 | sort: null,
282 | };
283 | }
284 |
285 | /**
286 | * @param {Object} options
287 | */
288 | constructor (options={}) {
289 | this.options = {...this.defaults(), ...options};
290 | this.client = this.options.client;
291 | this.logger = this.options.logger;
292 | }
293 |
294 | /**
295 | * Return a PrestaShop language id
296 | * @property
297 | * @type {Number}
298 | */
299 | get language () {
300 | return this.client.language;
301 | }
302 |
303 | /**
304 | * Resolve an array of Model instances
305 | * @async Promise
306 | * @param void
307 | * @return {Array}
308 | */
309 | list () {
310 | return this.client.get(this.options.root)
311 |
312 | .then((response) => response.clone().text())
313 | .then((xml) => this.parseModelIds(xml))
314 | .then((ids) => this.createModels(ids))
315 | .then((models) => {
316 | let {sort, filter} = this.options;
317 |
318 | if (filter) {
319 | models = models.filter(filter);
320 | }
321 | if (sort) {
322 | models = models.sort(sort);
323 | }
324 |
325 | return models;
326 | })
327 | }
328 |
329 | /**
330 | * Return the first member of a list() result
331 | * @param void
332 | * @return {models.Model}
333 | */
334 | first () {
335 | return this.list().then(models => models.shift() || null);
336 | }
337 |
338 | /**
339 | * Resolve a single Model instance, or null if there was an error of any kind
340 | * @async Promise
341 | * @param {mixed} id
342 | * @return {Model|null}
343 | */
344 | get (id) {
345 | let uri = `${this.options.root}/${id}`;
346 | let promise;
347 |
348 | try {
349 | promise = this.client.get(uri)
350 | }
351 | catch (e) {
352 | this.logger.log(`failed to acquire model properties on request path ${uri}`);
353 | this.logger.log(e.message);
354 | this.logger.log(e.stack);
355 |
356 | // FIXME
357 | return P.resolve(this.createModel());
358 | }
359 |
360 | return promise.then((response) => response.clone().text())
361 | .then((xml) => this.parseModelAttributes(xml))
362 | .then((attrs) => this.createModel(attrs))
363 | }
364 |
365 | /**
366 | * Map a list of model ids to model instances.
367 | * @async Promise
368 | * @param {Array} ids
369 | * @return {Array}
370 | */
371 | createModels (ids) {
372 | return P.all(ids.map((id) => this.get(id)));
373 | }
374 |
375 | /**
376 | * Given an object containing model properties, return a Model instance.
377 | * @param {Object} attrs
378 | * @return {Model}
379 | */
380 | createModel (attrs={}) {
381 | let {model: constructor} = this.options;
382 |
383 | return new constructor({
384 | client: this.client,
385 | resource: this,
386 | attrs: attrs,
387 | });
388 | }
389 |
390 | /**
391 | * Given the API response payload for a list of objects, return a list of
392 | * model ids.
393 | * @async Promise
394 | * @param {String} xml
395 | * @return {Array}
396 | */
397 | parseModelIds (xml) {
398 | let {api, nodetype} = this.options;
399 | return parse.model.ids(xml, api, nodetype);
400 | }
401 |
402 | /**
403 | * Given the API response payload for a single domain object, return a plain
404 | * object that contains model properties.
405 | * @param {String} xml
406 | * @return {Object}
407 | */
408 | parseModelAttributes (xml) {
409 | let nodetype = this.options.nodetype;
410 | let ns = parse[nodetype];
411 |
412 | if (!ns) {
413 | throw new UnexpectedValue(`parser namespace not found on node type ${nodetype}`);
414 | }
415 | if (!ns.attributes) {
416 | throw new UnexpectedValue(`model properties parser not found on node type ${nodetype}`);
417 | }
418 |
419 | return parse[nodetype].attributes(xml, this.language);
420 | }
421 |
422 | }
423 |
424 | resources.Products = class extends Resource {
425 |
426 | /**
427 | * inheritdoc
428 | */
429 | static get name () {
430 | return 'Products';
431 | }
432 | }
433 |
434 | resources.Images = class extends Resource {
435 |
436 | static get name () {
437 | return 'Images';
438 | }
439 |
440 | /**
441 | * @inheritdoc
442 | */
443 | list () {
444 | return this.client.get(this.options.root)
445 | .then((response) => response.clone().text())
446 | .then((xml) => this.parseImageAttributes(xml))
447 | .then((attrsets) => attrsets.map((attrs) => this.createModel(attrs)))
448 | }
449 |
450 | /**
451 | * Return a list of urls given an XML payload
452 | * @async Promise
453 | * @param {String} xml
454 | * @return {Array}
455 | */
456 | parseImageAttributes (xml) {
457 | return parse.image.attributes(xml);
458 | }
459 | }
460 |
461 |
462 | resources.Manufacturers = class extends Resource {
463 |
464 | /**
465 | * @inheritdoc
466 | */
467 | static get name () {
468 | return 'Manufacturers';
469 | }
470 | }
471 |
472 | resources.Combinations = class extends Resource {
473 |
474 | /**
475 | * @inheritdoc
476 | */
477 | static get name () {
478 | return 'Combinations';
479 | }
480 | }
481 |
482 | resources.StockAvailables = class extends Resource {
483 |
484 | /**
485 | * @inheritdoc
486 | */
487 | static get name () {
488 | return 'StockAvailables';
489 | }
490 | }
491 |
492 | resources.ProductOptionValues = class extends Resource {
493 |
494 | /**
495 | * inheritdoc
496 | */
497 | static get name () {
498 | return 'ProductOptionValues';
499 | }
500 |
501 | /**
502 | * @inheritdoc
503 | */
504 | defaults () {
505 | return {
506 | ...super.defaults(),
507 | sort: sort.ascending(model => model.position),
508 | };
509 | }
510 | }
511 |
512 |
513 | export default { Client, resources };
514 |
--------------------------------------------------------------------------------
/src/sort.js:
--------------------------------------------------------------------------------
1 | const sort = {};
2 |
3 | /**
4 | * Return a sort comparator given a function that returns a sortable value.
5 | * @param {Function} resolve
6 | * @return {Function}
7 | */
8 | export const ascending = sort.ascending = (resolve) => {
9 | return (a, b) => resolve(a) - resolve(b);
10 | };
11 |
12 | /**
13 | * Return a sort comparator given a function that returns a sortable value.
14 | * @param {Function} resolve
15 | * @return {Function}
16 | */
17 | export const descending = sort.descending = (resolve) => {
18 | return (a, b) => resolve(b) - resolve(a);
19 | };
20 |
21 | export default sort;
22 |
--------------------------------------------------------------------------------
/src/string.js:
--------------------------------------------------------------------------------
1 | import inflector from 'i';
2 | const inflect = inflector();
3 |
4 | const string = {};
5 |
6 | export default string;
7 |
8 | string.snake = (...args) => inflect.underscore(...args);
9 | string.studly = (...args) => inflect.camelize(...args);
10 |
--------------------------------------------------------------------------------
/src/xml.js:
--------------------------------------------------------------------------------
1 | import xml2js from 'xml2js';
2 | import { coerce } from './lang';
3 | const { bool, number, integer, string } = coerce;
4 | const P = Promise;
5 |
6 | ////////////////////////////////////////////////////////////////////////////////
7 | const xml = {};
8 |
9 | export default xml;
10 |
11 | /**
12 | * Return a plain object representation of the supplied XML payload
13 | * @async Promise
14 | * @param {String|Buffer} xml
15 | * @return {Object}
16 | */
17 | export const parse = xml.parse = (xml) => {
18 | return new P((resolve, reject) => {
19 | xml2js.parseString(String(xml), (err, result) => {
20 | err ? reject(err) : resolve(result);
21 | });
22 | })
23 | };
24 |
25 | /**
26 | * Extract the text value from a node
27 | * @param mixed input
28 | * @return {String}
29 | */
30 | const text = (input) => {
31 | let raw = Array.isArray(input) ? input[0] : input;
32 |
33 | if (typeof raw == 'object') {
34 | raw = raw._ || '';
35 | }
36 |
37 | return String(raw).trim();
38 | };
39 |
40 | /**
41 | * Extract a node attribute value
42 | * @param {mixed} input
43 | * @return {String}
44 | */
45 | const attr = (input, name) => {
46 | let raw = input;
47 |
48 | if (Array.isArray(raw)) {
49 | raw = raw[0];
50 | }
51 |
52 | if (raw.$ && raw.$[name] !== undefined) {
53 | raw = raw.$[name];
54 | }
55 |
56 | return String(raw).trim();
57 | };
58 |
59 | ////////////////////////////////////////////////////////////////////////////////
60 |
61 | parse.model = {};
62 |
63 | /**
64 | * Parse the supplied XML payload and return an array of model ids.
65 | * @async Promise
66 | * @param {String|Buffer} xml
67 | * @param {String} api
68 | * @param {String} nodetype
69 | * @return {Array}
70 | */
71 | parse.model.ids = (xml, api, nodetype) => {
72 | return parse(xml)
73 |
74 | .then((obj) => {
75 | let list = obj.prestashop[api][0][nodetype];
76 | return list.map((obj) => integer(attr(obj, 'id')));
77 | });
78 | };
79 |
80 | ////////////////////////////////////////////////////////////////////////////////
81 |
82 | parse.product = {};
83 |
84 | /**
85 | * @async Promise
86 | * @param {String} xml
87 | * @return {Object}
88 | */
89 | parse.product.attributes = (xml, language=1) => {
90 | return parse(xml)
91 |
92 | .then((obj) => {
93 | let base = obj.prestashop.product[0];
94 | let assocs = base.associations[0] || {};
95 | let lang = (obj) => attr(obj, 'id') == language;
96 |
97 | let names = base.name[0].language;
98 | let descs = base.description[0].language;
99 | let shortdescs = base.description_short[0].language;
100 | let combos = assocs.combinations[0].combination || [];
101 | let images = assocs.images[0].image || [];;
102 |
103 | return {
104 | 'id': text(base.id),
105 | 'name': text(names.filter(lang).pop()),
106 | 'description': text(descs.filter(lang).pop()),
107 | 'description_short': text(shortdescs.filter(lang).pop()),
108 | 'price': text(base.price),
109 | 'available_for_order': text(base.available_for_order),
110 | 'manufacturer_name': text(base.manufacturer_name),
111 | 'related': {
112 | 'manufacturer': integer(text(base.id_manufacturer)),
113 | 'combinations': combos.map(combo => integer(text(combo.id))),
114 | 'images': images.map(image => integer(text(image.id))),
115 | },
116 | };
117 | });
118 | };
119 |
120 | ////////////////////////////////////////////////////////////////////////////////
121 |
122 | parse.combination = {};
123 |
124 | /**
125 | * @async Promise
126 | * @param {String} xml
127 | * @return {Object}
128 | */
129 | parse.combination.attributes = (xml) => {
130 | return parse(xml)
131 |
132 | .then((obj) => {
133 | let base = obj.prestashop.combination[0];
134 | let assocs = base.associations[0] || {};
135 | let povs = assocs.product_option_values || [];
136 |
137 | return {
138 | 'id': text(base.id),
139 | 'id_product': text(base.id_product),
140 | 'location': text(base.location),
141 | 'ean13': text(base.ean13),
142 | 'upc': text(base.upc),
143 | 'quantity': text(base.quantity),
144 | 'reference': text(base.reference),
145 | 'supplier_reference': text(base.supplier_reference),
146 | 'wholesale_price': text(base.wholesale_price),
147 | 'price': text(base.price),
148 | 'ecotax': text(base.ecotax),
149 | 'weight': text(base.weight),
150 | 'unit_price_impact': text(base.unit_price_impact),
151 | 'minimal_quantity': text(base.minimal_quantity),
152 | 'default_on': text(base.default_on),
153 | 'available_date': text(base.available_date),
154 | 'related': {
155 | 'product': integer(text(base.id_product)),
156 | 'product_option_values': integer(text(povs[0].product_option_value[0].id)),
157 | },
158 | };
159 | })
160 | }
161 |
162 | ////////////////////////////////////////////////////////////////////////////////
163 |
164 | parse.image = {};
165 |
166 | /**
167 | * @async Promise
168 | * @param {String} xml
169 | * @return {Object}
170 | */
171 | parse.image.attributes = (xml) => {
172 | return parse(xml)
173 |
174 | .then((obj) => {
175 | let decs = obj.prestashop.image[0].declination;
176 |
177 | return decs.map((dec) => {
178 | return {
179 | 'id': dec.$.id,
180 | 'src': dec.$['xlink:href'],
181 | };
182 | });
183 | })
184 | };
185 |
186 | ////////////////////////////////////////////////////////////////////////////////
187 |
188 | parse.manufacturer = {};
189 |
190 | parse.manufacturer.attributes = (xml, language=1) => {
191 | return parse(xml)
192 |
193 | .then((obj) => {
194 | let base = obj.prestashop.manufacturer[0];
195 | let lang = (obj) => attr(obj, 'id') == language;
196 | let descs = base.description[0].language;
197 |
198 | return {
199 | 'id': text(base.id),
200 | 'active': text(base.active),
201 | 'name': text(base.name),
202 | 'description': text(descs.filter(lang).pop()),
203 | };
204 | })
205 | };
206 |
207 | ////////////////////////////////////////////////////////////////////////////////
208 |
209 | parse.stock_available = {};
210 |
211 | parse.stock_available.attributes = (xml, language=1) => {
212 | return parse(xml)
213 |
214 | .then((obj) => {
215 | let base = obj.prestashop.stock_available[0];
216 |
217 | return {
218 | 'id': text(base.id),
219 | 'id_product': text(base.id_product),
220 | 'id_product_attribute': text(base.id_product_attribute),
221 | 'id_shop': text(base.id_shop),
222 | 'id_shop_group': text(base.id_shop_group),
223 | 'quantity': text(base.quantity),
224 | 'depends_on_stock': text(base.depends_on_stock),
225 | 'out_of_stock': text(base.out_of_stock),
226 | };
227 | })
228 | };
229 |
230 | ////////////////////////////////////////////////////////////////////////////////
231 |
232 | parse.product_option_value = {};
233 |
234 | parse.product_option_value.attributes = (xml, language=1) => {
235 | return parse(xml)
236 |
237 | .then((obj) => {
238 | let base = obj.prestashop.product_option_value[0];
239 | let names = base.name[0].language;
240 | let lang = (obj) => attr(obj, 'id') == language;
241 |
242 | return {
243 | 'id': text(base.id),
244 | 'id_attribute_group': text(base.id_attribute_group),
245 | 'color': text(base.color),
246 | 'position': text(base.position),
247 | 'name': text(names.filter(lang).pop()),
248 | };
249 | })
250 | };
251 |
252 |
--------------------------------------------------------------------------------
/tests/bootstrap.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import sinon from 'sinon';
3 | import fs from 'fs';
4 | import path from 'path';
5 | import assert from 'assert';
6 |
7 | global.assert = assert;
8 | global.expect = expect;
9 | global.sinon = sinon;
10 | global.match = sinon.match;
11 | global.spy = sinon.spy;
12 | global.stub = sinon.stub;
13 | global.mock = sinon.mock;
14 |
15 | /**
16 | * @param {String} abspath
17 | * @return {String}
18 | */
19 | const readfile = (abspath) => {
20 | return String(fs.readFileSync(abspath)).trim();
21 | };
22 |
23 | const FIXTURE_PATH = path.normalize(path.join(__dirname, 'fixtures'));
24 |
25 | /**
26 | * @param {String} relpath
27 | * @return {String}
28 | */
29 | global.fixture = (relpath) => {
30 | return readfile(path.join(FIXTURE_PATH, relpath));
31 | };
32 |
33 |
--------------------------------------------------------------------------------
/tests/fixtures/combination-47.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tests/fixtures/manufacturer-1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/tests/fixtures/manufacturers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/fixtures/product-10.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
Silver gelatin print measuring
65 |
27.9 × 35.6 cm, unframed.
66 |
Printed under the direct supervision of the artist.
67 |
One of a signed, limited edition of 10.
68 |
69 |
]]>
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
--------------------------------------------------------------------------------
/tests/fixtures/product-8-images.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/fixtures/product-8.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | Silver gelatin print measuring
27.9 × 35.6 cm, unframed.
Printed under the direct supervision of the artist.
One of a signed, limited edition of 10.
]]>
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
--------------------------------------------------------------------------------
/tests/fixtures/product-9.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | Silver gelatin print measuring
27.9 × 35.6 cm, unframed.
Printed under the direct supervision of the artist.
One of a signed, limited edition of 10.
]]>
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
--------------------------------------------------------------------------------
/tests/fixtures/product-option-values-25.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/fixtures/products.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/fixtures/stock-available-80.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/fixtures/stock-availables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/tests/fixtures/valid.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tests/functional/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/tests/unit/exceptions.spec.js:
--------------------------------------------------------------------------------
1 | import {HttpException} from '../../src/exceptions';
2 |
3 | describe('exceptions', () => {
4 | describe('HttpException', () => {
5 | describe('#constructor()', () => {
6 | it('accepts a status argument', () => {
7 | let invokation = () => new HttpException('not found', null, null, 404);
8 | expect(invokation).not.to.throw();
9 | });
10 |
11 | it('assigns the #status property', () => {
12 | let e = new HttpException('not found', null, null, 404);
13 | expect(e.status).to.equal(404);
14 | });
15 | });
16 | });
17 | });
18 |
19 |
--------------------------------------------------------------------------------
/tests/unit/lang.spec.js:
--------------------------------------------------------------------------------
1 | import { empty, tuples } from '../../src/lang';
2 | import assert from 'assert';
3 |
4 | describe('lang', () => {
5 |
6 | describe('.empty()', () => {
7 |
8 | const TestClass = function() {};
9 | TestClass.prototype = {constructor: TestClass, foo: 1};
10 |
11 | it('false is empty', () => expect(empty(false)).to.equal(true));
12 | it('true is not empty', () => expect(empty(true)).to.equal(false));
13 |
14 | it('null is empty', () => expect(empty(null)).to.equal(true));
15 | it('undefined is empty', () => expect(empty(undefined)).to.equal(true));
16 |
17 | it('zero-length string is empty', () => expect(empty('')).to.equal(true));
18 | it('non-zero length string is not empty', () => expect(empty(' ')).to.equal(false));
19 |
20 | it('zero-length array is empty', () => expect(empty([])).to.equal(true));
21 | it('non-zero length array is not empty', () => expect(empty([0])).to.equal(false));
22 |
23 | it('integer zero is empty', () => expect(empty(0)).to.equal(true));
24 | it('number other than integer zero is not empty', () => expect(empty(-1)).to.equal(false));
25 | it('NaN is not empty', () => expect(empty(NaN)).to.equal(false));
26 |
27 | it('Map with no keys is empty', () => expect(empty(new Map())).to.equal(true));
28 | it('Set with no values is empty', () => expect(empty(new Set())).to.equal(true));
29 |
30 | it('object that owns no properties is empty', () => {
31 | assert(Object.getOwnPropertyNames({}).length === 0);
32 | expect(empty({})).to.equal(true);
33 | });
34 |
35 | it('object that owns at least one property is not empty', () => {
36 | let TestClass = function() {};
37 |
38 | TestClass.prototype = {constructor: TestClass};
39 |
40 | let obj = new TestClass();
41 | obj.bar = 2;
42 | expect(empty(obj)).to.equal(false);
43 | });
44 |
45 | it('Map with at least one key is not empty', () => {
46 | let map = new Map([['foo', 1]]);
47 | expect(empty(map)).to.equal(false);
48 | });
49 |
50 | it('Set with at least one value is not empty', () => {
51 | let set = new Set(['foo']);
52 | expect(empty(set)).to.equal(false);
53 | });
54 | });
55 |
56 |
57 | describe('.tuples()', () => {
58 | it('converts an Object to an Array of two-element tuples', () => {
59 | let input = {foo: 1, bar: 2};
60 | let output = tuples(input);
61 |
62 | expect(output).to.be.an.instanceof(Array);
63 | expect(output.length).to.equal(2);
64 |
65 | expect(output[0]).to.be.an.instanceof(Array);
66 | expect(output[1]).to.be.an.instanceof(Array);
67 |
68 | expect(output[0].length).to.equal(2);
69 | expect(output[1].length).to.equal(2);
70 | expect(output[0][0]).to.equal('foo');
71 | expect(output[0][1]).to.equal(1);
72 | expect(output[1][0]).to.equal('bar');
73 | expect(output[1][1]).to.equal(2);
74 | });
75 |
76 | it('converts a Map to an Array of two-element tuples', () => {
77 | let input = new Map([['foo', 1], ['bar', 2]]);
78 | let output = tuples(input);
79 |
80 | expect(output).to.be.an.instanceof(Array);
81 | expect(output.length).to.equal(2);
82 |
83 | expect(output[0]).to.be.an.instanceof(Array);
84 | expect(output[1]).to.be.an.instanceof(Array);
85 |
86 | expect(output[0].length).to.equal(2);
87 | expect(output[1].length).to.equal(2);
88 | expect(output[0][0]).to.equal('foo');
89 | expect(output[0][1]).to.equal(1);
90 | expect(output[1][0]).to.equal('bar');
91 | expect(output[1][1]).to.equal(2);
92 | });
93 |
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/tests/unit/models/Model.spec.js:
--------------------------------------------------------------------------------
1 | import models from '../../../src/models';
2 | import rest from '../../../src/rest';
3 |
4 | const { Model } = models;
5 |
6 | describe('models.Model', () => {
7 |
8 | describe('#constructor()', () => {
9 | it('mass-assigns supplied properties', () => {
10 | let model = new Model({attrs: {foo: 2}});
11 | expect(model.attrs.foo).to.equal(2);
12 | });
13 | });
14 |
15 | describe('#set()', () => {
16 | it('mass-assigns supplied properties at arity 1', () => {
17 | let model = new Model();
18 | model.set({foo: 2, bar: 3});
19 | expect(model.attrs.foo).to.equal(2);
20 | expect(model.attrs.bar).to.equal(3);
21 | });
22 |
23 | it('assigns a single property at arity 2', () => {
24 | let model = new Model();
25 | model.set('foo', 2);
26 | expect(model.attrs.foo).to.equal(2);
27 | })
28 |
29 | it('coerces property values via #mutators()', () => {
30 | let model = new Model();
31 | model.mutators = stub().returns({foo: [x=>'scooby-doo']});
32 | model.set('foo', 2);
33 |
34 | expect(model.attrs.foo).to.equal('scooby-doo');
35 | expect(model.mutators.calledOnce).to.be.ok;
36 | });
37 | });
38 |
39 | describe('#mutators()', () => {
40 | it('returns an object', () => {
41 | let model = new Model();
42 | let mutators = model.mutators();
43 |
44 | expect(mutators).to.be.ok;
45 | expect(mutators).to.be.an.instanceof(Object);
46 | expect(mutators).not.to.be.an.instanceof(Array)
47 | });
48 | });
49 |
50 | });
51 |
--------------------------------------------------------------------------------
/tests/unit/models/Product.spec.js:
--------------------------------------------------------------------------------
1 | import models from '../../../src/models';
2 | import { resources } from '../../../src/rest';
3 | const { Product } = models;
4 |
5 | describe('models.Product', () => {
6 |
7 | describe('#images()', () => {
8 | it('returns a rest.resources.Images instance', () => {
9 | let model = new Product({props: {id: 8}});
10 | expect(model.images()).to.be.an.instanceof(resources.Images);
11 | });
12 | })
13 |
14 | describe('#combinations()', () => {
15 | it('returns a rest.resources.Combinations instance', () => {
16 | let model = new Product({props: {id: 8}});
17 | expect(model.combinations()).to.be.an.instanceof(resources.Combinations);
18 | });
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/tests/unit/querystring.spec.js:
--------------------------------------------------------------------------------
1 | import querystring from '../../src/querystring';
2 |
3 | describe('querystring', () => {
4 | describe('.stringify()', () => {
5 | it('converts an Object to a query string', () => {
6 | let input = {foo: 'one', bar: 2};
7 | let expected = 'foo=one&bar=2';
8 | let actual = querystring.stringify(input);
9 |
10 | expect(actual).to.equal(expected);
11 | });
12 |
13 | it('converts a list of tuples to a query string', () => {
14 | let input = [['foo', 'one'], ['bar', 2]];
15 | let expected = 'foo=one&bar=2';
16 | let actual = querystring.stringify(input);
17 |
18 | expect(actual).to.equal(expected);
19 | });
20 |
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tests/unit/rest/Client.spec.js:
--------------------------------------------------------------------------------
1 | import { Client } from '../../../src/rest';
2 | const P = Promise;
3 | const error = new Error();
4 | const pass = () => P.resolve();
5 | const fail = () => P.reject();
6 |
7 | describe('rest.Client', () => {
8 |
9 | describe('#language', () => {
10 | it('is a language id', () => {
11 | let client = new Client({
12 | language: 'es',
13 | languages: {
14 | 'en': 1,
15 | 'es': 2,
16 | },
17 | });
18 |
19 | expect(client.language).to.equal(2);
20 | });
21 | });
22 |
23 | describe('#url()', () => {
24 | it('derives a web service url', () => {
25 | let client = new Client({
26 | webservice: {scheme: 'https', host: 'api.local:3000', root: '/api'},
27 | });
28 |
29 | let expected = 'https://api.local:3000/api/foo/bar';
30 | let actual = client.url('/foo/bar');
31 |
32 | expect(actual).to.equal(expected);
33 | });
34 |
35 | it('derives a web service url with query parameters', () => {
36 | let client = new Client({
37 | webservice: {scheme: 'https', host: 'api.local:3000', root: '/api'},
38 | });
39 |
40 | let expected = 'https://api.local:3000/api/foo/bar?a=1&b=2';
41 | let actual = client.url('/foo/bar', {a: 1, b: 2});
42 |
43 | expect(actual).to.equal(expected);
44 | });
45 |
46 | it('query string is deterministic', () => {
47 | let client = new Client({
48 | webservice: {scheme: 'https', host: 'api.local:3000', root: '/api'},
49 | });
50 |
51 | let query1 = {a: 1, b: 2, c: 3};
52 | let query2 = {c: 3, a: 1, b: 2};
53 |
54 | let url1 = client.url('/foo/bar', query1);
55 | let url2 = client.url('/foo/bar', query2);
56 |
57 | expect(url1).to.equal(url2);
58 | });
59 |
60 | });
61 |
62 | describe('#get()', () => {
63 | it('sends a GET request given a relative path', () => {
64 | let fetch = stub();
65 | let response = {ok: true, clone: () => response};
66 | let client = new Client({fetch: {algo: fetch}});
67 |
68 | fetch.withArgs(match.string, match.object).returns(P.resolve(response));
69 |
70 | return client.get('/foo/bar')
71 |
72 | .then((res) => {
73 | expect(fetch.calledOnce).to.be.ok;
74 | expect(res).to.equal(response);
75 | })
76 | });
77 |
78 | it('sends a GET request given a fully qualified url', () => {
79 | let fetch = stub();
80 | let response = {ok: true, clone: () => response};
81 | let client = new Client({fetch: {algo: fetch}});
82 | let url = 'http://prestashop-api-host/api/images/products/8';
83 |
84 | fetch.withArgs(url, match.object).returns(P.resolve(response));
85 |
86 | return client.get(url)
87 |
88 | .then((res) => {
89 | expect(fetch.calledOnce).to.be.ok;
90 | expect(res).to.equal(response);
91 | })
92 | });
93 |
94 | it('throws an Error on non-OK response', () => {
95 | let fetch = stub();
96 | let response = {ok: false, clone: () => response};
97 | let promise = P.resolve(response);
98 | let client = new Client({fetch: {algo: fetch}});
99 |
100 | fetch.withArgs(match.string, match.object).returns(promise);
101 |
102 | return client.get('/foo/bar')
103 | .then(fail)
104 | .catch(pass);
105 | });
106 |
107 | xit('concurrent requests on the same URL converge on a single promise', () => {
108 | let promise = P.resolve();
109 | let fetch = stub().returns(promise);
110 | let client = new Client({fetch: {algo: fetch}});
111 | let reqpath = '/foo/bar';
112 |
113 | expect(client.get(reqpath)).to.equal(promise);
114 | expect(client.get(reqpath)).to.equal(promise);
115 | expect(fetch.calledOnce).to.be.ok;
116 | });
117 |
118 | });
119 |
120 | describe('root resource access', () => {
121 | let client = new Client();
122 |
123 | describe('#resource()', () => {
124 | it('returns Products resource on key "products"', () => {
125 | expect(client.resource('products')).to.be.ok;
126 | });
127 | it('returns Combinations resource on key "combinations"', () => {
128 | expect(client.resource('combinations')).to.be.ok;
129 | });
130 | it('returns Manufacturers resource on key "manufacturers"', () => {
131 | expect(client.resource('manufacturers')).to.be.ok;
132 | });
133 | it('returns Images resource on key "images"', () => {
134 | expect(client.resource('images')).to.be.ok;
135 | });
136 | })
137 | });
138 |
139 | describe('#createAuthorizationHeader()', () => {
140 | it('returns Authorization header value', () => {
141 | let client = new Client();
142 | let key = 'IW6SQL9FICVWMJ6BWBASP24ABCNSSEZW';
143 | let expected = 'Basic SVc2U1FMOUZJQ1ZXTUo2QldCQVNQMjRBQkNOU1NFWlc6';
144 | let actual = client.createAuthorizationHeader(key);
145 |
146 | expect(actual).to.equal(expected);
147 | });
148 | });
149 |
150 | describe('#createFetchOptions()', () => {
151 | it('includes Authorization header', () => {
152 | let client = new Client({webservice: {key: 'foo'}});
153 | let Authorization = 'Basic SVc2U1FMOUZJQ1ZXTUo2QldCQVNQMjRBQkNOU1NFWlc6';
154 |
155 | client.createAuthorizationHeader = stub().returns(Authorization);
156 |
157 | let fetchopts = client.createFetchOptions();
158 | expect(fetchopts.headers).to.be.an('object');
159 | expect(fetchopts.headers['Authorization']).to.equal(Authorization);
160 | });
161 | });
162 |
163 | describe('#setLanguageIso()', () => {
164 | it('changes the current language', () => {
165 | let client = new Client({languages: {en: 1, es: 2}});
166 | assert(client.language != 2);
167 | client.setLanguageIso('es');
168 |
169 | expect(client.language).to.equal(2);
170 | });
171 |
172 | it('complains if the supplied ISO code is invalid', () => {
173 | let client = new Client({languages: {en: 1, es: 2}});
174 | let invocation = () => client.setLanguageIso('zh');
175 |
176 | expect(invocation).to.throw(Error);
177 | });
178 | });
179 |
180 | describe('#getLanguage()', () => {
181 | it('returns the current language ISO code', () => {
182 | let client = new Client({language: 'en', languages: {en: 1, es: 2}});
183 | assert(client.language === 1);
184 |
185 | expect(client.getLanguageIso()).to.equal('en');
186 | });
187 | });
188 |
189 | });
190 |
191 |
--------------------------------------------------------------------------------
/tests/unit/rest/resources/Images.spec.js:
--------------------------------------------------------------------------------
1 | import { resources } from '../../../../src/rest';
2 | import { Image } from '../../../../src/models';
3 | const { Images } = resources;
4 | const P = Promise;
5 |
6 | describe('rest.resources', () => {
7 | describe('Image', () => {
8 | describe('#list()', () => {
9 |
10 | it('returns a list of Image models', (done) => {
11 | let client = {get: stub()};
12 |
13 | let resource = new Images({
14 | client: client,
15 | root: '/images/products/8',
16 | });
17 |
18 | let text = P.resolve(fixture('product-8-images.xml'));
19 | let response = {ok: true, text: stub().returns(text), clone: () => response};
20 |
21 | client.get.withArgs('/images/products/8').returns(P.resolve(response));
22 |
23 | resource.list()
24 |
25 | .then((models) => {
26 | expect(client.get.calledOnce).to.be.ok;
27 | expect(response.text.calledOnce).to.be.ok;
28 | expect(models).to.be.an.instanceof(Array);
29 | expect(models.length).to.equal(1);
30 | expect(models[0]).to.be.an.instanceof(Image);
31 | expect(models[0].attrs.src).to.equal('http://localhost/api/images/products/8/24');
32 | })
33 |
34 | .then(done)
35 | .catch(done)
36 | });
37 |
38 | });
39 | });
40 | });
41 |
42 |
--------------------------------------------------------------------------------
/tests/unit/rest/resources/Manufacturers.spec.js:
--------------------------------------------------------------------------------
1 | import { resources } from '../../../../src/rest';
2 | import { Manufacturer } from '../../../../src/models';
3 |
4 | const { Manufacturers } = resources;
5 | const P = Promise;
6 | const error = () => new Error();
7 |
8 | describe('rest.resources', () => {
9 | describe('Manufacturers', () => {
10 |
11 | describe('#get()', () => {
12 |
13 | it('returns a single Manufacturer model', (done) => {
14 | let client = {get: stub()};
15 | let resource = new Manufacturers({client});
16 |
17 | let text = P.resolve(fixture('manufacturer-1.xml'));
18 | let response = {ok: true, text: stub().returns(text), clone: () => response};
19 |
20 | client.get.withArgs('/manufacturers/1').returns(P.resolve(response));
21 |
22 | resource.get(1)
23 |
24 | .then((model) => {
25 | expect(client.get.calledOnce).to.be.ok;
26 | expect(response.text.calledOnce).to.be.ok;
27 | expect(model).to.be.an.instanceof(Manufacturer);
28 | })
29 |
30 | .then(done)
31 | .catch(done)
32 | });
33 |
34 | });
35 |
36 | describe('#list()', () => {
37 |
38 | it('returns a list of Manufacturer models', (done) => {
39 | let client = {get: stub()};
40 | let resource = new Manufacturers({client: client});
41 |
42 | // the manufacturer list response
43 | let response1 = {ok: true, text: stub().returns(fixture('manufacturers.xml')), clone: () => response1};
44 | client.get.withArgs('/manufacturers').returns(P.resolve(response1));
45 |
46 | // responses for each manufacturer id in the list response
47 | let response2 = {ok: true, text: stub().returns(fixture('manufacturer-1.xml')), clone: () => response2};
48 | client.get.withArgs('/manufacturers/1').returns(P.resolve(response2));
49 |
50 | resource.list()
51 |
52 | .then((models) => {
53 | expect(client.get.callCount).to.equal(2);
54 | expect(models).to.be.an.instanceof(Array);
55 | expect(models.length).to.equal(1);
56 |
57 | let [manufacturer1] = models;
58 |
59 | expect(manufacturer1.attrs.id).to.equal(1);
60 | })
61 |
62 | .then(done)
63 | .catch(done)
64 | });
65 | });
66 | });
67 | });
68 |
69 |
--------------------------------------------------------------------------------
/tests/unit/rest/resources/Products.spec.js:
--------------------------------------------------------------------------------
1 | import { resources } from '../../../../src/rest';
2 | import { Product } from '../../../../src/models';
3 |
4 | const { Products } = resources;
5 | const P = Promise;
6 | const error = () => new Error();
7 |
8 | describe('rest.resources', () => {
9 | describe('Products', () => {
10 | describe('#get()', () => {
11 |
12 | it('returns a single Product model', (done) => {
13 | let client = {get: stub()};
14 | let resource = new Products({client: client});
15 |
16 | let text = P.resolve(fixture('product-8.xml'));
17 | let response = {ok: true, text: stub().returns(text), clone: () => response};
18 |
19 | client.get.withArgs('/products/8').returns(P.resolve(response));
20 |
21 | resource.get(8)
22 |
23 | .then((model) => {
24 | expect(client.get.calledOnce).to.be.ok;
25 | expect(response.text.calledOnce).to.be.ok;
26 | expect(model).to.be.an.instanceof(Product);
27 | })
28 |
29 | .then(done)
30 | .catch(done)
31 | });
32 |
33 | });
34 |
35 | describe('#list()', () => {
36 |
37 | it('returns a list of Product models', (done) => {
38 | let client = {get: stub()};
39 | let resource = new Products({client: client});
40 |
41 | // the product list response
42 | let response1 = {ok: true, text: stub().returns(fixture('products.xml')), clone: () => response1};
43 | client.get.withArgs('/products').returns(P.resolve(response1));
44 |
45 | // responses for each product id in the list response
46 | let response2 = {ok: true, text: stub().returns(fixture('product-8.xml')), clone: () => response2};
47 | let response3 = {ok: true, text: stub().returns(fixture('product-9.xml')), clone: () => response3};
48 | client.get.withArgs('/products/8').returns(P.resolve(response2));
49 | client.get.withArgs('/products/9').returns(P.resolve(response3));
50 |
51 | resource.list()
52 |
53 | .then((models) => {
54 | expect(client.get.callCount).to.equal(3);
55 | expect(models).to.be.an.instanceof(Array);
56 | expect(models.length).to.equal(2);
57 |
58 | let [product8, product9] = models;
59 |
60 | expect(product8.attrs.id).to.equal(8);
61 | expect(product9.attrs.id).to.equal(9);
62 | })
63 |
64 | .then(done)
65 | .catch(done)
66 | });
67 | });
68 | });
69 | });
70 |
71 |
--------------------------------------------------------------------------------
/tests/unit/rest/resources/Resource.spec.js:
--------------------------------------------------------------------------------
1 | import { resources } from '../../../../src/rest';
2 | import assert from 'assert';
3 | const { Resource } = resources;
4 | const P = Promise;
5 | const DummyModel = class {};
6 |
7 | describe('rest.resources', () => {
8 | describe('Resource', () => {
9 |
10 | describe('#language property', () => {
11 | it('reflects the client language', () => {
12 | let client = {language: 2};
13 | let resource = new Resource({client});
14 |
15 | expect(resource.language).to.equal(client.language);
16 |
17 | client.language = 3;
18 | expect(resource.language).to.equal(client.language);
19 | });
20 | });
21 |
22 | describe ('#get()', () => {
23 | it('returns a Model if client raises an exception', () => {
24 | let client = {get: stub().throws(new Error('http-related-error'))};
25 | let Model = () => {};
26 | let resource = new Resource({client, model: Model, root: '/foo/bar'});
27 |
28 | assert(resource.client === client);
29 |
30 | return resource.get('blah')
31 | .then((model) => expect(model).to.be.an.instanceof(Model))
32 | .catch((e) => {
33 | console.log(e);
34 | throw e;
35 | });
36 |
37 | });
38 | });
39 | });
40 | });
41 |
42 |
--------------------------------------------------------------------------------
/tests/unit/sort.spec.js:
--------------------------------------------------------------------------------
1 | import sort from '../../src/sort';
2 | const noop = () => {};
3 |
4 | describe('sort', () => {
5 |
6 | describe('.ascending()', () => {
7 | it('accepts a value resolver function', () => {
8 | let invokation = () => sort.ascending(noop);
9 | expect(sort.ascending(() => {})).not.to.throw();
10 | })
11 |
12 | it('returns a sort comparator', () => {
13 | expect(sort.ascending(noop)).to.be.a('function');
14 | });
15 |
16 | it('comparator sorts in ascending order', () => {
17 | let values = [{prop: 3}, {prop: 1}, {prop: 2}];
18 | let evaluate = value => value.prop;
19 | let comparator = sort.ascending(evaluate);
20 | let sorted = values.sort(comparator);
21 |
22 | expect(sorted[0].prop).to.equal(1);
23 | expect(sorted[1].prop).to.equal(2);
24 | expect(sorted[2].prop).to.equal(3);
25 | });
26 | });
27 |
28 | describe('.descending()', () => {
29 | it('accepts a value resolver function', () => {
30 | let invokation = () => sort.descending(noop);
31 | expect(sort.descending(() => {})).not.to.throw();
32 | })
33 |
34 | it('returns a sort comparator', () => {
35 | expect(sort.descending(noop)).to.be.a('function');
36 | });
37 |
38 | it('comparator sorts in descending order', () => {
39 | let values = [{prop: 3}, {prop: 1}, {prop: 2}];
40 | let evaluate = value => value.prop;
41 | let comparator = sort.descending(evaluate);
42 | let sorted = values.sort(comparator);
43 |
44 | expect(sorted[0].prop).to.equal(3);
45 | expect(sorted[1].prop).to.equal(2);
46 | expect(sorted[2].prop).to.equal(1);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/tests/unit/xml.spec.js:
--------------------------------------------------------------------------------
1 | import { parse } from '../../src/xml';
2 |
3 | describe('xml', () => {
4 |
5 | describe('.parse()', () => {
6 | it('converts XML string to a plain object', () => {
7 | return parse(fixture('products.xml'))
8 | .then((obj) => expect(obj).to.be.ok)
9 | });
10 | });
11 |
12 | describe('.parse', () => {
13 | describe('.product.attributes()', () => {
14 |
15 | it('parses Product model attributes (product-8.xml)', () => {
16 | let xml = fixture('product-8.xml');
17 |
18 | return parse.product.attributes(xml)
19 |
20 | .then((props) => {
21 | expect(Number(props.id)).to.equal(8);
22 | expect(props.name).to.equal('product-8-name-en');
23 | expect(props.description).to.be.ok;
24 | expect(props.description).to.be.a('string');
25 | expect(props.description_short).to.equal('');
26 | expect(props.description_short).to.be.a('string');
27 | expect(props.related.manufacturer).to.equal(0);
28 | expect(props.related.combinations).to.include(46, 47, 48, 49, 50, 51, 52, 53);
29 | expect(props.related.images).to.include(24);
30 | })
31 | });
32 |
33 | it('parses Product model attributes (product-10.xml)', () => {
34 | let xml = fixture('product-10.xml');
35 |
36 | return parse.product.attributes(xml)
37 |
38 | .then((props) => {
39 | expect(Number(props.id)).to.equal(10);
40 | expect(props.name).to.equal('product-10-name-en');
41 | expect(props.description).to.be.a('string');
42 | expect(props.description_short).to.be.a('string');
43 | expect(props.related.manufacturer).to.equal(0);
44 | expect(props.related.combinations).to.include(64, 65, 66, 67, 68, 69, 70, 71, 72, 73);
45 | expect(props.related.images).to.be.an.instanceof(Array);
46 | })
47 | });
48 |
49 | });
50 |
51 | });
52 |
53 | describe('.parse', () => {
54 | describe('.combination.attributes()', () => {
55 | it('parses Combination attributes', () => {
56 | let xml = fixture('combination-47.xml');
57 |
58 | return parse.combination.attributes(xml)
59 |
60 | .then((props) => {
61 | let floatzero = 0.000000;
62 |
63 | expect(Number(props.id)).to.equal(47);
64 | expect(Number(props.id_product)).to.equal(8);
65 | expect(props.location).to.equal('');
66 | expect(props.ean13).to.equal('');
67 | expect(props.upc).to.equal('');
68 | expect(Number(props.quantity)).to.equal(1);
69 | expect(props.reference).to.equal('001');
70 | expect(props.supplier_reference).to.equal('');
71 | expect(Number(props.wholesale_price)).to.equal(floatzero);
72 | expect(Number(props.price)).to.equal(floatzero);
73 | expect(Number(props.ecotax)).to.equal(floatzero);
74 | expect(Number(props.weight)).to.equal(floatzero);
75 | expect(Number(props.unit_price_impact)).to.equal(floatzero);
76 | expect(Number(props.minimal_quantity)).to.equal(1);
77 | expect(props.default_on).to.equal('');
78 | expect(props.available_date).to.equal('0000-00-00');
79 | expect(props.related.product).to.equal(8);
80 | expect(props.related.product_option_values).to.equal(26);
81 | });
82 | });
83 | });
84 | });
85 |
86 | describe('.parse', () => {
87 | describe('.manufacturer.attributes()', () => {
88 | it('parses Manufacturer attributes', () => {
89 | let xml = fixture('manufacturer-1.xml');
90 |
91 | return parse.manufacturer.attributes(xml)
92 |
93 | .then((props) => {
94 | expect(Number(props.id)).to.equal(1);
95 | expect(Number(props.active)).to.equal(1);
96 | expect(props.name).to.equal('Fashion Manufacturer');
97 | expect(props.description).to.equal('');
98 | });
99 | });
100 | });
101 | });
102 |
103 | describe('.parse', () => {
104 | describe('.stock_available.attributes()', () => {
105 | it('parses StockAvailable attributes', () => {
106 | let xml = fixture('stock-available-80.xml');
107 |
108 | return parse.stock_available.attributes(xml)
109 |
110 | .then((props) => {
111 | expect(Number(props.id)).to.equal(80);
112 | expect(Number(props.id_product)).to.equal(10);
113 | expect(Number(props.id_product_attribute)).to.equal(0);
114 | expect(Number(props.id_shop)).to.equal(1);
115 | expect(Number(props.id_shop_group)).to.equal(0);
116 | expect(Number(props.depends_on_stock)).to.equal(0);
117 | expect(Number(props.out_of_stock)).to.equal(2);
118 | });
119 | });
120 | });
121 | });
122 |
123 |
124 | describe('.parse', () => {
125 | describe('.product_option_value.attributes', () => {
126 | it('parses ProductOptionValue attributes', () => {
127 | let xml = fixture('product-option-values-25.xml');
128 |
129 | return parse.product_option_value.attributes(xml)
130 |
131 | .then((props) => {
132 | expect(Number(props.id)).to.equal(25);
133 | expect(Number(props.id_attribute_group)).to.equal(4);
134 | expect(Number(props.position)).to.equal(0);
135 | expect(props.name).to.equal('1');
136 | });
137 | });
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------