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',
+ );
+ });
+ });
+ });
+ });
+ });
});