├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── pull_request_template.md
├── .gitignore
├── .mocharc.json
├── .npmignore
├── .project
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── babel.config.js
├── cloudinary.js
├── lib
├── analysis
│ └── index.js
├── api.js
├── api_client
│ ├── call_account_api.js
│ ├── call_analysis_api.js
│ ├── call_api.js
│ └── execute_request.js
├── auth_token.js
├── cache.js
├── cache
│ ├── FileKeyValueStorage.js
│ └── KeyValueCacheAdapter.js
├── cloudinary.js
├── config.js
├── preloaded_file.js
├── provisioning
│ └── account.js
├── upload_stream.js
├── uploader.js
├── utils
│ ├── analytics
│ │ ├── encodeVersion.js
│ │ ├── getSDKVersions.js
│ │ ├── index.js
│ │ ├── removePatchFromSemver.js
│ │ ├── reverseVersion.js
│ │ └── stringPad.js
│ ├── consts.js
│ ├── crc32.js
│ ├── encoding
│ │ ├── base64Encode.js
│ │ ├── base64EncodeURL.js
│ │ ├── base64Map.js
│ │ ├── encodeDoubleArray.js
│ │ └── smart_escape.js
│ ├── ensureOption.js
│ ├── ensurePresenceOf.js
│ ├── entries.js
│ ├── generateBreakpoints.js
│ ├── index.js
│ ├── isRemoteUrl.js
│ ├── parsing
│ │ ├── consumeOption.js
│ │ └── toArray.js
│ ├── rimraf.js
│ ├── srcsetUtils.js
│ └── utf8_encode.js
└── v2
│ ├── api.js
│ ├── index.js
│ ├── search.js
│ ├── search_folders.js
│ └── uploader.js
├── package.json
├── samples
├── .eslintrc.json
├── basic
│ ├── basic.js
│ ├── lake.jpg
│ ├── package.json
│ └── pizza.jpg
├── photo_album
│ ├── config
│ │ └── schema.js
│ ├── controllers
│ │ └── photos_controller.js
│ ├── env.sample
│ ├── package.json
│ ├── public
│ │ ├── cloudinary_cors.html
│ │ ├── css
│ │ │ └── photo_album.css
│ │ └── js
│ │ │ ├── canvas-to-blob.min.js
│ │ │ └── photo_album.js
│ ├── server.js
│ └── views
│ │ ├── errors
│ │ ├── 404.ejs
│ │ ├── 500.ejs
│ │ └── dotenv.ejs
│ │ ├── layouts
│ │ └── default.ejs
│ │ └── photos
│ │ ├── add.ejs
│ │ ├── add_direct.ejs
│ │ ├── add_direct_unsigned.ejs
│ │ ├── create_direct.ejs
│ │ ├── create_through_server.ejs
│ │ └── index.ejs
└── readme.md
├── test
├── .eslintrc.json
├── .resources
│ ├── CloudBookStudy-HD.mp4
│ ├── TheCompleteWorksOfShakespeare.mobi
│ ├── big-image.jpg
│ ├── docx.docx
│ ├── empty.gif
│ ├── favicon.ico
│ ├── logo.png
│ └── sample.jpg
├── integration
│ ├── api
│ │ ├── admin
│ │ │ ├── api_spec.js
│ │ │ ├── config_spec.js
│ │ │ ├── folders_api_spec.js
│ │ │ ├── related_assets_spec.js
│ │ │ └── structured_metadata_spec.js
│ │ ├── analysis
│ │ │ └── analyze_spec.js
│ │ ├── authorization
│ │ │ └── oAuth_authorization_spec.js
│ │ ├── provisioning
│ │ │ ├── access_keys_spec.js
│ │ │ └── account_spec.js
│ │ ├── search
│ │ │ ├── search_folders_spec.js
│ │ │ ├── search_spec.js
│ │ │ └── visual_search_spec.js
│ │ └── uploader
│ │ │ ├── archivespec.js
│ │ │ ├── auto_chaptering_spec.js
│ │ │ ├── auto_transcription_spec.js
│ │ │ ├── custom_region_spec.js
│ │ │ ├── slideshow_spec.js
│ │ │ └── uploader_spec.js
│ ├── cache_spec.js
│ └── streaming_profiles_spec.js
├── setup.js
├── spechelper.js
├── testUtils
│ ├── assertions
│ │ ├── beADatasource.js
│ │ ├── beAMetadataField.js
│ │ ├── beAMulti.js
│ │ ├── beASignedDownloadUrl.js
│ │ ├── beASprite.js
│ │ ├── beServedByCloudinary.js
│ │ ├── emptyOptions.js
│ │ └── produceUrl.js
│ ├── createTestConfig.js
│ ├── helpers
│ │ ├── retry.js
│ │ └── wait.js
│ ├── reusableTests
│ │ ├── api
│ │ │ ├── toAcceptNextCursor.js
│ │ │ └── toBeACursor.js
│ │ ├── image
│ │ │ └── imageAttributesWithClientHInts.js
│ │ └── reusableTests.js
│ ├── suite.js
│ ├── testBootstrap.js
│ └── testConstants.js
├── unit
│ ├── access_control_spec.js
│ ├── authToken
│ │ ├── authTokenImageTag_spec.js
│ │ ├── authTokenURL_spec.js
│ │ ├── authTokenUtils_spec.js
│ │ └── utils
│ │ │ └── commonAuthSetupAndTeardown.js
│ ├── cache
│ │ ├── DummyCacheStorage.js
│ │ ├── FileKeyValueCache_spec.js
│ │ └── KeyValueCacheAdapter_spec.js
│ ├── cloudinaryUtils
│ │ ├── getUserAgent.spec.js
│ │ └── processLayer.spec.js
│ ├── cloudinary_spec.js
│ ├── config.spec.js
│ ├── normalize_expression_spec.js
│ ├── sdkAnalytics
│ │ └── imageTagWithAnalytics.spec.js
│ ├── search
│ │ └── search_spec.js
│ ├── tags
│ │ ├── image_spec.js
│ │ ├── source_tag_spec.js
│ │ └── video_spec.js
│ └── url
│ │ ├── overlay_underlay_spec.js
│ │ ├── sign_url_spec.js
│ │ ├── user_defined_variables_spec.js
│ │ └── video_url_spec.js
└── utils
│ └── utils_spec.js
├── tools
├── createTestCloud.js
├── scripts
│ ├── ditslint.sh
│ ├── docs.sh
│ ├── lint.sh
│ ├── test.es6.sh
│ ├── test.es6.unit.sh
│ ├── test.sh
│ └── tests-with-temp-cloud.sh
└── update_version
├── tsconfig.json
└── types
├── cloudinary_ts_spec.ts
├── index.d.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 |
4 | ["env", {
5 | "targets": {
6 | "node": "4"
7 | }
8 | }],
9 | "stage-0"
10 | ],
11 | "plugins": []
12 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.min.js
2 | node_modules
3 | test_cache/
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "extends": "airbnb-base",
8 | "globals": {
9 | "Atomics": "readonly",
10 | "SharedArrayBuffer": "readonly",
11 | "expect" :"readonly"
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2018
15 | },
16 | "rules": {
17 | "arrow-body-style": "off",
18 | "arrow-spacing": ["error", { "before": true, "after": true }],
19 | "arrow-parens": "off",
20 | "camelcase": "off",
21 | "class-methods-use-this": "off",
22 | "comma-dangle": ["error", "never"],
23 | "comma-spacing":["error", {"before": false, "after": true}],
24 | "consistent-return": "off",
25 | "dot-notation": "error",
26 | "function-paren-newline": "off",
27 | "func-names": "off",
28 | "global-require": "off",
29 | "guard-for-in": "off",
30 | "indent": ["error", 2],
31 | "import/no-extraneous-dependencies": "off",
32 | "import/no-unresolved": "off",
33 | "import/newline-after-import": "off",
34 | "import/no-dynamic-require": "off",
35 | "import/order": "off",
36 | "max-len": "off",
37 | "new-cap": "off",
38 | "newline-per-chained-call": "off",
39 | "no-unused-expressions": "off",
40 | "no-useless-catch": "off",
41 | "no-underscore-dangle": "off",
42 | "no-bitwise": "off",
43 | "no-buffer-constructor": "off",
44 | "no-cond-assign": "off",
45 | "no-confusing-arrow": "off",
46 | "no-console": "off",
47 | "no-dupe-keys": "error",
48 | "no-else-return": "off",
49 | "no-empty": "error",
50 | "no-empty-function": "error",
51 | "no-lonely-if": "off",
52 | "no-loop-func": "off",
53 | "no-mixed-operators": "off",
54 | "no-multi-assign": "off",
55 | "no-param-reassign": "off",
56 | "no-plusplus": "off",
57 | "no-prototype-builtins": "off",
58 | "no-restricted-syntax": "off",
59 | "no-return-assign": "off",
60 | "no-shadow": "error",
61 | "no-throw-literal": "off",
62 | "no-unreachable": "error",
63 | "no-unused-vars": "off",
64 | "no-trailing-spaces": ['error',{
65 | "skipBlankLines" : true
66 | }],
67 | "no-multiple-empty-lines": "off",
68 | "no-use-before-define": "off",
69 | "no-useless-concat": "off",
70 | "no-useless-constructor": "off",
71 | "no-useless-escape": "off",
72 | "no-var": "off",
73 | "no-void": "off",
74 | "object-curly-spacing": "off",
75 | "object-curly-newline": [
76 | "error",
77 | {
78 | "consistent": true
79 | }
80 | ],
81 | "object-shorthand": "off",
82 | "one-var": "off",
83 | "one-var-declaration-per-line": "off",
84 | "operator-linebreak": "off",
85 | "operator-assignment": "off",
86 | "prefer-object-spread": "off",
87 | "prefer-arrow-callback": "off",
88 | "prefer-const": "off",
89 | "prefer-destructuring": "off",
90 | "prefer-template": "off",
91 | "quote-props": "off",
92 | "quotes": "off",
93 | "radix": "off",
94 | "semi": "off",
95 | "space-before-function-paren": 'off',
96 | "space-before-blocks": "off",
97 | "space-infix-ops": "off",
98 | "vars-on-top": "off",
99 | "wrap-iife": "off"
100 | }
101 | };
102 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Bug report for Cloudinary NPM SDK
11 | Before proceeding, please update to latest version and test if the issue persists
12 |
13 | ## Describe the bug in a sentence or two.
14 | …
15 |
16 | ## Issue Type (Can be multiple)
17 | [ ] Build - Can’t install or import the SDK
18 | [ ] Babel - Babel errors or cross browser issues
19 | [ ] Performance - Performance issues
20 | [ ] Behaviour - Functions aren’t working as expected (Such as generate URL)
21 | [ ] Documentation - Inconsistency between the docs and behaviour
22 | [ ] Incorrect Types - For typescript users who are having problems with our d.ts files
23 | [ ] Other (Specify)
24 |
25 | ## Steps to reproduce
26 | … if applicable
27 |
28 | ## Error screenshots
29 | … if applicable
30 |
31 | ## Browsers (if issue relates to UI, else ignore)
32 | [ ] Chrome
33 | [ ] Firefox
34 | [ ] Safari
35 | [ ] Other (Specify)
36 | [ ] All
37 |
38 | ## Versions and Libraries (fill in the version numbers)
39 | Cloudinary_NPM SDK version
40 | Node - 0.0.0
41 | NPM - 0.0.0
42 |
43 | ## Config Files (Please paste the following files if possible)
44 | Package.json
45 |
46 | ## Repository
47 | If possible, please provide a link to a reproducible repository that showcases the problem
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Feature request for Cloudinary_NPM SDK
11 | …(If your feature is for other SDKs, please request them there)
12 |
13 |
14 | ## Explain your use case
15 | … (A high-level explanation of why you need this feature)
16 |
17 | ## Describe the problem you’re trying to solve
18 | … (A more technical view of what you’d like to accomplish, and how this feature will help you achieve it)
19 |
20 | ## Do you have a proposed solution?
21 | … (yes, no? Please elaborate if needed)
22 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### Brief Summary of Changes
2 |
5 |
6 | #### What Does This PR Address?
7 | - [ ] GitHub issue (Add reference - #XX)
8 | - [ ] Refactoring
9 | - [ ] New feature
10 | - [ ] Bug fix
11 | - [ ] Adds more tests
12 |
13 | #### Are Tests Included?
14 | - [ ] Yes
15 | - [ ] No
16 |
17 | #### Reviewer, Please Note:
18 |
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bower_components/
3 | .idea
4 | .DS_Store
5 | .env
6 | bin
7 | *.map
8 | !lib
9 | test_cache/
10 | .nyc_output
11 | docs
12 | coverage
13 | # contains temporary cloudianry_url for test accounts
14 | tools/cloudinary_url.sh
15 | package-lock.json
16 | npm-debug.log
17 | .vscode/
18 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "recursive": true,
3 | "retries": 3
4 | }
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | .project
3 | src/
4 | samples/
5 | test/
6 | .idea
7 | .DS_Store
8 | .env
9 | .travis.yml
10 | !lib/**
11 | !package.json
12 | !babel.config.js
13 | !.npmignore
14 | !.gitignore
15 | !.babelrc
16 | !CHANGELOG.md
17 | !cloudinary.js
18 | !README.md
19 |
20 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | cloudinary_npm
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | dist: focal
3 | matrix:
4 | include:
5 | - node_js: "9"
6 | script: npm run test-with-temp-cloud
7 | - node_js: "10"
8 | script: npm run test-with-temp-cloud
9 | - node_js: "12"
10 | script: npm run test-with-temp-cloud
11 | - node_js: "14"
12 | script: npm run test-with-temp-cloud
13 | - node_js: "16"
14 | script: npm run test-with-temp-cloud
15 | - node_js: "18"
16 | script: npm run test-with-temp-cloud
17 | - node_js: "20"
18 | script: npm run test-with-temp-cloud
19 | - node_js: "22"
20 | script: npm run test-with-temp-cloud
21 |
22 |
23 | notifications:
24 | email:
25 | recipients:
26 | - sdk_developers@cloudinary.com
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Cloudinary Node SDK
2 | =========================
3 | ## About
4 | The Cloudinary Node SDK allows you to quickly and easily integrate your application with Cloudinary.
5 | Effortlessly optimize, transform, upload and manage your cloud's assets.
6 |
7 |
8 | #### Note
9 | This Readme provides basic installation and usage information.
10 | For the complete documentation, see the [Node SDK Guide](https://cloudinary.com/documentation/node_integration).
11 |
12 | ## Table of Contents
13 | - [Key Features](#key-features)
14 | - [Version Support](#Version-Support)
15 | - [Installation](#installation)
16 | - [Usage](#usage)
17 | - [Setup](#Setup)
18 | - [Transform and Optimize Assets](#Transform-and-Optimize-Assets)
19 | - [Generate Image and HTML Tags](#Generate-Image-and-Video-HTML-Tags)
20 |
21 |
22 | ## Key Features
23 | - [Transform](https://cloudinary.com/documentation/node_video_manipulation#video_transformation_examples) and
24 | [optimize](https://cloudinary.com/documentation/node_image_manipulation#image_optimizations) assets.
25 | - Generate [image](https://cloudinary.com/documentation/node_image_manipulation#deliver_and_transform_images) and
26 | [video](https://cloudinary.com/documentation/node_video_manipulation#video_element) tags.
27 | - [Asset Management](https://cloudinary.com/documentation/node_asset_administration).
28 | - [Secure URLs](https://cloudinary.com/documentation/video_manipulation_and_delivery#generating_secure_https_urls_using_sdks).
29 |
30 |
31 |
32 | ## Version Support
33 | | SDK Version | Node version |
34 | |-------------|--------------|
35 | | 1.x.x | Node@6 & up |
36 | | 2.x.x | Node@9 & up |
37 |
38 | ## Installation
39 | ```bash
40 | npm install cloudinary
41 | ```
42 |
43 | # Usage
44 | ### Setup
45 | ```js
46 | // Require the Cloudinary library
47 | const cloudinary = require('cloudinary').v2
48 | ```
49 |
50 | ### Transform and Optimize Assets
51 | - [See full documentation](https://cloudinary.com/documentation/node_image_manipulation).
52 |
53 | ```js
54 | cloudinary.url("sample.jpg", {width: 100, height: 150, crop: "fill", fetch_format: "auto"})
55 | ```
56 |
57 | ### Upload
58 | - [See full documentation](https://cloudinary.com/documentation/node_image_and_video_upload).
59 | - [Learn more about configuring your uploads with upload presets](https://cloudinary.com/documentation/upload_presets).
60 | ```js
61 | cloudinary.v2.uploader.upload("/home/my_image.jpg", {upload_preset: "my_preset"}, (error, result)=>{
62 | console.log(result, error);
63 | });
64 | ```
65 | ### Large/Chunked Upload
66 | - [See full documentation](https://cloudinary.com/documentation/node_image_and_video_upload#node_js_video_upload).
67 | ```js
68 | cloudinary.v2.uploader.upload_large(LARGE_RAW_FILE, {
69 | chunk_size: 7000000
70 | }, (error, result) => {console.log(error)});
71 | ```
72 | ### Security options
73 | - [See full documentation](https://cloudinary.com/documentation/solution_overview#security).
74 |
75 | ## Contributions
76 | - Ensure tests run locally (add test command)
77 | - Open a PR and ensure Travis tests pass
78 |
79 |
80 | ## Get Help
81 | If you run into an issue or have a question, you can either:
82 | - Issues related to the SDK: [Open a Github issue](https://github.com/cloudinary/cloudinary_npm/issues).
83 | - Issues related to your account: [Open a support ticket](https://cloudinary.com/contact)
84 |
85 |
86 | ## About Cloudinary
87 | Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive and personalized visual-media experiences—irrespective of the viewing device.
88 |
89 |
90 | ## Additional Resources
91 | - [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): Comprehensive references, including syntax and examples for all SDKs.
92 | - [MediaJams.dev](https://mediajams.dev/): Bite-size use-case tutorials written by and for Cloudinary Developers
93 | - [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on YouTube.
94 | - [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and on-site courses.
95 | - [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop for all code explorers, Postman collections, and feature demos found in the docs.
96 | - [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should develop next.
97 | - [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to other Cloudinary developers.
98 | - [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration.
99 | - [Cloudinary Website](https://cloudinary.com): Learn about Cloudinary's products, partners, customers, pricing, and more.
100 |
101 |
102 | ## Licence
103 | Released under the MIT license.
104 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const presets = [
2 | [
3 | [
4 | "env",
5 | {
6 | targets: { node: "4" }
7 | }
8 | ],
9 | "stage-0"
10 | ]
11 | ];
12 | const plugins = ["transform-object-rest-spread"];
13 |
14 | module.exports = { presets, plugins };
15 |
--------------------------------------------------------------------------------
/cloudinary.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/cloudinary');
2 |
--------------------------------------------------------------------------------
/lib/analysis/index.js:
--------------------------------------------------------------------------------
1 | const utils = require("../utils");
2 | const {call_analysis_api} = require('../api_client/call_analysis_api');
3 |
4 | function analyze_uri(uri, analysis_type, options = {}, callback) {
5 | const params = {
6 | uri,
7 | analysis_type
8 | }
9 |
10 | if (analysis_type === 'custom') {
11 | if (!('model_name' in options) || !('model_version' in options)) {
12 | throw new Error('Setting analysis_type to "custom" requires additional params: "model_name" and "model_version"');
13 | }
14 | params.parameters = {
15 | custom: {
16 | model_name: options.model_name,
17 | model_version: options.model_version
18 | }
19 | }
20 | }
21 |
22 | let api_uri = ['analysis', 'analyze', 'uri'];
23 | return call_analysis_api('POST', api_uri, params, callback, options);
24 | }
25 |
26 | module.exports = {
27 | analyze_uri
28 | };
29 |
--------------------------------------------------------------------------------
/lib/api_client/call_account_api.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/order
2 | const config = require("../config");
3 | const utils = require("../utils");
4 | const ensureOption = require('../utils/ensureOption').defaults(config());
5 | const execute_request = require('./execute_request');
6 |
7 | const { ensurePresenceOf } = utils;
8 |
9 | function call_account_api(method, uri, params, callback, options) {
10 | ensurePresenceOf({ method, uri });
11 | const cloudinary = ensureOption(options, "upload_prefix", "https://api.cloudinary.com");
12 | const account_id = ensureOption(options, "account_id");
13 | const api_url = [cloudinary, "v1_1", "provisioning", "accounts", account_id].concat(uri).join("/");
14 | const auth = {
15 | key: ensureOption(options, "provisioning_api_key"),
16 | secret: ensureOption(options, "provisioning_api_secret")
17 | };
18 |
19 | return execute_request(method, params, auth, api_url, callback, options);
20 | }
21 |
22 | module.exports = call_account_api;
23 |
--------------------------------------------------------------------------------
/lib/api_client/call_analysis_api.js:
--------------------------------------------------------------------------------
1 | const utils = require("../utils");
2 | const config = require("../config");
3 | const ensureOption = require('../utils/ensureOption').defaults(config());
4 | const execute_request = require("./execute_request");
5 |
6 | const {ensurePresenceOf} = utils;
7 |
8 | function call_analysis_api(method, uri, params, callback, options) {
9 | ensurePresenceOf({
10 | method,
11 | uri
12 | });
13 | const api_url = utils.base_api_url_v2()(uri, options);
14 | let auth = {};
15 | if (options.oauth_token || config().oauth_token) {
16 | auth = {
17 | oauth_token: ensureOption(options, "oauth_token")
18 | };
19 | } else {
20 | auth = {
21 | key: ensureOption(options, "api_key"),
22 | secret: ensureOption(options, "api_secret")
23 | };
24 | }
25 | options.content_type = 'json';
26 |
27 | return execute_request(method, params, auth, api_url, callback, options);
28 | }
29 |
30 | module.exports = {
31 | call_analysis_api
32 | };
33 |
--------------------------------------------------------------------------------
/lib/api_client/call_api.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/order
2 | const config = require("../config");
3 | const utils = require("../utils");
4 | const ensureOption = require('../utils/ensureOption').defaults(config());
5 | const execute_request = require('./execute_request');
6 |
7 | const { ensurePresenceOf } = utils;
8 |
9 | function call_api(method, uri, params, callback, options) {
10 | ensurePresenceOf({ method, uri });
11 | const api_url = utils.base_api_url_v1()(uri, options);
12 | let auth = {};
13 | if (options.oauth_token || config().oauth_token){
14 | auth = {
15 | oauth_token: ensureOption(options, "oauth_token")
16 | };
17 | } else {
18 | auth = {
19 | key: ensureOption(options, "api_key"),
20 | secret: ensureOption(options, "api_secret")
21 | };
22 | }
23 | return execute_request(method, params, auth, api_url, callback, options);
24 | }
25 |
26 | module.exports = call_api;
27 |
--------------------------------------------------------------------------------
/lib/auth_token.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Authorization Token
3 | * @module auth_token
4 | */
5 |
6 | const crypto = require('crypto');
7 | const smart_escape = require('./utils/encoding/smart_escape');
8 |
9 | const unsafe = /([ "#%&'/:;<=>?@[\]^`{|}~]+)/g;
10 |
11 | function digest(message, key) {
12 | return crypto.createHmac("sha256", Buffer.from(key, "hex")).update(message).digest('hex');
13 | }
14 |
15 | /**
16 | * Escape url using lowercase hex code
17 | * @param {string} url a url string
18 | * @return {string} escaped url
19 | */
20 | function escapeToLower(url) {
21 | const safeUrl = smart_escape(url, unsafe);
22 | return safeUrl.replace(/%../g, function (match) {
23 | return match.toLowerCase();
24 | });
25 | }
26 |
27 | /**
28 | * Auth token options
29 | * @typedef {object} authTokenOptions
30 | * @property {string} [token_name="__cld_token__"] The name of the token.
31 | * @property {string} key The secret key required to sign the token.
32 | * @property {string} ip The IP address of the client.
33 | * @property {number} start_time=now The start time of the token in seconds from epoch.
34 | * @property {string} expiration The expiration time of the token in seconds from epoch.
35 | * @property {string} duration The duration of the token (from start_time).
36 | * @property {string|Array} acl The ACL(s) for the token.
37 | * @property {string} url The URL to authentication in case of a URL token.
38 | *
39 | */
40 |
41 | /**
42 | * Generate an authorization token
43 | * @param {authTokenOptions} options
44 | * @returns {string} the authorization token
45 | */
46 | module.exports = function (options) {
47 | const tokenName = options.token_name ? options.token_name : "__cld_token__";
48 | const tokenSeparator = "~";
49 | if (options.expiration == null) {
50 | if (options.duration != null) {
51 | let start = options.start_time != null ? options.start_time : Math.round(Date.now() / 1000);
52 | options.expiration = start + options.duration;
53 | } else {
54 | throw new Error("Must provide either expiration or duration");
55 | }
56 | }
57 | let tokenParts = [];
58 | if (options.ip != null) {
59 | tokenParts.push(`ip=${options.ip}`);
60 | }
61 | if (options.start_time != null) {
62 | tokenParts.push(`st=${options.start_time}`);
63 | }
64 | tokenParts.push(`exp=${options.expiration}`);
65 | if (options.acl != null) {
66 | if (Array.isArray(options.acl) === true) {
67 | options.acl = options.acl.join("!");
68 | }
69 | tokenParts.push(`acl=${escapeToLower(options.acl)}`);
70 | }
71 | let toSign = [...tokenParts];
72 | if (options.url != null && options.acl == null) {
73 | let url = escapeToLower(options.url);
74 | toSign.push(`url=${url}`);
75 | }
76 | let auth = digest(toSign.join(tokenSeparator), options.key);
77 | tokenParts.push(`hmac=${auth}`);
78 |
79 | if (!options.url && !options.acl) {
80 | throw 'authToken must contain either an acl or a url property'
81 | }
82 |
83 | return `${tokenName}=${tokenParts.join(tokenSeparator)}`;
84 | };
85 |
--------------------------------------------------------------------------------
/lib/cache.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 |
3 | const CACHE = Symbol.for("com.cloudinary.cache");
4 | const CACHE_ADAPTER = Symbol.for("com.cloudinary.cacheAdapter");
5 | const { ensurePresenceOf, generate_transformation_string } = require('./utils');
6 |
7 | /**
8 | * The adapter used to communicate with the underlying cache storage
9 | */
10 | class CacheAdapter {
11 | /**
12 | * Get a value from the cache
13 | * @param {string} publicId
14 | * @param {string} type
15 | * @param {string} resourceType
16 | * @param {string} transformation
17 | * @param {string} format
18 | * @return {*} the value associated with the provided arguments
19 | */
20 | get(publicId, type, resourceType, transformation, format) {}
21 |
22 | /**
23 | * Set a new value in the cache
24 | * @param {string} publicId
25 | * @param {string} type
26 | * @param {string} resourceType
27 | * @param {string} transformation
28 | * @param {string} format
29 | * @param {*} value
30 | */
31 | set(publicId, type, resourceType, transformation, format, value) {}
32 |
33 | /**
34 | * Delete all values in the cache
35 | */
36 | flushAll() {}
37 | }
38 | /**
39 | * @class Cache
40 | * Stores and retrieves values identified by publicId / options pairs
41 | */
42 | const Cache = {
43 | /**
44 | * The adapter interface. Extend this class to implement a specific adapter.
45 | * @type CacheAdapter
46 | */
47 | CacheAdapter,
48 | /**
49 | * Set the cache adapter
50 | * @param {CacheAdapter} adapter The cache adapter
51 | */
52 | setAdapter(adapter) {
53 | if (this.adapter) {
54 | console.warn("Overriding existing cache adapter");
55 | }
56 | this.adapter = adapter;
57 | },
58 | /**
59 | * Get the adapter the Cache is using
60 | * @return {CacheAdapter} the current cache adapter
61 | */
62 | getAdapter() {
63 | return this.adapter;
64 | },
65 | /**
66 | * Get an item from the cache
67 | * @param {string} publicId
68 | * @param {object} options
69 | * @return {*}
70 | */
71 | get(publicId, options) {
72 | if (!this.adapter) { return undefined; }
73 | ensurePresenceOf({ publicId });
74 | let transformation = generate_transformation_string({ ...options });
75 | return this.adapter.get(
76 | publicId, options.type || 'upload',
77 | options.resource_type || 'image',
78 | transformation,
79 | options.format
80 | );
81 | },
82 | /**
83 | * Set a new value in the cache
84 | * @param {string} publicId
85 | * @param {object} options
86 | * @param {*} value
87 | * @return {*}
88 | */
89 | set(publicId, options, value) {
90 | if (!this.adapter) { return undefined; }
91 | ensurePresenceOf({ publicId, value });
92 | let transformation = generate_transformation_string({ ...options });
93 | return this.adapter.set(
94 | publicId,
95 | options.type || 'upload',
96 | options.resource_type || 'image',
97 | transformation,
98 | options.format,
99 | value
100 | );
101 | },
102 | /**
103 | * Clear all items in the cache
104 | * @return {*} Returns the value from the adapter's flushAll() method
105 | */
106 | flushAll() {
107 | if (!this.adapter) { return undefined; }
108 | return this.adapter.flushAll();
109 | }
110 |
111 | };
112 |
113 | // Define singleton property
114 | Object.defineProperty(Cache, "instance", {
115 | get() {
116 | return global[CACHE];
117 | }
118 | });
119 | Object.defineProperty(Cache, "adapter", {
120 | /**
121 | *
122 | * @return {CacheAdapter} The current cache adapter
123 | */
124 | get() {
125 | return global[CACHE_ADAPTER];
126 | },
127 | /**
128 | * Set the cache adapter to be used by Cache
129 | * @param {CacheAdapter} adapter Cache adapter
130 | */
131 | set(adapter) {
132 | global[CACHE_ADAPTER] = adapter;
133 | }
134 | });
135 | Object.freeze(Cache);
136 |
137 | // Instantiate the singleton
138 | let symbols = Object.getOwnPropertySymbols(global);
139 | if (symbols.indexOf(CACHE) < 0) {
140 | global[CACHE] = Cache;
141 | }
142 |
143 | /**
144 | * Store key value pairs
145 |
146 | */
147 | module.exports = Cache;
148 |
--------------------------------------------------------------------------------
/lib/cache/FileKeyValueStorage.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const rimraf = require('../utils/rimraf');
4 |
5 | class FileKeyValueStorage {
6 | constructor({ baseFolder } = {}) {
7 | this.init(baseFolder);
8 | }
9 |
10 | init(baseFolder) {
11 | if (baseFolder) {
12 | try {
13 | fs.accessSync(baseFolder);
14 | this.baseFolder = baseFolder;
15 | } catch (err) {
16 | throw err;
17 | }
18 | } else {
19 | if (!fs.existsSync('test_cache')) {
20 | fs.mkdirSync('test_cache');
21 | }
22 | this.baseFolder = fs.mkdtempSync('test_cache/cloudinary_cache_');
23 | console.info("Created temporary cache folder at " + this.baseFolder);
24 | }
25 | }
26 |
27 | get(key) {
28 | let value = fs.readFileSync(this.getFilename(key));
29 | try {
30 | return JSON.parse(value);
31 | } catch (e) {
32 | throw "Cannot parse cache value";
33 | }
34 | }
35 |
36 | set(key, value) {
37 | fs.writeFileSync(this.getFilename(key), JSON.stringify(value));
38 | }
39 |
40 | clear() {
41 | let files = fs.readdirSync(this.baseFolder);
42 | files.forEach(file => fs.unlinkSync(path.join(this.baseFolder, file)));
43 | }
44 |
45 | deleteBaseFolder() {
46 | rimraf(this.baseFolder);
47 | }
48 |
49 | getFilename(key) {
50 | return path.format({ name: key, base: key, ext: '.json', dir: this.baseFolder });
51 | }
52 | }
53 |
54 | module.exports = FileKeyValueStorage;
55 |
--------------------------------------------------------------------------------
/lib/cache/KeyValueCacheAdapter.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const CacheAdapter = require('../cache').CacheAdapter;
3 |
4 | /**
5 | *
6 | */
7 | class KeyValueCacheAdapter extends CacheAdapter {
8 | constructor(storage) {
9 | super();
10 | this.storage = storage;
11 | }
12 |
13 | /** @inheritDoc */
14 | get(publicId, type, resourceType, transformation, format) {
15 | let key = KeyValueCacheAdapter.generateCacheKey(publicId, type, resourceType, transformation, format);
16 | return KeyValueCacheAdapter.extractData(this.storage.get(key));
17 | }
18 |
19 | /** @inheritDoc */
20 | set(publicId, type, resourceType, transformation, format, value) {
21 | let key = KeyValueCacheAdapter.generateCacheKey(publicId, type, resourceType, transformation, format);
22 | this.storage.set(
23 | key,
24 | KeyValueCacheAdapter.prepareData(
25 | publicId,
26 | type,
27 | resourceType,
28 | transformation,
29 | format,
30 | value
31 | )
32 | );
33 | }
34 |
35 | /** @inheritDoc */
36 | flushAll() {
37 | this.storage.clear();
38 | }
39 |
40 | /** @inheritDoc */
41 | delete(publicId, type, resourceType, transformation, format) {
42 | let key = KeyValueCacheAdapter.generateCacheKey(publicId, type, resourceType, transformation, format);
43 | return this.storage.delete(key);
44 | }
45 |
46 | static generateCacheKey(publicId, type, resourceType, transformation, format) {
47 | type = type || "upload";
48 | resourceType = resourceType || "image";
49 | let sha1 = crypto.createHash('sha1');
50 | return sha1.update([publicId, type, resourceType, transformation, format].filter(i => i).join('/')).digest('hex');
51 | }
52 |
53 | static prepareData(publicId, type, resourceType, transformation, format, data) {
54 | return { publicId, type, resourceType, transformation, format, breakpoints: data };
55 | }
56 |
57 | static extractData(data) {
58 | return data ? data.breakpoints : null;
59 | }
60 | }
61 |
62 | module.exports = KeyValueCacheAdapter;
63 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Assign a value to a nested object
3 | * @function putNestedValue
4 | * @param params the parent object - this argument will be modified!
5 | * @param key key in the form nested[innerkey]
6 | * @param value the value to assign
7 | * @return the modified params object
8 | */
9 | const url = require('url');
10 | const extend = require("lodash/extend");
11 | const isObject = require("lodash/isObject");
12 | const isString = require("lodash/isString");
13 | const isUndefined = require("lodash/isUndefined");
14 | const isEmpty = require("lodash/isEmpty");
15 | const entries = require('./utils/entries');
16 |
17 | let cloudinary_config = void 0;
18 |
19 | /**
20 | * Sets a value in an object using a nested key
21 | * @param {object} params The object to assign the value in.
22 | * @param {string} key The key of the value. A period is used to denote inner keys.
23 | * @param {*} value The value to set.
24 | * @returns {object} The params argument.
25 | * @example
26 | * let o = {foo: {bar: 1}};
27 | * putNestedValue(o, 'foo.bar', 2); // {foo: {bar: 2}}
28 | * putNestedValue(o, 'foo.inner.key', 'this creates an inner object');
29 | * // {{foo: {bar: 2}, inner: {key: 'this creates an inner object'}}}
30 | */
31 | function putNestedValue(params, key, value) {
32 | let chain = key.split(/[\[\]]+/).filter(i => i.length);
33 | let outer = params;
34 | let lastKey = chain.pop();
35 | for (let j = 0; j < chain.length; j++) {
36 | let innerKey = chain[j];
37 | let inner = outer[innerKey];
38 | if (inner == null) {
39 | inner = {};
40 | outer[innerKey] = inner;
41 | }
42 | outer = inner;
43 | }
44 | outer[lastKey] = value;
45 | return params;
46 | }
47 |
48 | function parseCloudinaryConfigFromEnvURL(ENV_STR) {
49 | let conf = {};
50 |
51 | let uri = url.parse(ENV_STR, true);
52 |
53 | if (uri.protocol === 'cloudinary:') {
54 | conf = Object.assign({}, conf, {
55 | cloud_name: uri.host,
56 | api_key: uri.auth && uri.auth.split(":")[0],
57 | api_secret: uri.auth && uri.auth.split(":")[1],
58 | private_cdn: uri.pathname != null,
59 | secure_distribution: uri.pathname && uri.pathname.substring(1)
60 | });
61 | } else if (uri.protocol === 'account:') {
62 | conf = Object.assign({}, conf, {
63 | account_id: uri.host,
64 | provisioning_api_key: uri.auth && uri.auth.split(":")[0],
65 | provisioning_api_secret: uri.auth && uri.auth.split(":")[1]
66 | });
67 | }
68 |
69 | return conf;
70 | }
71 |
72 | function extendCloudinaryConfigFromQuery(ENV_URL, confToExtend = {}) {
73 | let uri = url.parse(ENV_URL, true);
74 | if (uri.query != null) {
75 | entries(uri.query).forEach(([key, value]) => putNestedValue(confToExtend, key, value));
76 | }
77 | }
78 |
79 | function extendCloudinaryConfig(parsedConfig, confToExtend = {}) {
80 | entries(parsedConfig).forEach(([key, value]) => {
81 | if (value !== undefined) {
82 | confToExtend[key] = value;
83 | }
84 | });
85 |
86 | return confToExtend;
87 | }
88 |
89 | module.exports = function (new_config, new_value) {
90 | if ((cloudinary_config == null) || new_config === true) {
91 | if (cloudinary_config == null) {
92 | cloudinary_config = {};
93 | } else {
94 | Object.keys(cloudinary_config).forEach(key => delete cloudinary_config[key]);
95 | }
96 |
97 | let CLOUDINARY_ENV_URL = process.env.CLOUDINARY_URL;
98 | let CLOUDINARY_ENV_ACCOUNT_URL = process.env.CLOUDINARY_ACCOUNT_URL;
99 | let CLOUDINARY_API_PROXY = process.env.CLOUDINARY_API_PROXY;
100 |
101 | if (CLOUDINARY_ENV_URL && !CLOUDINARY_ENV_URL.toLowerCase().startsWith('cloudinary://')) {
102 | throw new Error("Invalid CLOUDINARY_URL protocol. URL should begin with 'cloudinary://'");
103 | }
104 | if (CLOUDINARY_ENV_ACCOUNT_URL && !CLOUDINARY_ENV_ACCOUNT_URL.toLowerCase().startsWith('account://')) {
105 | throw new Error("Invalid CLOUDINARY_ACCOUNT_URL protocol. URL should begin with 'account://'");
106 | }
107 | if (!isEmpty(CLOUDINARY_API_PROXY)) {
108 | extendCloudinaryConfig({ api_proxy: CLOUDINARY_API_PROXY }, cloudinary_config);
109 | }
110 |
111 | [CLOUDINARY_ENV_URL, CLOUDINARY_ENV_ACCOUNT_URL].forEach((ENV_URL) => {
112 | if (ENV_URL) {
113 | let parsedConfig = parseCloudinaryConfigFromEnvURL(ENV_URL);
114 | extendCloudinaryConfig(parsedConfig, cloudinary_config);
115 | // Provide Query support in ENV url cloudinary://key:secret@test123?foo[bar]=value
116 | // expect(cloudinary_config.foo.bar).to.eql('value')
117 | extendCloudinaryConfigFromQuery(ENV_URL, cloudinary_config);
118 | }
119 | });
120 | }
121 | if (!isUndefined(new_value)) {
122 | cloudinary_config[new_config] = new_value;
123 | } else if (isString(new_config)) {
124 | return cloudinary_config[new_config];
125 | } else if (isObject(new_config)) {
126 | extend(cloudinary_config, new_config);
127 | }
128 | return cloudinary_config;
129 | };
130 |
--------------------------------------------------------------------------------
/lib/preloaded_file.js:
--------------------------------------------------------------------------------
1 | let PRELOADED_CLOUDINARY_PATH, config, utils;
2 |
3 | utils = require("./utils");
4 |
5 | config = require("./config");
6 |
7 | PRELOADED_CLOUDINARY_PATH = /^([^\/]+)\/([^\/]+)\/v(\d+)\/([^#]+)#([^\/]+)$/;
8 |
9 | class PreloadedFile {
10 | constructor(file_info) {
11 | let matches, public_id_and_format;
12 | matches = file_info.match(PRELOADED_CLOUDINARY_PATH);
13 | if (!matches) {
14 | throw "Invalid preloaded file info";
15 | }
16 | this.resource_type = matches[1];
17 | this.type = matches[2];
18 | this.version = matches[3];
19 | this.filename = matches[4];
20 | this.signature = matches[5];
21 | public_id_and_format = PreloadedFile.split_format(this.filename);
22 | this.public_id = public_id_and_format[0];
23 | this.format = public_id_and_format[1];
24 | }
25 |
26 | is_valid() {
27 | let expected_signature;
28 | expected_signature = utils.api_sign_request({
29 | public_id: this.public_id,
30 | version: this.version
31 | }, config().api_secret);
32 | return this.signature === expected_signature;
33 | }
34 |
35 | static split_format(identifier) {
36 | let format, last_dot, public_id;
37 | last_dot = identifier.lastIndexOf(".");
38 | if (last_dot === -1) {
39 | return [identifier, null];
40 | }
41 | public_id = identifier.substr(0, last_dot);
42 | format = identifier.substr(last_dot + 1);
43 | return [public_id, format];
44 | }
45 |
46 | identifier() {
47 | return `v${this.version}/${this.filename}`;
48 | }
49 |
50 | toString() {
51 | return `${this.resource_type}/${this.type}/v${this.version}/${this.filename}#${this.signature}`;
52 | }
53 |
54 | toJSON() {
55 | let result = {};
56 | Object.getOwnPropertyNames(this).forEach((key) => {
57 | let val = this[key];
58 | if (typeof val !== 'function') {
59 | result[key] = val;
60 | }
61 | });
62 | return result;
63 | }
64 | }
65 |
66 | module.exports = PreloadedFile;
67 |
--------------------------------------------------------------------------------
/lib/upload_stream.js:
--------------------------------------------------------------------------------
1 |
2 | const Transform = require("stream").Transform;
3 |
4 | class UploadStream extends Transform {
5 | constructor(options) {
6 | super();
7 | this.boundary = options.boundary;
8 | }
9 |
10 | _transform(data, encoding, next) {
11 | let buffer = ((Buffer.isBuffer(data)) ? data : Buffer.from(data, encoding));
12 | this.push(buffer);
13 | next();
14 | }
15 |
16 | _flush(next) {
17 | this.push(Buffer.from("\r\n", 'ascii'));
18 | this.push(Buffer.from("--" + this.boundary + "--", 'ascii'));
19 | return next();
20 | }
21 | }
22 |
23 | module.exports = UploadStream;
24 |
--------------------------------------------------------------------------------
/lib/utils/analytics/encodeVersion.js:
--------------------------------------------------------------------------------
1 | const reverseVersion = require('./reverseVersion');
2 | const stringPad = require('./stringPad');
3 | const base64Map = require('../encoding/base64Map');
4 |
5 | /**
6 | * @private
7 | * @description Encodes a semVer-like version string
8 | * @param {string} semVer Input can be either x.y.z or x.y
9 | * @return {string} A string built from 3 characters of the base64 table that encode the semVer
10 | */
11 | module.exports = (semVer) => {
12 | let strResult = '';
13 |
14 | // support x.y or x.y.z by using 'parts' as a variable
15 | let parts = semVer.split('.').length;
16 | let paddedStringLength = parts * 6; // we pad to either 12 or 18 characters
17 |
18 | // reverse (but don't mirror) the version. 1.5.15 -> 15.5.1
19 | // Pad to two spaces, 15.5.1 -> 15.05.01
20 | let paddedReversedSemver = reverseVersion(semVer);
21 |
22 | // turn 15.05.01 to a string '150501' then to a number 150501
23 | let num = parseInt(paddedReversedSemver.split('.').join(''));
24 |
25 | // Represent as binary, add left padding to 12 or 18 characters.
26 | // 150,501 -> 100100101111100101
27 |
28 | let paddedBinary = num.toString(2);
29 | paddedBinary = stringPad(paddedBinary, paddedStringLength, '0');
30 |
31 | // Stop in case an invalid version number was provided
32 | // paddedBinary must be built from sections of 6 bits
33 | if (paddedBinary.length % 6 !== 0) {
34 | throw 'Version must be smaller than 43.21.26)';
35 | }
36 |
37 | // turn every 6 bits into a character using the base64Map
38 | paddedBinary.match(/.{1,6}/g).forEach((bitString) => {
39 | // console.log(bitString);
40 | strResult += base64Map[bitString];
41 | });
42 |
43 | return strResult;
44 | };
45 |
--------------------------------------------------------------------------------
/lib/utils/analytics/getSDKVersions.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const sdkCode = 'M'; // Constant per SDK
4 |
5 | function readSdkSemver() {
6 | const pkgJsonPath = path.join(__dirname, '../../../package.json');
7 | try {
8 | const pkgJSONFile = fs.readFileSync(pkgJsonPath, 'utf-8');
9 | return JSON.parse(pkgJSONFile).version
10 | } catch (e) {
11 | if (e.code === 'ENOENT') {
12 | return '0.0.0'
13 | }
14 | return 'n/a';
15 | }
16 | }
17 |
18 | /**
19 | * @description Gets the relevant versions of the SDK(package version, node version and sdkCode)
20 | * @param {'default' | 'x.y.z' | 'x.y' | string} useSDKVersion Default uses package.json version
21 | * @param {'default' | 'x.y.z' | 'x.y' | string} useNodeVersion Default uses process.versions.node
22 | * @return {{sdkSemver:string, techVersion:string, sdkCode:string}} A map of relevant versions and codes
23 | */
24 | function getSDKVersions(useSDKVersion = 'default', useNodeVersion = 'default') {
25 | // allow to pass a custom SDKVersion
26 | const sdkSemver = useSDKVersion === 'default' ? readSdkSemver() : useSDKVersion;
27 |
28 | // allow to pass a custom techVersion (Node version)
29 | const version = process.version.slice(1);
30 | const techVersion = useNodeVersion === 'default' ? version : useNodeVersion;
31 |
32 | const product = 'A';
33 |
34 | return {
35 | sdkSemver,
36 | techVersion,
37 | sdkCode,
38 | product
39 | };
40 | }
41 |
42 | module.exports = getSDKVersions;
43 |
--------------------------------------------------------------------------------
/lib/utils/analytics/index.js:
--------------------------------------------------------------------------------
1 | const removePatchFromSemver = require('./removePatchFromSemver');
2 | const encodeVersion = require('./encodeVersion');
3 |
4 | /**
5 | * @description Gets the SDK signature by encoding the SDK version and tech version
6 | * @param {{
7 | * [techVersion]:string,
8 | * [sdkSemver]: string,
9 | * [sdkCode]: string,
10 | * [product]: string,
11 | * [feature]: string
12 | * }} analyticsOptions
13 | * @return {string} sdkAnalyticsSignature
14 | */
15 | function getSDKAnalyticsSignature(analyticsOptions = {}) {
16 | try {
17 | const twoPartVersion = removePatchFromSemver(analyticsOptions.techVersion);
18 | const encodedSDKVersion = encodeVersion(analyticsOptions.sdkSemver);
19 | const encodedTechVersion = encodeVersion(twoPartVersion);
20 | const featureCode = analyticsOptions.feature;
21 | const SDKCode = analyticsOptions.sdkCode;
22 | const product = analyticsOptions.product;
23 | const algoVersion = 'B'; // The algo version is determined here, it should not be an argument
24 |
25 | return `${algoVersion}${product}${SDKCode}${encodedSDKVersion}${encodedTechVersion}${featureCode}`;
26 | } catch (e) {
27 | // Either SDK or Node versions were unparsable
28 | return 'E';
29 | }
30 | }
31 |
32 | /**
33 | * @description Gets the analyticsOptions from options - should include sdkSemver, techVersion, sdkCode, and feature
34 | * @param options
35 | * @returns {{sdkSemver: (string), sdkCode, product, feature: string, techVersion: (string)} || {}}
36 | */
37 | function getAnalyticsOptions(options) {
38 | let analyticsOptions = {
39 | sdkSemver: options.sdkSemver,
40 | techVersion: options.techVersion,
41 | sdkCode: options.sdkCode,
42 | product: options.product,
43 | feature: '0'
44 | };
45 | if (options.urlAnalytics) {
46 | if (options.accessibility) {
47 | analyticsOptions.feature = 'D';
48 | }
49 | if (options.loading === 'lazy') {
50 | analyticsOptions.feature = 'C';
51 | }
52 | if (options.responsive) {
53 | analyticsOptions.feature = 'A';
54 | }
55 | if (options.placeholder) {
56 | analyticsOptions.feature = 'B';
57 | }
58 | return analyticsOptions;
59 | } else {
60 | return {};
61 | }
62 | }
63 |
64 | module.exports = {
65 | getSDKAnalyticsSignature,
66 | getAnalyticsOptions
67 | };
68 |
--------------------------------------------------------------------------------
/lib/utils/analytics/removePatchFromSemver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Removes patch version from the semver if it exists
3 | * Turns x.y.z OR x.y into x.y
4 | * @param {'x.y.z' || 'x.y' || string} semVerStr
5 | */
6 | module.exports = (semVerStr) => {
7 | let parts = semVerStr.split('.');
8 | return `${parts[0]}.${parts[1]}`;
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/analytics/reverseVersion.js:
--------------------------------------------------------------------------------
1 | const stringPad = require('./stringPad');
2 |
3 | /**
4 | * @description A semVer like string, x.y.z or x.y is allowed
5 | * Reverses the version positions, x.y.z turns to z.y.x
6 | * Pads each segment with '0' so they have length of 2
7 | * Example: 1.2.3 -> 03.02.01
8 | * @param {string} semVer Input can be either x.y.z or x.y
9 | * @return {string} in the form of zz.yy.xx (
10 | */
11 | module.exports = (semVer) => {
12 | if (semVer.split('.').length < 2) {
13 | throw new Error('invalid semVer, must have at least two segments');
14 | }
15 |
16 | // Split by '.', reverse, create new array with padded values and concat it together
17 | return semVer.split('.').reverse().map((segment) => {
18 | return stringPad(segment, 2, '0');
19 | }).join('.');
20 | };
21 |
--------------------------------------------------------------------------------
/lib/utils/analytics/stringPad.js:
--------------------------------------------------------------------------------
1 | function repeatStringNumTimes(string, times) {
2 | let repeatedString = "";
3 | while (times > 0) {
4 | repeatedString += string;
5 | times--;
6 | }
7 | return repeatedString;
8 | }
9 |
10 | module.exports = (value, targetLength, padString) => {
11 | targetLength = targetLength >> 0; // truncate if number or convert non-number to 0;
12 | padString = String((typeof padString !== 'undefined' ? padString : ' '));
13 | if (value.length > targetLength) {
14 | return String(value);
15 | } else {
16 | targetLength = targetLength - value.length;
17 | if (targetLength > padString.length) {
18 | padString += repeatStringNumTimes(padString, targetLength / padString.length);
19 | }
20 | return padString.slice(0, targetLength) + String(value);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/utils/consts.js:
--------------------------------------------------------------------------------
1 | const DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = {
2 | width: "auto",
3 | crop: "limit"
4 | };
5 |
6 | const DEFAULT_POSTER_OPTIONS = {
7 | format: 'jpg',
8 | resource_type: 'video'
9 | };
10 |
11 | const DEFAULT_VIDEO_SOURCE_TYPES = ['webm', 'mp4', 'ogv'];
12 |
13 | const CONDITIONAL_OPERATORS = {
14 | "=": 'eq',
15 | "!=": 'ne',
16 | "<": 'lt',
17 | ">": 'gt',
18 | "<=": 'lte',
19 | ">=": 'gte',
20 | "&&": 'and',
21 | "||": 'or',
22 | "*": "mul",
23 | "/": "div",
24 | "+": "add",
25 | "-": "sub",
26 | "^": "pow"
27 | };
28 |
29 | let SIMPLE_PARAMS = [
30 | ["audio_codec", "ac"],
31 | ["audio_frequency", "af"],
32 | ["bit_rate", 'br'],
33 | ["color_space", "cs"],
34 | ["default_image", "d"],
35 | ["delay", "dl"],
36 | ["density", "dn"],
37 | ["duration", "du"],
38 | ["end_offset", "eo"],
39 | ["fetch_format", "f"],
40 | ["gravity", "g"],
41 | ["page", "pg"],
42 | ["prefix", "p"],
43 | ["start_offset", "so"],
44 | ["streaming_profile", "sp"],
45 | ["video_codec", "vc"],
46 | ["video_sampling", "vs"]
47 | ];
48 |
49 | const PREDEFINED_VARS = {
50 | "aspect_ratio": "ar",
51 | "aspectRatio": "ar",
52 | "current_page": "cp",
53 | "currentPage": "cp",
54 | "duration": "du",
55 | "face_count": "fc",
56 | "faceCount": "fc",
57 | "height": "h",
58 | "initial_aspect_ratio": "iar",
59 | "initial_height": "ih",
60 | "initial_width": "iw",
61 | "initialAspectRatio": "iar",
62 | "initialHeight": "ih",
63 | "initialWidth": "iw",
64 | "initial_duration": "idu",
65 | "initialDuration": "idu",
66 | "page_count": "pc",
67 | "page_x": "px",
68 | "page_y": "py",
69 | "pageCount": "pc",
70 | "pageX": "px",
71 | "pageY": "py",
72 | "tags": "tags",
73 | "width": "w"
74 | };
75 |
76 | const TRANSFORMATION_PARAMS = [
77 | 'angle',
78 | 'aspect_ratio',
79 | 'audio_codec',
80 | 'audio_frequency',
81 | 'background',
82 | 'bit_rate',
83 | 'border',
84 | 'color',
85 | 'color_space',
86 | 'crop',
87 | 'default_image',
88 | 'delay',
89 | 'density',
90 | 'dpr',
91 | 'duration',
92 | 'effect',
93 | 'end_offset',
94 | 'fetch_format',
95 | 'flags',
96 | 'fps',
97 | 'gravity',
98 | 'height',
99 | 'if',
100 | 'keyframe_interval',
101 | 'offset',
102 | 'opacity',
103 | 'overlay',
104 | 'page',
105 | 'prefix',
106 | 'quality',
107 | 'radius',
108 | 'raw_transformation',
109 | 'responsive_width',
110 | 'size',
111 | 'start_offset',
112 | 'streaming_profile',
113 | 'transformation',
114 | 'underlay',
115 | 'variables',
116 | 'video_codec',
117 | 'video_sampling',
118 | 'width',
119 | 'x',
120 | 'y',
121 | 'zoom' // + any key that starts with '$'
122 | ];
123 |
124 | const LAYER_KEYWORD_PARAMS = {
125 | font_weight: "normal",
126 | font_style: "normal",
127 | text_decoration: "none",
128 | text_align: null,
129 | stroke: "none"
130 | };
131 |
132 | const UPLOAD_PREFIX = "https://api.cloudinary.com";
133 |
134 | const SUPPORTED_SIGNATURE_ALGORITHMS = ["sha1", "sha256"];
135 | const DEFAULT_SIGNATURE_ALGORITHM = "sha1";
136 |
137 | module.exports = {
138 | DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION,
139 | DEFAULT_POSTER_OPTIONS,
140 | DEFAULT_VIDEO_SOURCE_TYPES,
141 | CONDITIONAL_OPERATORS,
142 | PREDEFINED_VARS,
143 | LAYER_KEYWORD_PARAMS,
144 | TRANSFORMATION_PARAMS,
145 | SIMPLE_PARAMS,
146 | UPLOAD_PREFIX,
147 | SUPPORTED_SIGNATURE_ALGORITHMS,
148 | DEFAULT_SIGNATURE_ALGORITHM
149 | };
150 |
--------------------------------------------------------------------------------
/lib/utils/crc32.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-bitwise */
2 | // http://kevin.vanzonneveld.net
3 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
4 | // + improved by: T0bsn
5 | // + improved by: http://stackoverflow.com/questions/2647935/javascript-crc32-function-and-php-crc32-not-matching
6 | // - depends on: utf8_encode
7 | // * example 1: crc32('Kevin van Zonneveld')
8 | // * returns 1: 1249991249
9 |
10 | const utf8_encode = require('./utf8_encode');
11 |
12 | /**
13 | * Compute the crc32 checksum if the given string
14 | * @private
15 | * @param {string} str
16 | * @return {number|*}
17 | */
18 | function crc32(str) {
19 | let crc, i, iTop, table, x, y;
20 | str = utf8_encode(str);
21 | table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D";
22 | crc = 0;
23 | x = 0;
24 | y = 0;
25 | crc = crc ^ (-1);
26 | i = 0;
27 | iTop = str.length;
28 | while (i < iTop) {
29 | y = (crc ^ str.charCodeAt(i)) & 0xFF;
30 | x = "0x" + table.substr(y * 9, 8);
31 | crc = (crc >>> 8) ^ x;
32 | i++;
33 | }
34 | crc = crc ^ (-1);
35 | if (crc < 0) {
36 | crc += 4294967296;
37 | }
38 | return crc;
39 | }
40 |
41 | module.exports = crc32;
42 |
--------------------------------------------------------------------------------
/lib/utils/encoding/base64Encode.js:
--------------------------------------------------------------------------------
1 | function base64Encode(input) {
2 | if (!(input instanceof Buffer)) {
3 | input = Buffer.from(String(input), 'binary');
4 | }
5 | return input.toString('base64');
6 | }
7 |
8 | module.exports.base64Encode = base64Encode;
9 |
--------------------------------------------------------------------------------
/lib/utils/encoding/base64EncodeURL.js:
--------------------------------------------------------------------------------
1 | const { base64Encode } = require('./base64Encode')
2 |
3 | function base64EncodeURL(sourceUrl) {
4 | try {
5 | sourceUrl = decodeURI(sourceUrl);
6 | } catch (error) {
7 | // ignore errors
8 | }
9 | sourceUrl = encodeURI(sourceUrl);
10 | return base64Encode(sourceUrl)
11 | .replace(/\+/g, '-') // Convert '+' to '-'
12 | .replace(/\//g, '_') // Convert '/' to '_'
13 | .replace(/=+$/, ''); // Remove ending '=';
14 | }
15 |
16 |
17 | module.exports.base64EncodeURL = base64EncodeURL;
18 |
--------------------------------------------------------------------------------
/lib/utils/encoding/base64Map.js:
--------------------------------------------------------------------------------
1 | const stringPad = require('../analytics/stringPad');
2 |
3 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
4 | let num = 0;
5 |
6 | /**
7 | * Map of six-bit binary codes to Base64 characters
8 | */
9 | let base64Map = {};
10 |
11 | [...chars].forEach((char) => {
12 | let key = num.toString(2);
13 | key = stringPad(key, 6, '0');
14 | base64Map[key] = char;
15 | num++;
16 | });
17 |
18 | module.exports = base64Map;
19 |
--------------------------------------------------------------------------------
/lib/utils/encoding/encodeDoubleArray.js:
--------------------------------------------------------------------------------
1 | const isArray = require('lodash/isArray');
2 | const toArray = require('../parsing/toArray');
3 |
4 | /**
5 | * Serialize an array of arrays into a string
6 | * @param {string[] | Array.>} array - An array of arrays.
7 | * If the first element is not an array the argument is wrapped in an array.
8 | * @returns {string} A string representation of the arrays.
9 | */
10 | function encodeDoubleArray(array) {
11 | array = toArray(array);
12 | if (!isArray(array[0])) {
13 | array = [array];
14 | }
15 | return array.map(e => toArray(e).join(",")).join("|");
16 | }
17 |
18 | module.exports = encodeDoubleArray;
19 |
--------------------------------------------------------------------------------
/lib/utils/encoding/smart_escape.js:
--------------------------------------------------------------------------------
1 | // Based on CGI::unescape. In addition does not escape / :
2 | // smart_escape = (string) => encodeURIComponent(string).replace(/%3A/g, ":").replace(/%2F/g, "/")
3 | function smart_escape(string, unsafe = /([^a-zA-Z0-9_.\-\/:]+)/g) {
4 | return string.replace(unsafe, function (match) {
5 | return match.split("").map(function (c) {
6 | return "%" + c.charCodeAt(0).toString(16).toUpperCase();
7 | }).join("");
8 | });
9 | }
10 |
11 | module.exports = smart_escape;
12 |
--------------------------------------------------------------------------------
/lib/utils/ensureOption.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns an ensureOption function that relies on the provided `defaultOptions` argument
3 | * for default values.
4 | * @private
5 | * @param {object} defaultOptions
6 | * @return {function(*, *, *=): *}
7 | */
8 | function defaults(defaultOptions) {
9 | return function ensureOption(options, name, defaultValue) {
10 | let value;
11 |
12 | if (typeof options[name] !== 'undefined') {
13 | value = options[name];
14 | } else if (typeof defaultOptions[name] !== 'undefined') {
15 | value = defaultOptions[name];
16 | } else if (typeof defaultValue !== 'undefined') {
17 | value = defaultValue;
18 | } else {
19 | throw new Error(`Must supply ${name}`);
20 | }
21 |
22 | return value;
23 | };
24 | }
25 |
26 | /**
27 | * Get the option `name` from options, the global config, or the default value.
28 | * If the value is not defined and no default value was provided,
29 | * the method will throw an error.
30 | * @private
31 | * @param {object} options
32 | * @param {string} name
33 | * @param {*} [defaultValue]
34 | * @return {*} the value associated with the provided `name` or the default.
35 | *
36 | */
37 | module.exports = defaults({});
38 |
39 | module.exports.defaults = defaults;
40 |
--------------------------------------------------------------------------------
/lib/utils/ensurePresenceOf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Validate that the given values are defined
3 | * @private
4 | * @param {object} parameters where each key value pair is the name and value of the argument to validate.
5 | *
6 | * @example
7 | *
8 | * function foo(bar){
9 | * ensurePresenceOf({bar});
10 | * // ...
11 | * }
12 | */
13 | function ensurePresenceOf(parameters) {
14 | let missing = Object.keys(parameters).filter(key => parameters[key] === undefined);
15 | if (missing.length) {
16 | console.error(missing.join(',') + " cannot be undefined");
17 | }
18 | }
19 |
20 | module.exports = ensurePresenceOf;
21 |
--------------------------------------------------------------------------------
/lib/utils/entries.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.entries ? Object.entries : function (obj) {
2 | let ownProps = Object.keys(obj),
3 | i = ownProps.length,
4 | resArray = new Array(i); // preallocate the Array
5 | while (i--) {
6 | resArray[i] = [ownProps[i], obj[ownProps[i]]];
7 | }
8 |
9 | return resArray;
10 | };
11 |
--------------------------------------------------------------------------------
/lib/utils/generateBreakpoints.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Helper function. Gets or populates srcset breakpoints using provided parameters
4 | * Either the breakpoints or min_width, max_width, max_images must be provided.
5 | *
6 | * @module utils
7 | * @private
8 | * @param {srcset} srcset Options with either `breakpoints` or `min_width`, `max_width`, and `max_images`
9 | *
10 | * @return {number[]} Array of breakpoints
11 | *
12 | */
13 | function generateBreakpoints(srcset) {
14 | let breakpoints = srcset.breakpoints || [];
15 | if (breakpoints.length) {
16 | return breakpoints;
17 | }
18 | let [min_width, max_width, max_images] = [srcset.min_width, srcset.max_width, srcset.max_images].map(Number);
19 | if ([min_width, max_width, max_images].some(Number.isNaN)) {
20 | throw 'Either (min_width, max_width, max_images) '
21 | + 'or breakpoints must be provided to the image srcset attribute';
22 | }
23 |
24 | if (min_width > max_width) {
25 | throw 'min_width must be less than max_width';
26 | }
27 |
28 | if (max_images <= 0) {
29 | throw 'max_images must be a positive integer';
30 | } else if (max_images === 1) {
31 | min_width = max_width;
32 | }
33 |
34 | let stepSize = Math.ceil((max_width - min_width) / Math.max(max_images - 1, 1));
35 | for (let current = min_width; current < max_width; current += stepSize) {
36 | breakpoints.push(current);
37 | }
38 | breakpoints.push(max_width);
39 | return breakpoints;
40 | }
41 | module.exports = generateBreakpoints;
42 |
--------------------------------------------------------------------------------
/lib/utils/isRemoteUrl.js:
--------------------------------------------------------------------------------
1 | const isString = require('lodash/isString');
2 |
3 | /**
4 | * Checks whether a given url or path is a local file
5 | * @param {string} url the url or path to the file
6 | * @returns {boolean} true if the given url is a remote location or data
7 | */
8 | function isRemoteUrl(url) {
9 | const SUBSTRING_LENGTH = 120;
10 | const urlSubstring = isString(url) && url.substring(0, SUBSTRING_LENGTH);
11 | return isString(url) && /^ftp:|^https?:|^gs:|^s3:|^data:([\w-.]+\/[\w-.]+(\+[\w-.]+)?)?(;[\w-.]+=[\w-.]+)*;base64,([a-zA-Z0-9\/+\n=]+)$/.test(urlSubstring);
12 | }
13 |
14 | module.exports = isRemoteUrl;
15 |
--------------------------------------------------------------------------------
/lib/utils/parsing/consumeOption.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Deletes `option_name` from `options` and return the value if present.
3 | * If `options` doesn't contain `option_name` the default value is returned.
4 | * @param {Object} options a collection
5 | * @param {String} option_name the name (key) of the desired value
6 | * @param {*} [default_value] the value to return is option_name is missing
7 | */
8 |
9 | function consumeOption(options, option_name, default_value) {
10 | let result = options[option_name];
11 | delete options[option_name];
12 | return result != null ? result : default_value;
13 | }
14 |
15 | module.exports = consumeOption;
16 |
--------------------------------------------------------------------------------
/lib/utils/parsing/toArray.js:
--------------------------------------------------------------------------------
1 | const isArray = require('lodash/isArray');
2 |
3 | /**
4 | * @desc Turns arguments that aren't arrays into arrays
5 | * @param arg
6 | * @returns { any | any[] }
7 | */
8 | function toArray(arg) {
9 | switch (true) {
10 | case arg == null:
11 | return [];
12 | case isArray(arg):
13 | return arg;
14 | default:
15 | return [arg];
16 | }
17 | }
18 |
19 | module.exports = toArray;
20 |
--------------------------------------------------------------------------------
/lib/utils/rimraf.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | /**
5 | * Remove directory recursively
6 | * @param {string} dir_path
7 | * @see https://stackoverflow.com/a/42505874/3027390
8 | */
9 | function rimraf(dir_path) {
10 | if (fs.existsSync(dir_path)) {
11 | fs.readdirSync(dir_path).forEach(function (entry) {
12 | let entry_path = path.join(dir_path, entry);
13 | if (fs.lstatSync(entry_path).isDirectory()) {
14 | rimraf(entry_path);
15 | } else {
16 | fs.unlinkSync(entry_path);
17 | }
18 | });
19 | fs.rmdirSync(dir_path);
20 | }
21 | }
22 |
23 | module.exports = rimraf;
24 |
--------------------------------------------------------------------------------
/lib/utils/utf8_encode.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-bitwise */
2 | // http://kevin.vanzonneveld.net
3 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
4 | // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
5 | // + improved by: sowberry
6 | // + tweaked by: Jack
7 | // + bugfixed by: Onno Marsman
8 | // + improved by: Yves Sucaet
9 | // + bugfixed by: Onno Marsman
10 | // + bugfixed by: Ulrich
11 | // + bugfixed by: Rafal Kukawski
12 | // + improved by: kirilloid
13 | // * example 1: utf8_encode('Kevin van Zonneveld')
14 | // * returns 1: 'Kevin van Zonneveld'
15 |
16 | /**
17 | * Encode the given string
18 | * @private
19 | * @param {string} argString the string to encode
20 | * @return {string}
21 | */
22 | module.exports = function utf8_encode(argString) {
23 | let c1, enc, n;
24 | if (argString == null) {
25 | return "";
26 | }
27 | let string = argString + "";
28 | let utftext = "";
29 | let start = 0;
30 | let end = 0;
31 | let stringl = string.length;
32 | n = 0;
33 | while (n < stringl) {
34 | c1 = string.charCodeAt(n);
35 | enc = null;
36 | if (c1 < 128) {
37 | end++;
38 | } else if (c1 > 127 && c1 < 2048) {
39 | enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128);
40 | } else {
41 | enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128);
42 | }
43 | if (enc !== null) {
44 | if (end > start) {
45 | utftext += string.slice(start, end);
46 | }
47 | utftext += enc;
48 | start = n + 1;
49 | end = start;
50 | }
51 | n++;
52 | }
53 | if (end > start) {
54 | utftext += string.slice(start, stringl);
55 | }
56 | return utftext;
57 | };
58 |
--------------------------------------------------------------------------------
/lib/v2/api.js:
--------------------------------------------------------------------------------
1 | const api = require('../api');
2 | const v1_adapters = require('../utils').v1_adapters;
3 |
4 | v1_adapters(exports, api, {
5 | ping: 0,
6 | usage: 0,
7 | resource_types: 0,
8 | resources: 0,
9 | resources_by_tag: 1,
10 | resources_by_context: 2,
11 | resources_by_moderation: 2,
12 | resource_by_asset_id: 1,
13 | resources_by_asset_ids: 1,
14 | resources_by_ids: 1,
15 | resources_by_asset_folder: 1,
16 | resource: 1,
17 | restore: 1,
18 | update: 1,
19 | delete_resources: 1,
20 | delete_resources_by_prefix: 1,
21 | delete_resources_by_tag: 1,
22 | delete_all_resources: 0,
23 | delete_derived_resources: 1,
24 | tags: 0,
25 | transformations: 0,
26 | transformation: 1,
27 | delete_transformation: 1,
28 | update_transformation: 2,
29 | create_transformation: 2,
30 | upload_presets: 0,
31 | upload_preset: 1,
32 | delete_upload_preset: 1,
33 | update_upload_preset: 1,
34 | create_upload_preset: 0,
35 | root_folders: 0,
36 | sub_folders: 1,
37 | delete_folder: 1,
38 | rename_folder: 2,
39 | create_folder: 1,
40 | upload_mappings: 0,
41 | upload_mapping: 1,
42 | delete_upload_mapping: 1,
43 | update_upload_mapping: 1,
44 | create_upload_mapping: 1,
45 | list_streaming_profiles: 0,
46 | get_streaming_profile: 1,
47 | delete_streaming_profile: 1,
48 | update_streaming_profile: 1,
49 | create_streaming_profile: 1,
50 | publish_by_ids: 1,
51 | publish_by_tag: 1,
52 | publish_by_prefix: 1,
53 | update_resources_access_mode_by_prefix: 2,
54 | update_resources_access_mode_by_tag: 2,
55 | update_resources_access_mode_by_ids: 2,
56 | search: 1,
57 | search_folders: 1,
58 | visual_search: 1,
59 | delete_derived_by_transformation: 2,
60 | add_metadata_field: 1,
61 | list_metadata_fields: 1,
62 | delete_metadata_field: 1,
63 | metadata_field_by_field_id: 1,
64 | update_metadata_field: 2,
65 | update_metadata_field_datasource: 2,
66 | delete_datasource_entries: 2,
67 | restore_metadata_field_datasource: 2,
68 | order_metadata_field_datasource: 3,
69 | reorder_metadata_fields: 2,
70 | list_metadata_rules: 1,
71 | add_metadata_rule: 1,
72 | delete_metadata_rule: 1,
73 | update_metadata_rule: 2,
74 | add_related_assets: 2,
75 | add_related_assets_by_asset_id: 2,
76 | delete_related_assets: 2,
77 | delete_related_assets_by_asset_id: 2,
78 | delete_backed_up_assets: 2,
79 | config: 0
80 | });
81 |
--------------------------------------------------------------------------------
/lib/v2/index.js:
--------------------------------------------------------------------------------
1 | const v1 = require('../cloudinary');
2 | const api = require('./api');
3 | const uploader = require('./uploader');
4 | const search = require('./search');
5 | const search_folders = require('./search_folders');
6 |
7 | const v2 = {
8 | ...v1,
9 | api,
10 | uploader,
11 | search,
12 | search_folders
13 | };
14 | module.exports = v2;
15 |
--------------------------------------------------------------------------------
/lib/v2/search.js:
--------------------------------------------------------------------------------
1 | const api = require('./api');
2 | const config = require('../config');
3 | const {
4 | isEmpty,
5 | isNumber,
6 | compute_hash,
7 | build_distribution_domain,
8 | clear_blank,
9 | sort_object_by_key
10 | } = require('../utils');
11 | const {base64Encode} = require('../utils/encoding/base64Encode');
12 |
13 | const Search = class Search {
14 | constructor() {
15 | this.query_hash = {
16 | sort_by: [],
17 | aggregate: [],
18 | with_field: [],
19 | fields: []
20 | };
21 | this._ttl = 300;
22 | }
23 |
24 | static instance() {
25 | return new Search();
26 | }
27 |
28 | static expression(value) {
29 | return this.instance().expression(value);
30 | }
31 |
32 | static max_results(value) {
33 | return this.instance().max_results(value);
34 | }
35 |
36 | static next_cursor(value) {
37 | return this.instance().next_cursor(value);
38 | }
39 |
40 | static aggregate(value) {
41 | return this.instance().aggregate(value);
42 | }
43 |
44 | static with_field(value) {
45 | return this.instance().with_field(value);
46 | }
47 |
48 | static fields(value) {
49 | return this.instance().fields(value);
50 | }
51 |
52 | static sort_by(field_name, dir = 'asc') {
53 | return this.instance().sort_by(field_name, dir);
54 | }
55 |
56 | static ttl(newTtl) {
57 | return this.instance().ttl(newTtl);
58 | }
59 |
60 | static execute(options, callback) {
61 | return this.instance().execute(options, callback);
62 | }
63 |
64 | expression(value) {
65 | this.query_hash.expression = value;
66 | return this;
67 | }
68 |
69 | max_results(value) {
70 | this.query_hash.max_results = value;
71 | return this;
72 | }
73 |
74 | next_cursor(value) {
75 | this.query_hash.next_cursor = value;
76 | return this;
77 | }
78 |
79 | aggregate(value) {
80 | const found = this.query_hash.aggregate.find(v => v === value);
81 |
82 | if (!found) {
83 | this.query_hash.aggregate.push(value);
84 | }
85 |
86 | return this;
87 | }
88 |
89 | with_field(value) {
90 | if (Array.isArray(value)) {
91 | this.query_hash.with_field = this.query_hash.with_field.concat(value);
92 | } else {
93 | this.query_hash.with_field.push(value);
94 | }
95 |
96 | this.query_hash.with_field = Array.from(new Set(this.query_hash.with_field));
97 | return this;
98 | }
99 |
100 | fields(value) {
101 | if (Array.isArray(value)) {
102 | this.query_hash.fields = this.query_hash.fields.concat(value);
103 | } else {
104 | this.query_hash.fields.push(value);
105 | }
106 |
107 | this.query_hash.fields = Array.from(new Set(this.query_hash.fields));
108 | return this;
109 | }
110 |
111 | sort_by(field_name, dir = "desc") {
112 | let sort_bucket;
113 | sort_bucket = {};
114 | sort_bucket[field_name] = dir;
115 |
116 | // Check if this field name is already stored in the hash
117 | const previously_sorted_obj = this.query_hash.sort_by.find((sort_by) => sort_by[field_name]);
118 |
119 | // Since objects are references in Javascript, we can update the reference we found
120 | // For example,
121 | if (previously_sorted_obj) {
122 | previously_sorted_obj[field_name] = dir;
123 | } else {
124 | this.query_hash.sort_by.push(sort_bucket);
125 | }
126 |
127 | return this;
128 | }
129 |
130 | ttl(newTtl) {
131 | if (isNumber(newTtl)) {
132 | this._ttl = newTtl;
133 | return this;
134 | }
135 |
136 | throw new Error('New TTL value has to be a Number.');
137 | }
138 |
139 | to_query() {
140 | Object.keys(this.query_hash).forEach((k) => {
141 | let v = this.query_hash[k];
142 | if (!isNumber(v) && isEmpty(v)) {
143 | delete this.query_hash[k];
144 | }
145 | });
146 | return this.query_hash;
147 | }
148 |
149 | execute(options, callback) {
150 | if (callback === null) {
151 | callback = options;
152 | }
153 | options = options || {};
154 | return api.search(this.to_query(), options, callback);
155 | }
156 |
157 | to_url(ttl, next_cursor, options = {}) {
158 | const apiSecret = 'api_secret' in options ? options.api_secret : config().api_secret;
159 | if (!apiSecret) {
160 | throw new Error('Must supply api_secret');
161 | }
162 |
163 | const urlTtl = ttl || this._ttl;
164 |
165 | const query = this.to_query();
166 |
167 | let urlCursor = next_cursor;
168 | if (query.next_cursor && !next_cursor) {
169 | urlCursor = query.next_cursor;
170 | }
171 | delete query.next_cursor;
172 |
173 | const dataOrderedByKey = sort_object_by_key(clear_blank(query));
174 | const encodedQuery = base64Encode(JSON.stringify(dataOrderedByKey));
175 |
176 | const urlPrefix = build_distribution_domain(options.source, options);
177 |
178 | const signature = compute_hash(`${urlTtl}${encodedQuery}${apiSecret}`, 'sha256', 'hex');
179 |
180 | const urlWithoutCursor = `${urlPrefix}/search/${signature}/${urlTtl}/${encodedQuery}`;
181 | return urlCursor ? `${urlWithoutCursor}/${urlCursor}` : urlWithoutCursor;
182 | }
183 | };
184 |
185 | module.exports = Search;
186 |
--------------------------------------------------------------------------------
/lib/v2/search_folders.js:
--------------------------------------------------------------------------------
1 | const Search = require('./search');
2 | const api = require('./api');
3 |
4 | const SearchFolders = class SearchFolders extends Search {
5 | constructor() {
6 | super();
7 | }
8 |
9 | static instance() {
10 | return new SearchFolders();
11 | }
12 |
13 | execute(options, callback) {
14 | if (callback === null) {
15 | callback = options;
16 | }
17 | options = options || {};
18 | return api.search_folders(this.to_query(), options, callback);
19 | }
20 | };
21 |
22 | module.exports = SearchFolders;
23 |
--------------------------------------------------------------------------------
/lib/v2/uploader.js:
--------------------------------------------------------------------------------
1 | const uploader = require('../uploader');
2 | const v1_adapters = require('../utils').v1_adapters;
3 |
4 | v1_adapters(exports, uploader, {
5 | unsigned_upload_stream: 1,
6 | upload_stream: 0,
7 | unsigned_upload: 2,
8 | upload: 1,
9 | upload_large_part: 0,
10 | upload_large: 1,
11 | upload_chunked: 1,
12 | upload_chunked_stream: 0,
13 | explicit: 1,
14 | destroy: 1,
15 | rename: 2,
16 | text: 1,
17 | generate_sprite: 1,
18 | multi: 1,
19 | explode: 1,
20 | add_tag: 2,
21 | remove_tag: 2,
22 | remove_all_tags: 1,
23 | add_context: 2,
24 | remove_all_context: 1,
25 | replace_tag: 2,
26 | create_archive: 0,
27 | create_zip: 0,
28 | update_metadata: 2
29 | });
30 |
31 | exports.direct_upload = uploader.direct_upload;
32 | exports.upload_tag_params = uploader.upload_tag_params;
33 | exports.upload_url = uploader.upload_url;
34 | exports.image_upload_tag = uploader.image_upload_tag;
35 | exports.unsigned_image_upload_tag = uploader.unsigned_image_upload_tag;
36 | exports.create_slideshow = uploader.create_slideshow;
37 | exports.download_generated_sprite = uploader.download_generated_sprite;
38 | exports.download_multi = uploader.download_multi;
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Cloudinary ",
3 | "name": "cloudinary",
4 | "description": "Cloudinary NPM for node.js integration",
5 | "version": "2.6.1",
6 | "homepage": "https://cloudinary.com",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/cloudinary/cloudinary_npm.git"
11 | },
12 | "main": "cloudinary.js",
13 | "dependencies": {
14 | "lodash": "^4.17.21",
15 | "q": "^1.5.1"
16 | },
17 | "devDependencies": {
18 | "@types/expect.js": "^0.3.29",
19 | "@types/mocha": "^7.0.2",
20 | "@types/node": "^13.5.0",
21 | "date-fns": "^2.16.1",
22 | "dotenv": "4.x",
23 | "dtslint": "^0.9.1",
24 | "eslint": "^6.8.0",
25 | "eslint-config-airbnb-base": "^14.2.1",
26 | "eslint-plugin-import": "^2.20.2",
27 | "expect.js": "0.3.x",
28 | "glob": "^7.1.6",
29 | "jsdoc": "^4.0.4",
30 | "jsdom": "^9.12.0",
31 | "jsdom-global": "2.1.1",
32 | "mocha": "^6.2.3",
33 | "nyc": "^14.1.1",
34 | "rimraf": "^3.0.0",
35 | "sinon": "^6.1.4",
36 | "typescript": "^3.7.5",
37 | "webpack-cli": "^3.2.1"
38 | },
39 | "files": [
40 | "lib/**/*",
41 | "cloudinary.js",
42 | "babel.config.js",
43 | "package.json",
44 | "types/index.d.ts"
45 | ],
46 | "types": "types",
47 | "scripts": {
48 | "test": "tools/scripts/test.sh",
49 | "test:unit": "tools/scripts/test.es6.unit.sh",
50 | "test-with-temp-cloud": "tools/scripts/tests-with-temp-cloud.sh",
51 | "dtslint": "tools/scripts/ditslint.sh",
52 | "lint": "tools/scripts/lint.sh",
53 | "coverage": "tools/scripts/test.es6.sh --coverage",
54 | "test-es6": "tools/scripts/test.es6.sh",
55 | "docs": "tools/scripts/docs.sh"
56 | },
57 | "engines": {
58 | "node": ">=9"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/samples/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jquery": true,
4 | "browser": true
5 | },
6 | "rules": {
7 | "import/no-unresolved": "off"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/basic/lake.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/samples/basic/lake.jpg
--------------------------------------------------------------------------------
/samples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "0.0.0",
4 | "description": "Basic node sample of Cloudinary npm",
5 | "main": "basic.js",
6 | "dependencies": {
7 | "cloudinary": "^2.5.1",
8 | "dotenv": "16.x"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1",
12 | "start": "node basic.js"
13 | },
14 | "author": "",
15 | "license": "ISC"
16 | }
17 |
--------------------------------------------------------------------------------
/samples/basic/pizza.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/samples/basic/pizza.jpg
--------------------------------------------------------------------------------
/samples/photo_album/config/schema.js:
--------------------------------------------------------------------------------
1 | const Schema = require('jugglingdb').Schema;
2 |
3 | const schema = new Schema('memory');
4 | // Uncomment if you want to use mongodb adapter
5 | // const schema = new Schema('mongodb');
6 |
7 | // Define models
8 | schema.define('Photo', {
9 | title: { type: String, length: 255 },
10 | image: { type: JSON }
11 | });
12 |
13 | module.exports = schema;
14 |
--------------------------------------------------------------------------------
/samples/photo_album/env.sample:
--------------------------------------------------------------------------------
1 | PORT=9000
2 | CLOUDINARY_URL=cloudinary://xxxx
3 |
4 | # Rename this file to .env and add your Cloudinary URL
5 |
--------------------------------------------------------------------------------
/samples/photo_album/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-photo-album",
3 | "version": "0.0.0",
4 | "description": "Cloudinary Express demo",
5 | "main": "server.js",
6 | "scripts": {
7 | "dev": "nodemon -e 'js,ejs' server.js"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@cloudinary/url-gen": "^1.21.0",
13 | "cloudinary": "^2.5.1",
14 | "cloudinary-jquery-file-upload": "^2.13.1",
15 | "dotenv": "^16.4.5",
16 | "ejs": "^3.1.10",
17 | "ejs-locals": "^1.0.2",
18 | "express": "^4.21.1",
19 | "jugglingdb": "^2.0.1",
20 | "jugglingdb-mongodb": "^0.2.0",
21 | "method-override": "^3.0.0",
22 | "multer": "^1.4.5-lts.1"
23 | },
24 | "devDependencies": {
25 | "nodemon": "^3.1.7"
26 | },
27 | "overrides": {
28 | "jugglingdb-mongodb": {
29 | "mongodb": {
30 | "bson": "1.1.4"
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/samples/photo_album/public/css/photo_album.css:
--------------------------------------------------------------------------------
1 | /* Dark Theme and Centered Layout */
2 | body {
3 | background-color: #1e1e1e;
4 | color: #dcdcdc;
5 | font-family: Helvetica, Arial, sans-serif;
6 | margin: 0 auto;
7 | padding: 20px;
8 | max-width: 960px;
9 | text-align: center; /* Center all text by default */
10 | }
11 |
12 | #posterframe {
13 | position: absolute;
14 | right: 10px;
15 | top: 10px;
16 | }
17 |
18 | h1 {
19 | color: #4db5ff; /* Light blue for headings */
20 | font-size: 24px;
21 | text-align: center;
22 | }
23 |
24 | h2 {
25 | color: #a1a1a1; /* Lighter gray for secondary headings */
26 | font-size: 20px;
27 | text-align: center;
28 | }
29 |
30 | p {
31 | font-size: 16px;
32 | line-height: 24px;
33 | text-align: justify; /* Make paragraphs more readable */
34 | }
35 |
36 | #logo {
37 | height: 51px;
38 | width: 241px;
39 | margin: 0 auto;
40 | }
41 |
42 | a {
43 | color: #4db5ff; /* Light blue for links */
44 | }
45 |
46 | .actions {
47 | margin: 20px 0;
48 | }
49 |
50 | .upload_link {
51 | background-color: #333;
52 | border: 1px solid #555;
53 | color: #fff;
54 | font-size: 18px;
55 | font-weight: bold;
56 | margin: 10px 0 20px 0;
57 | padding: 10px 20px;
58 | text-align: center;
59 | text-decoration: none;
60 | width: auto;
61 | display: inline-block;
62 | }
63 |
64 | .photo {
65 | border-top: 2px solid #555;
66 | margin: 10px;
67 | padding: 10px;
68 | align-items: start;
69 | text-align: left; /* Align photos to the left */
70 | }
71 |
72 | .photo .thumbnail {
73 | border: none;
74 | display: block;
75 | margin-top: 10px;
76 | max-width: 200px;
77 | text-align: left; /* Ensure the image itself aligns to the left */
78 | align-items: left;
79 | }
80 |
81 | .photo h2 {
82 | color: #a1a1a1;
83 | font-size: 20px;
84 | text-align: left;
85 | }
86 |
87 | .toggle_info {
88 | color: #ff7851; /* Brighter color for toggle */
89 | display: block;
90 | font-weight: bold;
91 | margin-top: 10px;
92 | }
93 |
94 | .thumbnail_holder {
95 | height: 182px;
96 | margin-bottom: 5px;
97 | margin-right: 10px;
98 | text-align: left; /* Align thumbnail holder to the left */
99 | }
100 |
101 | .info td, .uploaded_info td {
102 | font-size: 14px;
103 | }
104 |
105 | .note {
106 | margin: 20px 0;
107 | }
108 |
109 | .more_info, .show_more_info .less_info {
110 | display: none;
111 | }
112 |
113 | .show_more_info .more_info, .less_info {
114 | display: inline-block;
115 | }
116 |
117 | .inline {
118 | display: inline-block;
119 | }
120 |
121 | td {
122 | padding-right: 5px;
123 | vertical-align: top;
124 | }
125 |
126 | #backend_upload, #direct_upload {
127 | border: 1px solid #555;
128 | background-color: #2b2b2b;
129 | margin: 20px 0;
130 | padding: 20px 0;
131 | }
132 |
133 | #backend_upload h1, #direct_upload h1 {
134 | margin: 0 0 15px 0;
135 | color: #4db5ff;
136 | }
137 |
138 | .back_link {
139 | display: block;
140 | font-size: 16px;
141 | font-weight: bold;
142 | margin: 10px 0;
143 | color: #4db5ff;
144 | }
145 |
146 | form {
147 | border: 1px solid #444;
148 | background-color: #333;
149 | border-radius: 4px;
150 | margin: 15px 0;
151 | padding: 15px 0;
152 | }
153 |
154 | form .form_line {
155 | margin-bottom: 20px;
156 | }
157 |
158 | form .form_controls {
159 | margin: 0 auto;
160 | }
161 |
162 | form label {
163 | float: none;
164 | display: inline-block;
165 | padding-top: 3px;
166 | text-align: left;
167 | width: auto;
168 | color: #ddd;
169 | }
170 |
171 | form .error {
172 | color: #ff7851;
173 | margin: 0 10px;
174 | }
175 |
176 | #direct_upload {
177 | border: 4px dashed #555;
178 | padding: 40px;
179 | }
180 |
181 | .upload_details {
182 | border-top: 1px solid #555;
183 | font-size: 12px;
184 | margin: 20px;
185 | word-wrap: break-word;
186 | text-align: left;
187 | }
188 |
189 | .upload_details h2 {
190 | color: #a1a1a1;
191 | font-size: 20px;
192 | text-align: left;
193 | }
194 |
195 | .preview {
196 | position: relative;
197 | text-align: center;
198 | }
199 |
200 | .preview .delete_by_token {
201 | font-size: 12px;
202 | left: -12px;
203 | line-height: 9px;
204 | position: absolute;
205 | text-decoration: none;
206 | top: 0;
207 | }
208 |
209 | .upload_button_holder {
210 | display: inline-block;
211 | overflow: hidden;
212 | position: relative;
213 | }
214 |
215 | .upload_button_holder .upload_button {
216 | background-color: #4db5ff;
217 | border: 1px solid #000;
218 | border-radius: 4px;
219 | color: #fff;
220 | cursor: pointer;
221 | display: block;
222 | font-size: 14px;
223 | font-weight: bold;
224 | height: 24px;
225 | padding: 5px 0;
226 | text-align: center;
227 | text-decoration: none;
228 | width: 120px;
229 | margin: 10px auto;
230 | }
231 |
232 | .upload_button_holder:hover .upload_button {
233 | background-color: #5fc1ff;
234 | }
235 |
236 | .upload_button_holder .cloudinary-fileupload {
237 | border: none;
238 | cursor: pointer;
239 | opacity: 0;
240 | height: 100%;
241 | left: 0;
242 | margin: 0;
243 | padding: 0;
244 | position: absolute;
245 | top: 0;
246 | width: 100%;
247 | }
248 |
--------------------------------------------------------------------------------
/samples/photo_album/public/js/canvas-to-blob.min.js:
--------------------------------------------------------------------------------
1 | (function (global) {
2 | "use strict";
3 |
4 | const canvasPrototype = global.HTMLCanvasElement && global.HTMLCanvasElement.prototype;
5 |
6 | const isBlobSupported = global.Blob && (() => {
7 | try {
8 | return Boolean(new Blob());
9 | } catch {
10 | return false;
11 | }
12 | })();
13 |
14 | const isUint8ArrayBlobSupported = isBlobSupported && global.Uint8Array && (() => {
15 | try {
16 | return new Blob([new Uint8Array(100)]).size === 100;
17 | } catch {
18 | return false;
19 | }
20 | })();
21 |
22 | const BlobBuilder = global.BlobBuilder || global.WebKitBlobBuilder || global.MozBlobBuilder || global.MSBlobBuilder;
23 |
24 | const dataURLtoBlob = (dataURL) => {
25 | const [header, base64Data] = dataURL.split(",");
26 | const isBase64 = header.indexOf("base64") >= 0;
27 | const binaryString = isBase64 ? atob(base64Data) : decodeURIComponent(base64Data);
28 |
29 | const arrayBuffer = new ArrayBuffer(binaryString.length);
30 | const uintArray = new Uint8Array(arrayBuffer);
31 |
32 | for (let i = 0; i < binaryString.length; i++) {
33 | uintArray[i] = binaryString.charCodeAt(i);
34 | }
35 |
36 | const mimeType = header.split(":")[1].split(";")[0];
37 |
38 | return isBlobSupported ? new Blob([isUint8ArrayBlobSupported ? uintArray : arrayBuffer], { type: mimeType })
39 | : (() => {
40 | const builder = new BlobBuilder();
41 | builder.append(arrayBuffer);
42 | return builder.getBlob(mimeType);
43 | })();
44 | };
45 |
46 | if (global.HTMLCanvasElement && !canvasPrototype.toBlob) {
47 | if (canvasPrototype.mozGetAsFile) {
48 | canvasPrototype.toBlob = function (callback, mimeType, quality) {
49 | if (quality && this.toDataURL && dataURLtoBlob) {
50 | callback(dataURLtoBlob(this.toDataURL(mimeType, quality)));
51 | } else {
52 | callback(this.mozGetAsFile("blob", mimeType));
53 | }
54 | };
55 | } else if (canvasPrototype.toDataURL && dataURLtoBlob) {
56 | canvasPrototype.toBlob = function (callback, mimeType, quality) {
57 | callback(dataURLtoBlob(this.toDataURL(mimeType, quality)));
58 | };
59 | }
60 | }
61 |
62 | if (typeof define === "function" && define.amd) {
63 | define(() => dataURLtoBlob);
64 | } else {
65 | global.dataURLtoBlob = dataURLtoBlob;
66 | }
67 |
68 | })(this);
69 |
--------------------------------------------------------------------------------
/samples/photo_album/public/js/photo_album.js:
--------------------------------------------------------------------------------
1 | $(document).ready(() => {
2 | $('.toggle_info').click(() => {
3 | $(this).closest('.photo').toggleClass('show_more_info');
4 | return false;
5 | });
6 | });
7 |
--------------------------------------------------------------------------------
/samples/photo_album/server.js:
--------------------------------------------------------------------------------
1 | // Load environment variables
2 | require('dotenv').config();
3 |
4 | const { v2: cloudinary} = require('cloudinary');
5 |
6 | if (typeof (process.env.CLOUDINARY_URL) === 'undefined') {
7 | console.warn('!! cloudinary config is undefined !!');
8 | console.warn('export CLOUDINARY_URL or set dotenv file');
9 | } else {
10 | console.log('cloudinary config:');
11 | console.log(cloudinary.config());
12 | }
13 | console.log('-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --');
14 | const path = require('path');
15 | const express = require('express');
16 | const engine = require('ejs-locals');
17 | const methodOverride = require('method-override');
18 | require('./config/schema');
19 |
20 | // Start express server
21 | const app = express();
22 | app.use(express.json());
23 | app.use(express.urlencoded({ extended: false }));
24 | app.use(methodOverride());
25 | app.use(express.static(path.join(__dirname, '/public')));
26 | app.set('views', path.join(__dirname, '/views/'));
27 | app.use('/node_modules', express.static(path.join(__dirname, '/node_modules')));
28 | app.engine('ejs', engine);
29 | app.set('view engine', 'ejs');
30 |
31 | // Wire request 'pre' actions
32 | wirePreRequest(app);
33 | // Wire request controllers
34 | const photosController = require('./controllers/photos_controller');
35 |
36 | photosController.wire(app);
37 |
38 | // Wire request 'post' actions
39 | wirePostRequest(app);
40 |
41 | function wirePreRequest(application) {
42 | application.use((req, res, next) => {
43 | console.log(`${req.method} ${req.url}`);
44 | res.locals.req = req;
45 | res.locals.res = res;
46 |
47 | if (typeof (process.env.CLOUDINARY_URL) === 'undefined') {
48 | throw new Error('Missing CLOUDINARY_URL environment variable');
49 | } else {
50 | // Expose cloudinary package to view
51 | res.locals.cloudinary = cloudinary;
52 | next();
53 | }
54 | });
55 | }
56 |
57 | function wirePostRequest(application) {
58 | application.use((err, req, res, next) => {
59 | if (err.message && (err.message.indexOf('not found') !== -1 || err.message.indexOf('Cast to ObjectId failed') !== -1)) {
60 | return next();
61 | }
62 | console.log(`error (500) ${err.message}`);
63 | console.log(err.stack);
64 | if (err.message.includes('CLOUDINARY_URL')) {
65 | res.status(500).render('errors/dotenv', { error: err });
66 | } else {
67 | res.status(500).render('errors/500', { error: err });
68 | }
69 | return undefined;
70 | });
71 | }
72 |
73 | // Assume 404 since no middleware responded
74 | app.use((req, res) => {
75 | console.log('error (404)');
76 | res.status(404).render('errors/404', {
77 | url: req.url,
78 | error: 'Not found'
79 | });
80 | });
81 |
82 | const server = app.listen(process.env.PORT || 9000, () => {
83 | console.log(`Listening on port ${server.address().port}`);
84 | });
85 |
--------------------------------------------------------------------------------
/samples/photo_album/views/errors/404.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 | <%=url%>
4 |
5 |
6 | <%=error%>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/samples/photo_album/views/errors/500.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 | <%=error.message%>
4 |
5 |
6 | <%=error.stack%>
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/samples/photo_album/views/errors/dotenv.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 | Missing .env file
4 |
5 |
6 | You will need a Cloudinary account to run this demo project.
7 |
8 |
9 | If you don't have an account, sign up for free
10 | here .
11 |
12 |
13 | customized dotenv.example configuration file
14 |
15 |
16 | Rename dotenv.example to .env
17 |
18 |
19 | Restart your Node server process.
20 |
21 |
22 |
23 | Example .env file
24 |
25 | PORT=9000
26 | CLOUDINARY_URL=cloudinary:/xxxxx
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/samples/photo_album/views/layouts/default.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PhotoAlbum on Express
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | <%-body%>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/add.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 |
4 |
5 |
New Photo
6 |
Image file is uploaded through the server
7 |
29 |
30 | Back to list
31 |
32 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/add_direct.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 |
New Photo
4 |
Direct upload from the browser
5 |
You can also drag and drop an image file into the dashed area.
6 |
39 |
40 |
41 | Back to list
42 |
43 |
44 |
45 |
46 |
47 | <%- cloudinary.cloudinary_js_config()%>
48 |
49 |
120 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/add_direct_unsigned.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 |
New Photo
4 |
Direct upload from the browser Unsigned upload using a preset
5 |
You can also drag and drop an image file into the dashed area.
6 |
40 |
41 |
42 | Back to list
43 |
44 |
45 |
46 |
47 |
48 | <%- cloudinary.cloudinary_js_config()%>
49 |
50 |
120 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/create_direct.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 | Your photo was uploaded sucessfully!
3 |
4 |
10 |
11 | Back to list
12 |
13 | <% if (upload){ %>
14 |
15 |
Upload metadata:
16 |
17 | <% for (let attr in upload){%>
18 | <%= attr %> <%= JSON.stringify(upload[attr])%>
19 | <% }%>
20 |
21 |
22 | <% } %>
23 |
24 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/create_through_server.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 | Your photo was uploaded sucessfully!
3 |
4 |
10 |
11 | Back to list
12 |
13 | <% if (upload){ %>
14 |
15 |
Upload metadata:
16 |
17 | <% for (let attr in upload){%>
18 | <%= attr %> <%= JSON.stringify(upload[attr])%>
19 | <% }%>
20 |
21 |
22 | <% } %>
23 |
24 |
--------------------------------------------------------------------------------
/samples/photo_album/views/photos/index.ejs:
--------------------------------------------------------------------------------
1 | <% layout('../layouts/default') %>
2 |
3 |
4 |
5 |
7 | <%-cloudinary.image('officialchucknorrispage',{type:'facebook',format:'png',transformation:[{height:95,width:95,crop:'thumb',gravity:'face',effect:'sepia',radius:20},{angle:10}]})%>
8 |
9 |
10 | Welcome!
11 |
12 |
13 | This is the main demo page of the PhotoAlbum sample Express application of Cloudinary.
14 | Here you can see all images you have uploaded to this application and find some information on how
15 | to implement your own Express application storing, manipulating and serving your photos using Cloudinary!
16 |
17 |
18 |
19 | All of the images you see here are transformed and served by Cloudinary.
20 | For instance, the logo and the poster frame.
21 | These two pictures weren't even have to be uploaded to Cloudinary, they are retrieved by the service, transformed, cached and distributed through a CDN.
22 |
23 |
24 | Your Photos
25 |
26 |
31 |
32 |
33 | <% if ( photos.length==0 ) { %>
34 |
No photos were added yet.
35 | <% } else { %>
36 |
37 | <% for(let i=0; i
38 | <% let photo = photos[i] ; %>
39 |
40 |
<%= photo.title %>
41 |
42 | <%-cloudinary.image(photo.image.public_id, {width: 150, height: 150, quality: 80,format:'jpg',class:'thumbnail inline'})%>
43 |
44 |
45 |
48 |
49 |
50 |
Hide transformations
51 |
52 | <% const transformations = [
53 | { crop : "fill", radius : 10, height : 150, width : 150 },
54 | { crop : "scale", height : 150, width : 150 },
55 | { crop : "fit", height : 150, width : 150 },
56 | { crop : "thumb", gravity : "face", height : 150, width : 150 },
57 | { format : "png", angle : 20, transformation : {
58 | crop : "fill", gravity : "north", effect : "sepia", height : 150, width : 150 } }
59 | ] %>
60 | <% transformations.forEach(function(image_params) {%>
61 |
62 |
63 |
68 |
69 | <% for (let key in image_params){%>
70 |
71 | <%= key %>
72 | <%= JSON.stringify(image_params[key]) %>
73 |
74 | <% } %>
75 |
76 |
77 |
78 | <% }) %>
79 |
80 |
81 | Take a look at our documentation of
Image Transformations for a full list of supported transformations.
82 |
83 |
84 |
85 |
86 | <% } %>
87 |
88 | <% } %>
89 |
90 |
91 |
--------------------------------------------------------------------------------
/samples/readme.md:
--------------------------------------------------------------------------------
1 | # Cloudinary Node Sample Projects #
2 |
3 | ## Basic sample
4 |
5 | The basic sample uploads local and remote image to Cloudinary and generates URLs for applying various image transformations on the uploaded files.
6 |
7 | ### Setting up
8 |
9 | 1. Before running the sample, copy the Environment variable configuration parameters from Cloudinary's [Management Console](https://cloudinary.com/console) of your account into `.env` file of the project or export it (i.e. export CLOUDINARY_URL=xxx).
10 | 1. Run `npm install` in project directory to bring all the required modules.
11 | 1. Run the sample using `npm run start`.
12 |
13 | ## Photo Album sample
14 |
15 | Simple application for uploading images and displaying them in a list.
16 | This sample uses [jugglingdb orm](https://github.com/1602/jugglingdb).
17 | See [schema.js](photo_album/config/schema.js) for adapter configuration.
18 |
19 | ### Setting up
20 | 1. Before running the sample, copy the Environment variable configuration parameters from Cloudinary's [Management Console](https://cloudinary.com/console) of your account into `.env` file of the project or export it (i.e. export CLOUDINARY_URL=xxx).
21 | 1. In the project directory, run `npm install` to install all the required dependencies.
22 | 1. Run `npm start` to start the server , and if you want to run a
23 | development mode server (which reloads automatically) run `npm run dev`.
24 | 1. Open the sample page in a browser: http://localhost:9000
25 |
26 |
27 | ## Additional resources ##
28 |
29 | * [Node integration documentation](http://cloudinary.com/documentation/node_integration)
30 | * [Image transformations documentation](http://cloudinary.com/documentation/node_image_manipulation)
31 | * [Node Image Upload](http://cloudinary.com/documentation/node_image_upload)
32 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | "browser": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/test/.resources/CloudBookStudy-HD.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/CloudBookStudy-HD.mp4
--------------------------------------------------------------------------------
/test/.resources/TheCompleteWorksOfShakespeare.mobi:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/TheCompleteWorksOfShakespeare.mobi
--------------------------------------------------------------------------------
/test/.resources/big-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/big-image.jpg
--------------------------------------------------------------------------------
/test/.resources/docx.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/docx.docx
--------------------------------------------------------------------------------
/test/.resources/empty.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/empty.gif
--------------------------------------------------------------------------------
/test/.resources/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/favicon.ico
--------------------------------------------------------------------------------
/test/.resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/logo.png
--------------------------------------------------------------------------------
/test/.resources/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudinary/cloudinary_npm/c7f784df4c5e7c0a0245db172e39ac65594177ff/test/.resources/sample.jpg
--------------------------------------------------------------------------------
/test/integration/api/admin/config_spec.js:
--------------------------------------------------------------------------------
1 | const sinon = require('sinon');
2 |
3 | const cloudinary = require('../../../../lib/cloudinary');
4 | const api_http = require("https");
5 | const ClientRequest = require('_http_client').ClientRequest;
6 |
7 | describe('Admin API - Config', () => {
8 | const mocked = {};
9 |
10 | beforeEach(function () {
11 | mocked.xhr = sinon.useFakeXMLHttpRequest();
12 | mocked.write = sinon.spy(ClientRequest.prototype, 'write');
13 | mocked.request = sinon.spy(api_http, 'request');
14 | });
15 |
16 | afterEach(function () {
17 | mocked.request.restore();
18 | mocked.write.restore();
19 | mocked.xhr.restore();
20 | });
21 |
22 | describe('config', () => {
23 | it('should send a request to config endpoint', () => {
24 | cloudinary.v2.api.config();
25 |
26 | sinon.assert.calledWith(mocked.request, sinon.match({
27 | pathname: sinon.match('config'),
28 | method: sinon.match('GET'),
29 | query: sinon.match('')
30 | }));
31 | });
32 |
33 | it('should send a request to config endpoint with optional parameters', () => {
34 | cloudinary.v2.api.config({ settings: true });
35 |
36 | sinon.assert.calledWith(mocked.request, sinon.match({
37 | pathname: sinon.match('config'),
38 | method: sinon.match('GET'),
39 | query: sinon.match('settings=true')
40 | }));
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/test/integration/api/admin/folders_api_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const sinon = require('sinon');
3 |
4 | const cloudinary = require('../../../../lib/cloudinary');
5 | const createTestConfig = require('../../../testUtils/createTestConfig');
6 | const helper = require('../../../spechelper');
7 | const api_http = require("https");
8 | const ClientRequest = require('_http_client').ClientRequest;
9 |
10 | describe('Admin API - Folders', () => {
11 | const mocked = {};
12 |
13 | beforeEach(function () {
14 | mocked.xhr = sinon.useFakeXMLHttpRequest();
15 | mocked.write = sinon.spy(ClientRequest.prototype, 'write');
16 | mocked.request = sinon.spy(api_http, 'request');
17 | });
18 |
19 | afterEach(function () {
20 | mocked.request.restore();
21 | mocked.write.restore();
22 | mocked.xhr.restore();
23 | });
24 |
25 | describe('rename_folder', () => {
26 | it('should send a request to update folder endpoint with correct parameters', () => {
27 | cloudinary.v2.api.rename_folder('old/path', 'new/path');
28 |
29 | sinon.assert.calledWith(mocked.request, sinon.match({
30 | pathname: sinon.match('old%2Fpath'),
31 | method: sinon.match('PUT')
32 | }));
33 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('to_folder', 'new/path')));
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/integration/api/analysis/analyze_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const sinon = require('sinon');
3 | const ClientRequest = require('_http_client').ClientRequest;
4 | const api_http = require('https');
5 |
6 | const cloudinary = require('../../../../cloudinary');
7 | const helper = require('../../../spechelper');
8 |
9 | describe('Analyze API', () => {
10 | describe('uri analysis', () => {
11 | const mocked = {};
12 | const config = {};
13 |
14 | beforeEach(function () {
15 | mocked.xhr = sinon.useFakeXMLHttpRequest();
16 | mocked.write = sinon.spy(ClientRequest.prototype, 'write');
17 | mocked.request = sinon.spy(api_http, 'request');
18 |
19 | config.cloud_name = cloudinary.config().cloud_name;
20 | });
21 |
22 | afterEach(function () {
23 | mocked.request.restore();
24 | mocked.write.restore();
25 | mocked.xhr.restore();
26 | });
27 |
28 | it('should call analyze endpoint with non-custom analysis_type', () => {
29 | cloudinary.analysis.analyze_uri('https://example.com', 'captioning');
30 |
31 | sinon.assert.calledWith(mocked.request, sinon.match({
32 | pathname: sinon.match(new RegExp(`/v2/${config.cloud_name}/analysis/analyze/uri`)),
33 | method: sinon.match('POST')
34 | }));
35 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('uri', 'https://example.com')));
36 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('analysis_type', 'captioning')));
37 | });
38 |
39 | it('should call analyze endpoint with custom analysis_type', () => {
40 | cloudinary.analysis.analyze_uri('https://example.com', 'custom', {
41 | model_name: 'my_model',
42 | model_version: 1
43 | });
44 |
45 | sinon.assert.calledWith(mocked.request, sinon.match({
46 | pathname: sinon.match(new RegExp(`/v2/${config.cloud_name}/analysis/analyze/uri`)),
47 | method: sinon.match('POST')
48 | }));
49 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('uri', 'https://example.com')));
50 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('analysis_type', 'custom')));
51 | sinon.assert.calledWith(mocked.write, sinon.match(helper.apiJsonParamMatcher('parameters', {
52 | custom: {
53 | model_name: 'my_model',
54 | model_version: 1
55 | }
56 | })));
57 | });
58 |
59 | it('should not allow calling analyze endpoint with incorrect custom analysis parameters', () => {
60 | assert.throws(() => {
61 | cloudinary.analysis.analyze_uri('https://example.com', 'custom');
62 | }, {
63 | message: 'Setting analysis_type to "custom" requires additional params: "model_name" and "model_version"'
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/integration/api/authorization/oAuth_authorization_spec.js:
--------------------------------------------------------------------------------
1 | const sinon = require('sinon');
2 | const cloudinary = require("../../../../cloudinary");
3 | const helper = require("../../../spechelper");
4 | const describe = require('../../../testUtils/suite');
5 | const testConstants = require('../../../testUtils/testConstants');
6 | const { PUBLIC_IDS } = testConstants;
7 | const { PUBLIC_ID } = PUBLIC_IDS;
8 |
9 | describe("oauth_token", function(){
10 | it("should send the oauth_token option to the server (admin_api)", function() {
11 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
12 | cloudinary.v2.api.resource(PUBLIC_ID, { oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' });
13 | return sinon.assert.calledWith(requestSpy,
14 | sinon.match.has("headers",
15 | sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4")
16 | ));
17 | });
18 | });
19 |
20 | it("should send the oauth_token config to the server (admin_api)", function() {
21 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
22 | cloudinary.config({
23 | api_key: undefined,
24 | api_secret: undefined,
25 | oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4'
26 | });
27 | cloudinary.v2.api.resource(PUBLIC_ID);
28 | return sinon.assert.calledWith(requestSpy,
29 | sinon.match.has("headers",
30 | sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4")
31 | ));
32 | });
33 | });
34 |
35 | it("should not fail when only providing api_key and secret (admin_api)", function() {
36 | cloudinary.config({
37 | api_key: "1234",
38 | api_secret: "1234",
39 | oauth_token: undefined
40 | });
41 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
42 | cloudinary.v2.api.resource(PUBLIC_ID);
43 | return sinon.assert.calledWith(requestSpy, sinon.match({ auth: "1234:1234" }));
44 | });
45 | });
46 |
47 | it("should fail when missing all credentials (admin_api)", function() {
48 | cloudinary.config({
49 | api_key: undefined,
50 | api_secret: undefined,
51 | oauth_token: undefined
52 | });
53 | expect(() => {
54 | cloudinary.v2.api.resource(PUBLIC_ID)
55 | }).to.throwError(/Must supply api_key/);
56 | });
57 |
58 | it("oauth_token as option should take priority with secret and key (admin_api)", function() {
59 | cloudinary.config({
60 | api_key: '1234',
61 | api_secret: '1234'
62 | });
63 | return cloudinary.v2.api.resource(PUBLIC_ID, {oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4'})
64 | .then(
65 | () => expect().fail()
66 | ).catch(({ error }) => expect(error.message).to.contain("Invalid token"));
67 | });
68 |
69 | it("should send the oauth_token option to the server (upload_api)", function() {
70 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
71 | cloudinary.v2.uploader.upload(PUBLIC_ID, { oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' });
72 | return sinon.assert.calledWith(requestSpy,
73 | sinon.match.has("headers",
74 | sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4")
75 | ));
76 | });
77 | });
78 |
79 | it("should send the oauth_token config to the server (upload_api)", function() {
80 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
81 | cloudinary.config({
82 | api_key: undefined,
83 | api_secret: undefined,
84 | oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4'
85 | });
86 | cloudinary.v2.uploader.upload(PUBLIC_ID);
87 | return sinon.assert.calledWith(requestSpy,
88 | sinon.match.has("headers",
89 | sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4")
90 | ));
91 | });
92 | });
93 |
94 | it("should not fail when only providing api_key and secret (upload_api)", function() {
95 | cloudinary.config({
96 | api_key: "1234",
97 | api_secret: "1234",
98 | oauth_token: undefined
99 | });
100 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
101 | cloudinary.v2.uploader.upload(PUBLIC_ID)
102 | return sinon.assert.calledWith(requestSpy, sinon.match({ auth: null }));
103 | });
104 | });
105 |
106 | it("should fail when missing all credentials (upload_api)", function() {
107 | cloudinary.config({
108 | api_key: undefined,
109 | api_secret: undefined,
110 | oauth_token: undefined
111 | });
112 | expect(() => {
113 | cloudinary.v2.uploader.upload(PUBLIC_ID)
114 | }).to.throwError(/Must supply api_key/);
115 | });
116 |
117 | it("should not need credentials for unsigned upload", function() {
118 | cloudinary.config({
119 | api_key: undefined,
120 | api_secret: undefined,
121 | oauth_token: undefined
122 | });
123 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
124 | cloudinary.v2.uploader.unsigned_upload(PUBLIC_ID, 'preset')
125 | return sinon.assert.calledWith(requestSpy, sinon.match({ auth: null }));
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/test/integration/api/search/search_folders_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require('../../../../cloudinary');
2 | const {TIMEOUT} = require('../../../testUtils/testConstants');
3 | const describe = require('../../../testUtils/suite');
4 | const wait = require('../../../testUtils/helpers/wait');
5 |
6 | const folderNames = ['testFolder1', 'testFolder2'];
7 |
8 | describe('search_folders_api', function () {
9 | describe('unit', function () {
10 | it('should create empty json', function () {
11 | const query = cloudinary.v2.search_folders.instance().to_query();
12 | expect(query).to.eql({});
13 | });
14 |
15 | it('should always return same object in fluent interface', function () {
16 | const instance = cloudinary.v2.search_folders.instance();
17 | const searchOptions = [
18 | 'expression',
19 | 'sort_by',
20 | 'max_results',
21 | 'next_cursor',
22 | 'aggregate',
23 | 'with_field'
24 | ];
25 | searchOptions.forEach(method => expect(instance).to.eql(instance[method]('emptyarg')));
26 | });
27 |
28 | it('should correctly transform whole query into search payload', function () {
29 | const query = cloudinary.v2.search_folders
30 | .expression('expression-key:expression-value')
31 | .sort_by('sort_by_field', 'asc')
32 | .max_results(1)
33 | .next_cursor('next_cursor')
34 | .aggregate('aggregate1').aggregate('aggregate2')
35 | .with_field('field1').with_field('field2')
36 | .to_query();
37 |
38 | expect(query).to.eql({
39 | expression: 'expression-key:expression-value',
40 | sort_by: [{sort_by_field: 'asc'}],
41 | max_results: 1,
42 | next_cursor: 'next_cursor',
43 | aggregate: ['aggregate1', 'aggregate2'],
44 | with_field: ['field1', 'field2']
45 | })
46 | });
47 | });
48 |
49 | describe('integration', function () {
50 | this.timeout(TIMEOUT.LONG);
51 |
52 | before(function () {
53 | return Promise.all(folderNames.map(folderName => {
54 | return cloudinary.v2.api.create_folder(folderName);
55 | })).then(wait(2));
56 | });
57 |
58 | after(function () {
59 | return Promise.all(folderNames.map(folderName => {
60 | return cloudinary.v2.api.delete_folder(folderName);
61 | }));
62 | });
63 |
64 | it('should return a search response with folders', function () {
65 | return cloudinary.v2.search_folders.expression('name=testFolder*')
66 | .execute()
67 | .then(function (results) {
68 | expect(results).to.have.key('folders');
69 | });
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/test/integration/api/search/visual_search_spec.js:
--------------------------------------------------------------------------------
1 | const helper = require('../../../spechelper');
2 | const cloudinary = require('../../../../cloudinary');
3 | const {
4 | strictEqual,
5 | deepStrictEqual
6 | } = require('assert');
7 | const {TEST_CLOUD_NAME} = require('../../../testUtils/testConstants');
8 |
9 | describe('Visual search', () => {
10 | it('should pass the image_url parameter to the api call', () => {
11 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
12 | cloudinary.v2.api.visual_search({image_url: 'test-image-url'});
13 |
14 | const [calledWithUrl] = requestSpy.firstCall.args;
15 | strictEqual(calledWithUrl.method, 'GET');
16 | strictEqual(calledWithUrl.path, `/v1_1/${TEST_CLOUD_NAME}/resources/visual_search?image_url=test-image-url`);
17 | });
18 | });
19 |
20 | it('should pass the image_url parameter to the api call', () => {
21 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
22 | cloudinary.v2.api.visual_search({image_asset_id: 'image-asset-id'});
23 |
24 | const [calledWithUrl] = requestSpy.firstCall.args;
25 | strictEqual(calledWithUrl.method, 'GET');
26 | strictEqual(calledWithUrl.path, `/v1_1/${TEST_CLOUD_NAME}/resources/visual_search?image_asset_id=image-asset-id`);
27 | });
28 | });
29 |
30 | it('should pass the image_url parameter to the api call', () => {
31 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
32 | cloudinary.v2.api.visual_search({text: 'visual-search-input'});
33 |
34 | const [calledWithUrl] = requestSpy.firstCall.args;
35 | strictEqual(calledWithUrl.method, 'GET');
36 | strictEqual(calledWithUrl.path, `/v1_1/${TEST_CLOUD_NAME}/resources/visual_search?text=visual-search-input`);
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/test/integration/api/uploader/auto_chaptering_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const sinon = require('sinon');
3 |
4 | const cloudinary = require('../../../../lib/cloudinary');
5 | const createTestConfig = require('../../../testUtils/createTestConfig');
6 | const helper = require('../../../spechelper');
7 | const ClientRequest = require('_http_client').ClientRequest;
8 |
9 | describe('Uploader', () => {
10 | let spy;
11 | let xhr;
12 |
13 | before(() => {
14 | xhr = sinon.useFakeXMLHttpRequest();
15 | spy = sinon.spy(ClientRequest.prototype, 'write');
16 | });
17 |
18 | after(() => {
19 | spy.restore();
20 | xhr.restore();
21 | });
22 |
23 | describe('upload', () => {
24 | it('should send a request with auto_chaptering set to true if requested', () => {
25 | cloudinary.v2.uploader.upload('irrelevant', { auto_chaptering: true });
26 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_chaptering', '1')));
27 | });
28 | });
29 |
30 | describe('explicit', () => {
31 | it('should send a request with auto_chaptering set to true if requested', () => {
32 | cloudinary.v2.uploader.explicit('irrelevant', { auto_chaptering: true });
33 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_chaptering', '1')));
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/integration/api/uploader/auto_transcription_spec.js:
--------------------------------------------------------------------------------
1 | const sinon = require('sinon');
2 | const cloudinary = require('../../../../lib/cloudinary');
3 | const helper = require('../../../spechelper');
4 | const ClientRequest = require('_http_client').ClientRequest;
5 |
6 | describe('Uploader', () => {
7 | let spy;
8 | let xhr;
9 |
10 | before(() => {
11 | xhr = sinon.useFakeXMLHttpRequest();
12 | spy = sinon.spy(ClientRequest.prototype, 'write');
13 | });
14 |
15 | after(() => {
16 | spy.restore();
17 | xhr.restore();
18 | });
19 |
20 | describe('upload', () => {
21 | it('should send a request with auto_transcription set to true if requested', () => {
22 | cloudinary.v2.uploader.upload('irrelevant', { auto_transcription: true });
23 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_transcription', '1')));
24 | });
25 |
26 | it('should send a request with auto_transcription config if requested', () => {
27 | cloudinary.v2.uploader.upload('irrelevant', { auto_transcription: { translate: ['pl'] } });
28 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_transcription', '{"translate":["pl"]}')));
29 | });
30 | });
31 |
32 | describe('explicit', () => {
33 | it('should send a request with auto_transcription set to true if requested', () => {
34 | cloudinary.v2.uploader.explicit('irrelevant', { auto_transcription: true });
35 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_transcription', '1')));
36 | });
37 |
38 | it('should send a request with auto_transcription config if requested', () => {
39 | cloudinary.v2.uploader.explicit('irrelevant', { auto_transcription: { translate: ['pl'] } });
40 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('auto_transcription', '{"translate":["pl"]}')));
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/test/integration/api/uploader/custom_region_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const sinon = require('sinon');
3 |
4 | const cloudinary = require('../../../../lib/cloudinary');
5 | const createTestConfig = require('../../../testUtils/createTestConfig');
6 | const helper = require('../../../spechelper');
7 | const ClientRequest = require('_http_client').ClientRequest;
8 |
9 | describe('Uploader', () => {
10 | let spy;
11 | let xhr;
12 |
13 | before(() => {
14 | xhr = sinon.useFakeXMLHttpRequest();
15 | spy = sinon.spy(ClientRequest.prototype, 'write');
16 | });
17 |
18 | after(() => {
19 | spy.restore();
20 | xhr.restore();
21 | });
22 |
23 | describe('upload', () => {
24 | it('should send a request with encoded custom region gravity that represents a box', () => {
25 | cloudinary.v2.uploader.upload('irrelevant', {
26 | regions: {
27 | 'box_1': [[1, 2], [3, 4]],
28 | 'box_2': [[5, 6], [7, 8]]
29 | }
30 | });
31 |
32 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('regions', JSON.stringify({
33 | 'box_1': [[1, 2], [3, 4]],
34 | 'box_2': [[5, 6], [7, 8]]
35 | }))));
36 | });
37 |
38 | it('should send a request with encoded custom region gravity that represents a custom shape', () => {
39 | cloudinary.v2.uploader.upload('irrelevant', {
40 | regions: {
41 | 'custom_1': [[1, 2], [3, 4], [5, 6], [7, 8]],
42 | 'custom_2': [[10, 11], [12, 13], [14, 15]]
43 | }
44 | });
45 |
46 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('regions', JSON.stringify({
47 | 'custom_1': [[1, 2], [3, 4], [5, 6], [7, 8]],
48 | 'custom_2': [[10, 11], [12, 13], [14, 15]]
49 | }))));
50 | });
51 | });
52 |
53 | describe('explicit', () => {
54 | it('should send a request with encoded custom region gravity that represents a box', () => {
55 | cloudinary.v2.uploader.explicit('irrelevant', {
56 | regions: {
57 | 'box_1': [[1, 2], [3, 4]],
58 | 'box_2': [[5, 6], [7, 8]]
59 | }
60 | });
61 |
62 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('regions', JSON.stringify({
63 | 'box_1': [[1, 2], [3, 4]],
64 | 'box_2': [[5, 6], [7, 8]]
65 | }))));
66 | });
67 |
68 | it('should send a request with encoded custom region gravity that represents a custom shape', () => {
69 | cloudinary.v2.uploader.explicit('irrelevant', {
70 | regions: {
71 | 'custom_1': [[1, 2], [3, 4], [5, 6], [7, 8]],
72 | 'custom_2': [[10, 11], [12, 13], [14, 15]]
73 | }
74 | });
75 |
76 | sinon.assert.calledWith(spy, sinon.match(helper.uploadParamMatcher('regions', JSON.stringify({
77 | 'custom_1': [[1, 2], [3, 4], [5, 6], [7, 8]],
78 | 'custom_2': [[10, 11], [12, 13], [14, 15]]
79 | }))));
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/integration/api/uploader/slideshow_spec.js:
--------------------------------------------------------------------------------
1 | const Q = require('q');
2 | const cloudinary = require("../../../../cloudinary");
3 | const describe = require('../../../testUtils/suite');
4 | const TEST_ID = Date.now();
5 |
6 | const createTestConfig = require('../../../testUtils/createTestConfig');
7 |
8 | const testConstants = require('../../../testUtils/testConstants');
9 | const UPLOADER_V2 = cloudinary.v2.uploader;
10 |
11 | const {
12 | TIMEOUT,
13 | TAGS
14 | } = testConstants;
15 |
16 | const {
17 | TEST_TAG
18 | } = TAGS;
19 |
20 | require('jsdom-global')();
21 |
22 | describe("create slideshow tests", function () {
23 | this.timeout(TIMEOUT.LONG);
24 | after(function () {
25 | var config = cloudinary.config(true);
26 | if (!(config.api_key && config.api_secret)) {
27 | expect().fail("Missing key and secret. Please set CLOUDINARY_URL.");
28 | }
29 | return Q.allSettled([
30 | !cloudinary.config().keep_test_products ? cloudinary.v2.api.delete_resources_by_tag(TEST_TAG) : void 0,
31 | !cloudinary.config().keep_test_products ? cloudinary.v2.api.delete_resources_by_tag(TEST_TAG,
32 | {
33 | resource_type: "video"
34 | }) : void 0
35 | ]);
36 | });
37 | beforeEach(function () {
38 | cloudinary.config(true);
39 | cloudinary.config(createTestConfig());
40 | });
41 |
42 |
43 | it("should successfully create slideshow", async function () {
44 | // this.timeout(TIMEOUT.LONG);
45 | const slideshowManifest
46 | = 'w_352;h_240;du_5;fps_30;vars_(slides_((media_s64:aHR0cHM6Ly9y' +
47 | 'ZXMuY2xvdWRpbmFyeS5jb20vZGVtby9pbWFnZS91cGxvYWQvY291cGxl);(media_s64:aH' +
48 | 'R0cHM6Ly9yZXMuY2xvdWRpbmFyeS5jb20vZGVtby9pbWFnZS91cGxvYWQvc2FtcGxl)))';
49 |
50 | const slideshowManifestJson = {
51 | "w": 848,
52 | "h": 480,
53 | "du": 6,
54 | "fps": 30,
55 | "vars": {
56 | "sdur": 500,
57 | "tdur": 500,
58 | "slides": [
59 | {
60 | "media": "i:protests9"
61 | }, {
62 | "media": "i:protests8"
63 | },
64 | {
65 | "media": "i:protests7"
66 | },
67 | {
68 | "media": "i:protests6"
69 | },
70 | {
71 | "media": "i:protests2"
72 | },
73 | {
74 | "media": "i:protests1"
75 | }
76 | ]
77 | }
78 | }
79 |
80 |
81 | const res = await UPLOADER_V2.create_slideshow({
82 | manifest_transformation: {
83 | custom_function: {
84 | function_type: 'render',
85 | source: slideshowManifest
86 | }
87 | },
88 | transformation: {
89 | width: 100,
90 | height: 100,
91 | crop: 'scale'
92 | },
93 | // manifest_json: slideshowManifestJson,
94 | tags: [TEST_TAG],
95 | overwrite: true,
96 | public_id: TEST_ID,
97 | notification_url: 'https://example.com'
98 | });
99 |
100 | expect(res.status).to.be('processing');
101 | expect(res.public_id).to.be(TEST_ID.toString()); // TestID is int, Server returns a string and not an int.
102 | expect(res.batch_id.length).to.be.above(5); // some long string
103 | })
104 | })
105 |
--------------------------------------------------------------------------------
/test/integration/cache_spec.js:
--------------------------------------------------------------------------------
1 | var options;
2 | const { describe, before, it } = require('mocha');
3 |
4 | const path = require('path');
5 | const helper = require("../spechelper");
6 | const cloudinary = require('../../cloudinary').v2;
7 |
8 | const FileKeyValueStorage = require(`../../${helper.libPath}/cache/FileKeyValueStorage`);
9 | const KeyValueCacheAdapter = require(`../../${helper.libPath}/cache/KeyValueCacheAdapter`);
10 |
11 | const Cache = cloudinary.Cache;
12 | const IMAGE_FILE = helper.IMAGE_FILE;
13 | const PUBLIC_ID = "dummy";
14 | const BREAKPOINTS = [5, 3, 7, 5];
15 | const testConstants = require('../testUtils/testConstants');
16 |
17 | const {
18 | TIMEOUT,
19 | TAGS
20 | } = testConstants;
21 |
22 | const UPLOAD_TAGS = TAGS.UPLOAD_TAGS;
23 |
24 | const TRANSFORMATION_1 = {
25 | angle: 45,
26 | crop: 'scale'
27 | };
28 |
29 | const FORMAT_1 = 'png';
30 |
31 | describe("Cache", function () {
32 | before(function () {
33 | Cache.setAdapter(new KeyValueCacheAdapter(new FileKeyValueStorage()));
34 | });
35 | it("should be initialized", function () {
36 | expect(Cache).to.be.ok();
37 | });
38 | it("should set and get a value", function () {
39 | Cache.set(PUBLIC_ID, {}, BREAKPOINTS);
40 | expect(Cache.get(PUBLIC_ID, {})).to.eql(BREAKPOINTS);
41 | });
42 | describe("Upload integration", function () {
43 | this.timeout(TIMEOUT.LONG);
44 | before(function () {
45 | options = {
46 | tags: UPLOAD_TAGS,
47 | responsive_breakpoints: [
48 | {
49 | create_derived: false,
50 | transformation: { angle: 90 },
51 | format: 'gif'
52 | },
53 | {
54 | create_derived: false,
55 | transformation: TRANSFORMATION_1,
56 | format: FORMAT_1
57 | },
58 | {
59 | create_derived: false
60 | }
61 | ]
62 | };
63 | });
64 | after(function () {
65 | let config = cloudinary.config(true);
66 | if (!(config.api_key && config.api_secret)) {
67 | expect().fail("Missing key and secret. Please set CLOUDINARY_URL.");
68 | }
69 | if (!cloudinary.config().keep_test_products) {
70 | cloudinary.api.delete_resources_by_tag(helper.TEST_TAG);
71 | }
72 | });
73 | it("should save responsive breakpoints to cache after upload", function () {
74 | return cloudinary.uploader.upload(IMAGE_FILE, options)
75 | .then(function (results) {
76 | let public_id = results.public_id;
77 | let type = results.type;
78 | let resource_type = results.resource_type;
79 | results.responsive_breakpoints.forEach(function (bp) {
80 | let cachedBp = Cache.get(results.public_id, {
81 | public_id,
82 | type,
83 | resource_type,
84 | raw_transformation: bp.transformation,
85 | format: path.extname(bp.breakpoints[0].url).slice(1)
86 | });
87 | expect(cachedBp).to.eql(bp.breakpoints.map(i => i.width));
88 | });
89 | });
90 | });
91 | });
92 | it("should create srcset from cache", function () {
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | require('dotenv').load({
2 | silent: true
3 | });
4 |
5 | if (!process.env.CLOUDINARY_URL) {
6 | throw 'Could not start tests - Cloudinary URL is undefined'
7 | }
8 |
9 | global.expect = require('expect.js');
10 | require('./testUtils/testBootstrap');
11 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beADatasource.js:
--------------------------------------------------------------------------------
1 | const isEmpty = require("lodash/isEmpty");
2 | const includes = require('lodash/includes');
3 |
4 |
5 | const ERRORS = {
6 | MUST_CONTAIN_VALUES: `expected datasource to contain mandatory field: 'values'`,
7 | MUST_NOT_CONTAIN_VALUES: `expected datasource not to contain a 'values' field`,
8 | INNER_VALUE_MUST_BE_STRING: `expected datasource to contain item with mandatory field 'value' type string`,
9 | INNER_VALUE_MUST_NOT_BE_STRING: `expected datasource not to contain item with mandatory field 'value' type string`,
10 | EXTERNAL_ID_MUST_BE_STRING: `expected datasource field to contain item with mandatory field: 'value' type string`,
11 | EXTERNAL_ID_MUST_NOT_BE_STRING: `expected datasource not to contain item with mandatory field 'external_id' type string`,
12 | STATE_MUST_BE_ONE_OF: (states, state) => `expected datasource field state to be one of ${states}. Unknown state ${state} received`,
13 | STATE_MUST_NOT_BE_ONE_OF: states => `expected datasource field state not to be of ${states}`
14 | };
15 |
16 | /**
17 | * Asserts that a given object is a datasource.
18 | *
19 | * @returns {expect.Assertion}
20 | */
21 | expect.Assertion.prototype.beADatasource = function () {
22 | let datasource;
23 | datasource = this.obj;
24 | this.assert('values' in datasource, function () {
25 | return ERRORS.MUST_CONTAIN_VALUES;
26 | }, function () {
27 | return ERRORS.MUST_NOT_CONTAIN_VALUES;
28 | });
29 |
30 | if (!isEmpty(datasource.values)) {
31 | datasource.values.forEach((value) => {
32 | this.assert(typeof value.value === 'string', function () {
33 | return ERRORS.INNER_VALUE_MUST_BE_STRING;
34 | }, function () {
35 | return ERRORS.INNER_VALUE_MUST_NOT_BE_STRING;
36 | });
37 | this.assert(typeof value.external_id === 'string', function () {
38 | return ERRORS.EXTERNAL_ID_MUST_BE_STRING;
39 | }, function () {
40 | return ERRORS.EXTERNAL_ID_MUST_NOT_BE_STRING;
41 | });
42 | if (!isEmpty(value.state)) {
43 | const states = ['active', 'inactive'];
44 | this.assert(includes(states, value.state), function () {
45 | return ERRORS.STATE_MUST_BE_ONE_OF(states.join(', '), value.state);
46 | }, function () {
47 | return ERRORS.STATE_MUST_NOT_BE_ONE_OF(states.join(', '));
48 | });
49 | }
50 | });
51 | }
52 | return this;
53 | };
54 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beAMetadataField.js:
--------------------------------------------------------------------------------
1 | const includes = require('lodash/includes');
2 |
3 | /**
4 | * Asserts that a given object is a metadata field.
5 | * Optionally tests the values in the metadata field for equality
6 | *
7 | * @param {string} type The type of metadata field we expect
8 | * @returns {expect.Assertion}
9 | */
10 | expect.Assertion.prototype.beAMetadataField = function (type = '') {
11 | let metadataField, expectedValues;
12 | if (Array.isArray(this.obj)) {
13 | [metadataField, expectedValues] = this.obj;
14 | } else {
15 | metadataField = this.obj;
16 | }
17 | // Check that all mandatory keys exist
18 | const mandatoryKeys = ['type', 'external_id', 'label', 'mandatory', 'default_value', 'validation'];
19 | mandatoryKeys.forEach((key) => {
20 | this.assert(key in metadataField, function () {
21 | return `expected metadata field to contain mandatory field: ${key}`;
22 | }, function () {
23 | return `expected metadata field not to contain a ${key} field`;
24 | });
25 | });
26 |
27 | // If type is enum or set test it
28 | if (includes(['enum', 'set'], metadataField.type)) {
29 | this.assert('datasource' in metadataField, function () {
30 | return `expected metadata field of type ${metadataField.type} to contain a datasource field`;
31 | }, function () {
32 | return `expected metadata field of type ${metadataField.type} not to contain a datasource field`;
33 | });
34 | expect(metadataField.datasource).to.beADatasource();
35 | }
36 |
37 | // Make sure type is acceptable
38 | if (type) {
39 | this.assert(type === metadataField.type, function () {
40 | return `expected metadata field type to equal ${type}`;
41 | }, function () {
42 | return `expected metadata field type ${metadataField.type} not to equal ${type}`;
43 | });
44 | } else {
45 | const acceptableTypes = ['string', 'integer', 'date', 'enum', 'set'];
46 | this.assert(includes(acceptableTypes, metadataField.type), function () {
47 | return `expected metadata field type to be one of ${acceptableTypes.join(', ')}. Unknown field type ${metadataField.type} received`;
48 | }, function () {
49 | return `expected metadata field not to be of a certain type`;
50 | });
51 | }
52 | // Verify object values
53 | if (expectedValues) {
54 | Object.entries(expectedValues).forEach(([key, value]) => {
55 | this.assert(metadataField[key] === value, function () {
56 | return `expected metadata field's ${key} to equal ${value} but got ${metadataField[key]} instead`;
57 | }, function () {
58 | return `expected metadata field's ${key} not to equal ${value}`;
59 | });
60 | });
61 | }
62 |
63 | return this;
64 | };
65 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beAMulti.js:
--------------------------------------------------------------------------------
1 | const url = require("url");
2 |
3 | const ERRORS = {
4 | FIELD_VERSION_MUST_BE_NUMBER: `expected sprite to contain mandatory field 'version' of type string`,
5 | FIELD_VERSION_MUST_NOT_BE_NUMBER: `expected sprite not to contain mandatory field 'version' of type string`,
6 | FIELD_VALUE_MUST_BE_STRING: name => `expected sprite to contain mandatory field '${name}' of type string`,
7 | FIELD_VALUE_MUST_NOT_BE_STRING: name => `expected sprite not to contain mandatory field '${name}' of type string`,
8 | FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME: (name, protocol, formats) => `expected field '${name}' to contain a URL with protocol '${protocol}' and one of formats: ${formats}`,
9 | FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME: (name, protocol, formats) => `expected field '${name}' not to contain a URL with protocol '${protocol}' and one of formats: ${formats}`
10 | }
11 |
12 | function matchesSchema (urlStr, protocol, formats) {
13 | const urlObj = url.parse(urlStr);
14 | return urlObj.protocol === `${protocol}:` && formats.some(format => urlObj.pathname.endsWith(format));
15 | }
16 |
17 | /**
18 | * Asserts that a given object is a multi object.
19 | *
20 | * @returns {expect.Assertion}
21 | */
22 | expect.Assertion.prototype.beAMulti = function () {
23 | const multi = this.obj;
24 |
25 | const stringKeys = ["url", "secure_url", "public_id"];
26 | const supportedFormats = ["gif", "png", "webp", "webm", "mp4", "pdf"];
27 |
28 | // Check that certain mandatory keys are of the 'string' type
29 | stringKeys.forEach((key) => {
30 | this.assert(typeof multi[key] === "string", function () {
31 | return ERRORS.FIELD_VALUE_MUST_BE_STRING(key);
32 | }, function () {
33 | return ERRORS.FIELD_VALUE_MUST_NOT_BE_STRING(key);
34 | });
35 | });
36 |
37 | this.assert(typeof multi.version === "number", function () {
38 | return ERRORS.FIELD_VERSION_MUST_BE_NUMBER;
39 | }, function () {
40 | return ERRORS.FIELD_VERSION_MUST_NOT_BE_NUMBER;
41 | });
42 |
43 | // Check that 'url' fields match the protocol and file format
44 | this.assert(matchesSchema(multi.url, "http", supportedFormats), function () {
45 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("url", "http", supportedFormats);
46 | }, function () {
47 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("url", "http", supportedFormats);
48 | });
49 | this.assert(matchesSchema(multi.secure_url, "https", supportedFormats), function () {
50 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("secure_url", "https", supportedFormats);
51 | }, function () {
52 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("secure_url", "https", supportedFormats);
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beASignedDownloadUrl.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../../cloudinary");
2 | const url = require("url");
3 | const isEmpty = require("lodash/isEmpty");
4 | const isEqual = require("lodash/isEqual");
5 |
6 | const ERRORS = {
7 | MUST_CONTAIN_QUERY_PARAMETER: name => `expected query parameters to contain mandatory parameter: ${name}`,
8 | MUST_NOT_CONTAIN_QUERY_PARAMETER: name => `expected query parameters not to contain parameter: ${name}`,
9 | PATH_MUST_END_WITH: part => `expected path to end with: ${part}`,
10 | PATH_MUST_NOT_END_WITH: part => `expected path not to end with: ${part}`,
11 | FIELD_MUST_EQUAL_VALUE: (key, value, resultValue) => `expected field ${key} to equal ${value} but got ${resultValue} instead`,
12 | FIELD_MUST_NOT_EQUAL_VALUE: (key, value) => `expected field ${key} not to equal ${value}`
13 | }
14 |
15 | /**
16 | * Asserts that a given string is a signed url.
17 | *
18 | * @param {string} [path] Path that the url should end with
19 | * @param {Object} [params] Query paraneters that should be present in the url
20 | *
21 | * @returns {expect.Assertion}
22 | */
23 | expect.Assertion.prototype.beASignedDownloadUrl = function (path, params) {
24 | const apiUrl = this.obj;
25 |
26 | const urlOptions = url.parse(apiUrl, true)
27 | const queryParams = urlOptions.query;
28 |
29 | const defaultParams = {
30 | api_key: cloudinary.config().api_key,
31 | mode: "download"
32 | };
33 | const expectedParams = Object.assign(defaultParams, params);
34 |
35 | // Rename PHP-style multi-value params to strip '[]' from their names, e.g. urls[] -> urls
36 | for (let param in queryParams) {
37 | if (param.endsWith("[]")) {
38 | queryParams[param.slice(0, -2)] = queryParams[param];
39 | delete queryParams[param];
40 | }
41 | }
42 |
43 | this.assert("timestamp" in queryParams, function () {
44 | return ERRORS.MUST_CONTAIN_QUERY_PARAMETER("timestamp");
45 | }, function () {
46 | return ERRORS.MUST_NOT_CONTAIN_QUERY_PARAMETER("timestamp");
47 | });
48 |
49 | this.assert("signature" in queryParams, function () {
50 | return ERRORS.MUST_CONTAIN_QUERY_PARAMETER("signature");
51 | }, function () {
52 | return ERRORS.MUST_NOT_CONTAIN_QUERY_PARAMETER("signature");
53 | });
54 |
55 | if (!isEmpty(path)) {
56 | this.assert(urlOptions.pathname.endsWith(path), function () {
57 | return ERRORS.PATH_MUST_END_WITH(path);
58 | }, function () {
59 | return ERRORS.PATH_MUST_NOT_END_WITH(path);
60 | });
61 | }
62 |
63 | Object.keys(expectedParams).forEach((key) => {
64 | this.assert(isEqual(expectedParams[key], queryParams[key]), function () {
65 | return ERRORS.FIELD_MUST_EQUAL_VALUE(key, expectedParams[key], queryParams[key]);
66 | }, function() {
67 | return ERRORS.FIELD_MUST_NOT_EQUAL_VALUE(key, expectedParams[key]);
68 | });
69 | });
70 | };
71 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beASprite.js:
--------------------------------------------------------------------------------
1 | const url = require("url");
2 |
3 | const ERRORS = {
4 | FIELD_VERSION_MUST_BE_NUMBER: `expected sprite to contain mandatory field 'version' of type number`,
5 | FIELD_VERSION_MUST_NOT_BE_NUMBER: `expected sprite not to contain mandatory field 'version' of type number`,
6 | MUST_CONTAIN_IMAGE_INFOS: `expected sprite to contain mandatory field: 'image_infos'`,
7 | MUST_NOT_CONTAIN_IMAGE_INFOS: `expected sprite not to contain mandatory field: 'image_infos'`,
8 | FIELD_VALUE_MUST_BE_STRING: name => `expected sprite to contain mandatory field '${name}' of type string`,
9 | FIELD_VALUE_MUST_NOT_BE_STRING: name => `expected sprite not to contain mandatory field '${name}' of type string`,
10 | IMAGE_INFOS_VALUE_MUST_BE_NUMBER: name => `expected 'image_infos' item to contain mandatory field '${name}' of type number`,
11 | IMAGE_INFOS_VALUE_MUST_NOT_BE_NUMBER: name => `expected 'image_infos' item not to contain mandatory field '${name}' of type number`,
12 | FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME: (name, protocol, format) => `expected field '${name}' to contain a URL with protocol '${protocol}' and format '${format}'`,
13 | FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME: (name, protocol, format) => `expected field '${name}' not to contain a URL with protocol '${protocol}' and format '${format}'`
14 | }
15 |
16 | function matchesSchema (urlStr, protocol, format) {
17 | const urlObj = url.parse(urlStr);
18 | let isValid = urlObj.protocol === `${protocol}:`;
19 | if (format) {
20 | isValid = isValid && urlObj.pathname.endsWith(`.${format}`)
21 | }
22 | return isValid
23 | }
24 |
25 | /**
26 | * Asserts that a given object is a sprite object.
27 | *
28 | * @returns {expect.Assertion}
29 | */
30 | expect.Assertion.prototype.beASprite = function (format=null) {
31 | const sprite = this.obj;
32 | const stringKeys = ["css_url", "image_url", "json_url", "secure_css_url", "secure_image_url", "secure_json_url", "public_id"];
33 | const imageInfosKeys = ["width", "height", "x", "y"];
34 |
35 | // Check that certain mandatory keys are of the 'string' type
36 | stringKeys.forEach((key) => {
37 | this.assert(typeof sprite[key] === "string", function () {
38 | return ERRORS.FIELD_VALUE_MUST_BE_STRING(key);
39 | }, function () {
40 | return ERRORS.FIELD_VALUE_MUST_NOT_BE_STRING(key);
41 | });
42 | });
43 | this.assert(typeof sprite.version === "number", function () {
44 | return ERRORS.FIELD_VERSION_MUST_BE_NUMBER;
45 | }, function () {
46 | return ERRORS.FIELD_VERSION_MUST_NOT_BE_NUMBER;
47 | });
48 | this.assert("image_infos" in sprite, function () {
49 | return ERRORS.MUST_CONTAIN_IMAGE_INFOS;
50 | }, function () {
51 | return ERRORS.MUST_NOT_CONTAIN_IMAGE_INFOS;
52 | });
53 |
54 | // Check that all keys of 'image_infos' field are of the 'number' type
55 | Object.entries(sprite.image_infos).forEach(([key, imageInfos]) => {
56 | imageInfosKeys.forEach((imageInfoKey) => {
57 | this.assert(typeof imageInfos[imageInfoKey] === "number", function () {
58 | return ERRORS.IMAGE_INFOS_VALUE_MUST_BE_NUMBER(imageInfoKey);
59 | }, function () {
60 | return ERRORS.IMAGE_INFOS_VALUE_MUST_NOT_BE_NUMBER(imageInfoKey);
61 | });
62 | });
63 | });
64 |
65 | // Check that 'url' fields match the protocol and file format
66 | this.assert(matchesSchema(sprite.css_url, "http", "css"), function () {
67 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("css_url", "http", "css");
68 | }, function () {
69 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("css_url", "http", "css");
70 | });
71 | this.assert(matchesSchema(sprite.image_url, "http", format), function () {
72 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("image_url", "http", format);
73 | }, function () {
74 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("image_url", "http", format);
75 | });
76 | this.assert(matchesSchema(sprite.json_url, "http", "json"), function () {
77 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("json_url", "http", "json");
78 | }, function () {
79 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("json_url", "http", "json");
80 | });
81 | this.assert(matchesSchema(sprite.secure_css_url, "https", "css"), function () {
82 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("secure_css_url", "https", "css");
83 | }, function () {
84 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("secure_css_url", "https", "css");
85 | });
86 | this.assert(matchesSchema(sprite.secure_image_url, "https", format), function () {
87 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("secure_image_url", "https", format);
88 | }, function () {
89 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("secure_image_url", "https", format);
90 | });
91 | this.assert(matchesSchema(sprite.secure_json_url, "https", "json"), function () {
92 | return ERRORS.FIELD_URL_MUST_BE_ACCORDING_TO_THE_SCHEME("secure_json_url", "https", "json");
93 | }, function () {
94 | return ERRORS.FIELD_URL_MUST_NOT_BE_ACCORDING_TO_THE_SCHEME("secure_json_url", "https", "json");
95 | });
96 | };
97 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/beServedByCloudinary.js:
--------------------------------------------------------------------------------
1 | const cloneDeep = require('lodash/cloneDeep');
2 | const http = require('http');
3 | const https = require('https');
4 | const cloudinary = require('../../../cloudinary');
5 |
6 |
7 | expect.Assertion.prototype.beServedByCloudinary = function (done) {
8 | var actual, actualOptions, callHttp, options, public_id;
9 | [public_id, options] = this.obj;
10 | actualOptions = cloneDeep(options);
11 | actual = cloudinary.url(public_id, actualOptions);
12 | if (actual.startsWith("https")) {
13 | callHttp = https;
14 | } else {
15 | callHttp = http;
16 | }
17 | callHttp.get(actual, (res) => {
18 | this.assert(res.statusCode === 200, function () {
19 | return `Expected to get ${actual} but server responded with "${res.statusCode}: ${res.headers['x-cld-error']}"`;
20 | }, function () {
21 | return `Expeted not to get ${actual}.`;
22 | });
23 | return done();
24 | });
25 | return this;
26 | };
27 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/emptyOptions.js:
--------------------------------------------------------------------------------
1 | const isEmpty = require("lodash/isEmpty");
2 | const cloneDeep = require('lodash/cloneDeep');
3 | const utils = require('../../../cloudinary').utils;
4 |
5 | expect.Assertion.prototype.emptyOptions = function () {
6 | var actual, options, public_id;
7 | [public_id, options] = this.obj;
8 | actual = cloneDeep(options);
9 | utils.url(public_id, actual);
10 | this.assert(isEmpty(actual), function () {
11 | return `expected '${public_id}' and ${JSON.stringify(options)} to produce empty options but got ${JSON.stringify(actual)}`;
12 | }, function () {
13 | return `expected '${public_id}' and ${JSON.stringify(options)} not to produce empty options`;
14 | });
15 | return this;
16 | };
17 |
--------------------------------------------------------------------------------
/test/testUtils/assertions/produceUrl.js:
--------------------------------------------------------------------------------
1 | const cloneDeep = require('lodash/cloneDeep');
2 | const cloudinary = require('../../../cloudinary');
3 |
4 | expect.Assertion.prototype.produceUrl = function (url) {
5 | var actual, actualOptions, options, public_id;
6 | [public_id, options] = this.obj;
7 | actualOptions = cloneDeep(options);
8 | actual = cloudinary.url(public_id, actualOptions);
9 | this.assert(actual.match(url), function () {
10 | return `expected '${public_id}' and ${JSON.stringify(options)} to produce '${url}' but got '${actual}'`;
11 | }, function () {
12 | return `expected '${public_id}' and ${JSON.stringify(options)} not to produce '${url}' but got '${actual}'`;
13 | });
14 | return this;
15 | };
16 |
--------------------------------------------------------------------------------
/test/testUtils/createTestConfig.js:
--------------------------------------------------------------------------------
1 | const defaultConfigOptions = {
2 | urlAnalytics: false,
3 | analytics: false
4 | };
5 |
6 | /**
7 | * @description Creates a default config for testing, add properties to defaultConfigOptions to
8 | * make them global across all tests
9 | * @param {{}} [confOptions] Cloudinary's config options
10 | * @return {{} & {analytics: true} & any}
11 | */
12 | function createTestConfig(confOptions = {}) {
13 | return Object.assign({}, defaultConfigOptions, confOptions);
14 | }
15 |
16 | module.exports = createTestConfig;
17 |
--------------------------------------------------------------------------------
/test/testUtils/helpers/retry.js:
--------------------------------------------------------------------------------
1 | const { RETRY } = require('../testConstants');
2 | const wait = require('./wait')
3 |
4 | /**
5 | * Creates a proxy to retry the function call until it succeeds.
6 | * Custom retry function is used to be able to retry only assert-related
7 | * code segments of the tests and use delays between retries.
8 | *
9 | * @param {function} fn The function to retry.
10 | * @param {number} [limit=3] The number of attempts.
11 | * @param {number} [delay=1000] The delay between attempts, ms.
12 | *
13 | * @returns {function}
14 | */
15 | module.exports = async function retry(fn, limit = RETRY.LIMIT, delay= RETRY.DELAY) {
16 | while (limit--) {
17 | try {
18 | // eslint-disable-next-line no-await-in-loop
19 | return await fn();
20 | } catch (e) {
21 | if (!limit) {
22 | throw e;
23 | }
24 | }
25 |
26 | // eslint-disable-next-line no-await-in-loop
27 | await wait(delay)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/testUtils/helpers/wait.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Used when chained to promises, new Promise().then(wait(1000)).then(wait(2500)).then(log)
3 | * @param {number }ms
4 | * @return {function(...item): Promise<...item>}
5 | */
6 | function wait(ms = 0) {
7 | return (...rest) => {
8 | return new Promise((resolve) => {
9 | setTimeout(() => {
10 | resolve(...rest);
11 | }, ms);
12 | });
13 | }
14 | }
15 |
16 | module.exports = wait;
17 |
--------------------------------------------------------------------------------
/test/testUtils/reusableTests/api/toAcceptNextCursor.js:
--------------------------------------------------------------------------------
1 | const registerReusableTest = require('../reusableTests').registerReusableTest;
2 | const sinon = require('sinon');
3 | const helper = require("../../../spechelper");
4 |
5 | registerReusableTest("accepts next_cursor", function (testFunc, ...args) {
6 | it("Has a next cursor", function () {
7 | return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => {
8 | testFunc(...args, {
9 | next_cursor: 23452342
10 | });
11 |
12 | // TODO Why aren't we sure what's called here?
13 | if (writeSpy.called) {
14 | sinon.assert.calledWith(writeSpy, sinon.match(/next_cursor=23452342/));
15 | } else {
16 | sinon.assert.calledWith(requestSpy, sinon.match({
17 | query: sinon.match(/next_cursor=23452342/)
18 | }));
19 | }
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/testUtils/reusableTests/api/toBeACursor.js:
--------------------------------------------------------------------------------
1 | const registerReusableTest = require('../reusableTests').registerReusableTest;
2 | const sinon = require('sinon');
3 | const helper = require("../../../spechelper");
4 |
5 | registerReusableTest("a list with a cursor", function (testFunc, ...args) {
6 | it("Cursor has max results", function () {
7 | return helper.provideMockObjects(function (mockXHR, writeSpy, requestSpy) {
8 | testFunc(...args, {
9 | max_results: 10
10 | });
11 |
12 | // TODO why don't we know what is used?
13 | if (writeSpy.called) {
14 | sinon.assert.calledWith(writeSpy, sinon.match(/max_results=10/));
15 | } else {
16 | sinon.assert.calledWith(requestSpy, sinon.match({
17 | query: sinon.match(/max_results=10/)
18 | }));
19 | }
20 | });
21 | });
22 | it("Cursor has next cursor", function () {
23 | return helper.provideMockObjects(function (mockXHR, writeSpy, requestSpy) {
24 | testFunc(...args, {
25 | next_cursor: 23452342
26 | });
27 |
28 | // TODO why don't we know what is used?
29 | if (writeSpy.called) {
30 | sinon.assert.calledWith(writeSpy, sinon.match(/next_cursor=23452342/));
31 | } else {
32 | sinon.assert.calledWith(requestSpy, sinon.match({
33 | query: sinon.match(/next_cursor=23452342/)
34 | }));
35 | }
36 | });
37 | });
38 | });
39 |
40 |
--------------------------------------------------------------------------------
/test/testUtils/reusableTests/image/imageAttributesWithClientHInts.js:
--------------------------------------------------------------------------------
1 | const registerReusableTest = require('../reusableTests').registerReusableTest;
2 | const cloudinary = require('../../../../cloudinary');
3 |
4 | const UPLOAD_PATH = "https://res.cloudinary.com/test123/image/upload";
5 | const srcRegExp = function (name, path) {
6 | return RegExp(`${name}=["']${UPLOAD_PATH}/${path}["']`.replace("/", "\/"));
7 | };
8 |
9 | registerReusableTest("Expects correct image tag attributes when client hints are used", function(options) {
10 | it("should not use data-src or set responsive class", function() {
11 | var tag = cloudinary.image('sample.jpg', options);
12 | expect(tag).to.match(//);
13 | expect(tag).not.to.match(/<.*class.*>/);
14 | expect(tag).not.to.match(/\bdata-src\b/);
15 | expect(tag).to.match(srcRegExp("src", "c_scale,dpr_auto,w_auto/sample.jpg"));
16 | });
17 | it("should override responsive", function () {
18 | var tag;
19 | cloudinary.config({
20 | responsive: true
21 | });
22 | tag = cloudinary.image('sample.jpg', options);
23 | expect(tag).to.match(//);
24 | expect(tag).not.to.match(/<.*class.*>/);
25 | expect(tag).not.to.match(/\bdata-src\b/);
26 | expect(tag).to.match(srcRegExp("src", "c_scale,dpr_auto,w_auto/sample.jpg"));
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/testUtils/reusableTests/reusableTests.js:
--------------------------------------------------------------------------------
1 | const registeredTests = {};
2 |
3 | function registerReusableTest(name, testFn) {
4 | registeredTests[name] = testFn;
5 | }
6 |
7 | function callReusableTest(name, ...args) {
8 | registeredTests[name].apply(this, args);
9 | }
10 |
11 | module.exports = {registerReusableTest, callReusableTest};
12 |
--------------------------------------------------------------------------------
/test/testUtils/suite.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../cloudinary");
2 |
3 | function makeSuite(name, tests) {
4 | describe(name, function () {
5 | before("Verify configuration", function () {
6 | var config = cloudinary.config(true);
7 | if (!(config.api_key && config.api_secret)) {
8 | expect().fail("Missing key and secret. Please set CLOUDINARY_URL.");
9 | }
10 | });
11 | tests.apply(this);
12 | });
13 | }
14 |
15 | module.exports = makeSuite;
16 |
--------------------------------------------------------------------------------
/test/testUtils/testBootstrap.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 | const path = require('path');
3 |
4 | // Import all assertion extensions in /test/testUtils/assertions
5 | glob.sync(`${__dirname}/assertions/*.js`).forEach(function (file) {
6 | require(path.resolve(file));
7 | });
8 |
9 | // Import all reusable tests so they can be used with a name
10 | glob.sync(`${__dirname}/reusableTests/**/*.js`).forEach(function (file) {
11 | require(path.resolve(file));
12 | });
13 |
--------------------------------------------------------------------------------
/test/testUtils/testConstants.js:
--------------------------------------------------------------------------------
1 | require('dotenv').load({
2 | silent: true
3 | });
4 | const UNIQUE_JOB_SUFFIX_ID = process.env.TRAVIS_JOB_ID || Math.floor(Math.random() * 999999);
5 |
6 | // create public ID string for tests
7 | const PUBLIC_ID_PREFIX = "npm_api_test";
8 | const PUBLIC_ID = PUBLIC_ID_PREFIX + UNIQUE_JOB_SUFFIX_ID;
9 |
10 | // create test tag string for tests
11 | const SDK_TAG = "SDK_TEST"; // identifies resources created by all SDKs tests
12 | const TEST_TAG_PREFIX = "cloudinary_npm_test"; // identifies resources created by this SDKs tests
13 | const TEST_TAG = `${TEST_TAG_PREFIX}_${UNIQUE_JOB_SUFFIX_ID}`;
14 | const UPLOAD_TAGS = [TEST_TAG, TEST_TAG_PREFIX, SDK_TAG];
15 | const UNIQUE_TEST_FOLDER = `${TEST_TAG}_${UNIQUE_JOB_SUFFIX_ID}_folder`;
16 | const TEST_IMG_WIDTH = 241;
17 | const TEST_CLOUD_NAME = process.env.CLOUDINARY_URL.split('@')[1];
18 |
19 | const TEST_EVAL_STR = 'if (resource_info["width"] < 450) { upload_options["quality_analysis"] = true }; ' +
20 | 'upload_options["context"] = "width=" + resource_info["width"]';
21 | module.exports = {
22 | TEST_TAG_PREFIX,
23 | TEST_IMG_WIDTH,
24 | TEST_CLOUD_NAME,
25 | PUBLIC_ID_PREFIX,
26 | UNIQUE_TEST_FOLDER,
27 | TEST_EVAL_STR,
28 | TIMEOUT: {
29 | SHORT: 5000,
30 | MEDIUM: 20000,
31 | LONG: 50000,
32 | LARGE: 70000
33 | },
34 | RETRY: {
35 | LIMIT: 3,
36 | DELAY: 1000
37 | },
38 | UNIQUE_JOB_SUFFIX_ID,
39 | PUBLIC_IDS: {
40 | PUBLIC_ID,
41 | PUBLIC_ID_1: `${PUBLIC_ID}_1`,
42 | PUBLIC_ID_2: `${PUBLIC_ID}_2`,
43 | PUBLIC_ID_3: `${PUBLIC_ID}_3`,
44 | PUBLIC_ID_4: `${PUBLIC_ID}_4`,
45 | PUBLIC_ID_5: `${PUBLIC_ID}_5`,
46 | PUBLIC_ID_6: `${PUBLIC_ID}_6`,
47 | PUBLIC_ID_BACKUP_1: `${PUBLIC_ID_PREFIX}backup_1${Date.now()}`,
48 | PUBLIC_ID_BACKUP_2: `${PUBLIC_ID_PREFIX}backup_2${Date.now()}`,
49 | PUBLIC_ID_BACKUP_3: `${PUBLIC_ID_PREFIX}backup_3${Date.now()}`,
50 | PUBLIC_ID_OCR_1: `${PUBLIC_ID_PREFIX}ocr_1${Date.now()}`
51 |
52 | },
53 | PRESETS: {
54 | API_TEST_UPLOAD_PRESET1: `npm_api_test_upload_preset_1_${UNIQUE_JOB_SUFFIX_ID}`,
55 | API_TEST_UPLOAD_PRESET2: `npm_api_test_upload_preset_2_${UNIQUE_JOB_SUFFIX_ID}`,
56 | API_TEST_UPLOAD_PRESET3: `npm_api_test_upload_preset_3_${UNIQUE_JOB_SUFFIX_ID}`,
57 | API_TEST_UPLOAD_PRESET4: `npm_api_test_upload_preset_4_${UNIQUE_JOB_SUFFIX_ID}`
58 | },
59 | TRANSFORMATIONS: {
60 | NAMED_TRANSFORMATION: `npm_api_test_transformation_${UNIQUE_JOB_SUFFIX_ID}`,
61 | NAMED_TRANSFORMATION2: `npm_api_test_transformation_2_${UNIQUE_JOB_SUFFIX_ID}`,
62 | EXPLICIT_TRANSFORMATION_NAME: `c_scale,l_text:Arial_60:${TEST_TAG},w_100`,
63 | EXPLICIT_TRANSFORMATION_NAME2: `c_scale,l_text:Arial_60:${TEST_TAG},w_200`
64 | },
65 | TAGS: {
66 | UPLOAD_TAGS,
67 | TEST_TAG,
68 | SDK_TAG
69 | },
70 | URLS: {
71 | VIDEO_URL: "https://res.cloudinary.com/demo/video/upload/dog.mp4",
72 | IMAGE_URL: "https://res.cloudinary.com/demo/image/upload/sample"
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/test/unit/access_control_spec.js:
--------------------------------------------------------------------------------
1 | const describe = require('../testUtils/suite');
2 | const isString = require('lodash/isString');
3 | const cloudinary = require("../../cloudinary");
4 | const build_upload_params = cloudinary.utils.build_upload_params;
5 |
6 | const ACL = {
7 | access_type: 'anonymous',
8 | start: new Date(Date.UTC(2019, 1, 22, 16, 20, 57)),
9 | end: '2019-03-22 00:00 +0200'
10 | };
11 | const ACL_2 = {
12 | access_type: 'anonymous',
13 | start: '2019-02-22 16:20:57Z',
14 | end: '2019-03-22 00:00 +0200'
15 | };
16 | const ACL_STRING = '{"access_type":"anonymous","start":"2019-02-22 16:20:57 +0200","end":"2019-03-22 00:00 +0200"}';
17 |
18 | describe("Access Control", function () {
19 | describe("build_upload_params", function () {
20 | it("should accept a Hash value", function () {
21 | let params = build_upload_params({
22 | access_control: ACL
23 | });
24 | expect(params).to.have.key('access_control');
25 | expect(isString(params.access_control)).to.be.ok();
26 | expect(params.access_control).to.match(/^\[.+\]$/);
27 | });
28 | it("should accept an array of Hash values", function () {
29 | let params = build_upload_params({
30 | access_control: [ACL, ACL_2]
31 | });
32 | expect(params).to.have.key('access_control');
33 | expect(isString(params.access_control)).to.be.ok();
34 | expect(params.access_control).to.match(/^\[.+\]$/);
35 | let j = JSON.parse(params.access_control);
36 | expect(j.length).to.be(2);
37 | expect(j[0].access_type).to.equal(ACL.access_type);
38 | expect(Date.parse(j[0].start)).to.equal(Date.parse(ACL.start));
39 | expect(Date.parse(j[0].end)).to.equal(Date.parse(ACL.end));
40 | });
41 | it("should accept a JSON string", function () {
42 | let params = build_upload_params({
43 | access_control: ACL_STRING
44 | });
45 | expect(params).to.have.key('access_control');
46 | expect(isString(params.access_control)).to.be.ok();
47 | expect(params.access_control).to.equal(`[${ACL_STRING}]`);
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/unit/authToken/authTokenImageTag_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../../cloudinary");
2 | const commonAuthSetupAndTeardown = require('./utils/commonAuthSetupAndTeardown');
3 |
4 | describe("AuthToken tests using image tag", function () {
5 | // Sets auth token globally in config, sets before and after hooks
6 | commonAuthSetupAndTeardown({});
7 |
8 | it("should add token to an image tag url", function () {
9 | let tag = cloudinary.image("sample.jpg", {
10 | sign_url: true,
11 | type: "authenticated",
12 | version: "1486020273"
13 | });
14 |
15 | expect(tag).to.match(//);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/unit/authToken/authTokenURL_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../../cloudinary");
2 | const commonAuthSetupAndTeardown = require('./utils/commonAuthSetupAndTeardown');
3 |
4 | describe("AuthToken tests using cloudinary.url and a private CDN", function () {
5 | // Sets auth token globally in config, sets before and after hooks
6 | commonAuthSetupAndTeardown({
7 | private_cdn: true
8 | });
9 |
10 | it("Should add a token if signed_url equals true", function () {
11 | let url = cloudinary.url("sample.jpg", {
12 | sign_url: true,
13 | resource_type: "image",
14 | type: "authenticated",
15 | version: "1486020273"
16 | });
17 | expect(url).to.eql("https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3");
18 | });
19 |
20 | it("Should add token for a public resource type", function () {
21 | let url = cloudinary.url("sample.jpg", {
22 | sign_url: true,
23 | resource_type: "image",
24 | type: "public",
25 | version: "1486020273"
26 | });
27 | expect(url).to.eql("https://test123-res.cloudinary.com/image/public/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=c2b77d9f81be6d89b5d0ebc67b671557e88a40bcf03dd4a6997ff4b994ceb80e");
28 | });
29 |
30 | it("Should not add token if signed is false", function () {
31 | let url = cloudinary.url("sample.jpg", {
32 | type: "authenticated",
33 | version: "1486020273"
34 | });
35 | expect(url).to.eql("https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg");
36 | });
37 |
38 | it("should not add token if authToken is globally set but null auth token is explicitly set and signed = true", function () {
39 | let url = cloudinary.url("sample.jpg", {
40 | auth_token: false,
41 | sign_url: true,
42 | type: "authenticated",
43 | version: "1486020273"
44 | });
45 | expect(url).to.eql("https://test123-res.cloudinary.com/image/authenticated/s--v2fTPYTu--/v1486020273/sample.jpg");
46 | });
47 |
48 | it("explicit authToken should override global setting", function () {
49 | let url = cloudinary.url("sample.jpg", {
50 | sign_url: true,
51 | auth_token: {
52 | key: "CCBB2233FF00",
53 | start_time: 222222222,
54 | duration: 100
55 | },
56 | type: "authenticated",
57 | transformation: {
58 | crop: "scale",
59 | width: 300
60 | }
61 | });
62 | expect(url).to.eql("https://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=55cfe516530461213fe3b3606014533b1eca8ff60aeab79d1bb84c9322eebc1f");
63 | });
64 |
65 | it("should compute expiration as start time + duration", function () {
66 | let token = {
67 | start_time: 11111111,
68 | duration: 300
69 | };
70 | let url = cloudinary.url("sample.jpg", {
71 | sign_url: true,
72 | auth_token: token,
73 | resource_type: "image",
74 | type: "authenticated",
75 | version: "1486020273"
76 | });
77 | expect(url).to.eql("https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3");
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/test/unit/authToken/authTokenUtils_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../../cloudinary");
2 | const commonAuthSetupAndTeardown = require('./utils/commonAuthSetupAndTeardown');
3 |
4 | const utils = cloudinary.utils;
5 |
6 | describe("AuthToken tests using utils.generate_auth_token", function () {
7 | // Sets auth token globally in config, sets before and after hooks
8 | commonAuthSetupAndTeardown({});
9 |
10 | it("should generate with start and window", function () {
11 | let token = utils.generate_auth_token({
12 | start_time: 1111111111,
13 | acl: "/image/*",
14 | duration: 300
15 | });
16 |
17 | expect(token).to.eql("__cld_token__=st=1111111111~exp=1111111411~acl=%2fimage%2f*~hmac=1751370bcc6cfe9e03f30dd1a9722ba0f2cdca283fa3e6df3342a00a7528cc51");
18 | });
19 |
20 | it("should generate token string", function () {
21 | let token = utils.generate_auth_token({
22 | start_time: 222222222,
23 | key: "00112233FF99",
24 | duration: 300,
25 | acl: `/*/t_foobar`
26 | });
27 |
28 | expect(token).to.eql("__cld_token__=st=222222222~exp=222222522~acl=%2f*%2ft_foobar~hmac=8e39600cc18cec339b21fe2b05fcb64b98de373355f8ce732c35710d8b10259f");
29 | });
30 |
31 | it("should ignore URL in AuthToken generation if ACL is provided", function () {
32 | const token_options = {
33 | start_time: 222222222,
34 | key: "00112233FF99",
35 | duration: 300,
36 | acl: "/*/t_foobar"
37 | }
38 | const token = utils.generate_auth_token(token_options);
39 |
40 | token_options.url = "https://res.cloudinary.com/test123/image/upload/v1486020273/sample.jpg";
41 | const token_with_url = utils.generate_auth_token(token_options);
42 | expect(token).to.eql(token_with_url)
43 | });
44 |
45 | it("Should throw if generate_auth_token is missing acl or url", function () {
46 | expect(() => {
47 | utils.generate_auth_token({
48 | start_time: 1111111111,
49 | duration: 300
50 | });
51 | }).to.throwError();
52 |
53 | expect(() => {
54 | utils.generate_auth_token({
55 | start_time: 1111111111,
56 | duration: 300,
57 | acl: `/*/t_foobar`
58 | });
59 | }).not.to.throwError();
60 |
61 | expect(() => {
62 | utils.generate_auth_token({
63 | start_time: 1111111111,
64 | duration: 300,
65 | url: "https://res.cloudinary.com/test123/image/upload/v1486020273/sample.jpg"
66 | });
67 | }).not.to.throwError();
68 | });
69 |
70 | it("should support multiple ACLs", function () {
71 | let token = utils.generate_auth_token({
72 | start_time: 222222222,
73 | key: "00112233FF99",
74 | duration: 300,
75 | acl: ["/*/t_foobar", "t_foobar/*/"]
76 | });
77 |
78 | expect(token).to.eql("__cld_token__=st=222222222~exp=222222522~acl=%2f*%2ft_foobar!t_foobar%2f*%2f~hmac=45d51dd32dd26a3c2339155f454076c1d8fbd93c611965461569a0b4279bbdd5");
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/test/unit/authToken/utils/commonAuthSetupAndTeardown.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../../../cloudinary");
2 | const createTestConfig = require('../../../testUtils/createTestConfig');
3 |
4 | const AUTH_KEY_1 = "00112233FF99";
5 |
6 | /**
7 | *
8 | */
9 | function commonAuthSetupAndTeardown(config = {}) {
10 | let urlBackup = '';
11 |
12 | before(() => {
13 | urlBackup = process.env.CLOUDINARY_URL;
14 | });
15 |
16 | after(function () {
17 | process.env.CLOUDINARY_URL = urlBackup;
18 | cloudinary.config(true);
19 | });
20 |
21 | beforeEach(function () {
22 | // Reset the configuration to a known state before each test
23 | process.env.CLOUDINARY_URL = "cloudinary://a:b@test123";
24 |
25 | cloudinary.config(true); // reset
26 | cloudinary.config(createTestConfig(Object.assign({}, {
27 | auth_token: {
28 | key: AUTH_KEY_1,
29 | duration: 300,
30 | start_time: 11111111
31 | }
32 | }, config)));
33 | });
34 | }
35 |
36 | module.exports = commonAuthSetupAndTeardown;
37 |
--------------------------------------------------------------------------------
/test/unit/cache/DummyCacheStorage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * In-memory cache storage used in the cache tests.
3 | */
4 | class DummyCacheStorage {
5 | constructor() {
6 | this.dummyCache = {};
7 | }
8 |
9 | get(key) {
10 | return this.dummyCache[key];
11 | }
12 |
13 | set(key, value) {
14 | this.dummyCache[key] = value;
15 | }
16 |
17 | delete(key) {
18 | delete this.dummyCache[key];
19 | }
20 |
21 | clear() {
22 | this.dummyCache = {};
23 | }
24 | }
25 |
26 | module.exports = DummyCacheStorage;
27 |
--------------------------------------------------------------------------------
/test/unit/cache/FileKeyValueCache_spec.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const helper = require("../../spechelper");
4 |
5 | const FileKeyValueStorage = require("../../../" + helper.libPath + "/cache/FileKeyValueStorage");
6 |
7 | const KEY = "test_key";
8 | const VALUE = "test_value";
9 | const KEY2 = "test_key_2";
10 | const VALUE2 = "test_value_2";
11 |
12 | describe("FileKeyValueStorage", () => {
13 | var storage;
14 | var baseFolder;
15 |
16 | function getTestValue(key) {
17 | let storedValue = fs.readFileSync(storage.getFilename(key));
18 | return JSON.parse(storedValue);
19 | }
20 |
21 | before(() => {
22 | const cwd = process.cwd();
23 | const { sep } = path;
24 | baseFolder = fs.mkdtempSync(`${cwd}${sep}`);
25 | storage = new FileKeyValueStorage({ baseFolder });
26 | });
27 |
28 | after(() => {
29 | storage.deleteBaseFolder();
30 | expect(fs.existsSync(baseFolder)).to.be(false);
31 | });
32 |
33 | it("should set a value in a file", () => {
34 | storage.set(KEY, VALUE);
35 | let actual = getTestValue(KEY);
36 | expect(actual).to.eql(VALUE);
37 | });
38 | it("should get a value from a file", () => {
39 | storage.set(KEY, VALUE);
40 | let actual = storage.get(KEY);
41 | expect(actual).to.eql(VALUE);
42 | });
43 | it("should remove all files", () => {
44 | storage.set(KEY, VALUE);
45 | storage.set(KEY2, VALUE2);
46 | expect(storage.getFilename(KEY)).not.to.eql(storage.getFilename(KEY2));
47 | let actual = fs.existsSync(storage.getFilename(KEY)) && fs.existsSync(storage.getFilename(KEY2));
48 | expect(actual).to.be.ok();
49 | storage.clear();
50 | actual = fs.existsSync(storage.getFilename(KEY)) || fs.existsSync(storage.getFilename(KEY2));
51 | expect(actual).not.to.be.ok();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/test/unit/cache/KeyValueCacheAdapter_spec.js:
--------------------------------------------------------------------------------
1 | const helper = require("../../spechelper");
2 | const DummyCacheStorage = require('./DummyCacheStorage');
3 | const KeyValueCacheAdapter = require(`../../../${helper.libPath}/cache/KeyValueCacheAdapter`);
4 |
5 | var cache;
6 | var parameters = ["public_id", "upload", "image", "w_100", "jpg"];
7 | var value = [100, 200, 300, 399];
8 | var parameters2 = ["public_id2", "fetch", "image", "w_200", "png"];
9 | var value2 = [101, 201, 301, 398];
10 |
11 | describe("KeyValueCacheAdapter", () => {
12 | before(() => {
13 | cache = new KeyValueCacheAdapter(new DummyCacheStorage());
14 | });
15 | it("should generate cache keys", () => {
16 | [
17 | [ // valid values
18 | "467d06e5a695b15468f9362e5a58d44de523026b",
19 | ["public_id", "upload", "image", "w_100", "jpg"]
20 | ],
21 | [ // allow empty values
22 | "1576396c59fc50ac8dc37b75e1184268882c9bc2",
23 | ["public_id", "upload", "image", null, null]
24 | ]
25 | ].forEach(([key, args]) => {
26 | expect(KeyValueCacheAdapter.generateCacheKey.apply(this, args)).to.eql(key);
27 | });
28 | });
29 | it("should set and get values", () => {
30 | cache.set(...parameters, value);
31 | let actualValue = cache.get(...parameters);
32 |
33 | expect(actualValue).to.eql(value);
34 | });
35 | it("should delete values from the cache", () => {
36 | cache.set(...parameters, value);
37 | let actualValue = cache.get(...parameters);
38 | expect(actualValue).to.eql(value);
39 | cache.delete(...parameters);
40 | actualValue = cache.get(...parameters);
41 | expect(actualValue).to.be(null);
42 | });
43 | it("should flush all entries from cache", () => {
44 | cache.set(...parameters, value);
45 | cache.set(...parameters2, value2);
46 | cache.flushAll();
47 | let deletedValue = cache.get(...parameters);
48 | let deletedValue2 = cache.get(...parameters2);
49 | expect(deletedValue).to.be(null);
50 | expect(deletedValue2).to.be(null);
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/unit/cloudinaryUtils/getUserAgent.spec.js:
--------------------------------------------------------------------------------
1 |
2 | const cloudinary = require("../../../cloudinary");
3 |
4 | describe("getUserAgent", function () {
5 | var platform = "";
6 | before(function () {
7 | platform = cloudinary.utils.userPlatform;
8 | cloudinary.utils.userPlatform = "";
9 | });
10 | after(function () {
11 | cloudinary.utils.userPlatform = platform;
12 | });
13 | it("should add a user platform to USER_AGENT", function () {
14 | cloudinary.utils.userPlatform = "Spec/1.0 (Test)";
15 | expect(cloudinary.utils.getUserAgent()).to.match(/Spec\/1.0 \(Test\) CloudinaryNodeJS\/(\d+\.\d+\.\d+(?:-[\da-zA-Z]+(?:\.\d+)?)?) \(Node [\d.]+\)/);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/unit/cloudinaryUtils/processLayer.spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 |
3 | const {process_layer} = require('../../../lib/utils');
4 |
5 | describe('process_layer', () => {
6 | it('should process object layer containing only public_id', () => {
7 | const processed = process_layer({public_id: 'logo'});
8 | assert.deepStrictEqual(processed, 'logo');
9 | });
10 |
11 | it('should process layer containing public_id with prefix', () => {
12 | const processed = process_layer({public_id: 'folder/logo'});
13 | assert.deepStrictEqual(processed, 'folder:logo');
14 | });
15 |
16 | it('should process layer containing type', () => {
17 | const processed = process_layer({
18 | public_id: "logo",
19 | type: "private"
20 | });
21 | assert.deepStrictEqual(processed, 'private:logo');
22 | });
23 |
24 | it('should process layer containing format', () => {
25 | const processed = process_layer({
26 | 'public_id': "logo",
27 | 'format': "png"
28 | });
29 | assert.deepStrictEqual(processed, 'logo.png');
30 | });
31 |
32 | it('should process layer containing resource_type', () => {
33 | const processed = process_layer({
34 | 'resource_type': "video",
35 | 'public_id': "cat"
36 | });
37 | assert.deepStrictEqual(processed, 'video:cat');
38 | });
39 |
40 | it('should process layer containing text with font settings', () => {
41 | const processed = process_layer({
42 | 'text': "Hello World, Nice to meet you?",
43 | 'font_family': "Arial",
44 | 'font_size': "18"
45 | });
46 | assert.deepStrictEqual(processed, 'text:Arial_18:Hello%20World%252C%20Nice%20to%20meet%20you%3F');
47 | });
48 |
49 | it('should process layer containing text with full font settings', () => {
50 | const processed = process_layer({
51 | 'text': "Hello World, Nice to meet you?",
52 | 'font_family': "Arial",
53 | 'font_size': "18",
54 | 'font_weight': "bold",
55 | 'font_style': "italic",
56 | 'letter_spacing': 4,
57 | 'line_spacing': 3
58 | });
59 | assert.deepStrictEqual(processed, 'text:Arial_18_bold_italic_letter_spacing_4_line_spacing_3:Hello%20World%252C%20Nice%20to%20meet%20you%3F');
60 | });
61 |
62 | it('should process layer with resource_type subtitles', () => {
63 | const processed = process_layer({
64 | 'resource_type': "subtitles",
65 | 'public_id': "sample_sub_en.srt"
66 | });
67 | assert.deepStrictEqual(processed, 'subtitles:sample_sub_en.srt');
68 | });
69 |
70 | it('should process layer with resource_type subtitles and font settings', () => {
71 | const processed = process_layer({
72 | 'resource_type': "subtitles",
73 | 'public_id': "sample_sub_he.srt",
74 | 'font_family': "Arial",
75 | 'font_size': 40
76 | });
77 | assert.deepStrictEqual(processed, 'subtitles:Arial_40:sample_sub_he.srt');
78 | });
79 |
80 | it('should process layer with url', () => {
81 | const processed = process_layer({'url': "https://upload.wikimedia.org/wikipedia/commons/2/2b/고창갯벌.jpg"});
82 | assert.deepStrictEqual(processed, 'fetch:aHR0cHM6Ly91cGxvYWQud2lraW1lZGlhLm9yZy93aWtpcGVkaWEvY29tbW9ucy8yLzJiLyVFQSVCMyVBMCVFQyVCMCVCRCVFQSVCMCVBRiVFQiVCMiU4Qy5qcGc');
83 | });
84 |
85 | it('should process layer with url and resource_type video', () => {
86 | const processed = process_layer({
87 | 'url': 'https://demo-res.cloudinary.com/videos/dog.mp4',
88 | "resource_type": "video"
89 | });
90 | assert.deepStrictEqual(processed, 'video:fetch:aHR0cHM6Ly9kZW1vLXJlcy5jbG91ZGluYXJ5LmNvbS92aWRlb3MvZG9nLm1wNA');
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/test/unit/config.spec.js:
--------------------------------------------------------------------------------
1 |
2 | const cloudinary = require("../../cloudinary");
3 |
4 |
5 | describe("config", function () {
6 | let cloudinaryUrlBackup;
7 | let accountUrlBackup;
8 | let proxyBackup;
9 |
10 | before(function () {
11 | cloudinaryUrlBackup = process.env.CLOUDINARY_URL;
12 | accountUrlBackup = process.env.CLOUDINARY_ACCOUNT_URL;
13 | proxyBackup = process.env.CLOUDINARY_API_PROXY;
14 | });
15 |
16 | after(function () {
17 | process.env.CLOUDINARY_URL = cloudinaryUrlBackup || '';
18 | process.env.CLOUDINARY_ACCOUNT_URL = accountUrlBackup || '';
19 | process.env.CLOUDINARY_API_PROXY = proxyBackup || '';
20 | cloudinary.config(true);
21 | });
22 |
23 | it("should not have a `hide_sensitive` property by default", function () {
24 | const config = cloudinary.config();
25 | expect(config).not.to.have.property("hide_sensitive");
26 | });
27 |
28 | it("should support `hide_sensitive` config param", function () {
29 | const config = cloudinary.config({hide_sensitive: true});
30 | expect(config.hide_sensitive).to.eql(true)
31 | });
32 |
33 | it("should allow nested values in CLOUDINARY_URL", function () {
34 | process.env.CLOUDINARY_URL = "cloudinary://key:secret@test123?foo[bar]=value";
35 | cloudinary.config(true);
36 | const foo = cloudinary.config().foo;
37 | expect(foo && foo.bar).to.eql('value');
38 | });
39 |
40 | it("should load a properly formatted CLOUDINARY_URL", function () {
41 | process.env.CLOUDINARY_URL = "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test";
42 | cloudinary.config(true);
43 | });
44 |
45 | it("should not be sensitive to case in CLOUDINARY_URL's protocol", function () {
46 | process.env.CLOUDINARY_URL = "CLouDiNaRY://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test";
47 | cloudinary.config(true);
48 | });
49 |
50 | it("should throw error when CLOUDINARY_URL doesn't start with 'cloudinary://'", function () {
51 | process.env.CLOUDINARY_URL = "https://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test?cloudinary=foo";
52 | try {
53 | cloudinary.config(true);
54 | expect().fail();
55 | } catch (err) {
56 | expect(err.message).to.eql("Invalid CLOUDINARY_URL protocol. URL should begin with 'cloudinary://'");
57 | }
58 | });
59 |
60 | it("should not throw an error when CLOUDINARY_URL environment variable is missing", function () {
61 | delete process.env.CLOUDINARY_URL;
62 | cloudinary.config(true);
63 | });
64 |
65 | it("should allow nested values in CLOUDINARY_ACCOUNT_URL", function () {
66 | process.env.CLOUDINARY_ACCOUNT_URL = "account://key:secret@test123?foo[bar]=value";
67 | cloudinary.config(true);
68 | const foo = cloudinary.config().foo;
69 | expect(foo && foo.bar).to.eql('value');
70 | });
71 |
72 | it("should load a properly formatted CLOUDINARY_ACCOUNT_URL", function () {
73 | process.env.CLOUDINARY_ACCOUNT_URL = "account://635412789012345:ALKJdjklLJAjhkKJ45hBK92tam2@test1";
74 | cloudinary.config(true);
75 | });
76 |
77 | it("should not be sensitive to case in CLOUDINARY_ACCOUNT_URL's protocol", function () {
78 | process.env.CLOUDINARY_ACCOUNT_URL = "aCCouNT://635283989012345:ALKGssklLJAjhkKJ45hBK92tas5@test1";
79 | cloudinary.config(true);
80 | });
81 |
82 | it("should throw error when CLOUDINARY_ACCOUNT_URL doesn't start with 'account://'", function () {
83 | process.env.CLOUDINARY_ACCOUNT_URL = "https://635283989012345:ALKGssklLJAjhkKJ45hBK92tas5@test1?account=foo";
84 | try {
85 | cloudinary.config(true);
86 | expect().fail();
87 | } catch (err) {
88 | expect(err.message).to.eql("Invalid CLOUDINARY_ACCOUNT_URL protocol. URL should begin with 'account://'");
89 | }
90 | });
91 |
92 | it("should not throw an error when CLOUDINARY_ACCOUNT_URL environment variable is missing", function () {
93 | delete process.env.CLOUDINARY_ACCOUNT_URL;
94 | cloudinary.config(true);
95 | });
96 |
97 | it("should support CLOUDINARY_API_PROXY environment variable", function () {
98 | const proxy = "https://myuser:mypass@example.com"
99 | process.env.CLOUDINARY_API_PROXY = proxy;
100 | const config = cloudinary.config(true);
101 | expect(config.api_proxy).to.eql(proxy)
102 | });
103 |
104 | it("should support `api_proxy` config param", function () {
105 | const proxy = "https://myuser:mypass@example.com"
106 | const config = cloudinary.config({api_proxy: proxy});
107 | expect(config.api_proxy).to.eql(proxy)
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/test/unit/normalize_expression_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("../../cloudinary");
2 | const createTestConfig = require('../testUtils/createTestConfig');
3 | const helper = require("../spechelper");
4 | const {SIMPLE_PARAMS} = require(`../../${helper.libPath}/utils/consts`);
5 | const {generate_transformation_string} = require("../../lib/utils");
6 |
7 | describe("normalize_expression tests", function () {
8 | beforeEach(function () {
9 | cloudinary.config(createTestConfig({
10 | cloud_name: "test123",
11 | api_key: 'a',
12 | api_secret: 'b'
13 | }));
14 | });
15 |
16 | it('should normalize start_offset', function () {
17 | const result = generate_transformation_string({
18 | "start_offset": "idu - 5"
19 | });
20 | expect(result).to.equal("so_idu_sub_5");
21 | });
22 |
23 | it('should normalize end_offset', function () {
24 | const result = generate_transformation_string({
25 | "end_offset": "idu - 5"
26 | });
27 | expect(result).to.equal("eo_idu_sub_5");
28 | });
29 |
30 | it("Expression normalization", function () {
31 | const cases = {
32 | 'null is not affected': [null, null],
33 | 'None is not affected': ['None', 'None'],
34 | 'empty string is not affected': ['', ''],
35 | 'single space is replaced with a single underscore': [' ', '_'],
36 | 'blank string is replaced with a single underscore': [' ', '_'],
37 | 'underscore is not affected': ['_', '_'],
38 | 'sequence of underscores and spaces is replaced with a single underscore': [' _ __ _', '_'],
39 | 'arbitrary text is not affected': ['foobar', 'foobar'],
40 | 'double ampersand replaced with and operator': ['foo && bar', 'foo_and_bar'],
41 | 'double ampersand with no space at the end is not affected': ['foo&&bar', 'foo&&bar'],
42 | 'width recognized as variable and replaced with w': ['width', 'w'],
43 | 'initial aspect ratio recognized as variable and replaced with iar': ['initial_aspect_ratio', 'iar'],
44 | 'duration is recognized as a variable and replaced with du': ['duration', 'du'],
45 | 'duration after : is not a variable and is not affected': ['preview:duration_2', 'preview:duration_2'],
46 | '$width recognized as user variable and not affected': ['$width', '$width'],
47 | '$initial_aspect_ratio recognized as user variable followed by aspect_ratio variable': [
48 | '$initial_aspect_ratio',
49 | '$initial_ar'
50 | ],
51 | '$mywidth recognized as user variable and not affected': ['$mywidth', '$mywidth'],
52 | '$widthwidth recognized as user variable and not affected': ['$widthwidth', '$widthwidth'],
53 | '$_width recognized as user variable and not affected': ['$_width', '$_width'],
54 | '$__width recognized as user variable and not affected': ['$__width', '$_width'],
55 | '$$width recognized as user variable and not affected': ['$$width', '$$width'],
56 | '$height recognized as user variable and not affected': ['$height_100', '$height_100'],
57 | '$heightt_100 recognized as user variable and not affected': ['$heightt_100', '$heightt_100'],
58 | '$$height_100 recognized as user variable and not affected': ['$$height_100', '$$height_100'],
59 | '$heightmy_100 recognized as user variable and not affected': ['$heightmy_100', '$heightmy_100'],
60 | '$myheight_100 recognized as user variable and not affected': ['$myheight_100', '$myheight_100'],
61 | '$heightheight_100 recognized as user variable and not affected': [
62 | '$heightheight_100',
63 | '$heightheight_100'
64 | ],
65 | '$theheight_100 recognized as user variable and not affected': ['$theheight_100', '$theheight_100'],
66 | '$__height_100 recognized as user variable and not affected': ['$__height_100', '$_height_100']
67 | };
68 |
69 | Object.keys(cases).forEach(function (testDescription) {
70 | const [input, expected] = cases[testDescription];
71 | expect(cloudinary.utils.normalize_expression(input)).to.equal(expected);
72 | });
73 | });
74 | describe('Normalize only specific parameters', () => {
75 | const simpleTransformationParams = SIMPLE_PARAMS.map(param => param[0]);
76 | const value = "width * 2";
77 | const normalizedValue = "w_mul_2";
78 | const normalizedParams = ["angle", "aspect_ratio", "dpr", "effect", "height", "opacity", "quality", "radius",
79 | "width", "x", "y", "zoom", "end_offset", "start_offset"];
80 | const nonNormalizedParams = simpleTransformationParams.concat('overlay', 'underlay').filter(param => !normalizedParams.includes(param));
81 | normalizedParams.forEach((param) => {
82 | it(`should normalize value in ${param}`, () => {
83 | // c_scale needed to test h_ and w_ parameters that are ignored without crop mode
84 | const options = {
85 | crop: "scale",
86 | [param]: value
87 | };
88 | const result = cloudinary.utils.generate_transformation_string(options);
89 | expect(result).to.contain(normalizedValue);
90 | expect(result).to.not.contain(value);
91 | });
92 | });
93 | nonNormalizedParams.forEach((param) => {
94 | it(`should not normalize value in ${param}`, () => {
95 | const result = cloudinary.utils.generate_transformation_string({[param]: value});
96 | expect(result).to.contain(value);
97 | expect(result).to.not.contain(normalizedValue);
98 | });
99 | });
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/test/unit/sdkAnalytics/imageTagWithAnalytics.spec.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const assert = require("assert");
3 | const sinon = require("sinon");
4 | const fs = require('fs');
5 |
6 | const cloudinary = require('../../../cloudinary');
7 |
8 | const TEST_CLOUD_NAME = require('../../testUtils/testConstants').TEST_CLOUD_NAME;
9 |
10 | describe('SDK url analytics', () => {
11 | let processVersions = {};
12 |
13 | beforeEach(() => {
14 | cloudinary.config(true); // reset
15 | cloudinary.config({
16 | techVersion: '12.0.0'
17 | });
18 | });
19 |
20 | describe('when package json is available', () => {
21 | beforeEach(() => {
22 | cloudinary.config({
23 | sdkSemver: '1.24.0'
24 | });
25 | });
26 |
27 | it('can be turned off via options', () => {
28 | const imgStr = cloudinary.image("hello", {
29 | format: "png",
30 | analytics: false
31 | });
32 |
33 | assert.ok(!imgStr.includes('MAlhAM0'))
34 | });
35 |
36 | it('defaults to true even if analytics is not passed as an option', () => {
37 | const imgStr = cloudinary.image("hello", {
38 | format: "png"
39 | });
40 |
41 | assert.ok(imgStr.includes('MAlhAM0'))
42 | });
43 |
44 | it('reads from process.versions and package.json (Mocked)', () => {
45 | const imgStr = cloudinary.image("hello", {
46 | format: "png",
47 | analytics: true
48 | });
49 |
50 | assert.ok(imgStr.includes('?_a=BAMAlhAM0'));
51 | });
52 |
53 | it('reads from process.versions and package.json (Mocked) - Responsive', () => {
54 | const imgStr = cloudinary.image("hello", {
55 | format: 'png',
56 | responsive: true,
57 | analytics: true
58 | });
59 |
60 | assert.ok(imgStr.includes('?_a=BAMAlhAMA'));
61 | });
62 |
63 | it('reads from tracked analytics configuration', () => {
64 | cloudinary.config(true); // reset
65 |
66 | const imgStr = cloudinary.image("hello", {
67 | format: "png",
68 | analytics: true,
69 | sdk_code: "X",
70 | sdk_semver: "7.3.0",
71 | tech_version: "3.4.7",
72 | product: 'B'
73 | });
74 |
75 | assert.ok(imgStr.includes('?_a=BBXAEzGT0'));
76 | });
77 |
78 | it('should still accept analytics param passed as camel case', () => {
79 | const imgStr = cloudinary.image("hello", {
80 | format: "png",
81 | urlAnalytics: true,
82 | sdkCode: "X",
83 | sdkSemver: "7.3.0",
84 | techVersion: "3.4.7",
85 | product: 'B'
86 | });
87 |
88 | assert.ok(imgStr.includes('?_a=BBXAEzGT0'));
89 | });
90 |
91 | describe('with two different casings', () => {
92 | it('should treat camel case analytics param as more important than snake case', () => {
93 | const imgStr1 = cloudinary.image("hello", {
94 | format: "png",
95 | urlAnalytics: true,
96 | analytics: false,
97 | sdkCode: "X",
98 | sdkSemver: "7.3.0",
99 | techVersion: "3.4.7",
100 | product: 'B'
101 | });
102 | assert.ok(imgStr1.includes('?_a=BBXAEzGT0'));
103 |
104 | const imgStr2 = cloudinary.image("hello", {
105 | format: "png",
106 | analytics: true,
107 | sdkCode: "X",
108 | sdkSemver: "7.3.0",
109 | techVersion: "3.4.7",
110 | tech_version: "1.2.3",
111 | product: 'B'
112 | });
113 | assert.ok(imgStr2.includes('?_a=BBXAEzGT0'));
114 | });
115 | });
116 | });
117 |
118 | describe('when package.json is unavailable', () => {
119 | before(() => {
120 | const enoent = new Error('ENOENT');
121 | enoent.code = 'ENOENT';
122 | sinon.stub(fs, 'readFileSync').throws(enoent);
123 |
124 | cloudinary.config(true); // reset
125 | });
126 |
127 | after(() => {
128 | sinon.restore();
129 | });
130 |
131 | it('uses 0.0.0 as fallback sdk semver', () => {
132 | const urlWithToken = cloudinary.url("hello", {
133 | format: "png",
134 | analytics: true
135 | });
136 |
137 | const [url, analyticsToken] = urlWithToken.split('?_a=');
138 | assert.strictEqual(analyticsToken, 'BAMAAAAM0');
139 | });
140 | });
141 | });
142 |
--------------------------------------------------------------------------------
/test/unit/tags/source_tag_spec.js:
--------------------------------------------------------------------------------
1 |
2 | const cloudinary = require('../../../cloudinary');
3 |
4 | const extend = cloudinary.utils.extend;
5 | const UPLOAD_PATH = "https://res.cloudinary.com/test123/image/upload";
6 | const createTestConfig = require('../../testUtils/createTestConfig');
7 |
8 | describe('source helper', function () {
9 | const public_id = "sample";
10 | const image_format = "jpg";
11 | const FULL_PUBLIC_ID = `${public_id}.${image_format}`;
12 | var min_width;
13 | var max_width;
14 | var breakpoint_list;
15 | var common_srcset;
16 | var fill_transformation;
17 | beforeEach(function () {
18 | min_width = 100;
19 | max_width = 399;
20 | breakpoint_list = [min_width, 200, 300, max_width];
21 | common_srcset = {
22 | "breakpoints": breakpoint_list
23 | };
24 | fill_transformation = {
25 | "width": max_width,
26 | "height": max_width,
27 | "crop": "fill"
28 | };
29 | cloudinary.config(true); // Reset
30 | cloudinary.config(createTestConfig({
31 | cloud_name: "test123",
32 | api_secret: "1234"
33 | }));
34 | });
35 | it("should generate a source tag", function () {
36 | expect(cloudinary.source("sample.jpg")).to.eql(``);
37 | });
38 | it("should generate source tag with media query", function () {
39 | var expectedMedia, expectedTag, media, tag;
40 | media = { min_width, max_width };
41 | tag = cloudinary.source(FULL_PUBLIC_ID, {
42 | media: media
43 | });
44 | expectedMedia = `(min-width: ${min_width}px) and (max-width: ${max_width}px)`;
45 | expectedTag = ``;
46 | expect(tag).to.eql(expectedTag);
47 | });
48 | it("should generate source tag with responsive srcset", function () {
49 | var tag = cloudinary.source(FULL_PUBLIC_ID, {
50 | srcset: common_srcset
51 | });
52 | expect(tag).to.eql("");
53 | });
54 | it("should generate picture tag", function () {
55 | var exp_tag, tag;
56 | tag = cloudinary.picture(FULL_PUBLIC_ID, extend({
57 | sources: [
58 | {
59 | "max_width": min_width,
60 | "transformation": {
61 | "effect": "sepia",
62 | "angle": 17,
63 | "width": min_width
64 | }
65 | },
66 | {
67 | "min_width": min_width,
68 | "max_width": max_width,
69 | "transformation": {
70 | "effect": "colorize",
71 | "angle": 18,
72 | "width": max_width
73 | }
74 | },
75 | {
76 | "min_width": max_width,
77 | "transformation": {
78 | "effect": "blur",
79 | "angle": 19,
80 | "width": max_width
81 | }
82 | }
83 | ]
84 | }, fill_transformation));
85 | exp_tag = "" + "" + "" + "" + " " + " ";
86 | expect(tag).to.eql(exp_tag);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/test/unit/url/overlay_underlay_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const cloudinary = require('../../../lib/cloudinary');
3 |
4 | describe('URL utils with transformation parameters', () => {
5 | const CLOUD_NAME = 'test-cloud';
6 |
7 | before(() => {
8 | cloudinary.config({
9 | cloud_name: CLOUD_NAME
10 | });
11 | });
12 |
13 | it('should create a correct link with overlay string', () => {
14 | const url = cloudinary.utils.url('test', {
15 | overlay: {
16 | font_family: "arial",
17 | font_size: "30",
18 | text: "abc,αβγ/אבג"
19 | }
20 | });
21 |
22 | assert.strictEqual(url, `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/l_text:arial_30:abc%252C%CE%B1%CE%B2%CE%B3%252F%D7%90%D7%91%D7%92/test`);
23 | });
24 |
25 | it('should create a correct link with underlay string', () => {
26 | const url = cloudinary.utils.url('test', {
27 | underlay: {
28 | font_family: "arial",
29 | font_size: "30",
30 | text: "abc,αβγ/אבג"
31 | }
32 | });
33 |
34 | assert.strictEqual(url, `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/u_text:arial_30:abc%252C%CE%B1%CE%B2%CE%B3%252F%D7%90%D7%91%D7%92/test`);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/unit/url/sign_url_spec.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require('../../../lib/cloudinary');
2 | const assert = require('assert');
3 |
4 | const TEST_CLOUD_NAME = 'test123';
5 | const TEST_API_KEY = '1234';
6 | const TEST_API_SECRET = 'b';
7 |
8 |
9 | describe("URL for authenticated asset", () => {
10 | before(function () {
11 | cloudinary.config({
12 | cloud_name: TEST_CLOUD_NAME,
13 | api_key: TEST_API_KEY,
14 | api_secret: TEST_API_SECRET
15 | });
16 | });
17 |
18 | let TEST_PUBLIC_ID = 'image.jpg';
19 | it('should have signature for transformation and version', () => {
20 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
21 | version: 1234,
22 | transformation: {
23 | crop: "crop",
24 | width: 10,
25 | height: 20
26 | },
27 | sign_url: true
28 | });
29 |
30 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/upload/s--Ai4Znfl3--/c_crop,h_20,w_10/v1234/image.jpg');
31 | });
32 |
33 | it('should have signature for version', () => {
34 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
35 | version: 1234,
36 | sign_url: true
37 | });
38 |
39 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/upload/s----SjmNDA--/v1234/image.jpg');
40 | });
41 |
42 | it('should have signature for transformation', () => {
43 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
44 | transformation: {
45 | crop: "crop",
46 | width: 10,
47 | height: 20
48 | },
49 | sign_url: true
50 | });
51 |
52 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/upload/s--Ai4Znfl3--/c_crop,h_20,w_10/image.jpg');
53 | });
54 |
55 | it('should have signature for authenticated asset with transformation', () => {
56 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
57 | type: 'authenticated',
58 | transformation: {
59 | crop: "crop",
60 | width: 10,
61 | height: 20
62 | },
63 | sign_url: true
64 | });
65 |
66 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/authenticated/s--Ai4Znfl3--/c_crop,h_20,w_10/image.jpg');
67 | });
68 |
69 | it('should have signature for fetched asset', () => {
70 | const signedUrl = cloudinary.utils.url('http://google.com/path/to/image.png', {
71 | type: 'fetch',
72 | version: 1234,
73 | sign_url: true
74 | });
75 |
76 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/fetch/s--hH_YcbiS--/v1234/http://google.com/path/to/image.png');
77 | });
78 |
79 | it('should have signature for authenticated asset with text overlay transformation including encoded emoji', () => {
80 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
81 | type: 'authenticated',
82 | sign_url: true,
83 | transformation: {
84 | color: 'red',
85 | overlay: {
86 | text: 'Cool%F0%9F%98%8D',
87 | font_family: 'Times',
88 | font_size: 70,
89 | font_weight: 'bold'
90 | }
91 | }
92 | });
93 |
94 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/authenticated/s--Uqk1a-5W--/co_red,l_text:Times_70_bold:Cool%25F0%259F%2598%258D/image.jpg');
95 | });
96 |
97 | it('should have signature for raw transformation string', () => {
98 | const signedUrl = cloudinary.utils.url(TEST_PUBLIC_ID, {
99 | type: 'authenticated',
100 | sign_url: true,
101 | transformation: {
102 | raw_transformation: 'co_red,l_text:Times_70_bold:Cool%25F0%259F%2598%258D'
103 | }
104 | });
105 |
106 | assert.strictEqual(signedUrl, 'https://res.cloudinary.com/test123/image/authenticated/s--Uqk1a-5W--/co_red,l_text:Times_70_bold:Cool%25F0%259F%2598%258D/image.jpg');
107 | });
108 | })
109 |
--------------------------------------------------------------------------------
/test/unit/url/user_defined_variables_spec.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const cloudinary = require('../../../lib/cloudinary');
3 |
4 | describe('User Define Variables', function () {
5 | const CLOUD_NAME = 'test';
6 |
7 | before(() => {
8 | cloudinary.config({
9 | cloud_name: CLOUD_NAME
10 | });
11 | });
12 |
13 | it('array should define a set of variables', function () {
14 | var options, t;
15 | options = {
16 | if: 'face_count > 2',
17 | variables: [['$z', 5], ['$foo', '$z * 2']],
18 | crop: 'scale',
19 | width: '$foo * 200'
20 | };
21 | t = cloudinary.utils.generate_transformation_string(options);
22 | expect(t).to.eql('if_fc_gt_2,$z_5,$foo_$z_mul_2,c_scale,w_$foo_mul_200');
23 | });
24 |
25 | it('"$key" should define a variable', function () {
26 | var options, t;
27 | options = {
28 | transformation: [
29 | {
30 | $foo: 10
31 | },
32 | {
33 | if: 'face_count > 2'
34 | },
35 | {
36 | crop: 'scale',
37 | width: '$foo * 200 / face_count'
38 | },
39 | {
40 | if: 'end'
41 | }
42 | ]
43 | };
44 | t = cloudinary.utils.generate_transformation_string(options);
45 | expect(t).to.eql('$foo_10/if_fc_gt_2/c_scale,w_$foo_mul_200_div_fc/if_end');
46 | });
47 |
48 | it('should support power operator', function () {
49 | var options, t;
50 | options = {
51 | transformation: [
52 | {
53 | $small: 150,
54 | $big: '$small ^ 1.5'
55 | }
56 | ]
57 | };
58 | t = cloudinary.utils.generate_transformation_string(options);
59 | expect(t).to.eql('$big_$small_pow_1.5,$small_150');
60 | });
61 |
62 | it('should not change variable names even if they look like keywords', function () {
63 | var options, t;
64 | options = {
65 | transformation: [
66 | {
67 | $width: 10
68 | },
69 | {
70 | width: '$width + 10 + width'
71 | }
72 | ]
73 | };
74 | t = cloudinary.utils.generate_transformation_string(options);
75 | expect(t).to.eql('$width_10/w_$width_add_10_add_w');
76 | });
77 |
78 | it('should support text values', function () {
79 | const url = cloudinary.utils.url('sample', {
80 | effect: '$efname:100',
81 | $efname: '!blur!'
82 | });
83 |
84 | assert.strictEqual(url, `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/$efname_!blur!,e_$efname:100/sample`);
85 | });
86 |
87 | it('should support string interpolation', function () {
88 | const url = cloudinary.utils.url('sample', {
89 | crop: 'scale',
90 | overlay: {
91 | text: '$(start)Hello $(name)$(ext), $(no ) $( no)$(end)',
92 | font_family: 'Arial',
93 | font_size: '18'
94 | }
95 | });
96 |
97 | assert.strictEqual(url, `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/c_scale,l_text:Arial_18:$(start)Hello%20$(name)$(ext)%252C%20%24%28no%20%29%20%24%28%20no%29$(end)/sample`);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/tools/createTestCloud.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file when used as a script (node tmp.js) will create a new cloud within Cloudinary
3 | * The CLOUDINARY_URL environment variable string is created, and stored in ./cloudinary_url.sh
4 | * To use a fresh cloud in your tests, source ./cloudinary_url.sh before running the tests
5 | * Example: node tools/createTestCloud && source tools/cloudinary_url.sh && npm run test
6 | */
7 | const fs = require('fs');
8 | const path = require('path');
9 | const https = require('https');
10 | const ENV_FILE_PATH = path.resolve(__dirname, '../.env');
11 | const cloudinary = require('../cloudinary');
12 |
13 |
14 | function setup() {
15 | let req = https.request({
16 | method: 'POST',
17 | hostname: 'sub-account-testing.cloudinary.com',
18 | path: '/create_sub_account',
19 | port: 443
20 | }, (res) => {
21 | let data = '';
22 | res.on('data', (d) => {
23 | data += d;
24 | });
25 |
26 | res.on('end', () => {
27 | let cloudData = JSON.parse(data);
28 | let { payload: { cloudApiKey, cloudApiSecret, cloudName, id } } = cloudData;
29 | let URL = `CLOUDINARY_URL=cloudinary://${cloudApiKey}:${cloudApiSecret}@${cloudName}`;
30 |
31 | fs.writeFileSync(`tools/cloudinary_url.sh`, URL); // This is needed for Travis
32 | fs.writeFileSync(ENV_FILE_PATH, URL); // This is needed for local develoepr tests
33 |
34 | require('dotenv').load();
35 |
36 | cloudinary.config(true);
37 |
38 | cloudinary.v2.uploader.upload('./test/.resources/sample.jpg', {
39 | cloud_name: cloudName,
40 | api_key: cloudApiKey,
41 | api_secret: cloudApiSecret,
42 | public_id: 'sample'
43 | }).then((result) => {
44 | console.log('Successfully created a temporary cloud');
45 | console.log('Cloudname: ', cloudName);
46 | console.log('Sample image uploaded to: ', result.url);
47 | }).catch(() => {
48 | throw 'FATAL - Could not create sample asset';
49 | });
50 | });
51 | });
52 |
53 | req.on('error', (e) => {
54 | console.error(e);
55 | });
56 |
57 | req.end();
58 | }
59 |
60 | setup();
61 |
--------------------------------------------------------------------------------
/tools/scripts/ditslint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # IMPORTANT NOTE
4 | # The following line removes TS installed by dtslint because it caused a failure on Node.js < 12.
5 | # dtslint in its current version installs typescript@next which is failing on older Node.js.
6 | # Using --localTs does not fix the issue because interpreter first gets the code of dtslint and its dependencies.
7 | rm -rf node_modules/dtslint/node_modules/typescript
8 |
9 | dtslint --expectOnly --localTs node_modules/typescript/lib types
10 |
--------------------------------------------------------------------------------
/tools/scripts/docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | jsdoc -d docs -r -p lib/*
3 |
--------------------------------------------------------------------------------
/tools/scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e;
3 |
4 | eslint ./test ./lib
5 |
--------------------------------------------------------------------------------
/tools/scripts/test.es6.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | COLLECT_COVERAGE=0;
3 |
4 | for arg in "$@"
5 | do
6 | case $arg in
7 | --coverage)
8 | COLLECT_COVERAGE=1
9 | shift # Remove --initialize from processing
10 | esac
11 | done
12 |
13 | if [ "$COLLECT_COVERAGE" -eq "1" ]; then
14 | echo 'Running code coverage test on ES6 code'
15 |
16 | # --File ensures that setup.js runs first
17 | # This file should be in the config under a 'require' key
18 | # However Mocha 6 does not expose before, beforeEach after etc. at that time
19 | # When Removing support of Node 6 and 8 and using Mocha 8, we should move this to the mocharc.json file
20 | nyc --reporter=html mocha --file "./test/setup.js" "./test/**/*spec.js"
21 | exit;
22 | else
23 | echo 'Running tests on ES6 Code'
24 |
25 | # --File ensures that setup.js runs first
26 | # This file should be in the config under a 'require' key
27 | # However Mocha 6 does not expose before, beforeEach after etc. at that time
28 | # When Removing support of Node 6 and 8 and using Mocha 8, we should move this to the mocharc.json file
29 | mocha --file "./test/setup.js" "./test/**/*spec.js"
30 | fi
31 |
--------------------------------------------------------------------------------
/tools/scripts/test.es6.unit.sh:
--------------------------------------------------------------------------------
1 | # --File ensures that setup.js runs first
2 | # This file should be in the config under a 'require' key
3 | # However Mocha 6 does not expose before, beforeEach after etc. at that time
4 | # When Removing support of Node 6 and 8 and using Mocha 8, we should move this to the mocharc.json file
5 | mocha --file "./test/setup.js" "./test/unit/**/*spec.js"
6 |
--------------------------------------------------------------------------------
/tools/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e;
3 |
4 | npm run lint
5 | npm run test-es6
6 | npm run dtslint
7 |
--------------------------------------------------------------------------------
/tools/scripts/tests-with-temp-cloud.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # We 'sleep' here to let the sample file to finish uploading and indexing in the elastic
3 | node tools/createTestCloud && sleep 3 && source tools/cloudinary_url.sh && npm run test
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "noImplicitAny": true,
5 | "esModuleInterop": true,
6 | "allowSyntheticDefaultImports": true,
7 | "declaration": true
8 | },
9 | "include": [
10 | "dist/**/*",
11 | "types/**/*"
12 | ],
13 | "exclude": [
14 | "node_modules"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "lib": ["es6", "dom"],
5 | "noImplicitAny": true,
6 | "noImplicitThis": true,
7 | "strictNullChecks": true,
8 | "strictFunctionTypes": true,
9 | "noEmit": true,
10 | "baseUrl": ".",
11 | "paths": { "cloudinary": ["."] }
12 |
13 | },
14 | "exclude": [
15 | "node_modules"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------