<commit_before>/**
 * ACME client
 *
 * @namespace Client
 */

const crypto = require('crypto');
const debug = require('debug')('acme-client');
const Promise = require('bluebird');
const HttpClient = require('./http');
const AcmeApi = require('./api');
const verify = require('./verify');
const util = require('./util');
const auto = require('./auto');

/**
 * Default options
 */

const defaultOpts = {
    directoryUrl: undefined,
    accountKey: undefined,
    accountUrl: null,
    backoffAttempts: 5,
    backoffMin: 5000,
    backoffMax: 30000
};

/**
 * AcmeClient
 *
 * @class
 * @param {object} opts
 * @param {string} opts.directoryUrl ACME directory URL
 * @param {buffer|string} opts.accountKey PEM encoded account private key
 * @param {string} [opts.accountUrl] Account URL, default: `null`
 * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5`
 * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000`
 * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000`
 */

class AcmeClient {
    constructor(opts) {
        if (!Buffer.isBuffer(opts.accountKey)) {
            opts.accountKey = Buffer.from(opts.accountKey);
        }

        this.opts = Object.assign({}, defaultOpts, opts);

        this.backoffOpts = {
            attempts: this.opts.backoffAttempts,
            min: this.opts.backoffMin,
            max: this.opts.backoffMax
        };

        this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey);
        this.api = new AcmeApi(this.http, this.opts.accountUrl);
    }

    /**
     * Get Terms of Service URL
     *
     * @returns {Promise<string>} ToS URL
     */

    getTermsOfServiceUrl() {
        return this.api.getTermsOfServiceUrl();
    }

    /**
     * Get current account URL
     *
     * @returns {string} Account URL
     */

    getAccountUrl() {
        return this.api.getAccountUrl();
    }

    /**
     * Create a new account
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-creation
     *
     * @param {object} [data] Request data
     * @returns {Promise<object>} Account
     */

    async createAccount(data = {}) {
        try {
            this.getAccountUrl();

            /* Account URL exists */
            debug('Account URL exists, returning updateAccount()');
            return this.updateAccount(data);
        }
        catch (e) {
            const resp = await this.api.createAccount(data);

            /* HTTP 200: Account exists */
            if (resp.status === 200) {
                debug('Account already exists (HTTP 200), returning updateAccount()');
                return this.updateAccount(data);
            }

            return resp.data;
        }
    }

    /**
     * Update existing account
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-update
     *
     * @param {object} [data] Request data
     * @returns {Promise<object>} Account
     */

    async updateAccount(data = {}) {
        try {
            this.api.getAccountUrl();
        }
        catch (e) {
            debug('No account URL found, returning createAccount()');
            return this.createAccount(data);
        }

        /* Remove data only applicable to createAccount() */
        if ('onlyReturnExisting' in data) {
            delete data.onlyReturnExisting;
        }

        const resp = await this.api.updateAccount(data);
        return resp.data;
    }

    /**
     * Update account private key
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-key-roll-over
     *
     * @param {buffer|string} newAccountKey New PEM encoded private key
     * @param {object} [data] Additional request data
     * @returns {Promise<object>} Account
     */

    async updateAccountKey(newAccountKey, data = {}) {
        if (!Buffer.isBuffer(newAccountKey)) {
            newAccountKey = Buffer.from(newAccountKey);
        }

        const accountUrl = this.api.getAccountUrl();

        /* Create new HTTP and API clients using new key */
        const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey);
        const newApiClient = new AcmeApi(newHttpClient, accountUrl);

        /* Get new JWK */
        data.account = accountUrl;
        data.oldKey = await this.http.getJwk();

        /* TODO: Backward-compatibility with draft-ietf-acme-12, remove this in a later release */
        data.newKey = await newHttpClient.getJwk();

        /* Get signed request body from new client */
        const url = await newHttpClient.getResourceUrl('keyChange');
        const body = await newHttpClient.createSignedBody(url, data);

        /* Change key using old client */
        const resp = await this.api.updateAccountKey(body);

        /* Replace existing HTTP and API client */
        this.http = newHttpClient;
        this.api = newApiClient;

        return resp.data;
    }

    /**
     * Create a new order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#applying-for-certificate-issuance
     *
     * @param {object} data Request data
     * @returns {Promise<object>} Order
     */

    async createOrder(data) {
        const resp = await this.api.createOrder(data);

        if (!resp.headers.location) {
            throw new Error('Creating a new order did not return an order link');
        }

        /* Add URL to response */
        resp.data.url = resp.headers.location;
        return resp.data;
    }

    /**
     * Finalize order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#applying-for-certificate-issuance
     *
     * @param {object} order Order object
     * @param {buffer|string} csr PEM encoded Certificate Signing Request
     * @returns {Promise<object>} Order
     */

    async finalizeOrder(order, csr) {
        if (!order.finalize) {
            throw new Error('Unable to finalize order, URL not found');
        }

        if (!Buffer.isBuffer(csr)) {
            csr = Buffer.from(csr);
        }

        const body = util.getPemBody(csr);
        const data = { csr: util.b64escape(body) };

        const resp = await this.api.finalizeOrder(order.finalize, data);
        return resp.data;
    }

    /**
     * Get identifier authorizations from order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#identifier-authorization
     *
     * @param {object} order Order
     * @returns {Promise<object[]>} Authorizations
     */

    getAuthorizations(order) {
        return Promise.map((order.authorizations || []), async (url) => {
            const resp = await this.api.getAuthorization(url);

            /* Add URL to response */
            resp.data.url = url;
            return resp.data;
        });
    }

    /**
     * Deactivate identifier authorization
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#deactivating-an-authorization
     *
     * @param {object} authz Identifier authorization
     * @returns {Promise<object>} Authorization
     */

    async deactivateAuthorization(authz) {
        if (!authz.url) {
            throw new Error('Unable to deactivate identifier authorization, URL not found');
        }

        const data = {
            status: 'deactivated'
        };

        const resp = await this.api.updateAuthorization(authz.url, data);
        return resp.data;
    }

    /**
     * Get key authorization for ACME challenge
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#key-authorizations
     *
     * @param {object} challenge Challenge object returned by API
     * @returns {Promise<string>} Key authorization
     */

    async getChallengeKeyAuthorization(challenge) {
        const jwk = await this.http.getJwk();
        const keysum = crypto.createHash('sha256').update(JSON.stringify(jwk));
        const thumbprint = util.b64escape(keysum.digest('base64'));
        const result = `${challenge.token}.${thumbprint}`;

        /**
         * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge
         */

        if (challenge.type === 'http-01') {
            return result;
        }

        /**
         * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns-challenge
         * https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
         */

        if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
            const shasum = crypto.createHash('sha256').update(result);
            return util.b64escape(shasum.digest('base64'));
        }

        throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
    }

    /**
     * Verify that ACME challenge is satisfied
     *
     * @param {object} authz Identifier authorization
     * @param {object} challenge Authorization challenge
     * @returns {Promise}
     */

    async verifyChallenge(authz, challenge) {
        if (!authz.url || !challenge.url) {
            throw new Error('Unable to verify ACME challenge, URL not found');
        }

        if (typeof verify[challenge.type] === 'undefined') {
            throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
        }

        const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);

        const verifyFn = async () => {
            await verify[challenge.type](authz, challenge, keyAuthorization);
        };

        debug('Waiting for ACME challenge verification', this.backoffOpts);
        return util.retry(verifyFn, this.backoffOpts);
    }

    /**
     * Notify provider that challenge has been completed
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#responding-to-challenges
     *
     * @param {object} challenge Challenge object returned by API
     * @returns {Promise<object>} Challenge
     */

    async completeChallenge(challenge) {
        const data = {
            keyAuthorization: await this.getChallengeKeyAuthorization(challenge)
        };

        const resp = await this.api.completeChallenge(challenge.url, data);
        return resp.data;
    }

    /**
     * Wait for ACME provider to verify status on a order, authorization or challenge
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#responding-to-challenges
     *
     * @param {object} item An order, authorization or challenge object
     * @returns {Promise<object>} Valid order, authorization or challenge
     */

    async waitForValidStatus(item) {
        if (!item.url) {
            throw new Error('Unable to verify status of item, URL not found');
        }

        const verifyFn = async (abort) => {
            const resp = await this.api.apiRequest(item.url, null, [200]);

            /* Verify status */
            debug(`Item has status: ${resp.data.status}`);

            if (resp.data.status === 'invalid') {
                abort();
                throw new Error(util.formatResponseError(resp));
            }
            else if (resp.data.status === 'pending') {
                throw new Error('Operation is pending');
            }
            else if (resp.data.status === 'valid') {
                return resp.data;
            }

            throw new Error(`Unexpected item status: ${resp.data.status}`);
        };

        debug(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
        return util.retry(verifyFn, this.backoffOpts);
    }

    /**
     * Get certificate from ACME order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#downloading-the-certificate
     *
     * @param {object} order Order object
     * @returns {Promise<string>} Certificate
     */

    async getCertificate(order) {
        if (order.status !== 'valid') {
            order = await this.waitForValidStatus(order);
        }

        if (!order.certificate) {
            throw new Error('Unable to download certificate, URL not found');
        }

        const resp = await this.api.apiRequest(order.certificate, null, [200]);
        return resp.data;
    }

    /**
     * Revoke certificate
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#certificate-revocation
     *
     * @param {buffer|string} cert PEM encoded certificate
     * @param {object} [data] Additional request data
     * @returns {Promise}
     */

    async revokeCertificate(cert, data = {}) {
        const body = util.getPemBody(cert);
        data.certificate = util.b64escape(body);

        const resp = await this.api.revokeCert(data);
        return resp.data;
    }

    /**
     * Auto mode
     *
     * @param {object} opts
     * @param {buffer|string} opts.csr Certificate Signing Request
     * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge
     * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge
     * @param {string} [opts.email] Account email address
     * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false`
     * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']`
     * @returns {Promise<string>} Certificate
     */

    auto(opts) {
        return auto(this, opts);
    }
}

/* Export client */
module.exports = AcmeClient;
<commit_msg>POST-as-GET fix when updateAccount() is called with no payload<commit_after>/**
 * ACME client
 *
 * @namespace Client
 */

const crypto = require('crypto');
const debug = require('debug')('acme-client');
const Promise = require('bluebird');
const HttpClient = require('./http');
const AcmeApi = require('./api');
const verify = require('./verify');
const util = require('./util');
const auto = require('./auto');

/**
 * Default options
 */

const defaultOpts = {
    directoryUrl: undefined,
    accountKey: undefined,
    accountUrl: null,
    backoffAttempts: 5,
    backoffMin: 5000,
    backoffMax: 30000
};

/**
 * AcmeClient
 *
 * @class
 * @param {object} opts
 * @param {string} opts.directoryUrl ACME directory URL
 * @param {buffer|string} opts.accountKey PEM encoded account private key
 * @param {string} [opts.accountUrl] Account URL, default: `null`
 * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5`
 * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000`
 * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000`
 */

class AcmeClient {
    constructor(opts) {
        if (!Buffer.isBuffer(opts.accountKey)) {
            opts.accountKey = Buffer.from(opts.accountKey);
        }

        this.opts = Object.assign({}, defaultOpts, opts);

        this.backoffOpts = {
            attempts: this.opts.backoffAttempts,
            min: this.opts.backoffMin,
            max: this.opts.backoffMax
        };

        this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey);
        this.api = new AcmeApi(this.http, this.opts.accountUrl);
    }

    /**
     * Get Terms of Service URL
     *
     * @returns {Promise<string>} ToS URL
     */

    getTermsOfServiceUrl() {
        return this.api.getTermsOfServiceUrl();
    }

    /**
     * Get current account URL
     *
     * @returns {string} Account URL
     */

    getAccountUrl() {
        return this.api.getAccountUrl();
    }

    /**
     * Create a new account
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-creation
     *
     * @param {object} [data] Request data
     * @returns {Promise<object>} Account
     */

    async createAccount(data = {}) {
        try {
            this.getAccountUrl();

            /* Account URL exists */
            debug('Account URL exists, returning updateAccount()');
            return this.updateAccount(data);
        }
        catch (e) {
            const resp = await this.api.createAccount(data);

            /* HTTP 200: Account exists */
            if (resp.status === 200) {
                debug('Account already exists (HTTP 200), returning updateAccount()');
                return this.updateAccount(data);
            }

            return resp.data;
        }
    }

    /**
     * Update existing account
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-update
     *
     * @param {object} [data] Request data
     * @returns {Promise<object>} Account
     */

    async updateAccount(data = {}) {
        try {
            this.api.getAccountUrl();
        }
        catch (e) {
            debug('No account URL found, returning createAccount()');
            return this.createAccount(data);
        }

        /* Remove data only applicable to createAccount() */
        if ('onlyReturnExisting' in data) {
            delete data.onlyReturnExisting;
        }

        /* POST-as-GET */
        if (Object.keys(data).length === 0) {
            data = null;
        }

        const resp = await this.api.updateAccount(data);
        return resp.data;
    }

    /**
     * Update account private key
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#account-key-roll-over
     *
     * @param {buffer|string} newAccountKey New PEM encoded private key
     * @param {object} [data] Additional request data
     * @returns {Promise<object>} Account
     */

    async updateAccountKey(newAccountKey, data = {}) {
        if (!Buffer.isBuffer(newAccountKey)) {
            newAccountKey = Buffer.from(newAccountKey);
        }

        const accountUrl = this.api.getAccountUrl();

        /* Create new HTTP and API clients using new key */
        const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey);
        const newApiClient = new AcmeApi(newHttpClient, accountUrl);

        /* Get new JWK */
        data.account = accountUrl;
        data.oldKey = await this.http.getJwk();

        /* TODO: Backward-compatibility with draft-ietf-acme-12, remove this in a later release */
        data.newKey = await newHttpClient.getJwk();

        /* Get signed request body from new client */
        const url = await newHttpClient.getResourceUrl('keyChange');
        const body = await newHttpClient.createSignedBody(url, data);

        /* Change key using old client */
        const resp = await this.api.updateAccountKey(body);

        /* Replace existing HTTP and API client */
        this.http = newHttpClient;
        this.api = newApiClient;

        return resp.data;
    }

    /**
     * Create a new order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#applying-for-certificate-issuance
     *
     * @param {object} data Request data
     * @returns {Promise<object>} Order
     */

    async createOrder(data) {
        const resp = await this.api.createOrder(data);

        if (!resp.headers.location) {
            throw new Error('Creating a new order did not return an order link');
        }

        /* Add URL to response */
        resp.data.url = resp.headers.location;
        return resp.data;
    }

    /**
     * Finalize order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#applying-for-certificate-issuance
     *
     * @param {object} order Order object
     * @param {buffer|string} csr PEM encoded Certificate Signing Request
     * @returns {Promise<object>} Order
     */

    async finalizeOrder(order, csr) {
        if (!order.finalize) {
            throw new Error('Unable to finalize order, URL not found');
        }

        if (!Buffer.isBuffer(csr)) {
            csr = Buffer.from(csr);
        }

        const body = util.getPemBody(csr);
        const data = { csr: util.b64escape(body) };

        const resp = await this.api.finalizeOrder(order.finalize, data);
        return resp.data;
    }

    /**
     * Get identifier authorizations from order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#identifier-authorization
     *
     * @param {object} order Order
     * @returns {Promise<object[]>} Authorizations
     */

    getAuthorizations(order) {
        return Promise.map((order.authorizations || []), async (url) => {
            const resp = await this.api.getAuthorization(url);

            /* Add URL to response */
            resp.data.url = url;
            return resp.data;
        });
    }

    /**
     * Deactivate identifier authorization
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#deactivating-an-authorization
     *
     * @param {object} authz Identifier authorization
     * @returns {Promise<object>} Authorization
     */

    async deactivateAuthorization(authz) {
        if (!authz.url) {
            throw new Error('Unable to deactivate identifier authorization, URL not found');
        }

        const data = {
            status: 'deactivated'
        };

        const resp = await this.api.updateAuthorization(authz.url, data);
        return resp.data;
    }

    /**
     * Get key authorization for ACME challenge
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#key-authorizations
     *
     * @param {object} challenge Challenge object returned by API
     * @returns {Promise<string>} Key authorization
     */

    async getChallengeKeyAuthorization(challenge) {
        const jwk = await this.http.getJwk();
        const keysum = crypto.createHash('sha256').update(JSON.stringify(jwk));
        const thumbprint = util.b64escape(keysum.digest('base64'));
        const result = `${challenge.token}.${thumbprint}`;

        /**
         * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge
         */

        if (challenge.type === 'http-01') {
            return result;
        }

        /**
         * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns-challenge
         * https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
         */

        if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
            const shasum = crypto.createHash('sha256').update(result);
            return util.b64escape(shasum.digest('base64'));
        }

        throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
    }

    /**
     * Verify that ACME challenge is satisfied
     *
     * @param {object} authz Identifier authorization
     * @param {object} challenge Authorization challenge
     * @returns {Promise}
     */

    async verifyChallenge(authz, challenge) {
        if (!authz.url || !challenge.url) {
            throw new Error('Unable to verify ACME challenge, URL not found');
        }

        if (typeof verify[challenge.type] === 'undefined') {
            throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
        }

        const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);

        const verifyFn = async () => {
            await verify[challenge.type](authz, challenge, keyAuthorization);
        };

        debug('Waiting for ACME challenge verification', this.backoffOpts);
        return util.retry(verifyFn, this.backoffOpts);
    }

    /**
     * Notify provider that challenge has been completed
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#responding-to-challenges
     *
     * @param {object} challenge Challenge object returned by API
     * @returns {Promise<object>} Challenge
     */

    async completeChallenge(challenge) {
        const data = {
            keyAuthorization: await this.getChallengeKeyAuthorization(challenge)
        };

        const resp = await this.api.completeChallenge(challenge.url, data);
        return resp.data;
    }

    /**
     * Wait for ACME provider to verify status on a order, authorization or challenge
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#responding-to-challenges
     *
     * @param {object} item An order, authorization or challenge object
     * @returns {Promise<object>} Valid order, authorization or challenge
     */

    async waitForValidStatus(item) {
        if (!item.url) {
            throw new Error('Unable to verify status of item, URL not found');
        }

        const verifyFn = async (abort) => {
            const resp = await this.api.apiRequest(item.url, null, [200]);

            /* Verify status */
            debug(`Item has status: ${resp.data.status}`);

            if (resp.data.status === 'invalid') {
                abort();
                throw new Error(util.formatResponseError(resp));
            }
            else if (resp.data.status === 'pending') {
                throw new Error('Operation is pending');
            }
            else if (resp.data.status === 'valid') {
                return resp.data;
            }

            throw new Error(`Unexpected item status: ${resp.data.status}`);
        };

        debug(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
        return util.retry(verifyFn, this.backoffOpts);
    }

    /**
     * Get certificate from ACME order
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#downloading-the-certificate
     *
     * @param {object} order Order object
     * @returns {Promise<string>} Certificate
     */

    async getCertificate(order) {
        if (order.status !== 'valid') {
            order = await this.waitForValidStatus(order);
        }

        if (!order.certificate) {
            throw new Error('Unable to download certificate, URL not found');
        }

        const resp = await this.api.apiRequest(order.certificate, null, [200]);
        return resp.data;
    }

    /**
     * Revoke certificate
     *
     * https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#certificate-revocation
     *
     * @param {buffer|string} cert PEM encoded certificate
     * @param {object} [data] Additional request data
     * @returns {Promise}
     */

    async revokeCertificate(cert, data = {}) {
        const body = util.getPemBody(cert);
        data.certificate = util.b64escape(body);

        const resp = await this.api.revokeCert(data);
        return resp.data;
    }

    /**
     * Auto mode
     *
     * @param {object} opts
     * @param {buffer|string} opts.csr Certificate Signing Request
     * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge
     * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge
     * @param {string} [opts.email] Account email address
     * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false`
     * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']`
     * @returns {Promise<string>} Certificate
     */

    auto(opts) {
        return auto(this, opts);
    }
}

/* Export client */
module.exports = AcmeClient;
<|endoftext|>


<commit_before>import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { fromJS } from 'immutable';
import configureStore from './configureStore';

const initialState = fromJS(window.__INITIAL_STATE__);  // redux-immutable only allow immutable obj
const store = configureStore(initialState);
const history = syncHistoryWithStore(browserHistory, store, {
  selectLocationState: state => state.get('routing').toJS(),
});
const mountNode = document.getElementById('react-view');

const renderApp = () => {
  const routes = require('./routes').default;

  render(
    <Provider store={store}>
      <Router history={history} routes={routes} />
    </Provider>,
    mountNode
  );
};

// Enable hot reload by react-hot-loader
if (module.hot) {
  const reRenderApp = () => {
    try {
      renderApp();
    } catch (error) {
      const RedBox = require('redbox-react').default;

      render(<RedBox error={error} />, mountNode);
    }
  };

  module.hot.accept('./routes', () => {
    // Prevent the hot reloading error from react-router
    setImmediate(() => {
      unmountComponentAtNode(mountNode);
      reRenderApp();
    });
  });
}

renderApp();
<commit_msg>Code refine<commit_after>import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router/es6';
import { syncHistoryWithStore } from 'react-router-redux';
import { fromJS } from 'immutable';
import configureStore from './configureStore';

const initialState = fromJS(window.__INITIAL_STATE__);  // redux-immutable only allow immutable obj
const store = configureStore(initialState);
const history = syncHistoryWithStore(browserHistory, store, {
  selectLocationState: state => state.get('routing').toJS(),
});
const mountNode = document.getElementById('react-view');

const renderApp = () => {
  const routes = require('./routes').default;

  render(
    <Provider store={store}>
      <Router history={history} routes={routes} />
    </Provider>,
    mountNode
  );
};

// Enable hot reload by react-hot-loader
if (module.hot) {
  const reRenderApp = () => {
    try {
      renderApp();
    } catch (error) {
      const RedBox = require('redbox-react').default;

      render(<RedBox error={error} />, mountNode);
    }
  };

  module.hot.accept('./routes', () => {
    // Prevent the hot reloading error from react-router
    setImmediate(() => {
      unmountComponentAtNode(mountNode);
      reRenderApp();
    });
  });
}

renderApp();
<|endoftext|>


<commit_before>const helpers = require('./support/helpers');

beforeAll(() => {

  require('../../app/assets/javascripts/govuk/cookie-functions.js');
  require('../../app/assets/javascripts/analytics/analytics.js');
  require('../../app/assets/javascripts/analytics/init.js');
  require('../../app/assets/javascripts/cookieMessage.js');

});

afterAll(() => {

  require('./support/teardown.js');

});

describe("Cookie message", () => {

  let cookieMessage;

  beforeAll(() => {

    helpers.deleteCookie('cookies-policy');

  });

  beforeEach(() => {

    // add the script GA looks for in the document
    document.body.appendChild(document.createElement('script'));

    jest.spyOn(window.GOVUK, 'initAnalytics');

    cookieMessage = `

Cookies on GOV.UK Notify

We use small files called cookies to make GOV.UK Notify work.

              Set cookie preferences

We'd also like to use analytics cookies to help us improve our service.

Please let us know if this is OK.

                <button class="govuk-button notify-cookie-banner-button--secondary notify-cookie-banner-button--inline" type="submit" data-accept-cookies="true">Yes, I accept analytics cookies</button>

                <button class="govuk-button notify-cookie-banner-button--secondary notify-cookie-banner-button--inline" type="submit" data-accept-cookies="false">No, do not use analytics cookies</button>

            You can change your cookie settings at any time.

          <button class="notify-cookie-banner__hide-button" data-hide-cookie-banner="true">Hide</button>

`;

    document.body.innerHTML += cookieMessage;

  });

  afterEach(() => {

    document.body.innerHTML = '';

    // remove cookie set by tests
    helpers.deleteCookie('cookies_policy');

    // reset spies
    window.GOVUK.initAnalytics.mockClear();

    // remove analytics tracker
    delete window.GOVUK.analytics;

    // reset global variable to state when init.js loaded
    window['ga-disable-UA-26179049-1'] = true;

  });

  /*
    Note: If no JS, the cookie banner is hidden.

    This works through CSS, based on the presence of the `js-enabled` class on the  so is not tested here.
  */

  test("If the cookies set by the old banner still exist, they can be cleared with the `clearOldCookies` method", () => {

    helpers.setCookie('seen_cookie_message', 'true', { 'days': 365 });
    helpers.setCookie('_ga', 'GA1.1.123.123', { 'days': 365 });
    helpers.setCookie('_gid', 'GA1.1.456.456', { 'days': 1 });

    window.GOVUK.Modules.CookieBanner.clearOldCookies();

    expect(window.GOVUK.cookie('seen_cookie_message')).toBeNull();
    expect(window.GOVUK.cookie('_ga')).toBeNull();
    expect(window.GOVUK.cookie('_gid')).toBeNull();

  });

  test("If user has made a choice to give their consent or not, the cookie banner should be hidden", () => {

    window.GOVUK.setConsentCookie({ 'analytics': false });

    window.GOVUK.modules.start()

    expect(helpers.element(document.querySelector('.notify-cookie-banner')).is('hidden')).toBe(true);

  });

  describe("If user hasn't made a choice to give their consent or not", () => {

    beforeEach(() => {

      window.GOVUK.modules.start();

    });

    test("The cookie banner should show", () => {

      const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

      expect(banner.is('hidden')).toBe(false);

    });

    test("No analytics should run", () => {

      expect(window.GOVUK.initAnalytics).not.toHaveBeenCalled();

    });

    describe("If the user clicks the button to accept analytics", () => {

      beforeEach(() => {

        const acceptButton = document.querySelector('.notify-cookie-banner__button-accept button');

        helpers.triggerEvent(acceptButton, 'click');

      });

      test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => {

        confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation'));

        expect(confirmation.is('hidden')).toBe(false);
        expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You’ve accepted analytics cookies/));

      });

      test("If the user clicks the 'hide' button, the banner should be hidden", () => {

        const hideButton = document.querySelector('.notify-cookie-banner__hide-button');
        const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

        helpers.triggerEvent(hideButton, 'click');

        expect(banner.is('hidden')).toBe(true);

      });

      test("The consent cookie should be set, with analytics set to 'true'", () => {

        expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': true });

      });

      test("The analytics should be set up", () => {

        expect(window.GOVUK.analytics).toBeDefined();

      });

    });

    describe("If the user clicks the button to reject analytics", () => {

      beforeEach(() => {

        const rejectButton = document.querySelector('.notify-cookie-banner__button-reject button');

        helpers.triggerEvent(rejectButton, 'click');

      });

      test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => {

        confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation'));

        expect(confirmation.is('hidden')).toBe(false);
        expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You told us not to use analytics cookies/));

      });

      test("If the user clicks the 'hide' button, the banner should be hidden", () => {

        const hideButton = document.querySelector('.notify-cookie-banner__hide-button');
        const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

        helpers.triggerEvent(hideButton, 'click');

        expect(banner.is('hidden')).toBe(true);

      });

      test("The consent cookie should be set, with analytics set to 'true'", () => {

        expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': false });

      });

      test("The analytics should not be set up", () => {

        expect(window.GOVUK.analytics).not.toBeDefined();

      });

    });

  });

});
<commit_msg>Fix test description<commit_after>const helpers = require('./support/helpers');

beforeAll(() => {

  require('../../app/assets/javascripts/govuk/cookie-functions.js');
  require('../../app/assets/javascripts/analytics/analytics.js');
  require('../../app/assets/javascripts/analytics/init.js');
  require('../../app/assets/javascripts/cookieMessage.js');

});

afterAll(() => {

  require('./support/teardown.js');

});

describe("Cookie message", () => {

  let cookieMessage;

  beforeAll(() => {

    helpers.deleteCookie('cookies-policy');

  });

  beforeEach(() => {

    // add the script GA looks for in the document
    document.body.appendChild(document.createElement('script'));

    jest.spyOn(window.GOVUK, 'initAnalytics');

    cookieMessage = `

Cookies on GOV.UK Notify

We use small files called cookies to make GOV.UK Notify work.

              Set cookie preferences

We'd also like to use analytics cookies to help us improve our service.

Please let us know if this is OK.

                <button class="govuk-button notify-cookie-banner-button--secondary notify-cookie-banner-button--inline" type="submit" data-accept-cookies="true">Yes, I accept analytics cookies</button>

                <button class="govuk-button notify-cookie-banner-button--secondary notify-cookie-banner-button--inline" type="submit" data-accept-cookies="false">No, do not use analytics cookies</button>

            You can change your cookie settings at any time.

          <button class="notify-cookie-banner__hide-button" data-hide-cookie-banner="true">Hide</button>

`;

    document.body.innerHTML += cookieMessage;

  });

  afterEach(() => {

    document.body.innerHTML = '';

    // remove cookie set by tests
    helpers.deleteCookie('cookies_policy');

    // reset spies
    window.GOVUK.initAnalytics.mockClear();

    // remove analytics tracker
    delete window.GOVUK.analytics;

    // reset global variable to state when init.js loaded
    window['ga-disable-UA-26179049-1'] = true;

  });

  /*
    Note: If no JS, the cookie banner is hidden.

    This works through CSS, based on the presence of the `js-enabled` class on the  so is not tested here.
  */

  test("If the cookies set by the old banner still exist, they can be cleared with the `clearOldCookies` method", () => {

    helpers.setCookie('seen_cookie_message', 'true', { 'days': 365 });
    helpers.setCookie('_ga', 'GA1.1.123.123', { 'days': 365 });
    helpers.setCookie('_gid', 'GA1.1.456.456', { 'days': 1 });

    window.GOVUK.Modules.CookieBanner.clearOldCookies();

    expect(window.GOVUK.cookie('seen_cookie_message')).toBeNull();
    expect(window.GOVUK.cookie('_ga')).toBeNull();
    expect(window.GOVUK.cookie('_gid')).toBeNull();

  });

  test("If user has made a choice to give their consent or not, the cookie banner should be hidden", () => {

    window.GOVUK.setConsentCookie({ 'analytics': false });

    window.GOVUK.modules.start()

    expect(helpers.element(document.querySelector('.notify-cookie-banner')).is('hidden')).toBe(true);

  });

  describe("If user hasn't made a choice to give their consent or not", () => {

    beforeEach(() => {

      window.GOVUK.modules.start();

    });

    test("The cookie banner should show", () => {

      const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

      expect(banner.is('hidden')).toBe(false);

    });

    test("No analytics should run", () => {

      expect(window.GOVUK.initAnalytics).not.toHaveBeenCalled();

    });

    describe("If the user clicks the button to accept analytics", () => {

      beforeEach(() => {

        const acceptButton = document.querySelector('.notify-cookie-banner__button-accept button');

        helpers.triggerEvent(acceptButton, 'click');

      });

      test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => {

        confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation'));

        expect(confirmation.is('hidden')).toBe(false);
        expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You’ve accepted analytics cookies/));

      });

      test("If the user clicks the 'hide' button, the banner should be hidden", () => {

        const hideButton = document.querySelector('.notify-cookie-banner__hide-button');
        const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

        helpers.triggerEvent(hideButton, 'click');

        expect(banner.is('hidden')).toBe(true);

      });

      test("The consent cookie should be set, with analytics set to 'true'", () => {

        expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': true });

      });

      test("The analytics should be set up", () => {

        expect(window.GOVUK.analytics).toBeDefined();

      });

    });

    describe("If the user clicks the button to reject analytics", () => {

      beforeEach(() => {

        const rejectButton = document.querySelector('.notify-cookie-banner__button-reject button');

        helpers.triggerEvent(rejectButton, 'click');

      });

      test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => {

        confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation'));

        expect(confirmation.is('hidden')).toBe(false);
        expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You told us not to use analytics cookies/));

      });

      test("If the user clicks the 'hide' button, the banner should be hidden", () => {

        const hideButton = document.querySelector('.notify-cookie-banner__hide-button');
        const banner = helpers.element(document.querySelector('.notify-cookie-banner'));

        helpers.triggerEvent(hideButton, 'click');

        expect(banner.is('hidden')).toBe(true);

      });

      test("The consent cookie should be set, with analytics set to 'false'", () => {

        expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': false });

      });

      test("The analytics should not be set up", () => {

        expect(window.GOVUK.analytics).not.toBeDefined();

      });

    });

  });

});
<|endoftext|>


<commit_before>/*jshint multistr: true */

'use strict';

var $ = require('jquery'),
    PulsarFormComponent = require('../../../js/PulsarFormComponent');

describe('PulsarFormComponent', function() {

    beforeEach(function() {
        this.$html = $('

').appendTo('html');
        this.$body = $('

').appendTo(this.$html);
        this.$markup = $(`

\
                <form class="form">\
                    <select class="js-select2">
                        <option>foo</option>
                        <option>bar</option>
                        <option>baz</option>
                    </select>
                    <select class="js-select2" data-html="true">
                        <option>foo</option>
                        <option>bar</option>
                        <option>baz</option>
                    </select>

                        Radio Test

                                <input value="foo" name="foo" type="radio" class="form__control qa-foo radio">Foo

                                <input value="bar" name="foo" type="radio" class="form__control radio">Bar

                                <input value="baz" name="foo" type="radio" class="form__control radio" checked>Baz

                        Checkbox Test

                                <input value="foo" name="foo" type="checkbox" class="form__control qa-foo checkbox">Foo

                                <input value="bar" name="foo" type="checkbox" class="form__control checkbox">Bar

                                <input value="baz" name="foo" type="checkbox" class="form__control checkbox" checked>Baz

                        Time picker test

                            <input value="foo" name="foo" type="text" data-timepicker="true" class="form__control qa-foo" />

                        Hardcoded Date Picker Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" class="form__control qa-foo" />

                        Date Picker - Default Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="default" class="form__control qa-foo" />

                        Date Picker - US Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="us" class="form__control qa-foo" />

                        Date Picker - Reverse Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="reverse" class="form__control qa-foo" />

                </form>

            tab
            modal

                <select class="js-select2">
                    <option>foo</option>
                    <option>bar</option>
                    <option>baz</option>
                </select>

        `).appendTo(this.$body);

        this.$radioFoo = this.$html.find('.radio[value="foo"]');
        this.$radioBar = this.$html.find('.radio[value="bar"]');
        this.$radioBaz = this.$html.find('.radio[value="baz"]');
        this.$radioLabelFoo = this.$radioFoo.closest('.control__label');
        this.$radioLabelBaz = this.$radioBaz.closest('.control__label');

        this.$checkFoo = this.$html.find('.checkbox[value="foo"]');
        this.$checkBar = this.$html.find('.checkbox[value="bar"]');
        this.$checkBaz = this.$html.find('.checkbox[value="baz"]');
        this.$checkLabelFoo = this.$checkFoo.closest('.control__label');
        this.$checkLabelBar = this.$checkBar.closest('.control__label');
        this.$checkLabelBaz = this.$checkBaz.closest('.control__label');

        this.$datepicker = this.$html.find('[data-datepicker]');
        this.$datepickerDefault = this.$html.find('[data-format=default]');
        this.$datepickerUS = this.$html.find('[data-format=us]');
        this.$datepickerReverse = this.$html.find('[data-format=reverse]');
        this.$tabToggle = this.$html.find('[data-toggle="tab"]');
        this.$modalToggle = this.$html.find('[data-toggle="modal"]');
        this.$modal = this.$html.find('#modal-foo');
        this.$timepicker = this.$html.find('[data-timepicker="true"]');

        this.pulsarForm = new PulsarFormComponent(this.$html);

        $.fn.select2 = sinon.stub();
        $.fn.timepicker = sinon.stub();
        $.fn.spectrum = sinon.stub();
        $.fn.pikaday = sinon.stub();
    });

    afterEach(function() {
        delete $.fn.select2;
        delete $.fn.timepicker;
        delete $.fn.spectrum;
        delete $.fn.pikaday;
        this.$html.remove();
    });

    describe('Basic select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.initSelect2 = sinon.stub();
            this.pulsarForm.init();
        });

        it('should call the select2 plugin', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });

    });

    describe('Select2 elements with HTML', function() {

        beforeEach(function() {
            this.pulsarForm.initSelect2 = sinon.stub();
            this.pulsarForm.init();

        });

        it('should call the select2 plugin', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });

    });

    describe('Changing to a tab that contains select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.init();
            this.pulsarForm.initSelect2 = sinon.stub();
            this.$tabToggle.trigger('shown.bs.tab');
        });

        it('Should trigger the select2 init method', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });
    });

    describe('Opening a modal that contains select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.init();
            this.pulsarForm.initSelect2 = sinon.stub();
            this.$modalToggle.click();
            this.$modal.trigger('shown.bs.modal');
        });

        it('Should trigger the select2 init method', function(done) {
            setTimeout(() => {
                expect(this.pulsarForm.initSelect2).to.have.been.called;
                done();
            }, 500);
        });
    });

    describe('Clicking a choice block radio input', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should add the is-selected class to the label when input clicked', function() {
            this.$radioFoo.click();
            expect(this.$radioLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should remove the is-selected class to the label if another option selected on click', function() {
            this.$radioFoo.click();
            this.$radioBar.click();
            expect(this.$radioLabelFoo.hasClass('is-selected')).to.be.false;
        });

    });

    describe('Clicking a choice block checkbox input', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should add the is-selected class to the label on click', function() {
            this.$checkFoo.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should not remove the is-selected class if another option selected on click', function() {
            this.$checkFoo.click();
            this.$checkBar.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should remove the is-selected class from the label if unchecked on click', function() {
            this.$checkFoo.click();
            this.$checkFoo.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.false;
        });

    });

    describe('Pre-checked choice block inputs', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should have the is-selected class applied (radio)', function() {
            expect(this.$radioLabelBaz.hasClass('is-selected')).to.be.true;
        });

        it('Should have the is-selected class applied (checkbox)', function() {
            expect(this.$checkLabelBaz.hasClass('is-selected')).to.be.true;
        });

    });

    describe('Datepickers', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('should have the pikaday plugin attached', function() {
            expect($.fn.pikaday).to.have.been.called;
        });

        it('should get "DD/MM/YYYY" when data-datepicker is hardcoded', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'DD/MM/YYYY' });
            expect(this.$datepicker.attr('data-format')).to.be.undefined;
        });

        it('should get "DD/MM/YYYY" when date-format attribute = "default"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'DD/MM/YYYY' });
            expect(this.$datepickerDefault.attr('data-format') === 'default').to.be.true;
        });

        it('should get "MM/DD/YYYY" when date-format attribute is "US"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'MM/DD/YYYY' });
            expect(this.$datepickerUS.attr('data-format') === 'us').to.be.true;
        });

        it('should get "YYYY/MM/DD" when date-format attribute is "reverse"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'YYYY/MM/DD' });
            expect(this.$datepickerReverse.attr('data-format') === 'reverse').to.be.true;
        });

    });

    describe('Timepickers', function() {

        beforeEach(function() {
            this.pulsarForm.timePickerComponent.init = sinon.stub();
            this.pulsarForm.init();
        });

        it('should call the timePickerComponents init method', function() {
            expect(this.pulsarForm.timePickerComponent.init).to.have.been.called;
        });
    });

    describe('refresh', function () {
        beforeEach(function () {
            this.$html.append('

');
        });

        it('should initiate the colour pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.spectrum).to.have.been.calledOnce;
        });

        it('should initiate the date pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.pikaday).to.have.been.called4times;
        });

        it('should initiate time pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.timepicker).to.have.been.calledOnce;
        });
    });
});
<commit_msg>Remove Unecessary Test Cases<commit_after>/*jshint multistr: true */

'use strict';

var $ = require('jquery'),
    PulsarFormComponent = require('../../../js/PulsarFormComponent');

describe('PulsarFormComponent', function() {

    beforeEach(function() {
        this.$html = $('

').appendTo('html');
        this.$body = $('

').appendTo(this.$html);
        this.$markup = $(`

\
                <form class="form">\
                    <select class="js-select2">
                        <option>foo</option>
                        <option>bar</option>
                        <option>baz</option>
                    </select>
                    <select class="js-select2" data-html="true">
                        <option>foo</option>
                        <option>bar</option>
                        <option>baz</option>
                    </select>

                        Radio Test

                                <input value="foo" name="foo" type="radio" class="form__control qa-foo radio">Foo

                                <input value="bar" name="foo" type="radio" class="form__control radio">Bar

                                <input value="baz" name="foo" type="radio" class="form__control radio" checked>Baz

                        Checkbox Test

                                <input value="foo" name="foo" type="checkbox" class="form__control qa-foo checkbox">Foo

                                <input value="bar" name="foo" type="checkbox" class="form__control checkbox">Bar

                                <input value="baz" name="foo" type="checkbox" class="form__control checkbox" checked>Baz

                        Time picker test

                            <input value="foo" name="foo" type="text" data-timepicker="true" class="form__control qa-foo" />

                        Hardcoded Date Picker Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" class="form__control qa-foo" />

                        Date Picker - Default Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="default" class="form__control qa-foo" />

                        Date Picker - US Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="us" class="form__control qa-foo" />

                        Date Picker - Reverse Date Format Test

                            <input value="foo" name="foo" type="text" data-datepicker="true" data-format="reverse" class="form__control qa-foo" />

                </form>

            tab
            modal

                <select class="js-select2">
                    <option>foo</option>
                    <option>bar</option>
                    <option>baz</option>
                </select>

        `).appendTo(this.$body);

        this.$radioFoo = this.$html.find('.radio[value="foo"]');
        this.$radioBar = this.$html.find('.radio[value="bar"]');
        this.$radioBaz = this.$html.find('.radio[value="baz"]');
        this.$radioLabelFoo = this.$radioFoo.closest('.control__label');
        this.$radioLabelBaz = this.$radioBaz.closest('.control__label');

        this.$checkFoo = this.$html.find('.checkbox[value="foo"]');
        this.$checkBar = this.$html.find('.checkbox[value="bar"]');
        this.$checkBaz = this.$html.find('.checkbox[value="baz"]');
        this.$checkLabelFoo = this.$checkFoo.closest('.control__label');
        this.$checkLabelBar = this.$checkBar.closest('.control__label');
        this.$checkLabelBaz = this.$checkBaz.closest('.control__label');

        this.$datepicker = this.$html.find('[data-datepicker]');
        this.$datepickerDefault = this.$html.find('[data-format=default]');
        this.$datepickerUS = this.$html.find('[data-format=us]');
        this.$datepickerReverse = this.$html.find('[data-format=reverse]');
        this.$tabToggle = this.$html.find('[data-toggle="tab"]');
        this.$modalToggle = this.$html.find('[data-toggle="modal"]');
        this.$modal = this.$html.find('#modal-foo');
        this.$timepicker = this.$html.find('[data-timepicker="true"]');

        this.pulsarForm = new PulsarFormComponent(this.$html);

        $.fn.select2 = sinon.stub();
        $.fn.timepicker = sinon.stub();
        $.fn.spectrum = sinon.stub();
        $.fn.pikaday = sinon.stub();
    });

    afterEach(function() {
        delete $.fn.select2;
        delete $.fn.timepicker;
        delete $.fn.spectrum;
        delete $.fn.pikaday;
        this.$html.remove();
    });

    describe('Basic select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.initSelect2 = sinon.stub();
            this.pulsarForm.init();
        });

        it('should call the select2 plugin', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });

    });

    describe('Select2 elements with HTML', function() {

        beforeEach(function() {
            this.pulsarForm.initSelect2 = sinon.stub();
            this.pulsarForm.init();

        });

        it('should call the select2 plugin', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });

    });

    describe('Changing to a tab that contains select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.init();
            this.pulsarForm.initSelect2 = sinon.stub();
            this.$tabToggle.trigger('shown.bs.tab');
        });

        it('Should trigger the select2 init method', function() {
            expect(this.pulsarForm.initSelect2).to.have.been.called;
        });
    });

    describe('Opening a modal that contains select2 elements', function() {

        beforeEach(function() {
            this.pulsarForm.init();
            this.pulsarForm.initSelect2 = sinon.stub();
            this.$modalToggle.click();
            this.$modal.trigger('shown.bs.modal');
        });

        it('Should trigger the select2 init method', function(done) {
            setTimeout(() => {
                expect(this.pulsarForm.initSelect2).to.have.been.called;
                done();
            }, 500);
        });
    });

    describe('Clicking a choice block radio input', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should add the is-selected class to the label when input clicked', function() {
            this.$radioFoo.click();
            expect(this.$radioLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should remove the is-selected class to the label if another option selected on click', function() {
            this.$radioFoo.click();
            this.$radioBar.click();
            expect(this.$radioLabelFoo.hasClass('is-selected')).to.be.false;
        });

    });

    describe('Clicking a choice block checkbox input', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should add the is-selected class to the label on click', function() {
            this.$checkFoo.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should not remove the is-selected class if another option selected on click', function() {
            this.$checkFoo.click();
            this.$checkBar.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.true;
        });

        it('Should remove the is-selected class from the label if unchecked on click', function() {
            this.$checkFoo.click();
            this.$checkFoo.click();
            expect(this.$checkLabelFoo.hasClass('is-selected')).to.be.false;
        });

    });

    describe('Pre-checked choice block inputs', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('Should have the is-selected class applied (radio)', function() {
            expect(this.$radioLabelBaz.hasClass('is-selected')).to.be.true;
        });

        it('Should have the is-selected class applied (checkbox)', function() {
            expect(this.$checkLabelBaz.hasClass('is-selected')).to.be.true;
        });

    });

    describe('Datepickers', function() {

        beforeEach(function() {
            this.pulsarForm.init();
        });

        it('should have the pikaday plugin attached', function() {
            expect($.fn.pikaday).to.have.been.called;
        });

        it('should get "DD/MM/YYYY" when data-datepicker is hardcoded', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'DD/MM/YYYY' });
        });

        it('should get "DD/MM/YYYY" when date-format attribute = "default"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'DD/MM/YYYY' });
        });

        it('should get "MM/DD/YYYY" when date-format attribute is "US"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'MM/DD/YYYY' });
        });

        it('should get "YYYY/MM/DD" when date-format attribute is "reverse"', function() {
            expect($.fn.pikaday).to.have.been.calledWith({ format: 'YYYY/MM/DD' });
        });

    });

    describe('Timepickers', function() {

        beforeEach(function() {
            this.pulsarForm.timePickerComponent.init = sinon.stub();
            this.pulsarForm.init();
        });

        it('should call the timePickerComponents init method', function() {
            expect(this.pulsarForm.timePickerComponent.init).to.have.been.called;
        });
    });

    describe('refresh', function () {
        beforeEach(function () {
            this.$html.append('

');
        });

        it('should initiate the colour pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.spectrum).to.have.been.calledOnce;
        });

        it('should initiate the date pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.pikaday).to.have.been.called4times;
        });

        it('should initiate time pickers', function () {
            this.pulsarForm.refresh();

            expect($.fn.timepicker).to.have.been.calledOnce;
        });
    });
});
<|endoftext|>


<commit_before>'use strict';

const Discord = require('discord.js');

const chatCommandsFunc = require('./chatrouter');
const CONSTANTS = require('./constants');

const client = new Discord.Client();

const rolesByName = {};
const emojisByName = {};
const channelsByName = {};
var CHATCOMMANDS;
var GUILD;

// ** Helper functions: **
const getEmoji = (pokemon) => {
	var emoji = pokemon;
	if (pokemon === 'ho-oh') {
		emoji = 'hooh'; // special case for ho-oh
	}
	if (emojisByName[emoji]) {
		return '<:' + emoji + ':' + emojisByName[emoji].id + '>';
	}
	return '';
};

client.on('ready', (done) => {
	client.channels.forEach((channel) => {
		channelsByName[channel.name] = channel;
	});

	// todo : for the current design of the bot this is always a singleton
	client.guilds.forEach((guild) => {
		GUILD = guild;
	});

	if (GUILD) {
		GUILD.roles.forEach((role) => {
			rolesByName[role.name] = role;
		});
		GUILD.emojis.forEach((emoji) => {
			emojisByName[emoji.name] = emoji;
		});
	}

	CHATCOMMANDS = chatCommandsFunc({
		GUILD,
		rolesByName,
		channelsByName,
		getEmoji,
	});

	if (done) {
		done();
	} else {
		console.log('Asynchronous data loaded!'); // eslint-disable-line
	}
});

client.on('message', (message, cb) => {
	if (!cb) cb = data => {return data;};

	if ((message.member && message.member.id === CONSTANTS.BOTID) ||
		(message.author && message.author.id === CONSTANTS.BOTID)) return;

	if (message.channel.type === 'dm' || message.channel.type === 'group') {
		message.channel.send('I currently have no direct message functions. Please go to channel #start_here');
		return;
	}

	// todo : make the router do the routing

	let reply = '';
	const command = message.content.split(' ')[0].toLowerCase();

	if (message.member && command !== '!play')	{CHATCOMMANDS.checkNew(message);}

	if (message.content[0] !== '!') {
		return;
	}

	//Outside of Professor Redwood Channel, Message.member has NOT been null checked yet
	if (command === '!raid' || command === '!egg') {
		if (message.channel.name.indexOf('-') === -1) {
			reply = message.member.displayName + ', raid commands should only be run in the corresponding neighborhood channel';
			message.channel.send(reply);
			return reply;
		}
		if (command === '!raid') {return cb(CHATCOMMANDS.raid(message));}
		else {return cb(CHATCOMMANDS.egg(message));}
	}
	//Inside Professor Redwood Channel, Do not touch message.member
	else if (message.channel.name !== 'professor_redwood') {
		message.channel.send(message.member.displayName + ', you may only run this command in the ' + channelsByName['professor_redwood'] + ' channel');
		return;
	}

	if (command === '!breakpoint' || command === '!bp') {return cb(CHATCOMMANDS.breakpoint(message));}
	else if (command === '!cp') {return cb(CHATCOMMANDS.cp(message));}
	else if (command === '!counter' || command === '!counters') {return cb(CHATCOMMANDS.counters(message));}
	else if (command === '!help') {return cb(CHATCOMMANDS.help(message));}

	//Inside Professor Redwood Channel, OK to touch message.member
	if (reply === '' && !message.member) {
		message.channel.send('Member is a ' + getEmoji('gengar') + ' - Commands cannot be run for users who are invisible, please remove your invisible status');
		return;
	}

	if (command === '!play') {return cb(CHATCOMMANDS.play(message));}
	else if (command === '!hide') {return cb(CHATCOMMANDS.hide(message));}
	else if (command === '!team') {return cb(CHATCOMMANDS.team(message));}
	else if (command === '!want') {return cb(CHATCOMMANDS.want(message));}
	else if (command === '!reset') {return cb(CHATCOMMANDS.reset(message));}

	console.log('Command not found: ' + command); // eslint-disable-line
	return cb('Command not found: ' + command);
});

module.exports = client;
<commit_msg>replace multiple spaces with single space<commit_after>'use strict';

const Discord = require('discord.js');

const chatCommandsFunc = require('./chatrouter');
const CONSTANTS = require('./constants');

const client = new Discord.Client();

const rolesByName = {};
const emojisByName = {};
const channelsByName = {};
var CHATCOMMANDS;
var GUILD;

// ** Helper functions: **
const getEmoji = (pokemon) => {
	var emoji = pokemon;
	if (pokemon === 'ho-oh') {
		emoji = 'hooh'; // special case for ho-oh
	}
	if (emojisByName[emoji]) {
		return '<:' + emoji + ':' + emojisByName[emoji].id + '>';
	}
	return '';
};

client.on('ready', (done) => {
	client.channels.forEach((channel) => {
		channelsByName[channel.name] = channel;
	});

	// todo : for the current design of the bot this is always a singleton
	client.guilds.forEach((guild) => {
		GUILD = guild;
	});

	if (GUILD) {
		GUILD.roles.forEach((role) => {
			rolesByName[role.name] = role;
		});
		GUILD.emojis.forEach((emoji) => {
			emojisByName[emoji.name] = emoji;
		});
	}

	CHATCOMMANDS = chatCommandsFunc({
		GUILD,
		rolesByName,
		channelsByName,
		getEmoji,
	});

	if (done) {
		done();
	} else {
		console.log('Asynchronous data loaded!'); // eslint-disable-line
	}
});

client.on('message', (message, cb) => {
	if (!cb) cb = data => {return data;};

	if ((message.member && message.member.id === CONSTANTS.BOTID) ||
		(message.author && message.author.id === CONSTANTS.BOTID)) return;

	if (message.channel.type === 'dm' || message.channel.type === 'group') {
		message.channel.send('I currently have no direct message functions. Please go to channel #start_here');
		return;
	}

	// todo : make the router do the routing

	let reply = '';
	const command = message.content.split(' ')[0].toLowerCase();
	// replace any multiple spaces with a single space
	while (message.content.indexOf('  ') > -1) {message.content = message.content.replace('  ', ' ');}

	if (message.member && command !== '!play')	{CHATCOMMANDS.checkNew(message);}

	if (message.content[0] !== '!') {
		return;
	}

	//Outside of Professor Redwood Channel, Message.member has NOT been null checked yet
	if (command === '!raid' || command === '!egg') {
		if (message.channel.name.indexOf('-') === -1) {
			reply = message.member.displayName + ', raid commands should only be run in the corresponding neighborhood channel';
			message.channel.send(reply);
			return reply;
		}
		if (command === '!raid') {return cb(CHATCOMMANDS.raid(message));}
		else {return cb(CHATCOMMANDS.egg(message));}
	}
	//Inside Professor Redwood Channel, Do not touch message.member
	else if (message.channel.name !== 'professor_redwood') {
		message.channel.send(message.member.displayName + ', you may only run this command in the ' + channelsByName['professor_redwood'] + ' channel');
		return;
	}

	if (command === '!breakpoint' || command === '!bp') {return cb(CHATCOMMANDS.breakpoint(message));}
	else if (command === '!cp') {return cb(CHATCOMMANDS.cp(message));}
	else if (command === '!counter' || command === '!counters') {return cb(CHATCOMMANDS.counters(message));}
	else if (command === '!help') {return cb(CHATCOMMANDS.help(message));}

	//Inside Professor Redwood Channel, OK to touch message.member
	if (reply === '' && !message.member) {
		message.channel.send('Member is a ' + getEmoji('gengar') + ' - Commands cannot be run for users who are invisible, please remove your invisible status');
		return;
	}

	if (command === '!play') {return cb(CHATCOMMANDS.play(message));}
	else if (command === '!hide') {return cb(CHATCOMMANDS.hide(message));}
	else if (command === '!team') {return cb(CHATCOMMANDS.team(message));}
	else if (command === '!want') {return cb(CHATCOMMANDS.want(message));}
	else if (command === '!reset') {return cb(CHATCOMMANDS.reset(message));}

	console.log('Command not found: ' + command); // eslint-disable-line
	return cb('Command not found: ' + command);
});

module.exports = client;
<|endoftext|>


<commit_before>import 'babel-polyfill';
import { trigger } from 'redial';

import React from 'react';
import { render } from 'react-dom';
import Router from 'react-router/lib/Router';
import match from 'react-router/lib/match';
import browserHistory from 'react-router/lib/browserHistory';
import { Provider } from 'react-redux';
import { StyleSheet } from 'aphrodite';

// Your app's reducer and routes:
import createRoutes from './routes/root';
import { configureStore } from './store';

const initialState = window.INITIAL_STATE || {};

// Set up Redux (note: this API requires redux@>=3.1.0):
const store = configureStore(initialState);
const { dispatch } = store;
const { pathname, search, hash } = window.location;
const location = `${pathname}${search}${hash}`;
const routes = createRoutes(store);
const container = document.getElementById('root');
StyleSheet.rehydrate(window.renderedClassNames);

browserHistory.listen(location => {
  // Match routes based on location object:
  match({ routes, location }, (error, redirectLocation, renderProps) => {
    // Get array of route handler components:
    const { components } = renderProps;

    // Define locals to be provided to all lifecycle hooks:
    const locals = {
        path: renderProps.location.pathname,
        query: renderProps.location.query,
        params: renderProps.params,

        // Allow lifecycle hooks to dispatch Redux actions:
        dispatch,
      };

    // Don't fetch data for initial route, server has already done the work:
    if (window.INITIAL_STATE) {
      // Delete initial data so that subsequent data fetches can occur:
      delete window.INITIAL_STATE;
    } else {
      // Fetch mandatory data dependencies for 2nd route change onwards:
      trigger('fetch', components, locals);
    }

    // Fetch deferred, client-only data dependencies:
    trigger('defer', components, locals);

    // Render app with Redux and router context to container element:
    render((
      <Provider store={store}>
          <Router history={browserHistory} routes={routes} />
      </Provider>
    ), container);
  });
});
<commit_msg>Fix checksum invalid warning mentinoed in second part of #27<commit_after>import 'babel-polyfill';
import { trigger } from 'redial';

import React from 'react';
import { render } from 'react-dom';
import Router from 'react-router/lib/Router';
import match from 'react-router/lib/match';
import browserHistory from 'react-router/lib/browserHistory';
import { Provider } from 'react-redux';
import { StyleSheet } from 'aphrodite';

// Your app's reducer and routes:
import createRoutes from './routes/root';
import { configureStore } from './store';

const initialState = window.INITIAL_STATE || {};

// Set up Redux (note: this API requires redux@>=3.1.0):
const store = configureStore(initialState);
const { dispatch } = store;
const { pathname, search, hash } = window.location;
const location = `${pathname}${search}${hash}`;
const routes = createRoutes(store);
const container = document.getElementById('root');
StyleSheet.rehydrate(window.renderedClassNames);

// Pull child routes using match
match({ routes, location }, () => {
  // Render app with Redux and router context to container element:
  render((
    <Provider store={store}>
        <Router history={browserHistory} routes={routes} />
    </Provider>
  ), container);
});

browserHistory.listen(location => {
  // Match routes based on location object:
  match({ routes, location }, (error, redirectLocation, renderProps) => {
    // Get array of route handler components:
    const { components } = renderProps;

    // Define locals to be provided to all lifecycle hooks:
    const locals = {
        path: renderProps.location.pathname,
        query: renderProps.location.query,
        params: renderProps.params,

        // Allow lifecycle hooks to dispatch Redux actions:
        dispatch,
      };

    // Don't fetch data for initial route, server has already done the work:
    if (window.INITIAL_STATE) {
      // Delete initial data so that subsequent data fetches can occur:
      delete window.INITIAL_STATE;
    } else {
      // Fetch mandatory data dependencies for 2nd route change onwards:
      trigger('fetch', components, locals);
    }

    // Fetch deferred, client-only data dependencies:
    trigger('defer', components, locals);
  });
});
<|endoftext|>


<commit_before>const {Client, Collection, Permissions} = require("discord.js");
const {inspect, promisify} = require("util");
const fs = require("fs");
const path = require("path");
const readdir = promisify(fs.readdir);
const util = require("./util.js");

let _token;

function validatePermissions(perm) {
    return Object.keys(Permissions.FLAGS).includes(perm);
}

class SimpleClient extends Client {
    /**
     * @typedef {Object} SimpleClientOptions
     * @property {string} configDir - The optional directory of a config file.
     * @property {string} token - The token used for logging in.
     * @property {Array<string>} owners - The ID of the bot owners.
     * @property {string} prefix - The command prefix for the bot.
     * @property {string} suffix - The command suffix for the bot.
     * @property {string} game - The game to set on starting the bot.
     * @property {string} commandsDir - The relative path to the commands directory.
     * @property {boolean} basUseResponse - If the bot should display how to use an improperly called command.
     * @property {boolean} debug - Whether to enable additional logging.
     */

    /**
     * @class SimpleClient
     * @classdesc The SimpleDiscord client.
     * @extends Client
     * @param {SimpleClientOptions} options - The client options.
     */
    constructor(options = {}) {
        super(options);

        let data;

        if (options.configDir) {
            const dir = path.isAbsolute(options.configDir) ? options.configDir : path.join(process.cwd(), options.configDir);

            if (!fs.existsSync(dir)) throw new Error(`Simple-Discord - No config found at ${dir}. Is this path correct?`);

            const {ext} = path.parse(dir);

            if (ext && ext !== ".json") throw new TypeError("Simple-Discord - Config file extention type is expected to be json.");

            data = JSON.parse(fs.readFileSync(dir));
        } else {
            data = options;
        }

        _token = data.token;

        /**
         * The commands for the bot.
         * @type {Collection<string, Command>}
         * @public
         */
        this.commands = new Collection();

        /**
         * The command aliases.
         * @type {Collection<string, string>}
         * @public
         */
        this.aliases = new Collection();

        /**
         * The command prefix.
         * @member {string}
         * @public
         */
        this.prefix = data.prefix || null;

        /**
         * The command suffix.
         * @member {string}
         * @public
         */
        this.suffix = !this.prefix && data.suffix ? data.suffix : null;

        /**
         * The game to be set on ready.
         * @member {string}
         * @private
         */
        this._game = data.game || null;

        /**
         * An array of owner ids for the bot.
         * @member {Array<string>}
         * @private
         */
        this._owners = data.owners;

        /**
         * Boolean representation if the bot should display how to use a command when one is called improperly.
         * @member {boolean}
         * @private
         */
        this._badUseResponse = !!options.badUseResponse;

        /**
         * Boolean representation of if there should be extra logging.
         * @member {boolean}
         * @private
         */
        this._debug = !!data.debug;

        /**
         * The directory for bot commands
         * @member {string}
         * @private
         */
        this._commandsDir = data.commandsDir || null;

        /**
         * General utilities.
         * @member {Object}
         * @public
         */
        this.utils = util;

        this._validateConfig();

        this.once('ready', () => {
            this.utils.log(`Logged in as ${this.user.tag}!`);
            this.utils.log(`Ready to serve in ${this.guilds.size} guild${this.guilds.size === 1 ? "" : "s"}!`);

            if (!this.user.bot) {
                this.utils.log("Selfbot detected.");
                this._owners = [this.user.id];
            }

            this.user.setGame(this._game);
        });

        this.on('message', this._processMessage);
        this.on('messageUpdate', this._processMessage);
    }

    /**
     * Loads the included commands from src/commands
     * @method loadDefaults
     * @returns {SimpleClient}
     */
    loadDefaults() {
        readdir(path.join(__dirname, "commands")).then(files => {
            for (const file of files) {
                const Command = require(path.join(__dirname, "commands", file));
                const cmd = new Command(this);

                this.commands.set(cmd.name, cmd);

                if (cmd.aliases) for (const alias of cmd.aliases) this.aliases.set(alias, cmd.name);

                if (this._debug) this.utils.log(`Loaded ${cmd.name}!`);

                delete require.cache[require.resolve(path.join(__dirname, "commands", file))];
            }
        }).catch(this.utils.error);

        return this;
    }

    /**
     * Loads the commands from the provided commands directory.
     * @method loadCommands
     * @returns {SimpleClient}
     */
    loadCommands() {
        const dir = path.join(process.cwd(), this._commandsDir);

        readdir(dir).then(files => {
            for (const file of files) {
                const Command = require(path.join(dir, file));
                const cmd = new Command(this);

                this.commands.set(cmd.name, cmd);

                if (cmd.aliases) {
                    for (const alias of cmd.aliases) {
                        if (this.aliases.has(alias)) {
                            this.utils.error(`Command ${cmd.name} has duplicate alias ${alias}!`);
                            continue;
                        }
                        this.aliases.set(alias, cmd.name);
                    }
                }
                if (this._debug) this.utils.log(`Loaded ${cmd.name}!`);

                delete require.cache[require.resolve(path.join(dir, file))];
            }
        }).then(() => this.utils.log(`Loaded ${this.commands.size} commands.`)).catch(this.utils.error);

        return this;
    }

    /**
     * Logs the client into Discord.
     * @method login
     * @override
     * @returns {Promise<string>}
     */
    login() {
        return super.login(_token).catch(err => {
            this.utils.error(err);
            this.utils.error("There was an error on login.");
            this.utils.error("Please validate your token.");
            process.exit(1);
        });
    }

    /**
     * @private
     * @returns {string}
     */
    [inspect.custom](depth) {
        const user = this.user ? `${this.user.tag} (ID: ${this.user.id})` : null;

        return depth > 0 ? `SimpleClient {
  User: ${user},
  Guilds: { ${this.guilds.size} },
  Channels: { ${this.channels.size} },
  Users: { ${this.users.size} },
  Commands: { ${this.commands.size} }
}` : "[Object]";
    }

    /**
     * Internal message handler for both MESSAGE_CREATE and MESSAGE_UPDATE
     * @method _processMessage
     * @private
     */
    async _processMessage(...params) {
        if (params.length === 2 && params[0].content === params[1].content) return;

        const message = params[params.length - 1];

        if ((!this.user.bot && message.author.id !== this.user.id) || message.author.bot) return;

        let command, content;

        if (this.prefix) {
            if (!message.content.startsWith(this.prefix)) return;

            [command = "", ...content] = message.content.slice(this.prefix.length).split(/ +/);
        } else if (this.suffix) {
            if (!message.content.endsWith(this.suffix)) return;

            content = message.content.split(/ +/);
            command = content.pop().slice(0, -this.suffix.length);
        }
        command = command.toLowerCase();

        const cmd = this.commands.get(command) || this.commands.get(this.aliases.get(command));

        if (!cmd) return;

        if (cmd.guildOnly && message.channel.type !== "text") return;

        if (cmd.ownerOnly && !this._owners.includes(message.author.id)) return;

        if (cmd.permissions) {
            if (!message.guild) return;

            const perms = cmd.permissions.filter(validatePermissions);
            let missing = [];

            if (this.user.bot) {
                for (const perm of perms) if (!message.member.hasPermission(perm)) missing.push(perm);

                if (missing.length) return message.channel.send(`To run this command, you need the following permissions: \`\`\`\n${missing.join(", ")}\n\`\`\``);
            }
            missing = [];

            for (const perm of perms) if (!message.guild.me.hasPermission(perm)) missing.push(perm);

            if (missing.length) return message.channel.send(`To run this command, I need the following permissions: \`\`\`\n${missing.join(", ")}\n\`\`\``);
        }

        if (cmd.throttle(message.author.id)) return message.channel.send(`To run this command, you need to cool down, you're going too fast.`);

        const args = await this.utils.parseArgs(message, content, cmd.use);

        if (Array.isArray(args) && this._badUseResponse) {
            const use = cmd.use.map(a => a.required ? `<${a.name}>` : `[${a.name}]`);

            return message.reply(`Improperly ordered or missing args! Proper use: \`\`\`\n${this.prefix ? `${this.prefix}${cmd.name} ` : ""}${use.join(" ")}${this.suffix ? ` ${cmd.name}${this.suffix}` : ""}\n\`\`\``);
        }

        try {
            await cmd.run(message, args);
        } catch (err) {
            this.utils.error(err);
            message.channel.send(`There was an error running the ${cmd.name} command. \`\`\`xl\n${err}\`\`\``);
        }
    }

    /**
     * Validates the provided configuration options.
     * @method _validateConfig
     * @throws {Error|TypeError|RangeError}
     * @private
     */
    _validateConfig() {
        if (!_token) throw new Error("Simple-Discord - Please provide a login token.");

        if (typeof _token !== "string") throw new TypeError("Simple-Discord - Your token must be a string.");

        if (!this.prefix && !this.suffix) throw new Error("Simple-Discord - A prefix or a suffix is required.");

        if (this.prefix && typeof this.prefix !== "string") throw new TypeError("Simple-Discord - Your prefix must be a string.");
        if (this.suffix && typeof this.suffix !== "string") throw new TypeError("Simple-Discord - Your suffix must be a string.");

        if (this._game && typeof this._game !== "string") throw new TypeError("Simple-Discord - The start game must be a string if one is desired.");

        if (!Array.isArray(this._owners)) throw new TypeError("Simple-Discord - options.owners must be an array.");

        if (this._owners.length < 1) throw new RangeError("Simple-Discord - You must specify at least one owner ID.");

        if (!this._commandsDir) throw new Error("Simple-Discord - A commands directory is required.");

        if (typeof this._commandsDir !== "string") throw new TypeError("Simple-Discord - The command directory must be a string.");

        if (path.isAbsolute(this._commandsDir)) throw new Error(`Simple-Discord - ${this._commandsDir} is an absolute path. Please provide a relative path.\n\nFor example, a relative path from ${path.join("C:", "samples")} to ${path.join("C:", "samples", "text.txt")} would be ${[".", "text.txt"].join(path.sep)}`);
    }
}

module.exports = SimpleClient;
<commit_msg>More logging...<commit_after>const {Client, Collection, Permissions} = require("discord.js");
const {inspect, promisify} = require("util");
const fs = require("fs");
const path = require("path");
const readdir = promisify(fs.readdir);
const util = require("./util.js");

let _token;

function validatePermissions(perm) {
    return Object.keys(Permissions.FLAGS).includes(perm);
}

class SimpleClient extends Client {
    /**
     * @typedef {Object} SimpleClientOptions
     * @property {string} configDir - The optional directory of a config file.
     * @property {string} token - The token used for logging in.
     * @property {Array<string>} owners - The ID of the bot owners.
     * @property {string} prefix - The command prefix for the bot.
     * @property {string} suffix - The command suffix for the bot.
     * @property {string} game - The game to set on starting the bot.
     * @property {string} commandsDir - The relative path to the commands directory.
     * @property {boolean} basUseResponse - If the bot should display how to use an improperly called command.
     * @property {boolean} debug - Whether to enable additional logging.
     */

    /**
     * @class SimpleClient
     * @classdesc The SimpleDiscord client.
     * @extends Client
     * @param {SimpleClientOptions} options - The client options.
     */
    constructor(options = {}) {
        super(options);

        let data;

        if (options.configDir) {
            const dir = path.isAbsolute(options.configDir) ? options.configDir : path.join(process.cwd(), options.configDir);

            if (!fs.existsSync(dir)) throw new Error(`Simple-Discord - No config found at ${dir}. Is this path correct?`);

            const {ext} = path.parse(dir);

            if (ext && ext !== ".json") throw new TypeError("Simple-Discord - Config file extention type is expected to be json.");

            data = JSON.parse(fs.readFileSync(dir));
        } else {
            data = options;
        }

        _token = data.token;

        /**
         * The commands for the bot.
         * @type {Collection<string, Command>}
         * @public
         */
        this.commands = new Collection();

        /**
         * The command aliases.
         * @type {Collection<string, string>}
         * @public
         */
        this.aliases = new Collection();

        /**
         * The command prefix.
         * @member {string}
         * @public
         */
        this.prefix = data.prefix || null;

        /**
         * The command suffix.
         * @member {string}
         * @public
         */
        this.suffix = !this.prefix && data.suffix ? data.suffix : null;

        /**
         * The game to be set on ready.
         * @member {string}
         * @private
         */
        this._game = data.game || null;

        /**
         * An array of owner ids for the bot.
         * @member {Array<string>}
         * @private
         */
        this._owners = data.owners;

        /**
         * Boolean representation if the bot should display how to use a command when one is called improperly.
         * @member {boolean}
         * @private
         */
        this._badUseResponse = !!options.badUseResponse;

        /**
         * Boolean representation of if there should be extra logging.
         * @member {boolean}
         * @private
         */
        this._debug = !!data.debug;

        /**
         * The directory for bot commands
         * @member {string}
         * @private
         */
        this._commandsDir = data.commandsDir || null;

        /**
         * General utilities.
         * @member {Object}
         * @public
         */
        this.utils = util;

        this._validateConfig();

        this.once('ready', () => {
            this.utils.log(`Logged in as ${this.user.tag}!`);
            this.utils.log(`Ready to serve in ${this.guilds.size} guild${this.guilds.size === 1 ? "" : "s"}!`);

            if (!this.user.bot) {
                this.utils.log("Selfbot detected.");
                this._owners = [this.user.id];
            }

            this.user.setGame(this._game);
        });

        this.on('message', this._processMessage);
        this.on('messageUpdate', this._processMessage);
    }

    /**
     * Loads the included commands from src/commands
     * @method loadDefaults
     * @returns {SimpleClient}
     */
    loadDefaults() {
        readdir(path.join(__dirname, "commands")).then(files => {
            for (const file of files) {
                const Command = require(path.join(__dirname, "commands", file));
                const cmd = new Command(this);

                this.commands.set(cmd.name, cmd);

                if (cmd.aliases) for (const alias of cmd.aliases) this.aliases.set(alias, cmd.name);

                if (this._debug) this.utils.log(`Loaded ${cmd.name}!`);

                delete require.cache[require.resolve(path.join(__dirname, "commands", file))];
            }
        }).catch(this.utils.error);

        return this;
    }

    /**
     * Loads the commands from the provided commands directory.
     * @method loadCommands
     * @returns {SimpleClient}
     */
    loadCommands() {
        const dir = path.join(process.cwd(), this._commandsDir);

        readdir(dir).then(files => {
            for (const file of files) {
                const Command = require(path.join(dir, file));
                const cmd = new Command(this);

                this.commands.set(cmd.name, cmd);

                if (cmd.aliases) {
                    for (const alias of cmd.aliases) {
                        if (this.aliases.has(alias)) {
                            this.utils.error(`Command ${cmd.name} has duplicate alias ${alias}!`);
                            continue;
                        }
                        this.aliases.set(alias, cmd.name);
                    }
                }
                if (this._debug) this.utils.log(`Loaded ${cmd.name}!`);

                delete require.cache[require.resolve(path.join(dir, file))];
            }
        }).then(() => this.utils.log(`Loaded ${this.commands.size} commands.`)).catch(this.utils.error);

        return this;
    }

    /**
     * Logs the client into Discord.
     * @method login
     * @override
     * @returns {Promise<string>}
     */
    login() {
        return super.login(_token).catch(err => {
            this.utils.error(err);
            this.utils.error("There was an error on login.");
            this.utils.error("Please validate your token.");
            process.exit(1);
        });
    }

    /**
     * @private
     * @returns {string}
     */
    [inspect.custom](depth) {
        const user = this.user ? `${this.user.tag} (ID: ${this.user.id})` : null;

        return depth > 0 ? `SimpleClient {
  User: ${user},
  Guilds: { ${this.guilds.size} },
  Channels: { ${this.channels.size} },
  Users: { ${this.users.size} },
  Commands: { ${this.commands.size} }
}` : "[Object]";
    }

    /**
     * Internal message handler for both MESSAGE_CREATE and MESSAGE_UPDATE
     * @method _processMessage
     * @private
     */
    async _processMessage(...params) {
        if (params.length === 2 && params[0].content === params[1].content) return;

        const message = params[params.length - 1];

        if ((!this.user.bot && message.author.id !== this.user.id) || message.author.bot) return;

        let command, content;

        if (this.prefix) {
            if (!message.content.startsWith(this.prefix)) return;

            [command = "", ...content] = message.content.slice(this.prefix.length).split(/ +/);
        } else if (this.suffix) {
            if (!message.content.endsWith(this.suffix)) return;

            content = message.content.split(/ +/);
            command = content.pop().slice(0, -this.suffix.length);
        }
        command = command.toLowerCase();

        const cmd = this.commands.get(command) || this.commands.get(this.aliases.get(command));

        if (!cmd) return;

        if (cmd.guildOnly && message.channel.type !== "text") return;

        if (cmd.ownerOnly && !this._owners.includes(message.author.id)) return;

        if (cmd.permissions) {
            if (!message.guild) return;

            const perms = cmd.permissions.filter(validatePermissions);
            let missing = [];

            if (this.user.bot) {
                for (const perm of perms) if (!message.member.hasPermission(perm)) missing.push(perm);

                if (missing.length) return message.channel.send(`To run this command, you need the following permissions: \`\`\`\n${missing.join(", ")}\n\`\`\``);
            }
            missing = [];

            for (const perm of perms) if (!message.guild.me.hasPermission(perm)) missing.push(perm);

            if (missing.length) return message.channel.send(`To run this command, I need the following permissions: \`\`\`\n${missing.join(", ")}\n\`\`\``);
        }

        if (cmd.throttle(message.author.id)) return message.channel.send(`To run this command, you need to cool down, you're going too fast.`);

        const args = await this.utils.parseArgs(message, content, cmd.use);

        if (Array.isArray(args) && this._badUseResponse) {
            const use = cmd.use.map(a => a.required ? `<${a.name}>` : `[${a.name}]`);

            return message.reply(`Improperly ordered or missing args! Proper use: \`\`\`\n${this.prefix ? `${this.prefix}${cmd.name} ` : ""}${use.join(" ")}${this.suffix ? ` ${cmd.name}${this.suffix}` : ""}\n\`\`\``);
        }

        this.utils.log(args);

        try {
            await cmd.run(message, args);
        } catch (err) {
            this.utils.error(err);
            message.channel.send(`There was an error running the ${cmd.name} command. \`\`\`xl\n${err}\`\`\``);
        }
    }

    /**
     * Validates the provided configuration options.
     * @method _validateConfig
     * @throws {Error|TypeError|RangeError}
     * @private
     */
    _validateConfig() {
        if (!_token) throw new Error("Simple-Discord - Please provide a login token.");

        if (typeof _token !== "string") throw new TypeError("Simple-Discord - Your token must be a string.");

        if (!this.prefix && !this.suffix) throw new Error("Simple-Discord - A prefix or a suffix is required.");

        if (this.prefix && typeof this.prefix !== "string") throw new TypeError("Simple-Discord - Your prefix must be a string.");
        if (this.suffix && typeof this.suffix !== "string") throw new TypeError("Simple-Discord - Your suffix must be a string.");

        if (this._game && typeof this._game !== "string") throw new TypeError("Simple-Discord - The start game must be a string if one is desired.");

        if (!Array.isArray(this._owners)) throw new TypeError("Simple-Discord - options.owners must be an array.");

        if (this._owners.length < 1) throw new RangeError("Simple-Discord - You must specify at least one owner ID.");

        if (!this._commandsDir) throw new Error("Simple-Discord - A commands directory is required.");

        if (typeof this._commandsDir !== "string") throw new TypeError("Simple-Discord - The command directory must be a string.");

        if (path.isAbsolute(this._commandsDir)) throw new Error(`Simple-Discord - ${this._commandsDir} is an absolute path. Please provide a relative path.\n\nFor example, a relative path from ${path.join("C:", "samples")} to ${path.join("C:", "samples", "text.txt")} would be ${[".", "text.txt"].join(path.sep)}`);
    }
}

module.exports = SimpleClient;
<|endoftext|>