diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index bc80080fd..448b5786c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -124,7 +124,7 @@ startup and before any calls to `fromSharedOptions()` are made. | [options.deviceUrlsBase] | String | 'balena-devices.com' | the base balena device API url to use. | | [options.requestLimit] | Number | | the number of requests per requestLimitInterval that the SDK should respect. | | [options.requestLimitInterval] | Number | 60000 | the timespan that the requestLimit should apply to in milliseconds, defaults to 60000 (1 minute). | -| [options.dataDirectory] | String | '$HOME/.balena' | *ignored in the browser*, the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. | +| [options.dataDirectory] | String \| False | '$HOME/.balena' | *ignored in the browser unless false*, the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. Providing `false` creates an isolated in-memory instance. | | [options.isBrowser] | Boolean | | the flag to tell if the module works in the browser. If not set will be computed based on the presence of the global `window` value. | | [options.debug] | Boolean | | when set will print some extra debug information. | diff --git a/README.md b/README.md index a53cf06df..e27d9ff8e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Where the factory method accepts the following options: * `apiUrl`, string, *optional*, is the balena API url. Defaults to `https://api.balena-cloud.com/`, * `builderUrl`, string, *optional* , is the balena builder url. Defaults to `https://builder.balena-cloud.com/`, * `deviceUrlsBase`, string, *optional*, is the base balena device API url. Defaults to `balena-devices.com`, -* `dataDirectory`, string, *optional*, *ignored in the browser*, is the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. Defaults to `$HOME/.balena`, +* `dataDirectory`, string or false, *optional*, *ignored in the browser unless false*, specifies the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. Providing `false` creates an isolated in-memory instance. Defaults to `$HOME/.balena`, * `isBrowser`, boolean, *optional*, is the flag to tell if the module works in the browser. If not set will be computed based on the presence of the global `window` value, * `debug`, boolean, *optional*, when set will print some extra debug information. diff --git a/package.json b/package.json index aec989470..8f5cf9903 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@types/json-schema": "^7.0.9", "@types/node": "^14.0.0", "abortcontroller-polyfill": "^1.7.1", - "balena-auth": "^5.0.0", + "balena-auth": "^5.1.0", "balena-errors": "^4.8.0", "balena-hup-action-utils": "~5.0.0", "balena-register-device": "^8.0.7", diff --git a/src/index.ts b/src/index.ts index a7ade0143..2669b89d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,7 +120,7 @@ export interface SdkOptions { apiUrl?: string; builderUrl?: string; dashboardUrl?: string; - dataDirectory?: string; + dataDirectory?: string | false; isBrowser?: boolean; debug?: boolean; deviceUrlsBase?: string; @@ -496,7 +496,7 @@ export const getSdk = function ($opts?: SdkOptions) { * @param {String} [options.deviceUrlsBase='balena-devices.com'] - the base balena device API url to use. * @param {Number} [options.requestLimit] - the number of requests per requestLimitInterval that the SDK should respect. * @param {Number} [options.requestLimitInterval = 60000] - the timespan that the requestLimit should apply to in milliseconds, defaults to 60000 (1 minute). - * @param {String} [options.dataDirectory='$HOME/.balena'] - *ignored in the browser*, the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. + * @param {String|False} [options.dataDirectory='$HOME/.balena'] - *ignored in the browser unless false*, the directory where the user settings are stored, normally retrieved like `require('balena-settings-client').get('dataDirectory')`. Providing `false` creates an isolated in-memory instance. * @param {Boolean} [options.isBrowser] - the flag to tell if the module works in the browser. If not set will be computed based on the presence of the global `window` value. * @param {Boolean} [options.debug] - when set will print some extra debug information. * diff --git a/tests/integration/balena.spec.ts b/tests/integration/balena.spec.ts index 951b0d9cb..c3dfd920e 100644 --- a/tests/integration/balena.spec.ts +++ b/tests/integration/balena.spec.ts @@ -7,6 +7,8 @@ import { getSdk, sdkOpts, givenLoggedInUser, + credentials, + givenAnApplication, } from './setup'; import { timeSuite } from '../util'; @@ -19,39 +21,42 @@ describe('Balena SDK', function () { const validKeys = ['auth', 'models', 'logs', 'settings', 'version']; describe('factory function', function () { - describe('given no opts', () => + describe('given no opts', () => { it('should return an object with valid keys', function () { const mockBalena = getSdk(); - return expect(mockBalena).to.include.keys(validKeys); - })); + expect(mockBalena).to.include.keys(validKeys); + }); + }); - describe('given empty opts', () => + describe('given empty opts', () => { it('should return an object with valid keys', function () { const mockBalena = getSdk({}); - return expect(mockBalena).to.include.keys(validKeys); - })); + expect(mockBalena).to.include.keys(validKeys); + }); + }); - describe('given opts', () => + describe('given opts', () => { it('should return an object with valid keys', function () { const mockBalena = getSdk(sdkOpts); - return expect(mockBalena).to.include.keys(validKeys); - })); + expect(mockBalena).to.include.keys(validKeys); + }); + }); - describe('version', () => + describe('version', () => { it('should match the package.json version', function () { const mockBalena = getSdk(); - return expect(mockBalena).to.have.property( - 'version', - packageJSON.version, - ); - })); + expect(mockBalena).to.have.property('version', packageJSON.version); + }); + }); }); - it('should expose a pinejs client instance', () => - expect(balena.pine).to.exist); + it('should expose a pinejs client instance', () => { + expect(balena.pine).to.exist; + }); - it('should expose an balena-errors instance', () => - expect(balena.errors).to.exist); + it('should expose an balena-errors instance', () => { + expect(balena.errors).to.exist; + }); describe('interception Hooks', function () { let originalInterceptors: typeof balena.interceptors; @@ -340,36 +345,184 @@ describe('Balena SDK', function () { return expect(root['BALENA_SDK_SHARED_OPTIONS']).to.equal(opts); })); - describe('fromSharedOptions()', () => + describe('fromSharedOptions()', () => { it('should return an object with valid keys', function () { const mockBalena = balenaSdkExports.fromSharedOptions(); return expect(mockBalena).to.include.keys(validKeys); - })); - describe('constructor options', () => - describe('Given an apiKey', function () { + }); + }); + + describe('constructor options', () => { + describe('When initializing an SDK instance with an `apiKey` in the options', function () { givenLoggedInUser(before); - before(function () { - return balena.models.apiKey - .create('apiKey', 'apiKeyDescription') - .then((testApiKey) => { - this.testApiKey = testApiKey; - expect(this.testApiKey).to.be.a('string'); - return balena.auth.logout(); - }); + before(async function () { + const testApiKey = await balena.models.apiKey.create( + 'apiKey', + 'apiKeyDescription', + ); + this.testApiKey = testApiKey; + expect(this.testApiKey).to.be.a('string'); + await balena.auth.logout(); }); - it('should not be used in API requests', function () { + it('should not be used in API requests', async function () { expect(this.testApiKey).to.be.a('string'); - const testSdkOpts = Object.assign({}, sdkOpts, { + const testSdkOpts = { + ...sdkOpts, apiKey: this.testApiKey, - }); + }; const testSdk = getSdk(testSdkOpts); const promise = testSdk.models.apiKey.getAll({ $top: 1 }); - return expect(promise).to.be.rejected.and.eventually.have.property( + await expect(promise).to.be.rejected.and.eventually.have.property( 'code', 'BalenaNotLoggedIn', ); }); - })); + }); + }); + + describe('storage isolation', function () { + describe('given a logged in instance', function () { + givenLoggedInUser(before); + givenAnApplication(before); + + describe('creating an SDK instance with the same options', function () { + let testSdk: balenaSdk.BalenaSDK; + before(async function () { + testSdk = getSdk(sdkOpts); + }); + + describe('pine queries', async () => { + it('should be able to retrieve the user (using the key from the first instance)', async function () { + const [user] = await testSdk.pine.get({ + resource: 'user', + options: { + $select: 'username', + $filter: { + username: credentials.username, + }, + }, + }); + expect(user) + .to.be.an('object') + .and.have.property('username', credentials.username); + }); + + it('should be able to retrieve the application created by the first instance', async function () { + const apps = await testSdk.pine.get({ + resource: 'application', + options: { + $select: 'id', + $filter: { + id: this.application.id, + }, + }, + }); + expect(apps).to.have.lengthOf(1); + }); + }); + + describe('models.application.get', async () => { + it('should be able to retrieve the application created by the first instance', async function () { + const app = await testSdk.models.application.get( + this.application.id, + { + $select: 'id', + }, + ); + expect(app) + .to.be.an('object') + .and.have.property('id', this.application.id); + }); + }); + + describe('balena.auth.isLoggedIn()', async () => { + it('should return true', async function () { + expect(await testSdk.auth.isLoggedIn()).to.equal(true); + }); + }); + + describe('balena.auth.getToken()', async () => { + it('should return the same key as the first instance', async function () { + expect(await testSdk.auth.getToken()).to.equal( + await balena.auth.getToken(), + ); + }); + }); + }); + + describe('creating an SDK instance using dataDirectory: false', function () { + let testSdk: balenaSdk.BalenaSDK; + before(async function () { + testSdk = getSdk({ + ...sdkOpts, + dataDirectory: false, + }); + }); + + describe('pine queries', async () => { + it('should be unauthenticated and not be able to retrieve any user', async function () { + await expect( + testSdk.pine.get({ + resource: 'user', + options: { + $select: 'username', + $filter: { + username: credentials.username, + }, + }, + }), + ).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaNotLoggedIn', + ); + }); + + it('should be unauthenticated and not be able to retrieve the application created by the first instance', async function () { + const apps = await testSdk.pine.get({ + resource: 'application', + options: { + $select: 'id', + $filter: { + id: this.application.id, + }, + }, + }); + expect(apps).to.have.lengthOf(0); + }); + }); + + describe('models.application.get', async () => { + it('should be able to retrieve the application created by the first instance', async function () { + await expect( + testSdk.models.application.get(this.application.id, { + $select: 'id', + }), + ).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaApplicationNotFound', + ); + }); + }); + + describe('balena.auth.isLoggedIn()', async () => { + it('should return false', async function () { + expect(await testSdk.auth.isLoggedIn()).to.equal(false); + }); + }); + + describe('balena.auth.getToken()', async () => { + it('should return no key', async function () { + await expect( + testSdk.auth.getToken(), + ).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaNotLoggedIn', + ); + }); + }); + }); + }); + }); });