From a0f5ec9971eb7715e67a654e3f961bb01e697d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 11:39:55 +0100 Subject: [PATCH 01/19] feat: Add an URL normalization component The bar takes either a full URL (mostly from mobile apps) or a domain without protocol (mostly in case of web apps). This component takes care of normalizing all cases to a full url object, in accordance to the `secure` optional parameter. The purpose is to replace all other URL code in cozy-bar. --- src/lib/normalize-url.js | 47 +++++++++++++++++++++++++++ test/lib/normalize-url.spec.js | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/lib/normalize-url.js create mode 100644 test/lib/normalize-url.spec.js diff --git a/src/lib/normalize-url.js b/src/lib/normalize-url.js new file mode 100644 index 000000000..1ba388095 --- /dev/null +++ b/src/lib/normalize-url.js @@ -0,0 +1,47 @@ + +/** + * Gives an URL object from a partial URL + * + * Mobile apps give us full URL with domain and protocol. + * Web apps may also initialize with a full URL + * but they usually don't, and we the use the + * domain given by the stack through a DOM node + * in the HTML page (despite it not being a full URL). + * + * This function normalizes these different cases + * and returns a full URL object. + * + * The `secure` parameter, if given, always has the + * priority over the one from the URL. + * We use a secure protocol by default if we can't decide. + * + * @param {String} cozyURL - full URL or domain of the cozy + * @param {boolean} secure - optional, force the protocol to use ssl (or not) + * @returns {URL} + */ +export default function normalizeURL(cozyURL, secure) { + if (typeof cozyURL === 'undefined') { + throw new Error("Please give the Cozy URL when initializing the cozy-bar") + } + + if (cozyURL.match(/:\/\//)) { + // This is a full URL, probably received from a mobile app. + // The secure parameter overrides the protocol. + if (secure !== undefined) { + cozyURL = cozyURL.replace(/^https?:/, secure ? 'https:' : 'http:') + } + } else { + // We only have the domain, not a real URL. + // Let's decide what protocol we should use. + if (secure === undefined) { + if (window && window.location && window.location.protocol) { + secure = (window.location.protocol === 'https:') + } else { + secure = true + } + } + cozyURL = (secure ? 'https://' : 'http://') + cozyURL + } + return new URL(cozyURL) +} + diff --git a/test/lib/normalize-url.spec.js b/test/lib/normalize-url.spec.js new file mode 100644 index 000000000..b4cd6af2b --- /dev/null +++ b/test/lib/normalize-url.spec.js @@ -0,0 +1,58 @@ +import normalizeUrl from 'lib/normalize-url' + +let old_window ; + +const https_url = "https://test.mycozy.cloud/path/with?parameters=1" +const http_url = "http://test.mycozy.cloud/path/with?parameters=1" +const https_origin = "https://test.mycozy.cloud" +const http_origin = "http://test.mycozy.cloud" +const domain = "test.mycozy.cloud" +const https_app = "https://test-app.mycozy.cloud/path/with?parameters=1" +const http_app = "http://test-app.mycozy.cloud/path/with?parameters=1" + +describe('URL normalization', () => { + + beforeAll(() => { + old_window = global.window + global.window = Object.create(window); + delete global.window.location + }) + + beforeEach(() => { + global.window.location = undefined + }) + + afterAll(() => { + global.window = old_window + old_window = undefined + }) + + it('should accept a domain name with a secure parameter', () => { + expect( normalizeUrl(domain, true).origin ).toBe(https_origin) + }) + + it('default protocol should be the one of window.location', () => { + window.location = new URL(https_app) + expect( normalizeUrl(domain, undefined).origin ).toBe(https_origin) + window.location = new URL(http_app) + expect( normalizeUrl(domain, undefined).origin ).toBe(http_origin) + }) + + it('should accept a complete origin', () => { + expect( normalizeUrl(https_origin, undefined).origin ).toBe(https_origin) + }) + + it('should accept an unsecure protocol', () => { + expect( normalizeUrl(http_origin, undefined).origin ).toBe(http_origin) + }) + + it('should be able to override the protocol', () => { + expect( normalizeUrl(http_origin, true).origin ).toBe(https_origin) + expect( normalizeUrl(https_origin, false).origin ).toBe(http_origin) + }) + + it('should accept a full URL', () => { + expect( normalizeUrl(http_url, undefined).origin ).toBe(http_origin) + }) + +}) From f3295e3f8ffca5d86c11c1af27c46ebca425da6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 11:56:38 +0100 Subject: [PATCH 02/19] refactor: Use the new url normalization component - Remove `ssl` parameter normalization in the main index.jsx (send it as is to the lib/stack.js) - Remove the old inline url management code from lib/stack.js in favor of a call to the new url normalization component --- src/index.jsx | 25 +------------------------ src/lib/stack.js | 23 ++++++----------------- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 99b278cd2..c907aec41 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -142,29 +142,6 @@ const getUserActionRequired = () => { return undefined } -const determineSSL = (ssl, cozyURL) => { - if (typeof ssl !== 'undefined') return ssl - - let parsedURL - try { - parsedURL = new URL(cozyURL) - return parsedURL.protocol === 'https:' - } catch (error) { - console.warn( - `cozyURL parameter passed to Cozy-bar is not a valid URL (${ - error.message - }). Cozy-bar will rely on window.location to detect SSL.` - ) - } - - if (window && window.location && window.location.protocol) { - return window.location.protocol === 'https:' - } - - console.warn('Cozy-bar cannot detect SSL and will use default value (true)') - return true -} - let exposedAPI = {} const init = async ({ @@ -194,7 +171,7 @@ const init = async ({ token, onCreateApp: app => reduxStore.dispatch(receiveApp(app)), onDeleteApp: app => reduxStore.dispatch(deleteApp(app)), - ssl: determineSSL(ssl, cozyURL) + ssl }) if (lang) { reduxStore.dispatch(setLocale(lang)) diff --git a/src/lib/stack.js b/src/lib/stack.js index 338f47a4b..4b00ec02b 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -3,6 +3,7 @@ import realtime from 'cozy-realtime' import getIcon from './icon' +import normalizeURL from './normalize-url' import { ForbiddenException, @@ -159,28 +160,16 @@ async function initializeRealtime({ onCreateApp, onDeleteApp, url, token }) { } } -const determineURL = (cozyURL, ssl) => { - let url - let host - const protocol = ssl ? 'https' : 'http' - - try { - // only on mobile we get the full URL with the protocol - host = new URL(cozyURL).host - url = !!host && `${protocol}://${host}` - } catch (e) {} // eslint-disable-line no-empty - - host = host || cozyURL - url = url || `${protocol}://${host}` - - return { COZY_URL: url, COZY_HOST: host } -} module.exports = { async init({ cozyURL, token, onCreateApp, onDeleteApp, ssl }) { ;({ COZY_URL, COZY_HOST } = determineURL(cozyURL, ssl)) + const url = normalizeURL(cozyURL, ssl) + // The 4 following constant are global variables for the module + COZY_URL = url.origin + COZY_HOST = url.host + USE_SSL = (url.protocol === 'https:') COZY_TOKEN = token - USE_SSL = ssl await initializeRealtime({ onCreateApp, onDeleteApp, From e26a4564e89a2ed8b6d838d6b1845af1e538b482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 12:00:49 +0100 Subject: [PATCH 03/19] refactor: Remove unused `ssl` parameter sent to realtime init The parameter is not used in the receiving function anyways. --- src/lib/stack.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/stack.js b/src/lib/stack.js index 4b00ec02b..2483ff731 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -174,8 +174,7 @@ module.exports = { onCreateApp, onDeleteApp, token: COZY_TOKEN, - url: COZY_URL, - ssl: USE_SSL + url: COZY_URL }) }, updateAccessToken(token) { From 29349aa28d5d4351c0b999f8478d62bbce719dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Wed, 20 Feb 2019 10:49:01 +0100 Subject: [PATCH 04/19] refactor: Externalize the realtime initialization We now pass the `getApp` function in parameter (it was previously a closure) but there should be no change to the underlying function. --- src/lib/realtime.js | 40 ++++++++++++++++++++++++++++++++++++++++ src/lib/stack.js | 33 ++------------------------------- 2 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 src/lib/realtime.js diff --git a/src/lib/realtime.js b/src/lib/realtime.js new file mode 100644 index 000000000..0e12d17d3 --- /dev/null +++ b/src/lib/realtime.js @@ -0,0 +1,40 @@ +import realtime from 'cozy-realtime' + + +/** + * Initialize realtime sockets + * + * @private + * @param {object} + * @returns {Promise} + */ +async function initializeRealtime({ getApp, onCreateApp, onDeleteApp, url, token }) { + const realtimeConfig = { token, url } + + try { + realtime + .subscribe(realtimeConfig, 'io.cozy.apps') + .onCreate(async app => { + // Fetch directly the app to get attributes `related` as well. + let fullApp + try { + fullApp = await getApp(app.slug) + } catch (error) { + throw new Error(`Cannot fetch app ${app.slug}: ${error.message}`) + } + + if (typeof onCreateApp === 'function') { + onCreateApp(fullApp) + } + }) + .onDelete(app => { + if (typeof onDeleteApp === 'function') { + onDeleteApp(app) + } + }) + } catch (error) { + console.warn(`Cannot initialize realtime in Cozy-bar: ${error.message}`) + } +} + +export default initializeRealtime diff --git a/src/lib/stack.js b/src/lib/stack.js index 2483ff731..d82e4773b 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -1,8 +1,8 @@ /* global __TARGET__ */ /* eslint-env browser */ -import realtime from 'cozy-realtime' import getIcon from './icon' +import initializeRealtime from './realtime' import normalizeURL from './normalize-url' import { @@ -131,36 +131,6 @@ export const getAppIconProps = () => { } } -async function initializeRealtime({ onCreateApp, onDeleteApp, url, token }) { - const realtimeConfig = { token, url } - - try { - realtime - .subscribe(realtimeConfig, 'io.cozy.apps') - .onCreate(async app => { - // Fetch direclty the app to get attributes `related` as well. - let fullApp - try { - fullApp = await getApp(app.slug) - } catch (error) { - throw new Error(`Cannont fetch app ${app.slug}: ${error.message}`) - } - - if (typeof onCreateApp === 'function') { - onCreateApp(fullApp) - } - }) - .onDelete(app => { - if (typeof onDeleteApp === 'function') { - onDeleteApp(app) - } - }) - } catch (error) { - console.warn(`Cannot initialize realtime in Cozy-bar: ${error.message}`) - } -} - - module.exports = { async init({ cozyURL, token, onCreateApp, onDeleteApp, ssl }) { ;({ COZY_URL, COZY_HOST } = determineURL(cozyURL, ssl)) @@ -171,6 +141,7 @@ module.exports = { USE_SSL = (url.protocol === 'https:') COZY_TOKEN = token await initializeRealtime({ + getApp, onCreateApp, onDeleteApp, token: COZY_TOKEN, From 13d9def8a727dbc9ee5a55066636040896b02a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 12:29:01 +0100 Subject: [PATCH 05/19] refactor: Centralize all stack request in lib/stack.js --- src/lib/icon.js | 17 ++++++++++++++--- src/lib/stack.js | 14 +++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/lib/icon.js b/src/lib/icon.js index 4fede91f2..5625d6264 100644 --- a/src/lib/icon.js +++ b/src/lib/icon.js @@ -9,9 +9,20 @@ const mimeTypes = { svg: 'image/svg+xml' } +/** + * Get an icon URL usable in the HTML page from it's stack path + * + * @function + * @private + * @param {function} iconFetcher - takes an icon path on the stack + * and returns a fetch response with the icon + * @param {object} app - app object with a `links.icon` attribute + * @param {boolean} useCache + * @returns {Promise} url string of an icon usable in the HTML page + * may be empty if the `app` object didn't have an icon path + */ module.exports = async function getIcon( - cozyUrl, - fetchHeaders, + iconFetcher, app = {}, useCache = true ) { @@ -22,7 +33,7 @@ module.exports = async function getIcon( let icon try { - const resp = await fetch(`${cozyUrl}${url}`, fetchHeaders) + const resp = await iconFetcher(url) if (!resp.ok) throw new Error(`Error while fetching icon ${resp.statusText}: ${url}`) icon = await resp.blob() diff --git a/src/lib/stack.js b/src/lib/stack.js index d82e4773b..07491bfa2 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -120,7 +120,19 @@ function getApp(slug) { const cache = {} -const _fetchIcon = app => getIcon(COZY_URL, fetchOptions(), app, true) +/** + * Fetch an icon from the stack by it's path + * + * @function + * @private + * @param {string} iconPath - path of the icon + * @returns {Promise} Fetch response + */ +const _iconFetcher = function(iconPath) { + return fetch(COZY_URL + iconPath, fetchOptions()) +} + +const _fetchIcon = app => getIcon(_iconFetcher, app, true) export const getAppIconProps = () => { return __TARGET__ === 'mobile' ? { fetchIcon: _fetchIcon } From bb66bd56a5f55d8777107d19759c5dbbe3e2c428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Wed, 20 Feb 2019 10:50:19 +0100 Subject: [PATCH 06/19] feat: Inject cozy-client - stage 0 Prepare two files : - lib/stack-internal.js which will be the default legacy client (exact same code) - lib/stack-client.js which will handle cozy client For now stack-client.js and the main stack.js are only wrappers to the legacy client. --- src/lib/stack-client.js | 3 + src/lib/stack-internal.js | 201 +++++++++++++++++++++++++++++++++++++ src/lib/stack.js | 203 +------------------------------------- 3 files changed, 206 insertions(+), 201 deletions(-) create mode 100644 src/lib/stack-client.js create mode 100644 src/lib/stack-internal.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js new file mode 100644 index 000000000..372d59ab3 --- /dev/null +++ b/src/lib/stack-client.js @@ -0,0 +1,3 @@ +import internal from 'lib/stack-internal.js' + +export default internal \ No newline at end of file diff --git a/src/lib/stack-internal.js b/src/lib/stack-internal.js new file mode 100644 index 000000000..43894f584 --- /dev/null +++ b/src/lib/stack-internal.js @@ -0,0 +1,201 @@ +/* global __TARGET__ */ +/* eslint-env browser */ + +import getIcon from './icon' +import initializeRealtime from './realtime' +import normalizeURL from './normalize-url' + +import { + ForbiddenException, + ServerErrorException, + NotFoundException, + MethodNotAllowedException, + UnavailableStackException, + UnavailableSettingsException, + UnauthorizedStackException +} from './exceptions' + +// the option credentials:include tells fetch to include the cookies in the +// request even for cross-origin requests +function fetchOptions() { + return { + credentials: 'include', + headers: { + Authorization: `Bearer ${COZY_TOKEN}`, + Accept: 'application/json' + } + } +} + +let COZY_URL +let COZY_HOST +let COZY_TOKEN +let USE_SSL + +const errorStatuses = { + '401': UnauthorizedStackException, + '403': ForbiddenException, + '404': NotFoundException, + '405': MethodNotAllowedException, + '500': ServerErrorException +} + +function getApps() { + return fetchJSON(`${COZY_URL}/apps/`, fetchOptions()).then(json => { + if (json.error) throw new Error(json.error) + else return json.data + }) +} + +function fetchJSON(url, options) { + return fetch(url, options).then(res => { + if (typeof errorStatuses[res.status] === 'function') { + throw new errorStatuses[res.status]() + } + + return res.json() + }) +} + +// fetch function with the same interface than in cozy-client-js +function cozyFetchJSON(cozy, method, path, body) { + const requestOptions = Object.assign({}, fetchOptions(), { + method + }) + requestOptions.headers['Accept'] = 'application/json' + if (method !== 'GET' && method !== 'HEAD' && body !== undefined) { + if (requestOptions.headers['Content-Type']) { + requestOptions.body = body + } else { + requestOptions.headers['Content-Type'] = 'application/json' + requestOptions.body = JSON.stringify(body) + } + } + return fetchJSON(`${COZY_URL}${path}`, requestOptions).then(json => { + const responseData = Object.assign({}, json.data) + if (responseData.id) responseData._id = responseData.id + return Promise.resolve(responseData) + }) +} + +function getStorageData() { + return fetchJSON(`${COZY_URL}/settings/disk-usage`, fetchOptions()) + .then(json => { + return { + usage: parseInt(json.data.attributes.used, 10), + // TODO Better handling when no quota provided + quota: parseInt(json.data.attributes.quota, 10) || 100000000000, + isLimited: json.data.attributes.is_limited + } + }) + .catch(() => { + throw new UnavailableStackException() + }) +} + +function getContext(cache) { + return () => { + return cache['context'] + ? Promise.resolve(cache['context']) + : fetchJSON(`${COZY_URL}/settings/context`, fetchOptions()) + .then(context => { + cache['context'] = context + return context + }) + .catch(error => { + if (error.status && error.status === 404) cache['context'] = {} + }) + } +} + +function getApp(slug) { + if (!slug) { + throw new Error('Missing slug') + } + return fetchJSON(`${COZY_URL}/apps/${slug}`, fetchOptions()).then(json => { + if (json.error) throw new Error(json.error) + else return json.data + }) +} + +const cache = {} + +/** + * Fetch an icon from the stack by it's path + * + * @function + * @private + * @param {string} iconPath - path of the icon + * @returns {Promise} Fetch response + */ +const _iconFetcher = function(iconPath) { + return fetch(COZY_URL + iconPath, fetchOptions()) +} + +const _fetchIcon = app => getIcon(_iconFetcher, app, true) +export const getAppIconProps = () => { + return __TARGET__ === 'mobile' + ? { fetchIcon: _fetchIcon } + : { + // we mustn't give the protocol here + domain: COZY_HOST, + secure: USE_SSL + } +} + +module.exports = { + async init({ cozyURL, token, onCreateApp, onDeleteApp, ssl }) { + const url = normalizeURL(cozyURL, ssl) + // The 4 following constant are global variables for the module + COZY_URL = url.origin + COZY_HOST = url.host + USE_SSL = (url.protocol === 'https:') + COZY_TOKEN = token + await initializeRealtime({ + getApp, + onCreateApp, + onDeleteApp, + token: COZY_TOKEN, + url: COZY_URL + }) + }, + updateAccessToken(token) { + COZY_TOKEN = token + }, + get: { + app: getApp, + apps: getApps, + context: getContext(cache), + storageData: getStorageData, + iconProps: getAppIconProps, + cozyURL() { + return COZY_URL + }, + settingsAppURL() { + return getApp('settings').then(settings => { + if (!settings) { + throw new UnavailableSettingsException() + } + return settings.links.related + }) + } + }, + logout() { + const options = Object.assign({}, fetchOptions(), { + method: 'DELETE' + }) + + return fetch(`${COZY_URL}/auth/login`, options) + .then(res => { + if (res.status === 401) { + throw new UnauthorizedStackException() + } else if (res.status === 204) { + window.location.reload() + } + }) + .catch(() => { + throw new UnavailableStackException() + }) + }, + cozyFetchJSON // used in intents library +} diff --git a/src/lib/stack.js b/src/lib/stack.js index 07491bfa2..372d59ab3 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -1,202 +1,3 @@ -/* global __TARGET__ */ -/* eslint-env browser */ +import internal from 'lib/stack-internal.js' -import getIcon from './icon' -import initializeRealtime from './realtime' -import normalizeURL from './normalize-url' - -import { - ForbiddenException, - ServerErrorException, - NotFoundException, - MethodNotAllowedException, - UnavailableStackException, - UnavailableSettingsException, - UnauthorizedStackException -} from './exceptions' - -// the option credentials:include tells fetch to include the cookies in the -// request even for cross-origin requests -function fetchOptions() { - return { - credentials: 'include', - headers: { - Authorization: `Bearer ${COZY_TOKEN}`, - Accept: 'application/json' - } - } -} - -let COZY_URL -let COZY_HOST -let COZY_TOKEN -let USE_SSL - -const errorStatuses = { - '401': UnauthorizedStackException, - '403': ForbiddenException, - '404': NotFoundException, - '405': MethodNotAllowedException, - '500': ServerErrorException -} - -function getApps() { - return fetchJSON(`${COZY_URL}/apps/`, fetchOptions()).then(json => { - if (json.error) throw new Error(json.error) - else return json.data - }) -} - -function fetchJSON(url, options) { - return fetch(url, options).then(res => { - if (typeof errorStatuses[res.status] === 'function') { - throw new errorStatuses[res.status]() - } - - return res.json() - }) -} - -// fetch function with the same interface than in cozy-client-js -function cozyFetchJSON(cozy, method, path, body) { - const requestOptions = Object.assign({}, fetchOptions(), { - method - }) - requestOptions.headers['Accept'] = 'application/json' - if (method !== 'GET' && method !== 'HEAD' && body !== undefined) { - if (requestOptions.headers['Content-Type']) { - requestOptions.body = body - } else { - requestOptions.headers['Content-Type'] = 'application/json' - requestOptions.body = JSON.stringify(body) - } - } - return fetchJSON(`${COZY_URL}${path}`, requestOptions).then(json => { - const responseData = Object.assign({}, json.data) - if (responseData.id) responseData._id = responseData.id - return Promise.resolve(responseData) - }) -} - -function getStorageData() { - return fetchJSON(`${COZY_URL}/settings/disk-usage`, fetchOptions()) - .then(json => { - return { - usage: parseInt(json.data.attributes.used, 10), - // TODO Better handling when no quota provided - quota: parseInt(json.data.attributes.quota, 10) || 100000000000, - isLimited: json.data.attributes.is_limited - } - }) - .catch(() => { - throw new UnavailableStackException() - }) -} - -function getContext(cache) { - return () => { - return cache['context'] - ? Promise.resolve(cache['context']) - : fetchJSON(`${COZY_URL}/settings/context`, fetchOptions()) - .then(context => { - cache['context'] = context - return context - }) - .catch(error => { - if (error.status && error.status === 404) cache['context'] = {} - }) - } -} - -function getApp(slug) { - if (!slug) { - throw new Error('Missing slug') - } - return fetchJSON(`${COZY_URL}/apps/${slug}`, fetchOptions()).then(json => { - if (json.error) throw new Error(json.error) - else return json.data - }) -} - -const cache = {} - -/** - * Fetch an icon from the stack by it's path - * - * @function - * @private - * @param {string} iconPath - path of the icon - * @returns {Promise} Fetch response - */ -const _iconFetcher = function(iconPath) { - return fetch(COZY_URL + iconPath, fetchOptions()) -} - -const _fetchIcon = app => getIcon(_iconFetcher, app, true) -export const getAppIconProps = () => { - return __TARGET__ === 'mobile' - ? { fetchIcon: _fetchIcon } - : { - // we mustn't give the protocol here - domain: COZY_HOST, - secure: USE_SSL - } -} - -module.exports = { - async init({ cozyURL, token, onCreateApp, onDeleteApp, ssl }) { - ;({ COZY_URL, COZY_HOST } = determineURL(cozyURL, ssl)) - const url = normalizeURL(cozyURL, ssl) - // The 4 following constant are global variables for the module - COZY_URL = url.origin - COZY_HOST = url.host - USE_SSL = (url.protocol === 'https:') - COZY_TOKEN = token - await initializeRealtime({ - getApp, - onCreateApp, - onDeleteApp, - token: COZY_TOKEN, - url: COZY_URL - }) - }, - updateAccessToken(token) { - COZY_TOKEN = token - }, - get: { - app: getApp, - apps: getApps, - context: getContext(cache), - storageData: getStorageData, - iconProps: getAppIconProps, - cozyURL() { - return COZY_URL - }, - settingsAppURL() { - return getApp('settings').then(settings => { - if (!settings) { - throw new UnavailableSettingsException() - } - return settings.links.related - }) - } - }, - logout() { - const options = Object.assign({}, fetchOptions(), { - method: 'DELETE' - }) - - return fetch(`${COZY_URL}/auth/login`, options) - .then(res => { - if (res.status === 401) { - throw new UnauthorizedStackException() - } else if (res.status === 204) { - window.location.reload() - } - }) - .catch(() => { - throw new UnavailableStackException() - }) - }, - cozyFetchJSON // used in intents library -} +export default internal \ No newline at end of file From 7ca04c80fa24c319c586c0615d8aea8ac5e3eda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 12:58:30 +0100 Subject: [PATCH 07/19] feat: Inject cozy-client - stage 1 - switch between clients We introduce a proxy to access the stack client functions. It will redirect to the legacy one or to the cozy-client based one depending on initialization. --- src/lib/stack.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/lib/stack.js b/src/lib/stack.js index 372d59ab3..422aa3d4a 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -1,3 +1,66 @@ import internal from 'lib/stack-internal.js' +import client from "lib/stack-client" -export default internal \ No newline at end of file + +/** + * Reference to the current client depending + * on which one has been initialized + * + * @private + * @TODO We should set it to `undefined` + * and throw an error when it is not initialized + * Leaving with that default for today + * so I do not need to rewrite deeply the tests + * (they test some components without initializing + * the stack client first, and rely on non-initialized + * legacy client behaviour) + */ +let stack = internal + +/** + * Get the current stack client (legacy or cozy-client based) + * based on which one has been initialized + * + * @returns {Object} functions to call the stack + */ +const current = function() { + if (stack === undefined) { + throw new Error("client not initialized in cozy-bar") + } + return stack +} + +/** + * Initializes the functions to call the cozy stack + * + * @function + * @param {Object} arg + * @param {string} arg.cozyURL - URL or domain of the stack + * @param {boolean} arg.ssl - Tells if we should use a secure protocol + * required if cozyURL does not have a protocol + * @param {string} arg.token - Access token for the stack + * @param {Function} arg.onCreateApp + * @param {Function} arg.onDeleteApp + * @returns {Promise} + */ +const init = function(options) { + stack = internal + return stack.init(options) +} + +const get = { + app: (...args) => current().get.app(...args), + apps: (...args) => current().get.apps(...args), + context: (...args) => current().get.context(...args), + storageData: (...args) => current().get.storageData(...args), + iconProps: (...args) => current().get.iconProps(...args), + cozyURL: (...args) => current().get.cozyURL(...args), +} + +export default { + init, + get, + updateAccessToken: (...args) => current().updateAccessToken(...args), + logout: (...args) => current().logout(...args), + cozyFetchJSON: (...args) => current().cozyFetchJSON(...args) +} From 5ad92f8ecf2ce2d97072b0df9ee15d5c0cd792f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 20:15:48 +0100 Subject: [PATCH 08/19] feat: Inject cozy-client - stage 2 - initialize through a cozy-client --- src/index.jsx | 25 ++++++ src/lib/stack-client.js | 28 +++++- src/lib/stack.js | 6 +- .../stack-client/stack-client.init.spec.js | 55 ++++++++++++ test/lib/stack.spec.js | 87 +++++++++++++++++++ 5 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 test/lib/stack-client/stack-client.init.spec.js create mode 100644 test/lib/stack.spec.js diff --git a/src/index.jsx b/src/index.jsx index c907aec41..16d76aede 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -144,12 +144,36 @@ const getUserActionRequired = () => { let exposedAPI = {} +/** + * Initializes the cozy bar + * + * It can be initialized either with a cozyClient instance + * or a { cozyURL, ssl, token } tupple. + * + * @function + * @param {Object} arg + * @param {string} arg.appName - App name to be displayed in the bar + * @param {string} arg.appNamePrefix + * @param {string} arg.lang - Language for the bar + * @param {string} arg.iconPath - + * @param {Object} arg.cozyClient - a cozy client instance + * @param {string} arg.cozyURL - URL or domain of the stack + * @param {boolean} arg.ssl - Tells if we should use a secure + * protocol required if cozyURL does + * not have a protocol + * @param {string} arg.token - Access token for the stack + * @param {boolean} arg.replaceTitleOnMobile + * @param {boolean} arg.isPublic + * @param {Function} arg.onLogout + * @param {Function} arg.onDeleteApp + */ const init = async ({ appName, appNamePrefix = getAppNamePrefix(), appSlug = getAppSlug(), lang, iconPath = getDefaultIcon(), + cozyClient, cozyURL = getDefaultStackURL(), token = getDefaultToken(), replaceTitleOnMobile = false, @@ -167,6 +191,7 @@ const init = async ({ reduxStore.dispatch(setInfos(appName, appNamePrefix, appSlug)) stack.init({ + cozyClient, cozyURL, token, onCreateApp: app => reduxStore.dispatch(receiveApp(app)), diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 372d59ab3..434a67d9a 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -1,3 +1,29 @@ import internal from 'lib/stack-internal.js' -export default internal \ No newline at end of file +/** + * Cozy client instance + * @private + */ +let cozyClient + +/** + * Initializes the functions to call the cozy stack + * + * @function + * @param {Object} arg + * @param {Object} arg.cozyClient - a cozy client instance + * @param {Function} arg.onCreateApp + * @param {Function} arg.onDeleteApp + * @returns {Promise} + */ +const init = function(options) { + cozyClient = options.cozyClient + const legacyOptions = { + ...options, + cozyURL: cozyClient.getStackClient().uri, + token: cozyClient.getStackClient().token.token + } + return internal.init(legacyOptions) +} + +export default { ...internal, get: { ...internal.get}, init } \ No newline at end of file diff --git a/src/lib/stack.js b/src/lib/stack.js index 422aa3d4a..c59e76bde 100644 --- a/src/lib/stack.js +++ b/src/lib/stack.js @@ -33,8 +33,12 @@ const current = function() { /** * Initializes the functions to call the cozy stack * + * It can be initialized either with a cozy-client instance + * or a { cozyURL, ssl, token } tupple. + * * @function * @param {Object} arg + * @param {Object} arg.cozyClient - a cozy client instance * @param {string} arg.cozyURL - URL or domain of the stack * @param {boolean} arg.ssl - Tells if we should use a secure protocol * required if cozyURL does not have a protocol @@ -44,7 +48,7 @@ const current = function() { * @returns {Promise} */ const init = function(options) { - stack = internal + stack = (options.cozyClient) ? client : internal return stack.init(options) } diff --git a/test/lib/stack-client/stack-client.init.spec.js b/test/lib/stack-client/stack-client.init.spec.js new file mode 100644 index 000000000..950cb5880 --- /dev/null +++ b/test/lib/stack-client/stack-client.init.spec.js @@ -0,0 +1,55 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +const { + init, +} = stack + + +describe("stack client", () => { + + describe("init", () => { + let cozyClient = { + getStackClient: () => { + return { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud" + } + } + } + let params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(async () => { + jest.spyOn(internal, 'init').mockResolvedValue(undefined) + await init(params) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should called internal client", () => { + expect( internal.init ).toHaveBeenCalled() + }) + + it("should have set the cozy-client token", () => { + expect( internal.init.mock.calls[0][0].token ).toBe("mytoken") + }) + + it("should have set the cozy-client uri", () => { + expect( internal.init.mock.calls[0][0].cozyURL ).toBe("https://test.mycozy.cloud") + }) + + it("should pass onCreateApp and onDeleteApp functions", () => { + expect( internal.init.mock.calls[0][0].onDeleteApp ).toBeInstanceOf(Function) + expect( internal.init.mock.calls[0][0].onCreateApp ).toBeInstanceOf(Function) + }) + + }) + +}) diff --git a/test/lib/stack.spec.js b/test/lib/stack.spec.js new file mode 100644 index 000000000..088066de0 --- /dev/null +++ b/test/lib/stack.spec.js @@ -0,0 +1,87 @@ +import internal from 'lib/stack-internal.js' +import client from 'lib/stack-client.js' +import stack from 'lib/stack.js' + + +const cozyURL = "https://test.mycozy.cloud" +const token = "mytoken" +const onCreateApp = function() {} +const onDeleteApp = function() {} + +describe("stack proxy", () => { + + beforeAll(() => { + jest.spyOn(internal, 'init').mockResolvedValue(undefined) + jest.spyOn(internal.get, 'app').mockResolvedValue(undefined) + + jest.spyOn(client, 'init').mockResolvedValue(undefined) + jest.spyOn(client.get, 'app').mockResolvedValue(undefined) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + describe("when initialized with an cozyURL + token", () => { + + const params = { cozyURL, token, onCreateApp, onDeleteApp } + + beforeAll(() => { + jest.clearAllMocks() + + stack.init(params) + stack.get.app() + }) + + it("should call the internal stack init", () => { + expect( internal.init ).toHaveBeenCalled() + }) + + it("should not call the cozy-client stack init", () => { + expect( client.init ).not.toHaveBeenCalled() + }) + + it("should forward requests to the internal stack client", () => { + expect( internal.get.app ).toHaveBeenCalled() + }) + + it("should not forward requests to the cozy-client stack client", () => { + expect( client.get.app ).not.toHaveBeenCalled() + }) + + }) + + describe("when initialized with a cozy-client instance", () => { + + const cozyClient = { + getStackClient: () => { + return { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud" + } + } + } + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(() => { + jest.clearAllMocks() + + stack.init(params) + stack.get.app() + }) + + it("should call the client stack init", () => { + expect( client.init ).toHaveBeenCalled() + }) + + it("should forward requests to the cozy-client stack client", () => { + expect( client.get.app ).toHaveBeenCalled() + }) + + }) + +}) From f7eba27bb3e0c9189e36adb8a30bd5d6eb2ea871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 21:42:15 +0100 Subject: [PATCH 09/19] feat: Inject cozy-client - stage 3 - logout uses cozy-client --- src/lib/stack-client.js | 41 ++++++++++++++- .../stack-client/stack-client.logout.spec.js | 50 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 test/lib/stack-client/stack-client.logout.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 434a67d9a..1a82f560a 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -1,11 +1,50 @@ import internal from 'lib/stack-internal.js' +import { + UnavailableStackException, + UnauthorizedStackException +} from './exceptions' + /** * Cozy client instance * @private */ let cozyClient +/** + * Get the stackClient from the cozy-client instance + * + * @private + * @function + * @returns {Object} cozy-stack-client instance + */ +const getStackClient = function() { + return cozyClient.getStackClient() +} + +/** + * Logout and disconnect the user + * @function + * @TODO move this to cozy-stack-client + * @returns {Promise} + */ +const logout = function() { + return getStackClient().fetch('DELETE', '/auth/login').then( + resp => { + if (resp.status === 401) { + throw new UnauthorizedStackException() + } else if (resp.status === 204) { + window.location.reload() + } + return true + } + ).catch( + () => { + throw new UnavailableStackException() + } + ) +} + /** * Initializes the functions to call the cozy stack * @@ -26,4 +65,4 @@ const init = function(options) { return internal.init(legacyOptions) } -export default { ...internal, get: { ...internal.get}, init } \ No newline at end of file +export default { ...internal, get: { ...internal.get}, logout, init } \ No newline at end of file diff --git a/test/lib/stack-client/stack-client.logout.spec.js b/test/lib/stack-client/stack-client.logout.spec.js new file mode 100644 index 000000000..f0b055279 --- /dev/null +++ b/test/lib/stack-client/stack-client.logout.spec.js @@ -0,0 +1,50 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +describe("stack client", () => { + + describe("logout", () => { + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch: jest.fn().mockResolvedValue({status: 200}) + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(async () => { + jest.spyOn(internal, 'logout').mockResolvedValue(undefined) + await stack.init(params) + await stack.logout() + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should not forward to the old internal client", () => { + expect( internal.logout ).not.toHaveBeenCalled() + }) + + it("should have called cozy-client", () => { + expect( cozyClient.getStackClient().fetch ).toHaveBeenCalled() + }) + + it("should not throw", async () => { + expect( () => stack.logout() ).not.toThrow() + await expect( stack.logout() ).resolves.not.toBe(false) + }) + + }) + +}) From 2425f54de369ce17fc3689ada2294b047e673545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 21:45:01 +0100 Subject: [PATCH 10/19] feat: Inject cozy-client - stage 4 - updateAccessToken --- src/lib/stack-client.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 1a82f560a..742258897 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -45,6 +45,14 @@ const logout = function() { ) } +/** + * @deprecated + * @private + */ +const updateAccessToken = function(token) { + throw new Error("updateAccessToken should not be used with a cozy-client instance initialization") +} + /** * Initializes the functions to call the cozy stack * @@ -65,4 +73,12 @@ const init = function(options) { return internal.init(legacyOptions) } -export default { ...internal, get: { ...internal.get}, logout, init } \ No newline at end of file +export default { + ...internal, + get: { + ...internal.get + }, + updateAccessToken, + logout, + init  +} \ No newline at end of file From 7d2ae16b90cd72112b71ef222661fc8a2f6816cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 21:53:22 +0100 Subject: [PATCH 11/19] feat: Inject cozy-client - stage 5 - get.cozyURL --- src/lib/stack-client.js | 25 ++++++++++- .../stack-client/stack-client.cozyurl.spec.js | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/lib/stack-client/stack-client.cozyurl.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 742258897..10c9fd984 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -45,6 +45,26 @@ const logout = function() { ) } +/** + * Get a cozy URL object + * + * @function + * @returns {URL} + */ +const getCozyURL = function() { + return new URL(getStackClient().uri) +} + +/** + * Get a the cozy origin as an URL string + * + * @function + * @returns {string} + */ +const getCozyURLOrigin = function() { + return getCozyURL().origin +} + /** * @deprecated * @private @@ -67,7 +87,7 @@ const init = function(options) { cozyClient = options.cozyClient const legacyOptions = { ...options, - cozyURL: cozyClient.getStackClient().uri, + cozyURL: getCozyURLOrigin(), token: cozyClient.getStackClient().token.token } return internal.init(legacyOptions) @@ -76,7 +96,8 @@ const init = function(options) { export default { ...internal, get: { - ...internal.get + ...internal.get, + cozyURL: getCozyURLOrigin }, updateAccessToken, logout, diff --git a/test/lib/stack-client/stack-client.cozyurl.spec.js b/test/lib/stack-client/stack-client.cozyurl.spec.js new file mode 100644 index 000000000..47087e90c --- /dev/null +++ b/test/lib/stack-client/stack-client.cozyurl.spec.js @@ -0,0 +1,44 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +describe("stack client", () => { + + describe("cozyURL", () => { + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(async () => { + jest.spyOn(internal.get, 'cozyURL').mockResolvedValue(undefined) + await stack.init(params) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should give back the origin of cozy-client", () => { + expect( stack.get.cozyURL() ).toBe("https://test.mycozy.cloud") + }) + + it("should not forward to the old internal client", () => { + stack.get.cozyURL() + expect( internal.get.cozyURL ).not.toHaveBeenCalled() + }) + + }) + +}) From 8a8c4edc897d3519a2c12e8332b74c7bc8fc4d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 21:59:51 +0100 Subject: [PATCH 12/19] refactor: use local `getStackClient()` shortcut --- src/lib/stack-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 10c9fd984..bc729d142 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -88,7 +88,7 @@ const init = function(options) { const legacyOptions = { ...options, cozyURL: getCozyURLOrigin(), - token: cozyClient.getStackClient().token.token + token: getStackClient().token.token } return internal.init(legacyOptions) } From e444957edc62b875dc056e823642a2a00019cb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 22:37:52 +0100 Subject: [PATCH 13/19] feat: Inject cozy-client - stage 6 - get.apps Also in this commit : how to forward a fetch to cozyClient with `fetchJSON()` in a way compatible with the legacy internal client --- src/lib/stack-client.js | 71 ++++++++++++++++ .../stack-client/stack-client.getapps.spec.js | 83 +++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 test/lib/stack-client/stack-client.getapps.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index bc729d142..45004f67a 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -1,10 +1,22 @@ import internal from 'lib/stack-internal.js' import { + ForbiddenException, + ServerErrorException, + NotFoundException, + MethodNotAllowedException, UnavailableStackException, UnauthorizedStackException } from './exceptions' +const errorStatuses = { + '401': UnauthorizedStackException, + '403': ForbiddenException, + '404': NotFoundException, + '405': MethodNotAllowedException, + '500': ServerErrorException +} + /** * Cozy client instance * @private @@ -73,6 +85,64 @@ const updateAccessToken = function(token) { throw new Error("updateAccessToken should not be used with a cozy-client instance initialization") } +/** + * Fetch a resource with cozy-client + * + * Utility to maintain the compatibility with the legacy + * standalone cozy-bar client + * + * @function + * @private + * @returns {Promise} the full raw JSON playload + */ +const fetchJSON = function(method, path, body, options={}) { + + // We mirror here a few lines from cozy-stack-client + // because we want a customized fetchJSON + const headers = (options.headers = options.headers || {}) + headers['Accept'] = 'application/json' + if (method !== 'GET' && method !== 'HEAD' && body !== undefined) { + if (!headers['Content-Type']) { + headers['Content-Type'] = 'application/json' + body = JSON.stringify(body) + } + } + + return getStackClient().fetch(method, path, body, options).then( + resp => { + if (typeof errorStatuses[resp.status] === 'function') { + throw new errorStatuses[resp.status]() + } + const contentType = resp.headers.get('content-type') + const isJson = contentType.includes('json') + if (!isJson) { + throw new Error("Server response not in JSON") + } + return resp.json() + } + ) +} + +/** + * List all installed applications + * + * Returns only the `data` key of the + * whole JSON payload from the server + * + * @function + * @returns {Promise} + */ +const getApps = function () { + return fetchJSON('GET', '/apps/').then( + json => { + if (json.error) { + throw new Error(json.error) + } + return json.data + } + ) +} + /** * Initializes the functions to call the cozy stack * @@ -97,6 +167,7 @@ export default { ...internal, get: { ...internal.get, + apps: getApps, cozyURL: getCozyURLOrigin }, updateAccessToken, diff --git a/test/lib/stack-client/stack-client.getapps.spec.js b/test/lib/stack-client/stack-client.getapps.spec.js new file mode 100644 index 000000000..7d99bf59a --- /dev/null +++ b/test/lib/stack-client/stack-client.getapps.spec.js @@ -0,0 +1,83 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +describe("stack client", () => { + + describe("getApps", () => { + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch: jest.fn().mockResolvedValue( + { + status: 200, + headers: { get: (h) => "application/json" }, + json: () => { + // example from https://docs.cozy.io/en/cozy-stack/apps/ + return { + "data": [{ + "id": "4cfbd8be-8968-11e6-9708-ef55b7c20863", + "type": "io.cozy.apps", + "meta": { + "rev": "2-bbfb0fc32dfcdb5333b28934f195b96a" + }, + "attributes": { + "name": "calendar", + "state": "ready", + "slug": "calendar" + }, + "links": { + "self": "/apps/calendar", + "icon": "/apps/calendar/icon", + "related": "https://calendar.alice.example.com/" + } + }] + } + } + } + ) + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll( async () => { + jest.spyOn(internal.get, 'apps').mockResolvedValue(undefined) + await stack.init(params) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should not forward to the old internal client", async () => { + await stack.get.apps() + expect( internal.get.apps ).not.toHaveBeenCalled() + }) + + it("should return the `data` content", async () => { + const data = await stack.get.apps() + expect( data[0].id ).toBe("4cfbd8be-8968-11e6-9708-ef55b7c20863") + }) + + it("should have called cozy-client", async () => { + await stack.get.apps() + expect( cozyClient.getStackClient().fetch ).toHaveBeenCalled() + }) + + it("should not throw", async () => { + expect( () => stack.get.apps() ).not.toThrow() + await expect( stack.get.apps() ).resolves.not.toBe(false) + }) + + }) + +}) From a5be0f0cd3b4740346c0d6e2e17f973125001a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 22:47:11 +0100 Subject: [PATCH 14/19] feat: Inject cozy-client - stage 7 - get.app --- src/lib/stack-client.js | 25 ++++++ .../stack-client/stack-client.getapp.spec.js | 83 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 test/lib/stack-client/stack-client.getapp.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 45004f67a..010c251dd 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -143,6 +143,30 @@ const getApps = function () { ) } +/** + * Detail of an installed application by its slug + * + * Returns only the `data` key of the + * whole JSON payload from the server + * + * @function + * @param {string} slug + * @returns {Promise} + */ +const getApp = function(slug) { + if (!slug) { + throw new Error('Missing slug') + } + return fetchJSON('GET', `/apps/${slug}`).then( + json => { + if (json.error) { + throw new Error(json.error) + } + return json.data + } + ) +} + /** * Initializes the functions to call the cozy stack * @@ -167,6 +191,7 @@ export default { ...internal, get: { ...internal.get, + app: getApp, apps: getApps, cozyURL: getCozyURLOrigin }, diff --git a/test/lib/stack-client/stack-client.getapp.spec.js b/test/lib/stack-client/stack-client.getapp.spec.js new file mode 100644 index 000000000..5e412ca96 --- /dev/null +++ b/test/lib/stack-client/stack-client.getapp.spec.js @@ -0,0 +1,83 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +describe("stack client", () => { + + describe("getApp", () => { + + const slug = "testapp" + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch: jest.fn().mockResolvedValue({ + status: 200, + headers: { get: (h) => "application/json" }, + json: () => { + // example from https://docs.cozy.io/en/cozy-stack/apps/ + return { + "data": { + "id": "4cfbd8be-8968-11e6-9708-ef55b7c20863", + "type": "io.cozy.apps", + "meta": { + "rev": "2-bbfb0fc32dfcdb5333b28934f195b96a" + }, + "attributes": { + "name": "calendar", + "state": "ready", + "slug": "calendar" + }, + "links": { + "self": "/apps/calendar", + "icon": "/apps/calendar/icon", + "related": "https://calendar.alice.example.com/" + } + } + } + } + }) + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll( async () => { + jest.spyOn(internal.get, 'app').mockResolvedValue(undefined) + await stack.init(params) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should not forward to the old internal client", async () => { + await stack.get.app(slug) + expect( internal.get.app ).not.toHaveBeenCalled() + }) + + it("should return the `data` content", async () => { + const data = await stack.get.app(slug) + expect( data.id ).toBe("4cfbd8be-8968-11e6-9708-ef55b7c20863") + }) + + it("should have called cozy-client", async () => { + await stack.get.app(slug) + expect( cozyClient.getStackClient().fetch ).toHaveBeenCalled() + }) + + it("should not throw", async () => { + expect( () => stack.get.app(slug) ).not.toThrow() + await expect( stack.get.app(slug) ).resolves.not.toBe(false) + }) + + }) + +}) From 7cbb0816b40f51163c604e834d4c3d0960f7982a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 23:19:48 +0100 Subject: [PATCH 15/19] feat: Inject cozy-client - stage 8 - get.context With an utility function to cache the result --- src/lib/stack-client.js | 58 +++++++++ .../stack-client.getcontext.spec.js | 116 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 test/lib/stack-client/stack-client.getcontext.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 010c251dd..9ad2ecc55 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -123,6 +123,51 @@ const fetchJSON = function(method, path, body, options={}) { ) } +/** + * Test if an error is from an HTTP 404 + * + * @function + * @private + * @param {Function} error - received from a fetch + * @returns {boolean} + */ +const is404 = function(error) { + return ['NotFoundException', 'NotFound', 'FetchError'].includes(error.name) && + error.status && + error.status === 404 +} + +/** + * Memoize the result of a function which does an HTTP fetch + * + * If a call throws an error because the + * underlying HTTP request returned a 404 + * then this function returns a default value + * + * In the absence of any other error, the result is + * cached and reused in the next call to the function. + * + * + * @function + * @param {Function} fn - the function to memoize. It will be + * called without any parameter + * @param {Object} defaultValue - returned in case of 404 + * @returns {Function} async function + */ +const withCache = function(fn, defaultValue) { + let cache = undefined + return async function() { + if (cache === undefined) { + try { + cache = await fn() + } catch(error) { + cache = is404(error) ? defaultValue : undefined + } + } + return cache + } +} + /** * List all installed applications * @@ -167,6 +212,18 @@ const getApp = function(slug) { ) } + +/** + * Get settings context + * + * @function + * @return {Promise} + * @see https://docs.cozy.io/en/cozy-stack/settings/#get-settingscontext + */ +const getContext = function() { + return fetchJSON('GET', '/settings/context') +} + /** * Initializes the functions to call the cozy stack * @@ -193,6 +250,7 @@ export default { ...internal.get, app: getApp, apps: getApps, + context: withCache(getContext, {}), cozyURL: getCozyURLOrigin }, updateAccessToken, diff --git a/test/lib/stack-client/stack-client.getcontext.spec.js b/test/lib/stack-client/stack-client.getcontext.spec.js new file mode 100644 index 000000000..a5c2fdcd1 --- /dev/null +++ b/test/lib/stack-client/stack-client.getcontext.spec.js @@ -0,0 +1,116 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +import { + ForbiddenException, + ServerErrorException, + NotFoundException, + MethodNotAllowedException, + UnavailableStackException, + UnauthorizedStackException +} from 'lib/exceptions' + +describe("stack client", () => { + + describe("getContext", () => { + + const context404 = new NotFoundException() + + const context500 = new UnauthorizedStackException() + + const context200 = { + status: 200, + headers: { get: (h) => "application/json" }, + json: () => { + return { + "data": { + "type": "io.cozy.settings", + "id": "io.cozy.settings.context", + "attributes": { + "default_redirection": "drive/#/files", + "help_link": "https://forum.cozy.io/", + "onboarded_redirection": "collect/#/discovery/?intro" + }, + "links": { + "self": "/settings/context" + } + } + } + } + } + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch: jest.fn() + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll( async () => { + jest.spyOn(internal.get, 'context').mockResolvedValue(undefined) + await stack.init(params) + }) + + beforeEach( () => { + stackClient.fetch.mockClear() + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should not cache 500 errors", async () => { + stackClient.fetch.mockRejectedValue(context500) + await stack.get.context().catch(() => undefined) + await stack.get.context().catch(() => undefined) + expect( stackClient.fetch ).toHaveBeenCalledTimes(2) + }) + + it("should not forward to the old internal client", async () => { + stackClient.fetch.mockResolvedValue(context200) + await stack.get.context() + expect( internal.get.context ).not.toHaveBeenCalled() + }) + + it("should return the raw json content", async () => { + stackClient.fetch.mockResolvedValue(context200) + const data = await stack.get.context() + expect( data.data.id ).toBe("io.cozy.settings.context") + }) + + it("should be cached", async () => { + stackClient.fetch.mockResolvedValue(context200) + const data1 = await stack.get.context() + // If we did not called mockClear, we + // would have 0 or 1 call to the mock + // depending on preceding tests. + // Here we always should expect 0 + // in the next lines + stackClient.fetch.mockClear() + stackClient.fetch.mockRejectedValue(context404) + const data2 = await stack.get.context() + expect( data1 ).toBe(data2) + expect( stackClient.fetch ).toHaveBeenCalledTimes(0) + }) + + xit("should return the default value for a 404", async () => { + // @TODO deactivated because the previous tests already + // set the cache and the tested function is statefull + stackClient.fetch.mockRejectedValue(context404) + const data = await stack.get.context() + expect( data ).toEqual( { } ) + }) + + }) + +}) From aea4a99ec7fda983dc5a96141c8674442f4dd5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sat, 16 Feb 2019 23:38:35 +0100 Subject: [PATCH 16/19] feat: Inject cozy-client - stage 9 - get.storageData The way we return the data for an instance without quota is really unsatisfying. We should probably plan to do something smarter in the future --- src/lib/stack-client.js | 36 +++++++ .../stack-client.getstoragedata.spec.js | 96 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 test/lib/stack-client/stack-client.getstoragedata.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 9ad2ecc55..f424e6953 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -212,6 +212,41 @@ const getApp = function(slug) { ) } +/** + * default value when no quota is provided + * @private + */ +const defaultQuota = 10**12 // 1 Tera + +/** + * Get storage and quota usage + * + * When no quota is returned by the server + * the quota used is the larger between + * `defaultQuota` and 10 * usage + * + * @function + * @returns {Object} {usage, quota, isLimited} + */ +const getStorageData = function() { + return fetchJSON('GET', '/settings/disk-usage').then( + json => { + // parseInt because responses from the server are in text + const usage = parseInt(json.data.attributes.used, 10) + const realQuota = parseInt(json.data.attributes.quota, 10) + // @TODO this is a workaround, we should certainly do smarter + // and either not requiring this attribute + // or set it to something more real + const quota = realQuota || Math.max(defaultQuota, 10 * usage) + const isLimited = json.data.attributes.is_limited + return { usage, quota, isLimited } + } + ).catch( + error => { + throw new UnavailableStackException() + } + ) +} /** * Get settings context @@ -251,6 +286,7 @@ export default { app: getApp, apps: getApps, context: withCache(getContext, {}), + storageData: getStorageData, cozyURL: getCozyURLOrigin }, updateAccessToken, diff --git a/test/lib/stack-client/stack-client.getstoragedata.spec.js b/test/lib/stack-client/stack-client.getstoragedata.spec.js new file mode 100644 index 000000000..321cf24a0 --- /dev/null +++ b/test/lib/stack-client/stack-client.getstoragedata.spec.js @@ -0,0 +1,96 @@ +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +describe("stack client", () => { + + describe("getStorageData", () => { + + const r200 = { + status: 200, + headers: { get: (h) => "application/json" }, + json: () => { + return { + "data": { + "type": "io.cozy.settings", + "id": "io.cozy.settings.disk-usage", + "attributes": { + "is_limited": true, + "quota": "123456789", + "used": "12345678" + } + } + } + } + } + + const r200noquota = { + status: 200, + headers: { get: (h) => "application/json" }, + json: () => { + return { + "data": { + "type": "io.cozy.settings", + "id": "io.cozy.settings.disk-usage", + "attributes": { + "is_limited": false, + "used": "2000111222333" // 2To + } + } + } + } + } + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch: jest.fn() + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll( async () => { + jest.spyOn(internal.get, 'storageData').mockResolvedValue(undefined) + await stack.init(params) + }) + + beforeEach( () => { + stackClient.fetch.mockClear() + stackClient.fetch.mockResolvedValue(r200) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + it("should not forward to the old internal client", async () => { + await stack.get.storageData() + expect( internal.get.storageData ).not.toHaveBeenCalled() + }) + + it("should return a decidated object", async () => { + const data = await stack.get.storageData() + expect( data.usage ).toBe(12345678) + expect( data.quota ).toBe(123456789) + expect( data.isLimited ).toBe(true) + }) + + it("when no quota is provided, one should be given", async () => { + stackClient.fetch.mockResolvedValue(r200noquota) + const data = await stack.get.storageData() + expect( data.usage ).toBe(2000111222333) + expect( data.quota ).toBeGreaterThanOrEqual(data.usage) + expect( data.isLimited ).toBe(false) + }) + + }) + +}) From 9763822f26619475b97a446ab2209f2fa8ba4a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sun, 17 Feb 2019 00:26:47 +0100 Subject: [PATCH 17/19] feat: Inject cozy-client - stage 10 - get.iconProps --- src/lib/stack-client.js | 44 ++++++++++ .../stack-client.appiconprops.spec.js | 84 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 test/lib/stack-client/stack-client.appiconprops.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index f424e6953..cbe7ab576 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -1,4 +1,8 @@ +/* global __TARGET__ */ +/* eslint-env browser */ + import internal from 'lib/stack-internal.js' +import getIcon from 'lib/icon' import { ForbiddenException, @@ -248,6 +252,45 @@ const getStorageData = function() { ) } +/** + * Fetch an icon data from its path + * + * The purpose of this function is to be sent + * to AppIcon components for mobile devices. + * + * @private + * @function + * @param {string} iconPath - path of the icon in the stack + * @returns {Blob} + */ +const iconFetcher = function(iconPath) { + return getStackClient().fetch('GET', iconPath) +} + +/** + * Get a props object that can be sent to an AppIcon component + * + * Mobile devices and web browsers need different props + * + * @function + * @returns {Object} + */ +const getAppIconProps = function() { + const isMobile = (__TARGET__ === 'mobile') + + const mobileAppIconProps = { + fetchIcon: app => getIcon(iconFetcher, app, true) + } + + const browserAppIconProps = { + // we mustn't give the protocol here + domain: getCozyURL().host, + secure: (getCozyURL().protocol === 'https:') + } + + return isMobile ? mobileAppIconProps : browserAppIconProps +} + /** * Get settings context * @@ -287,6 +330,7 @@ export default { apps: getApps, context: withCache(getContext, {}), storageData: getStorageData, + iconProps: getAppIconProps, cozyURL: getCozyURLOrigin }, updateAccessToken, diff --git a/test/lib/stack-client/stack-client.appiconprops.spec.js b/test/lib/stack-client/stack-client.appiconprops.spec.js new file mode 100644 index 000000000..a46639e62 --- /dev/null +++ b/test/lib/stack-client/stack-client.appiconprops.spec.js @@ -0,0 +1,84 @@ +/* global __TARGET__ */ + +import stack from 'lib/stack-client' + +import internal from 'lib/stack-internal' + +let oldTarget + +describe("stack client", () => { + + describe("getAppIconProps", () => { + + const stackClient = { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + } + + const cozyClient = { + getStackClient: () => stackClient + } + + const params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(async () => { + oldTarget = global.__TARGET__ + jest.spyOn(internal.get, 'iconProps').mockResolvedValue(undefined) + await stack.init(params) + }) + + afterAll(() => { + jest.restoreAllMocks() + global.__TARGET__ = oldTarget + }) + + it("should not forward to the old internal client", () => { + stack.get.iconProps() + expect( internal.get.iconProps ).not.toHaveBeenCalled() + }) + + describe("for target=browser", () => { + + beforeAll(() => { + global.__TARGET__ = "browser" + }) + + it("should have `domain` and `secure` set", () => { + const data = stack.get.iconProps() + expect( data.domain ).toBe("test.mycozy.cloud") + expect( data.secure ).toBe(true) + }) + + it("should not have a `fetchIcon` function", () => { + const data = stack.get.iconProps() + expect( data.fetchIcon ).toBe(undefined) + }) + + }) + + describe("for target=mobile", () => { + + beforeAll(() => { + global.__TARGET__ = "mobile" + }) + + it("should note have `domain` and `secure` set", () => { + const data = stack.get.iconProps() + expect( data.domain ).toBeUndefined() + expect( data.secure ).toBeUndefined() + }) + + it("should have a `fetchIcon` function set", () => { + const data = stack.get.iconProps() + expect( data.fetchIcon ).toBeInstanceOf(Function) + }) + + }) + + }) + +}) From dd264692bf95ba3cbfe078544c35fe79dfd94918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sun, 17 Feb 2019 00:35:17 +0100 Subject: [PATCH 18/19] feat: Inject cozy-client - stage 11 - cozyFetchJSON simple wrapper around internal `fetchJSON()` maintaining compatibility with the old cozy-client-js --- src/lib/stack-client.js | 28 ++++++++++- .../stack-client.cozyfetchjson.spec.js | 49 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 test/lib/stack-client/stack-client.cozyfetchjson.spec.js diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index cbe7ab576..890d5b852 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -97,7 +97,7 @@ const updateAccessToken = function(token) { * * @function * @private - * @returns {Promise} the full raw JSON playload + * @returns {Promise} the full raw JSON payload */ const fetchJSON = function(method, path, body, options={}) { @@ -302,6 +302,29 @@ const getContext = function() { return fetchJSON('GET', '/settings/context') } +/** + * Fetch a resource on the cozy stack + * with a prototype compatible with the legacy cozy-client-js + * + * @function + * @param {object} cozy - cozy-client-js + * @param {string} method - HTTP method + * @param {string} path + * @param {object} body + * @returns {Promise} + */ +const cozyFetchJSON = function(cozy, method, path, body) { + return fetchJSON(method, path, body).then( + json => { + const responseData = Object.assign({}, json.data) + if (responseData.id) { + responseData._id = responseData.id + } + return responseData + } + ) +} + /** * Initializes the functions to call the cozy stack * @@ -333,7 +356,8 @@ export default { iconProps: getAppIconProps, cozyURL: getCozyURLOrigin }, - updateAccessToken, + updateAccessToken, + cozyFetchJSON, logout, init  } \ No newline at end of file diff --git a/test/lib/stack-client/stack-client.cozyfetchjson.spec.js b/test/lib/stack-client/stack-client.cozyfetchjson.spec.js new file mode 100644 index 000000000..d9a47e8f3 --- /dev/null +++ b/test/lib/stack-client/stack-client.cozyfetchjson.spec.js @@ -0,0 +1,49 @@ +import stack from 'lib/stack-client' + +describe("stack client", () => { + + describe("cozyFetchJSON", () => { + + let json = jest.fn().mockReturnValue({ + data: { id: "myid" } + }) + let fetch = jest.fn().mockResolvedValue({ + status:200, + headers: { + get: (header) => 'application/json' + }, + json + }) + let cozyClient = { + getStackClient: () => { + return { + token: { token: "mytoken"}, + uri: "https://test.mycozy.cloud", + fetch + } + } + } + let params = { + cozyClient, + onCreateApp: function() {}, + onDeleteApp: function() {}, + } + + beforeAll(async () => { + await stack.init(params) + }) + + it("should transform `id` in `_id`", async () => { + const data = await stack.cozyFetchJSON('_', 'GET', '/path') + expect( data._id ).toBe('myid') + }) + + it("should call fetch from cozy-client", async () => { + fetch.mockClear() + const data = await stack.cozyFetchJSON('_', 'GET', '/path') + expect( fetch ).toHaveBeenCalled() + }) + + }) + +}) From fc2d7c13dad47527e10ddf579c84db5d1fe3e68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20D?= <15271+edas@users.noreply.github.com> Date: Sun, 17 Feb 2019 00:57:04 +0100 Subject: [PATCH 19/19] feat: Inject cozy-client - stage 12 - new client is independant * Finish the `init` function so it directly initializes the realtime * Do not initialize or reference the old internal client anymore --- src/lib/stack-client.js | 25 +++++++++++-------- .../stack-client/stack-client.init.spec.js | 24 ++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/lib/stack-client.js b/src/lib/stack-client.js index 890d5b852..208458af2 100644 --- a/src/lib/stack-client.js +++ b/src/lib/stack-client.js @@ -1,8 +1,8 @@ /* global __TARGET__ */ /* eslint-env browser */ -import internal from 'lib/stack-internal.js' import getIcon from 'lib/icon' +import initializeRealtime from 'lib/realtime' import { ForbiddenException, @@ -335,20 +335,23 @@ const cozyFetchJSON = function(cozy, method, path, body) { * @param {Function} arg.onDeleteApp * @returns {Promise} */ -const init = function(options) { - cozyClient = options.cozyClient - const legacyOptions = { - ...options, - cozyURL: getCozyURLOrigin(), - token: getStackClient().token.token - } - return internal.init(legacyOptions) +const init = function({ + cozyClient: client, + onCreateApp, + onDeleteApp +}) { + cozyClient = client + return initializeRealtime({ + getApp, + onCreateApp, + onDeleteApp, + token: getStackClient().token.token, + url: getCozyURLOrigin(), + }) } export default { - ...internal, get: { - ...internal.get, app: getApp, apps: getApps, context: withCache(getContext, {}), diff --git a/test/lib/stack-client/stack-client.init.spec.js b/test/lib/stack-client/stack-client.init.spec.js index 950cb5880..1e849e5ac 100644 --- a/test/lib/stack-client/stack-client.init.spec.js +++ b/test/lib/stack-client/stack-client.init.spec.js @@ -1,6 +1,10 @@ import stack from 'lib/stack-client' import internal from 'lib/stack-internal' +import initializeRealtime from 'lib/realtime' + +jest.mock('lib/realtime'); +initializeRealtime.mockResolvedValue(Promise.resolve()) const { init, @@ -33,23 +37,15 @@ describe("stack client", () => { jest.restoreAllMocks() }) - it("should called internal client", () => { - expect( internal.init ).toHaveBeenCalled() - }) - - it("should have set the cozy-client token", () => { - expect( internal.init.mock.calls[0][0].token ).toBe("mytoken") + it("should not called internal client", () => { + expect( internal.init ).not.toHaveBeenCalled() }) - it("should have set the cozy-client uri", () => { - expect( internal.init.mock.calls[0][0].cozyURL ).toBe("https://test.mycozy.cloud") + it("should have initialized the realtime", () => { + expect( initializeRealtime ).toHaveBeenCalled() + expect( initializeRealtime.mock.calls[0][0].token ).toBe("mytoken") + expect( initializeRealtime.mock.calls[0][0].url ).toBe("https://test.mycozy.cloud") }) - - it("should pass onCreateApp and onDeleteApp functions", () => { - expect( internal.init.mock.calls[0][0].onDeleteApp ).toBeInstanceOf(Function) - expect( internal.init.mock.calls[0][0].onCreateApp ).toBeInstanceOf(Function) - }) - }) })