diff --git a/README.md b/README.md index e32194d0..d119ee23 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ You are now ready to use the tool by running the main command `sfcc-ci`. Use `sfcc-ci --help` or just `sfcc-ci` to get started and see the full list of commands available: -```bash +```txt Usage: cli [options] [command] Options: @@ -334,16 +334,13 @@ Use `sfcc-ci --help` or just `sfcc-ci` to get started and see the full list of c user:create [options] Create a new user user:update [options] Update a user user:delete [options] Delete a user - slas:tenant:list [options] Lists all tenants that belong to a given organization - slas:tenant:add [options] Adds a SLAS tenant to a given organization or updates an existing one - slas:tenant:get [options] Gets a SLAS tenant from a given organization - slas:tenant:delete [options] Deletes a SLAS tenant from a given organization - slas:client:add [options] Adds a SLAS client to a given tenant or updates an existing one - slas:client:get [options] Gets a SLAS client from a given tenant - slas:client:list [options] Lists all SLAS clients that belong to a given tenant - slas:client:delete [options] Deletes a SLAS client from a given tenant - - Environment: + slas:tenant:add [options] Add or update SLAS a tenant. + slas:tenant:get [options] Get a SLAS tenant. + slas:client:add [options] Add or update a SLAS client for a tenant + slas:client:get [options] Get a SLAS client for a tenant. + slas:client:list [options] List SLAS clients for a tenant. + slas:client:delete [options] Delete a SLAS client for a tenant. + Environment: $SFCC_LOGIN_URL set login url used for authentication $SFCC_OAUTH_LOCAL_PORT set Oauth local port for authentication flow @@ -401,6 +398,8 @@ The use of environment variables is optional. `sfcc-ci` respects the following e * `SFCC_OAUTH_USER_PASSWORD` user password used for authentication * `SFCC_SANDBOX_API_HOST` set sandbox API host * `SFCC_SANDBOX_API_POLLING_TIMEOUT` set timeout for sandbox polling in minutes +* `SFCC_SCAPI_SHORTCODE` the Salesforce Commerce (Headless) API Shortcode +* `SFCC_SCAPI_TENANTID` the Salesforce Commerce (Headless) API TenantId * `DEBUG` enable verbose output If you only want a single CLI command to write debug messages prepend the command using, e.g. `DEBUG=* sfcc-ci `. @@ -978,107 +977,70 @@ callback | (Function) | Callback function executed as a result. The err APIs available in `require('sfcc').slas`: -`tenant.add(tenantId, shortcode, description, merchantName, contact, emailAddress, fileName)` +`tenant.add({shortcode, tenant, file})` -Adds the tenant details to the SLAS organization +Add or update SLAS a tenant. Param | Type | Description ------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org -description | (String) | Description of the tenant -merchantName | (String) | Name of the merchant -contact | (String) | Username of the user -emailAddress | (String) | Email address of the user -fileName | (String) | Path of the file containing all the params required for the tenant creation. +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` +file | (String) | Path to a JSON file with object details, `file.json` **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the newly created tenant reponse. *** -`tenant.get(tenantId, shortcode)` +`tenant.get({shortcode, tenant})` -Gets the tenant matching the given `tenantId` for the given `shortCode` +Get a SLAS tenant. Param | Type | Description ------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the tenant reponse. *** -`tenant.list(shortcode)` - -Lists all the tenants for the given `shortCode` - -Param | Type | Description -------------- | ------------| -------------------------------- -shortcode | (String) | The short code of the org - -**Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is list of tenants reponse. - -*** - -`tenant.delete(tenantId, shortcode)` - -Deletes the tenant matching the given `tenantId` for the given `shortCode` - -Param | Type | Description -------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org - -**Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the deletion reponse. - -*** - -`client.add(tenantId, shortcode, file, clientid, clientname, privateclient, ecomtenant, ecomsite, secret, channels, scopes, redirecturis)` +`client.add({shortcode, tenant, client, body})` -Registers a new client within a given tenant +Add or update a SLAS client for a tenant Param | Type | Description ------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org -file | (String) | Path of the file containing all the params required for the client creation. -clientid | (String) | SLAS client id -clientname | (String) | The client name -privateclient | (Boolean) | Is the client a private client or not -ecomtenant | (String) | The ecom tenant -ecomsite | (String) | The ecom site -secret | (String) | The secret tied to the client ID -channels | (Array) | The list of channels for the client -scopes | (Array) | The list of scopes authorized for the client -redirecturis | (Array) | The list of redirect URIs authorized for the client +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` +client | (String) | Client ID, `aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa` +body | (Object) | Object with client's properties. **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the newly created client reponse. *** -`client.get(tenantId, shortcode, clientId)` +`client.get({shortcode, tenant, client})` -Gets the tenant matching the given `clientid` for the given `tenantId` and `shortCode` +Get a SLAS client for a tenant. Param | Type | Description ------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org -clientid | (String) | SLAS client id +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` +client | (String) | Client ID, `aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa` **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the client reponse. *** -`client.list(shortcode, tenantId)` +`client.list({shortcode, tenantId})` -Lists all the clients for the given `tenantId` and `shortCode` +List SLAS clients for a tenant. Param | Type | Description ------------- | ------------| -------------------------------- -shortcode | (String) | The short code of the org -tenantId | (String) | The tenant ID +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is list of clients reponse. @@ -1086,13 +1048,13 @@ tenantId | (String) | The tenant ID `client.delete(tenantId, shortcode, clientId)` -Deletes the client matching the given `clientId` for the given `tenantId` and `shortCode` +Delete a SLAS client for a tenant. Param | Type | Description ------------- | ------------| -------------------------------- -tenantId | (String) | The tenant ID -shortcode | (String) | The short code of the org -clientid | (String) | SLAS client id +shortcode | (String) | Realm short code, `kv7kzm78` +tenant | (String) | Tenant ID, `zzrf_001` +client | (String) | Client ID, `aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa` **Returns:** (Promise) The [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with its `resolve` or `reject` methods called respectively with the `result` or the `error`. The `result` variable here is the deletion reponse. diff --git a/cli.js b/cli.js index 39e492ea..e42a1fa1 100755 --- a/cli.js +++ b/cli.js @@ -1959,170 +1959,132 @@ program console.log(); }); +const SLAS_OPTIONS = { + 'shortcode': ['--shortcode ', 'Realm short code, `kv7kzm78`'], + 'tenant': ['--tenant ', 'Tenant ID, `zzrf_001`'], + 'client': ['--client ', 'Client ID, `aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa`'], + 'file': ['--file ', 'Path to a JSON file with object details, `file.json`'], + 'json': ['-j, --json', 'Format output in json', false], + 'username': ['-u, --username ', 'Email address of user'] +} program - .command('slas:tenant:list') - .description('Lists all tenants that belong to a given organization') - .option('--shortcode ', 'the organizations short code') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.tenant.list(options.shortcode, asJson); - - }).on('--help', function() { + .command('slas:tenant:create') + .description('Create or update SLAS a tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.file) + .option(...SLAS_OPTIONS.json) + .action(async (options) => { + await require('./lib/slas').cli.tenant.create(options); + }) + .on('--help', () => { console.log(); - }); - -program - .command('slas:tenant:add') - .description('Adds a SLAS tenant to a given organization or updates an existing one') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('--file ', 'JSON file with tenant details') - .option('--merchantname ', 'the name given for the tenant') - .option('--tenantdescription ', 'the tenant descriptions') - .option('--contact ', 'Contact person to manage tenants') - .option('--email ', 'Email to contact') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.tenant.add(options.tenant, options.shortcode, - options.tenantdescription, options.merchantname, options.contact, options.email, options.file, asJson); - - }).on('--help', function() { + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:tenant:create --shortcode kv7kzm78 --tenant zzrf_001'); + console.log(' $ sfcc-ci slas:tenant:create --shortcode kv7kzm78 --tenant zzrf_001 --file tenant.json'); console.log(); }); program .command('slas:tenant:get') - .description('Gets a SLAS tenant from a given organization') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.tenant.get(options.tenant, options.shortcode, asJson); - - }).on('--help', function() { + .description('Get a SLAS tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.json) + .action(async (options) => { + await require('./lib/slas').cli.tenant.get(options); + }) + .on('--help', () => { console.log(); - }); - -program - .command('slas:tenant:delete') - .description('Deletes a SLAS tenant from a given organization') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.tenant.get(options.tenant, options.shortcode, asJson); - - }).on('--help', function() { + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:tenant:get --shortcode kv7kzm78 --tenant zzrf_001'); console.log(); }); program - .command('slas:client:add') - .description('Adds a SLAS client to a given tenant or updates an existing one') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('--file ', 'The JSON File used to set up the slas client') - .option('--clientid ', 'The client ID to add') - .option('--clientname ', 'The name of the client ID') - .option('--privateclient ', 'true the client is private') - .option('--ecomtenant ', 'the ecom tenant') - .option('--ecomsite ', 'the ecom site') - .option('--secret ', 'the slas secret, can be different then the secret in \ - account manager, but shouldnt be') - .option('--channels ', 'comma separated list of site IDs this API client should support') - .option('--scopes ', 'comma separated list of auth z scopes this API client should support') - .option('--redirecturis ', 'comma separated list of redirect uris this API client should support') - - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - const clientid = options.clientid; - const clientname = options.clientname; - const privateclient = options.privateclient; - const ecomtenant = options.ecomtenant; - const ecomsite = options.ecomsite; - const secret = options.secret; - const channels = !options.channels || options.channels.split(',').map(item => item.trim()); - const scopes = !options.scopes || options.scopes.split(',').map(item => item.trim()); - const redirecturis = !options.redirecturis || options.redirecturis.split(',').map(item => item.trim()); - - const slas = require('./lib/slas'); - await slas.cli.client.add(options.tenant, options.shortcode, options.file, - clientid, clientname, privateclient, ecomtenant, ecomsite, secret, channels, scopes, redirecturis, asJson); - - }).on('--help', function() { + .command('slas:tenant:credential-quality') + .description('Get credential quality metrics for a SLAS tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.username) + .action(async (options) => { + await require('./lib/slas').cli.tenant.credentialQuality(options); + }) + .on('--help', () => { + console.log(); + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:tenant:credential-quality --shortcode kv7kzm78 --tenant zzrf_001'); + console.log(' $ sfcc-ci slas:tenant:credential-quality --shortcode kv7kzm78 --tenant zzrf_001 \\'); + console.log(' --shortcode kv7kzm78 \\') + console.log(' --tenant zzrf_001 \\') + console.log(' --username user@example.com \\') console.log(); }); program - .command('slas:client:get') - .description('Gets a SLAS client from a given tenant') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('--clientid ', 'The client ID to get information for') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.client.get(options.tenant, options.shortcode, options.clientid, asJson); - + .command('slas:client:create') + .description('Create or update a SLAS client for a tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.client) + .option(...SLAS_OPTIONS.file) + .action(async (options) => { + await require('./lib/slas').cli.client.create(options); }).on('--help', function() { console.log(); + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:client:create \\') + console.log(' --shortcode kv7kzm78 \\') + console.log(' --tenant zzrf_001 \\') + console.log(' --client aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \\') + console.log(' --file client.json') + console.log(); }); program .command('slas:client:list') - .description('Lists all SLAS clients that belong to a given tenant') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('-j, --json', 'Formats the output in json') - .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.client.list(options.tenant, options.shortcode, asJson); - + .description('List SLAS clients for a tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.client) + .option(...SLAS_OPTIONS.json) + .action(async (options) => { + await require('./lib/slas').cli.client.list(options); }).on('--help', function() { console.log(); + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:client:list --shortcode kv7kzm78 --tenant zzrf_001'); + console.log(' $ sfcc-ci slas:client:list \\') + console.log(' --shortcode kv7kzm78 \\') + console.log(' --tenant zzrf_001 \\') + console.log(' --client aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \\') + console.log(); }); program .command('slas:client:delete') - .description('Deletes a SLAS client from a given tenant') - .option('--tenant ', 'the tenant id used for slas') - .option('--shortcode ', 'the organizations short code') - .option('--clientid ', 'The Client ID to delete') - .option('-j, --json', 'Formats the output in json') + .description('Delete a SLAS client for a tenant.') + .option(...SLAS_OPTIONS.shortcode) + .option(...SLAS_OPTIONS.tenant) + .option(...SLAS_OPTIONS.client) .action(async function(options) { - - var asJson = ( options.json ? options.json : false ); - - const slas = require('./lib/slas'); - await slas.cli.client.get(options.tenant, options.shortcode, options.clientid, asJson); - + await require('./lib/slas').cli.client.delete(options); }).on('--help', function() { console.log(); + console.log(' Examples:'); + console.log(); + console.log(' $ sfcc-ci slas:client:delete \\') + console.log(' --shortcode kv7kzm78 \\') + console.log(' --tenant zzrf_001 \\') + console.log(' --client aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa \\') + + console.log(); }); program.on('--help', function() { diff --git a/lib/slas.js b/lib/slas.js index 607730d2..87de9905 100644 --- a/lib/slas.js +++ b/lib/slas.js @@ -1,33 +1,60 @@ -const fetch = require('node-fetch'); -const fs = require('fs'); +// SLAS: https://developer.salesforce.com/docs/commerce/commerce-api/references?meta=shopper-login-and-api-access-admin:Summary +const fetch = require("node-fetch"); +const fs = require("fs"); +const jsonwebtoken = require("jsonwebtoken"); -const auth = require('./auth'); -const secrets = require('./secrets'); +const auth = require("./auth"); +const secrets = require("./secrets"); + +function getAuthJWT() { + const token = auth.getToken(); + const data = jsonwebtoken.decode(token); + if (!data) { + throw new Error( + "Access Token is not a JWT. Check Account Manager API Client's Access Token Format is set to `JWT`." + ); + } + return token; +} /** * Generates a SLAS Admin Url * @param {string} tenantId the tenant found in BM - e.g bbsv_stg * @param {string} shortcode the shortcode found in BM - e.g acdefg - * @param {string} [clientId] if provided a client URL is generated + * @param {string} [clientId] if provided a client URL is generated */ -function getSlasUrl(tenantId, shortcode, clientId) { - return `https://${shortcode}.api.commercecloud.salesforce.com/shopper/auth-admin/v1/tenants/${tenantId - + (clientId ? ('/clients/' + clientId) : '')}`; +function getSlasUrl({ shortcode, tenant, client }) { + const bits = [ + `https://${shortcode}.api.commercecloud.salesforce.com/shopper/auth-admin/v1/tenants`, + ]; + tenant && bits.push(tenant); + client && bits.push(`clients/${client}`); + return bits.join("/"); } - /** * Handles fetch response * @param {object} response the http client response * @return {object} the parsed success response */ async function handleResponse(response) { - if (response.status > 299) { - throw new Error(`HTTP Fault ${response.status} (${response.statusText})`) + if (response.ok) { + if (response.status == 204) { + return { success: true }; + } + return await response.json(); } - const resultText = await response.text(); - return JSON.parse(resultText); + const contentType = response.headers.get("content-type"); + const isJSON = contentType && contentType.includes("application/json"); + let message; + if (isJSON) { + message = await response.json(); + } else { + message = await response.text(); + } + console.error(message); + throw new Error(`HTTP Fault ${response.status} (${response.statusText})`); } /** @@ -37,9 +64,9 @@ async function handleResponse(response) { */ function handleCLIOutput(result, asJson) { if (asJson) { - console.info(JSON.stringify(result, null, 4)) + console.info(JSON.stringify(result, null, 4)); } else { - console.table(result) + console.table(result); } } /** @@ -49,275 +76,288 @@ function handleCLIOutput(result, asJson) { */ function handleCLIError(prefix, message, asJson) { if (asJson) { - console.info(JSON.stringify({prefix, message}, null, 4)) + console.info(JSON.stringify({ prefix, message }, null, 4)); } else { - console.error(prefix + message) + console.error(prefix + message); } } - const slas = { cli: { - tenant : { - add: async (tenantId, shortcode, description, merchantName, contact, emailAddress, fileName, asJson) => { - let result + tenant: { + create: async ({ shortcode, tenant, file, json }) => { + shortcode = secrets.getScapiShortCode(shortcode); + tenant = secrets.getScapiTenantId(tenant); + try { - result = await slas.api.tenant.add(tenantId, shortcode, - description, merchantName, contact, emailAddress, fileName) - console.info('sucessfully add tenant') - handleCLIOutput(result, asJson) + const result = await slas.api.tenant.create({ + shortcode, + tenant, + file, + }); + console.info("Successfully created tenant"); + handleCLIOutput(result, json); } catch (e) { - handleCLIError('Could not add tenant: ', e.message, asJson) + handleCLIError( + "Could not create tenant: ", + e.message, + json + ); } }, - get: async (tenantId, shortcode, asJson) => { - let result + get: async ({ shortcode, tenant, json }) => { + shortcode = secrets.getScapiShortCode(shortcode); + tenant = secrets.getScapiTenantId(tenant); + try { - result = await slas.api.tenant.get(tenantId, shortcode) - handleCLIOutput(result, asJson) + const result = await slas.api.tenant.get({ + shortcode, + tenant, + }); + handleCLIOutput(result, json); } catch (e) { - handleCLIError('Could not get tenant: ', e.message, asJson) + handleCLIError("Could not get tenant: ", e.message, json); } }, - list: async (shortcode, asJson) => { - let result - try { - result = await slas.api.tenant.list(shortcode) - handleCLIOutput(result, asJson) - } catch (e) { - handleCLIError('Could not get tenants: ', e.message, asJson) + credentialQuality: async ({ shortcode, tenant, username }) => { + shortcode = secrets.getScapiShortCode(shortcode); + tenant = secrets.getScapiTenantId(tenant); + + if (username) { + try { + const result = + await slas.api.tenant.userCredentialQuality({ + shortcode, + tenant, + username, + }); + console.dir(result); + } catch (e) {} + return; } - }, - delete: async (tenantId, shortcode, asJson) => { - let result + try { - result = await slas.api.tenant.delete(tenantId, shortcode) - handleCLIOutput(result, asJson) + const result = await slas.api.tenant.credentialQuality({ + shortcode, + tenant, + }); + console.dir(result); } catch (e) { - handleCLIError('Could not delete tenant: ', e.message, asJson) + handleCLIError("Could not get tenant: ", e.message); } - } + }, }, - client : { - add: async (tenantId, shortcode, fileName,clientid, clientname, privateclient, - ecomtenant, ecomsite, secret, channels, scopes, redirecturis, asJson) => { - let result - try { - result = await slas.api.client.add(tenantId, shortcode, fileName, clientid, clientname, - privateclient, ecomtenant, ecomsite, secret, channels, scopes, redirecturis); - console.info('sucessfully add client ') - handleCLIOutput(result, asJson) - } catch (e) { - handleCLIError('Could not add client: ', e.message, asJson) + client: { + create: async ({ shortcode, tenant, client, file }) => { + if (!file) { + throw new Error("Option --file is required."); } - }, - get: async (tenantId, shortcode, clientId, asJson) => { - let result + + shortcode = secrets.getScapiShortCode(shortcode); + tenant = secrets.getScapiTenantId(tenant); + + // Ensure `file` is valid JSON. + const body = JSON.parse(fs.readFileSync(file, "utf-8")); + // Provided Client ID overrides JSON. + body.clientId = client; + try { - result = await slas.api.client.get(tenantId, shortcode, clientId) - handleCLIOutput(result, asJson) + const result = await slas.api.client.create({ + shortcode, + tenant, + client, + body, + }); + console.info("Successfully created client"); + handleCLIOutput(result, true); } catch (e) { - handleCLIError('Could not get tenant: ', e.message, asJson) + handleCLIError( + "Could not create client: ", + e.message, + true + ); } }, - list: async (shortcode, tenantId, asJson) => { - let result + list: async ({ shortcode, tenant, client, json }) => { + shortcode = secrets.getScapiShortCode(shortcode); + tenantId = secrets.getScapiTenantId(tenant); + + if (client) { + try { + const result = await slas.api.client.get({ + shortcode, + tenant, + client, + }); + + if (json) { + console.log(JSON.stringify(result, null, 4)); + } else { + console.dir(result); + } + return; + } catch (e) { + return handleCLIError( + "Could not get client: ", + e.message + ); + } + } + try { - result = await slas.api.client.list(shortcode, tenantId); - if (asJson) { - console.info(JSON.stringify(result, null, 4)); + const result = await slas.api.client.list({ + shortcode, + tenant, + }); + + if (json) { + console.log(JSON.stringify(result, null, 4)); } else { - result.data.forEach((element) => console.table(element)); + console.dir(result); } } catch (e) { - handleCLIError('Could not get tenants: ', e.message, asJson) + handleCLIError("Could not list clients: ", e.message); } }, - delete: async (tenantId, shortcode, clientId, asJson) => { - let result + delete: async ({ shortcode, tenant, client }) => { + shortcode = secrets.getScapiShortCode(shortcode); + tenant = secrets.getScapiTenantId(tenant); + try { - result = await slas.api.client.delete(tenantId, shortcode, clientId) - handleCLIOutput(result, asJson) + const result = await slas.api.client.delete({ + shortcode, + tenant, + client, + }); + handleCLIOutput(result); } catch (e) { - handleCLIError('Could not delete tenant: ', e.message, asJson) + handleCLIError("Could not delete tenant: ", e.message); } - } - } + }, + }, }, api: { tenant: { - add: async (tenantId, shortcode, description, merchantName, contact, emailAddress, fileName) => { - const token = auth.getToken(); - let params - // set fallbacks - shortcode = secrets.getScapiShortCode(shortcode); - if (!fileName) { - tenantId = secrets.getScapiTenantId(tenantId); - description = description || `Added by SFCC-CI at ${(new Date()).toISOString()}` - merchantName = merchantName || tenantId - contact = contact || auth.getUser() - emailAddress = emailAddress || (auth.getUser() ? auth.getUser() : 'noreply@salesforce.com') + create: async ({ shortcode, tenant, file }) => { + const token = getAuthJWT(); - params = { - instance: tenantId, - description, - merchantName, - contact, - emailAddress - } + let body; + if (!file) { + body = { + instance: tenant, + description: `Created by SFCC-CI at ${new Date().toISOString()}`, + emailAddress: jsonwebtoken.decode(token).sub, + merchantName: "_", + }; } else { - params = JSON.parse(fs.readFileSync(fileName, 'utf-8')); - tenantId = secrets.getScapiTenantId(tenantId || params.instance); + body = JSON.parse(fs.readFileSync(file, "utf-8")); + // Provided `tenant` overrides `instance` from JSON. + body.instance = tenant; } - const response = await fetch(getSlasUrl(tenantId, shortcode), { - method: 'PUT', + + const url = getSlasUrl({ shortcode, tenant }); + const options = { + method: "PUT", headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - body: JSON.stringify(params) - }); + body: JSON.stringify(body), + }; + const response = await fetch(url, options); return await handleResponse(response); }, - get: async (tenantId, shortcode) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl(tenantId, shortcode), { - method: 'GET', + get: async ({ shortcode, tenant }) => { + const url = getSlasUrl({ tenant, shortcode }); + const options = { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); - + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + const response = await fetch(url, options); return await handleResponse(response); }, - list: async (shortcode) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl('', shortcode), { - method: 'GET', + credentialQuality: async ({ shortcode, tenant }) => { + const url = + getSlasUrl({ tenant, shortcode }) + "/cred-qual/login"; + const options = { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + const response = await fetch(url, options); return await handleResponse(response); }, - delete: async (tenantId, shortcode) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl(tenantId, shortcode), { - method: 'DELETE', + userCredentialQuality: async ({ shortcode, tenant, username }) => { + const query = new URLSearchParams({ username }); + const url = + getSlasUrl({ tenant, shortcode }) + + `/cred-qual/user?${query}`; + const options = { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); - + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + const response = await fetch(url, options); return await handleResponse(response); }, }, client: { - add: async (tenantId, shortcode, file, clientid, clientname, privateclient, - ecomtenant, ecomsite, secret, channels, scopes, redirecturis,) => { - const token = auth.getToken(); - - // set fallbacks - shortcode = secrets.getScapiShortCode(shortcode); - let params; - if (file) { - params = JSON.parse(fs.readFileSync(file, 'utf-8')); - } else { - params = { - cliendId: clientid, - name: clientname, - isPrivateClient: privateclient, - ecomTenant: ecomtenant, - ecomSite: ecomsite, - secret: secret, - channels: channels, - scopes: scopes, - redirectUri: redirecturis - } - } - tenantId = secrets.getScapiTenantId(tenantId || params.ecomTenant); - const response = await fetch(getSlasUrl(tenantId, shortcode, params.clientId), { - method: 'PUT', + create: async ({ shortcode, tenant, client, body }) => { + const url = getSlasUrl({ shortcode, tenant, client }); + const options = { + method: "PUT", headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", }, - body: JSON.stringify(params) - }); - + body: JSON.stringify(body), + }; + const response = await fetch(url, options); return await handleResponse(response); }, - get: async (tenantId, shortcode, clientId) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl(tenantId, shortcode, clientId), { - method: 'GET', + get: async ({ shortcode, tenant, client }) => { + const url = getSlasUrl({ shortcode, tenant, client }); + const options = { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); - + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + const response = await fetch(url, options); return await handleResponse(response); }, - list: async (shortcode, tenantId) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl(tenantId, shortcode) + '/clients', { - method: 'GET', + list: async ({ shortcode, tenant }) => { + const url = getSlasUrl({ tenant, shortcode }) + "/clients"; + const options = { headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + // TODO: If no clients belong to this tenant, SLAS returns a HTTP 404. + const response = await fetch(url, options); return await handleResponse(response); }, - delete: async (tenantId, shortcode, clientId) => { - const token = auth.getToken(); - - // set fallbacks - tenantId = secrets.getScapiTenantId(tenantId); - shortcode = secrets.getScapiShortCode(shortcode); - - const response = await fetch(getSlasUrl(tenantId, shortcode, clientId), { - method: 'DELETE', + delete: async ({ shortcode, tenant, client }) => { + const url = getSlasUrl({ shortcode, tenant, client }); + const options = { + method: "DELETE", headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); - + Authorization: `Bearer ${getAuthJWT()}`, + "Content-Type": "application/json", + }, + }; + const response = await fetch(url, options); return await handleResponse(response); }, - } - } -} + }, + }, + getSlasUrl, +}; -module.exports = slas; \ No newline at end of file +module.exports = slas; diff --git a/test/unit/slas.js b/test/unit/slas.js new file mode 100644 index 00000000..be6022f8 --- /dev/null +++ b/test/unit/slas.js @@ -0,0 +1,188 @@ +const sinon = require("sinon"); +const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); +const jsonwebtoken = require("jsonwebtoken"); +const { Response } = require("node-fetch"); + +const TENANT = { + shortcode: "aaaaaaa", + tenant: "aaaa_001", +}; +const CLIENT = Object.assign( + { client: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }, + TENANT +); +const TOKEN = jsonwebtoken.sign( + { + sub: "user@example.com", + tenantFilter: "SLAS_ORGANIZATION_ADMIN:aaa_001", + roles: ["SLAS_ORGANIZATION_ADMIN"], + }, + "very-secret" +); + +const fetchStub = sinon.stub(); + +const slas = proxyquire("../../lib/slas", { + "node-fetch": fetchStub, + "./auth": { + getToken: () => TOKEN, + }, +}); + +describe("Shopper Login and API Access Service (SLAS)", () => { + afterEach(() => fetchStub.reset()); + + describe("Tenant CLI", () => { + it("Creates", async () => { + const response = { + contact: null, + description: "Created by SFCC-CI at 2022-01-01T12:00:00.000Z", + emailAddress: "user@example.com", + instance: "aaaa_001", + merchantName: "_", + phoneNo: null, + }; + + fetchStub.returns( + Promise.resolve( + new Response(JSON.stringify(response), { status: 200 }) + ) + ); + + await slas.cli.tenant.create(TENANT); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledWithMatch( + fetchStub, + slas.getSlasUrl(TENANT), + sinon.match({ + body: sinon.match.string, + headers: { + Authorization: `Bearer ${TOKEN}`, + "Content-Type": "application/json", + }, + method: "PUT", + }) + ); + }); + + it("Gets", async () => { + const response = { + contact: null, + description: "Created by SFCC-CI at 2022-01-01T12:00:00.000Z", + emailAddress: "user@example.com", + instance: "aaaa_001", + merchantName: "_", + phoneNo: null, + }; + + fetchStub.returns( + Promise.resolve( + new Response(JSON.stringify(response), { status: 200 }) + ) + ); + + await slas.cli.tenant.get(TENANT); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledWithMatch( + fetchStub, + slas.getSlasUrl(TENANT), + sinon.match({ + headers: { + Authorization: `Bearer ${TOKEN}`, + }, + }) + ); + }); + }); + + describe("Client CLI", () => { + it("Lists", async () => { + const response = { + data: [ + { + clientId: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + name: "test", + scopes: "sfcc.shopper-categories", + redirectUri: "http://localhost:3000/callback", + channels: ["RefArch", "RefArchGlobal"], + isPrivateClient: false, + }, + ], + }; + + fetchStub.returns( + Promise.resolve( + new Response(JSON.stringify(response), { status: 200 }) + ) + ); + + await slas.cli.client.list(TENANT); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledWithMatch( + fetchStub, + slas.getSlasUrl(TENANT), + sinon.match({ + headers: { + Authorization: `Bearer ${TOKEN}`, + "Content-Type": "application/json", + }, + }) + ); + }); + + it("Lists one Client", async () => { + const response = { + clientId: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + name: "test", + scopes: "sfcc.shopper-categories", + redirectUri: "http://localhost:3000/callback", + channels: ["RefArch", "RefArchGlobal"], + isPrivateClient: false, + }; + + fetchStub.returns( + Promise.resolve( + new Response(JSON.stringify(response), { status: 200 }) + ) + ); + + await slas.cli.client.list(CLIENT); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledWithMatch( + fetchStub, + slas.getSlasUrl(CLIENT), + sinon.match({ + headers: { + Authorization: `Bearer ${TOKEN}`, + "Content-Type": "application/json", + }, + }) + ); + }); + + it("Deletes", async () => { + fetchStub.returns( + Promise.resolve(new Response(null, { status: 204 })) + ); + + await slas.cli.client.delete(CLIENT); + + sinon.assert.calledOnce(fetchStub); + sinon.assert.calledWithMatch( + fetchStub, + slas.getSlasUrl(CLIENT), + sinon.match({ + headers: { + Authorization: `Bearer ${TOKEN}`, + "Content-Type": "application/json", + }, + method: "DELETE", + }) + ); + }); + }); +});