diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 3516c42cc..dfa679a40 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -12,11 +12,11 @@ inputs: runs: using: "composite" steps: - - name: Setup Node.js 14 + - name: Setup Node.js 16 if: ${{ env.os_value == 'ubuntu-20.04' }} uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 16 - name: Setup Node.js lts if: ${{ env.os_value != 'ubuntu-20.04' }} diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 448b5786c..407f615da 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -429,9 +429,8 @@ const sdk = fromSharedOptions(); * [.loginWithToken(authToken)](#balena.auth.loginWithToken) ⇒ Promise * [.isLoggedIn()](#balena.auth.isLoggedIn) ⇒ Promise * [.getToken()](#balena.auth.getToken) ⇒ Promise - * [.getUserId()](#balena.auth.getUserId) ⇒ Promise - * [.getUserActorId()](#balena.auth.getUserActorId) ⇒ Promise - * [.getEmail()](#balena.auth.getEmail) ⇒ Promise + * [.getUserInfo()](#balena.auth.getUserInfo) ⇒ Promise + * [.getActorId()](#balena.auth.getActorId) ⇒ Promise * [.logout()](#balena.auth.logout) ⇒ Promise * [.register(credentials)](#balena.auth.register) ⇒ Promise * [.verifyEmail(verificationPayload)](#balena.auth.verifyEmail) ⇒ Promise @@ -6483,9 +6482,8 @@ balena.models.billing.downloadInvoice(orgId, '0000').then(function(stream) { * [.loginWithToken(authToken)](#balena.auth.loginWithToken) ⇒ Promise * [.isLoggedIn()](#balena.auth.isLoggedIn) ⇒ Promise * [.getToken()](#balena.auth.getToken) ⇒ Promise - * [.getUserId()](#balena.auth.getUserId) ⇒ Promise - * [.getUserActorId()](#balena.auth.getUserActorId) ⇒ Promise - * [.getEmail()](#balena.auth.getEmail) ⇒ Promise + * [.getUserInfo()](#balena.auth.getUserInfo) ⇒ Promise + * [.getActorId()](#balena.auth.getActorId) ⇒ Promise * [.logout()](#balena.auth.logout) ⇒ Promise * [.register(credentials)](#balena.auth.register) ⇒ Promise * [.verifyEmail(verificationPayload)](#balena.auth.verifyEmail) ⇒ Promise @@ -6630,19 +6628,19 @@ balena.auth.loginWithToken(token); #### auth.whoami() ⇒ Promise -This will only work if you used [login](#balena.auth.login) to log in. +This will only work if you used [login](#balena.auth.login) or [loginWithToken](#balena.auth.loginWithToken) to log in. **Kind**: static method of [auth](#balena.auth) -**Summary**: Return current logged in username +**Summary**: Return current logged in information **Access**: public -**Fulfil**: (String\|undefined) - username, if it exists +**Fulfil**: (Object\|undefined) - actor information, if it exists **Example** ```js -balena.auth.whoami().then(function(username) { - if (!username) { +balena.auth.whoami().then(function(result) { + if (!result) { console.log('I\'m not logged in!'); } else { - console.log('My username is:', username); + console.log('My result is:', result); } }); ``` @@ -6740,49 +6738,34 @@ balena.auth.getToken().then(function(token) { console.log(token); }); ``` - + -#### auth.getUserId() ⇒ Promise +#### auth.getUserInfo() ⇒ Promise This will only work if you used [login](#balena.auth.login) to log in. **Kind**: static method of [auth](#balena.auth) -**Summary**: Get current logged in user's id +**Summary**: Get current logged in user's info **Access**: public -**Fulfil**: Number - user id +**Fulfil**: Object - user info **Example** ```js -balena.auth.getUserId().then(function(userId) { - console.log(userId); +balena.auth.getUserInfo().then(function(userInfo) { + console.log(userInfo); }); ``` - - -#### auth.getUserActorId() ⇒ Promise -This will only work if you used [login](#balena.auth.login) to log in. + -**Kind**: static method of [auth](#balena.auth) -**Summary**: Get current logged in user's actor id -**Access**: public -**Fulfil**: Number - user id -**Example** -```js -balena.auth.getUserActorId().then(function(userActorId) { - console.log(userActorId); -}); -``` - - -#### auth.getEmail() ⇒ Promise -This will only work if you used [login](#balena.auth.login) to log in. +#### auth.getActorId() ⇒ Promise +This will only work if you used [login](#balena.auth.login) or [loginWithToken](#balena.auth.loginWithToken) to log in. **Kind**: static method of [auth](#balena.auth) -**Summary**: Get current logged in user's email +**Summary**: Get current logged in actor id **Access**: public -**Fulfil**: String - user email +**Fulfil**: Number - actor id **Example** ```js -balena.auth.getEmail().then(function(email) { - console.log(email); +balena.auth.getActorId().then(function(actorId) { + console.log(actorId); }); ``` diff --git a/README.md b/README.md index e27d9ff8e..7de206652 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ $ npm install --save balena-sdk ## Platforms -We currently support NodeJS (14+) and the browser. +We currently support NodeJS (16+) and the browser. The following features are node-only: - OS image streaming download (`balena.models.os.download`), diff --git a/package.json b/package.json index 70c5821de..e32c0ae31 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "author": "Juan Cruz Viotti ", "license": "Apache-2.0", "engines": { - "node": ">=14.0" + "node": ">=16.0" }, "devDependencies": { "@balena/lint": "^6.1.1", @@ -119,7 +119,7 @@ "dependencies": { "@balena/es-version": "^1.0.0", "@types/json-schema": "^7.0.9", - "@types/node": "^14.0.0", + "@types/node": "^16.0.0", "abortcontroller-polyfill": "^1.7.1", "balena-auth": "^5.1.0", "balena-errors": "^4.8.0", diff --git a/src/auth.ts b/src/auth.ts index 1440f7ffd..a47d440e6 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -17,6 +17,7 @@ limitations under the License. import * as errors from 'balena-errors'; import memoizee from 'memoizee'; import type { InjectedDependenciesParam, InjectedOptionsParam } from '.'; +import { UserInfo, WhoamiResult } from './types/auth'; const getAuth = function ( deps: InjectedDependenciesParam, @@ -65,62 +66,55 @@ const getAuth = function ( opts, ); - interface WhoamiResult { - id: number; - username: string; - email: string; - } - - const userWhoami = async () => { + const actorWhoami = async () => { const { body } = await request.send({ method: 'GET', - url: '/user/v1/whoami', + url: '/actor/v1/whoami', baseUrl: apiUrl, }); return body; }; - const memoizedUserWhoami = memoizee(userWhoami, { + const memoizedActorWhoami = memoizee(actorWhoami, { primitive: true, promise: true, }); - const getUserDetails = async (noCache = false) => { + const getActorDetails = async (noCache = false) => { if (noCache) { - memoizedUserWhoami.clear(); + memoizedActorWhoami.clear(); } try { - return await memoizedUserWhoami(); + return await memoizedActorWhoami(); } catch (err) { throw normalizeAuthError(err); } }; /** - * @summary Return current logged in username + * @summary Return current logged in information * @name whoami * @public * @function * @memberof balena.auth * - * @description This will only work if you used {@link balena.auth.login} to log in. + * @description This will only work if you used {@link balena.auth.login} or {@link balena.auth.loginWithToken} to log in. * - * @fulfil {(String|undefined)} - username, if it exists + * @fulfil {(Object|undefined)} - actor information, if it exists * @returns {Promise} * * @example - * balena.auth.whoami().then(function(username) { - * if (!username) { + * balena.auth.whoami().then(function(result) { + * if (!result) { * console.log('I\'m not logged in!'); * } else { - * console.log('My username is:', username); + * console.log('My result is:', result); * } * }); */ - async function whoami(): Promise { + async function whoami(): Promise { try { - const userDetails = await getUserDetails(); - return userDetails?.username; + return await getActorDetails(); } catch (err) { if (err instanceof errors.BalenaNotLoggedIn) { return; @@ -203,7 +197,7 @@ const getAuth = function ( email: string; password: string; }): Promise { - memoizedUserWhoami.clear(); + memoizedActorWhoami.clear(); const token = await authenticate(credentials); await auth.setKey(token); } @@ -224,7 +218,7 @@ const getAuth = function ( * balena.auth.loginWithToken(authToken); */ function loginWithToken(authToken: string): Promise { - memoizedUserWhoami.clear(); + memoizedActorWhoami.clear(); return auth.setKey(authToken); } @@ -249,7 +243,7 @@ const getAuth = function ( */ async function isLoggedIn(): Promise { try { - await getUserDetails(true); + await getActorDetails(true); return true; } catch (err) { if ( @@ -286,75 +280,56 @@ const getAuth = function ( } /** - * @summary Get current logged in user's id - * @name getUserId + * @summary Get current logged in user's info + * @name getUserInfo * @public * @function * @memberof balena.auth * * @description This will only work if you used {@link balena.auth.login} to log in. * - * @fulfil {Number} - user id + * @fulfil {Object} - user info * @returns {Promise} * * @example - * balena.auth.getUserId().then(function(userId) { - * console.log(userId); + * balena.auth.getUserInfo().then(function(userInfo) { + * console.log(userInfo); * }); */ - async function getUserId(): Promise { - const { id } = await getUserDetails(); - return id; - } + async function getUserInfo(): Promise { + const actor = await getActorDetails(); - /** - * @summary Get current logged in user's actor id - * @name getUserActorId - * @public - * @function - * @memberof balena.auth - * - * @description This will only work if you used {@link balena.auth.login} to log in. - * - * @fulfil {Number} - user id - * @returns {Promise} - * - * @example - * balena.auth.getUserActorId().then(function(userActorId) { - * console.log(userActorId); - * }); - */ - async function getUserActorId(): Promise { - const { actor } = (await pine.get({ - resource: 'user', - id: await getUserId(), - options: { - $select: 'actor', - }, - }))!; - return actor; + if (actor.actorType !== 'user') { + throw new Error( + 'The authentication credentials in use are not of a user', + ); + } + return { + id: actor.actorTypeId, + email: actor.email, + username: actor.username, + }; } /** - * @summary Get current logged in user's email - * @name getEmail + * @summary Get current logged in actor id + * @name getActorId * @public * @function * @memberof balena.auth * - * @description This will only work if you used {@link balena.auth.login} to log in. + * @description This will only work if you used {@link balena.auth.login} or {@link balena.auth.loginWithToken} to log in. * - * @fulfil {String} - user email + * @fulfil {Number} - actor id * @returns {Promise} * * @example - * balena.auth.getEmail().then(function(email) { - * console.log(email); + * balena.auth.getActorId().then(function(actorId) { + * console.log(actorId); * }); */ - async function getEmail(): Promise { - const { email } = await getUserDetails(); - return email; + async function getActorId(): Promise { + return (await getActorDetails()).id; } /** @@ -370,7 +345,7 @@ const getAuth = function ( * balena.auth.logout(); */ function logout(): Promise { - memoizedUserWhoami.clear(); + memoizedActorWhoami.clear(); return auth.removeKey(); } @@ -474,7 +449,7 @@ const getAuth = function ( * */ async function requestVerificationEmail() { - const id = await getUserId(); + const { id } = await getUserInfo(); await pine.patch({ resource: 'user', id, @@ -492,9 +467,8 @@ const getAuth = function ( loginWithToken, isLoggedIn, getToken, - getUserId, - getUserActorId, - getEmail, + getActorId, + getUserInfo, logout, register, verifyEmail, diff --git a/src/index.ts b/src/index.ts index 4e1984a17..6c85da092 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ export * from './types/models'; export * from './types/jwt'; export * from './types/contract'; export * from './types/user-invite'; +export * from './types/auth'; export type { Interceptor }; export type { diff --git a/src/models/api-key.ts b/src/models/api-key.ts index 9c338dcfd..f41a04d21 100644 --- a/src/models/api-key.ts +++ b/src/models/api-key.ts @@ -138,7 +138,7 @@ const getApiKeysModel = function ( mergePineOptions( { $filter: { - is_of__actor: await sdkInstance.auth.getUserActorId(), + is_of__actor: await sdkInstance.auth.getActorId(), // the only way to reason whether it's // a named user api key vs a deprecated user api key // is whether it has a name. diff --git a/src/models/application.ts b/src/models/application.ts index 99b7fd725..d3e759fa8 100644 --- a/src/models/application.ts +++ b/src/models/application.ts @@ -46,7 +46,6 @@ import { withSupervisorLockedError, } from '../util'; -import { normalizeDeviceOsVersion } from '../util/device-os-version'; import { getCurrentServiceDetailsPineExpand, generateCurrentServiceDetails, @@ -185,15 +184,6 @@ const getApplicationModel = function ( } }; - const normalizeApplication = function (application: Application) { - if (Array.isArray(application.owns__device)) { - application.owns__device.forEach((device) => - normalizeDeviceOsVersion(device), - ); - } - return application; - }; - const isDirectlyAccessibleByUserFilter = { is_directly_accessible_by__user: { $any: { @@ -265,7 +255,7 @@ const getApplicationModel = function ( options ?? {}, ), }); - return apps.map(normalizeApplication); + return apps; }, /** @@ -365,7 +355,7 @@ const getApplicationModel = function ( if (application == null) { throw new errors.BalenaApplicationNotFound(slugOrUuidOrId); } - return normalizeApplication(application); + return application; }, /** @@ -517,7 +507,7 @@ const getApplicationModel = function ( throw new errors.BalenaAmbiguousApplication(appName); } const [application] = applications; - return normalizeApplication(application); + return application; }, /** @@ -560,7 +550,7 @@ const getApplicationModel = function ( if (application == null) { throw new errors.BalenaApplicationNotFound(`${owner}/${appName}`); } - return normalizeApplication(application); + return application; }, /** diff --git a/src/models/config.ts b/src/models/config.ts index dc107a652..d394d7d1c 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -73,28 +73,11 @@ const getConfigModel = function ( const { apiUrl } = opts; const normalizeDeviceTypes = ( - deviceTypes: DeviceTypeJson.DeviceType[], // Patch device types to be marked as ALPHA and BETA instead + deviceTypes: DeviceTypeJson.DeviceType[], ): DeviceTypeJson.DeviceType[] => - // of PREVIEW and EXPERIMENTAL, respectively. - // This logic is literally copy and pasted from balena UI, but - // there are plans to move this to `resin-device-types` so it - // should be a matter of time for this to be removed. deviceTypes.map(function (deviceType) { - // TODO: Drop in the next major the `deviceType.name.replace`s - if (deviceType.state === 'DISCONTINUED') { - deviceType.name = deviceType.name.replace( - /(\(PREVIEW|EXPERIMENTAL\))/, - '(DISCONTINUED)', - ); - } - if (deviceType.state === 'PREVIEW') { - deviceType.state = 'ALPHA'; - deviceType.name = deviceType.name.replace('(PREVIEW)', '(ALPHA)'); - } - if (deviceType.state === 'EXPERIMENTAL') { - deviceType.state = 'NEW'; - deviceType.name = deviceType.name.replace('(EXPERIMENTAL)', '(NEW)'); - } + // Remove the device-type.json instructions to enforce + // users to use the contract based ones. delete deviceType.instructions; return deviceType; }); diff --git a/src/models/device.ts b/src/models/device.ts index 8f48a5022..e75dfb017 100644 --- a/src/models/device.ts +++ b/src/models/device.ts @@ -57,7 +57,6 @@ import { import { getDeviceOsSemverWithVariant, ensureVersionCompatibility, - normalizeDeviceOsVersion, } from '../util/device-os-version'; import { getCurrentServiceDetailsPineExpand, @@ -230,13 +229,6 @@ const getDeviceModel = function ( } }; - const addExtraInfo = function < - T extends Parameters[0], - >(device: T) { - normalizeDeviceOsVersion(device); - return device; - }; - const getAppliedConfigVariableValue = async ( uuidOrId: string | number, name: string, @@ -333,7 +325,7 @@ const getDeviceModel = function ( resource: 'device', options: mergePineOptions({ $orderby: 'device_name asc' }, options), }); - return devices.map(addExtraInfo) as Device[]; + return devices as Device[]; } async function startOsUpdate( @@ -638,7 +630,7 @@ const getDeviceModel = function ( if (device == null) { throw new errors.BalenaDeviceNotFound(uuidOrId); } - return addExtraInfo(device) as Device; + return device as Device; }, /** @@ -1370,28 +1362,29 @@ const getDeviceModel = function ( $expand: { is_for__device_type: deviceTypeOptions }, } as const; - const [userId, apiKey, application, deviceType] = await Promise.all([ - sdkInstance.auth.getUserId(), - sdkInstance.models.application.generateProvisioningKey( - applicationSlugOrUuidOrId, - ), - sdkInstance.models.application.get( - applicationSlugOrUuidOrId, - applicationOptions, - ) as Promise>, - typeof deviceTypeSlug === 'string' - ? (sdkInstance.models.deviceType.get(deviceTypeSlug, { - $select: 'slug', - $expand: { - is_of__cpu_architecture: { - $select: 'slug', + const [{ id: userId }, apiKey, application, deviceType] = + await Promise.all([ + sdkInstance.auth.getUserInfo(), + sdkInstance.models.application.generateProvisioningKey( + applicationSlugOrUuidOrId, + ), + sdkInstance.models.application.get( + applicationSlugOrUuidOrId, + applicationOptions, + ) as Promise>, + typeof deviceTypeSlug === 'string' + ? (sdkInstance.models.deviceType.get(deviceTypeSlug, { + $select: 'slug', + $expand: { + is_of__cpu_architecture: { + $select: 'slug', + }, }, - }, - }) as Promise< - PineTypedResult - >) - : null, - ]); + }) as Promise< + PineTypedResult + >) + : null, + ]); if (deviceType != null) { const isCompatibleParameter = sdkInstance.models.os.isArchitectureCompatibleWith( diff --git a/src/models/key.ts b/src/models/key.ts index a7c2de2dd..883de3a63 100644 --- a/src/models/key.ts +++ b/src/models/key.ts @@ -123,7 +123,7 @@ const getKeyModel = function (deps: InjectedDependenciesParam) { // Avoid ugly whitespaces key = key.trim(); - const userId = await sdkInstance.auth.getUserId(); + const { id: userId } = await sdkInstance.auth.getUserInfo(); return await pine.post({ resource: 'user__has__public_key', body: { diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 000000000..0f0dc257b --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,32 @@ +export interface UserKeyWhoAmIResponse { + id: number; + actorType: 'user'; + actorTypeId: number; + username: string; + email: string | null; +} + +export interface ApplicationKeyWhoAmIResponse { + id: number; + actorType: 'application'; + actorTypeId: number; + slug: string; +} + +export interface DeviceKeyWhoAmIResponse { + id: number; + actorType: 'device'; + actorTypeId: number; + uuid: string; +} + +export type WhoamiResult = + | UserKeyWhoAmIResponse + | ApplicationKeyWhoAmIResponse + | DeviceKeyWhoAmIResponse; + +export interface UserInfo { + id: number; + username: string; + email: string | null; +} diff --git a/src/util/device-os-version.ts b/src/util/device-os-version.ts index 74440b8ee..08cfce3f2 100644 --- a/src/util/device-os-version.ts +++ b/src/util/device-os-version.ts @@ -1,21 +1,5 @@ import bSemver = require('balena-semver'); import type * as BalenaSdk from '..'; -import { isProvisioned } from './device'; - -// TODO: Drop in the next major -export const normalizeDeviceOsVersion = ( - device: Partial< - Pick & Parameters[0] - >, -) => { - if ( - device.os_version != null && - device.os_version.length === 0 && - isProvisioned(device) - ) { - device.os_version = 'Resin OS 1.0.0-pre'; - } -}; export const getDeviceOsSemverWithVariant = ({ os_version, diff --git a/tests/integration/auth.spec.ts b/tests/integration/auth.spec.ts index fb2653361..044e64855 100644 --- a/tests/integration/auth.spec.ts +++ b/tests/integration/auth.spec.ts @@ -9,7 +9,14 @@ import { givenLoggedInUser, givenLoggedInUserWithApiKey, loginUserWith2FA, + givenLoggedInWithADeviceApiKey, + givenLoggedInWithAnApplicationApiKey, } from './setup'; +import { + UserKeyWhoAmIResponse, + DeviceKeyWhoAmIResponse, + ApplicationKeyWhoAmIResponse, +} from '../../src'; describe('SDK authentication', function () { timeSuite(before); @@ -82,9 +89,9 @@ describe('SDK authentication', function () { }); }); - describe('balena.auth.getEmail()', () => { + describe('balena.auth.getUserInfo()', () => { it('should be rejected with an error', async function () { - const promise = balena.auth.getEmail(); + const promise = balena.auth.getUserInfo(); await expect(promise).to.be.rejected.and.eventually.have.property( 'code', 'BalenaNotLoggedIn', @@ -92,19 +99,9 @@ describe('SDK authentication', function () { }); }); - describe('balena.auth.getUserId()', () => { + describe('balena.auth.getActorId()', () => { it('should be rejected with an error', async function () { - const promise = balena.auth.getUserId(); - await expect(promise).to.be.rejected.and.eventually.have.property( - 'code', - 'BalenaNotLoggedIn', - ); - }); - }); - - describe('balena.auth.getUserActorId()', () => { - it('should be rejected with an error', async function () { - const promise = balena.auth.getUserActorId(); + const promise = balena.auth.getActorId(); await expect(promise).to.be.rejected.and.eventually.have.property( 'code', 'BalenaNotLoggedIn', @@ -118,7 +115,7 @@ describe('SDK authentication', function () { email: credentials.register.email, password: credentials.register.password, }); - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); await balena.request.send({ method: 'DELETE', url: `/v2/user(${userId})`, @@ -198,19 +195,9 @@ describe('SDK authentication', function () { }); }); - describe('balena.auth.getEmail()', () => { - it('should be rejected with an error', async function () { - const promise = balena.auth.getEmail(); - await expect(promise).to.be.rejected.and.eventually.have.property( - 'code', - 'BalenaNotLoggedIn', - ); - }); - }); - - describe('balena.auth.getUserId()', () => { + describe('balena.auth.getUserInfo()', () => { it('should be rejected with an error', async function () { - const promise = balena.auth.getUserId(); + const promise = balena.auth.getUserInfo(); await expect(promise).to.be.rejected.and.eventually.have.property( 'code', 'BalenaNotLoggedIn', @@ -218,9 +205,9 @@ describe('SDK authentication', function () { }); }); - describe('balena.auth.getUserActorId()', () => { + describe('balena.auth.getActorId()', () => { it('should be rejected with an error', async function () { - const promise = balena.auth.getUserActorId(); + const promise = balena.auth.getActorId(); await expect(promise).to.be.rejected.and.eventually.have.property( 'code', 'BalenaNotLoggedIn', @@ -239,43 +226,103 @@ describe('SDK authentication', function () { }); describe('balena.auth.whoami()', () => { - it('should eventually be the username', async function () { - expect(await balena.auth.whoami()).to.equal(credentials.username); + it('should eventually be the user whoami response', async function () { + const whoamiResult = + (await balena.auth.whoami()) as UserKeyWhoAmIResponse; + expect(whoamiResult?.actorType).to.equal('user'); + expect(whoamiResult?.username).to.equal(credentials.username); + expect(whoamiResult?.email).to.equal(credentials.email); + expect(whoamiResult).to.have.property('id').that.is.a('number'); + expect(whoamiResult) + .to.have.property('actorTypeId') + .that.is.a('number'); }); }); - describe('balena.auth.getEmail()', () => { - it('should eventually be the email', async function () { - expect(await balena.auth.getEmail()).to.equal(credentials.email); + describe('balena.auth.getUserInfo()', () => { + it('should be rejected with an error', async function () { + const userInfo = await balena.auth.getUserInfo(); + expect(userInfo.email).to.equal(credentials.email); + expect(userInfo.username).to.equal(credentials.username); + expect(userInfo.id).to.be.a('number'); + expect(userInfo.id).to.be.greaterThan(0); }); }); - describe('balena.auth.getUserId()', () => { - it('should eventually be a user id', async () => { - const userId = await balena.auth.getUserId(); + describe('balena.auth.getActorId()', () => { + it('should eventually be an actor id', async () => { + const userId = await balena.auth.getActorId(); expect(userId).to.be.a('number'); expect(userId).to.be.greaterThan(0); }); }); - describe('balena.auth.getUserActorId()', () => { - it('should eventually be a user id', async () => { - const userId = await balena.auth.getUserActorId(); + describe('balena.auth.logout()', () => { + it('should logout the user', async () => { + await balena.auth.logout(); + expect(await balena.auth.isLoggedIn()).to.be.false; + }); + }); + }); + + describe('when logged in with a device API Key', function () { + givenLoggedInWithADeviceApiKey(before); + + describe('balena.auth.isLoggedIn()', () => { + it('should eventually be true', async function () { + expect(await balena.auth.isLoggedIn()).to.be.true; + }); + }); + + describe('balena.auth.whoami()', () => { + it('should eventually be the device whoami response', async function () { + const whoamiResult = + (await balena.auth.whoami()) as DeviceKeyWhoAmIResponse; + expect(whoamiResult?.actorType).to.equal('device'); + expect(whoamiResult).to.have.property('uuid').that.is.a('string'); + expect(whoamiResult).to.have.property('id').that.is.a('number'); + expect(whoamiResult) + .to.have.property('actorTypeId') + .that.is.a('number'); + }); + }); + + describe('balena.auth.getUserInfo()', () => { + it('should be rejected with an error', async function () { + const promise = balena.auth.getUserInfo(); + await expect(promise).to.be.rejected.and.eventually.have.property( + 'message', + 'The authentication credentials in use are not of a user', + ); + }); + }); + + describe('balena.auth.getActorId()', () => { + it('should eventually be an actor id', async () => { + const userId = await balena.auth.getActorId(); expect(userId).to.be.a('number'); expect(userId).to.be.greaterThan(0); }); }); - describe('balena.auth.logout()', () => { + describe('balena.auth.logout()', function () { it('should logout the user', async () => { await balena.auth.logout(); expect(await balena.auth.isLoggedIn()).to.be.false; }); + + it('...should reset the token on logout', async () => { + const promise = balena.auth.getToken(); + await expect(promise).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaNotLoggedIn', + ); + }); }); }); - describe('when logged in with API key', function () { - givenLoggedInUserWithApiKey(before); + describe('when logged in with an application API Key', function () { + givenLoggedInWithAnApplicationApiKey(before); describe('balena.auth.isLoggedIn()', () => { it('should eventually be true', async function () { @@ -284,28 +331,87 @@ describe('SDK authentication', function () { }); describe('balena.auth.whoami()', () => { - it('should eventually be the username', async function () { - expect(await balena.auth.whoami()).to.equal(credentials.username); + it('should eventually be the application whoami response', async function () { + const whoamiResult = + (await balena.auth.whoami()) as ApplicationKeyWhoAmIResponse; + expect(whoamiResult?.actorType).to.equal('application'); + expect(whoamiResult).to.have.property('slug').that.is.a('string'); + expect(whoamiResult).to.have.property('id').that.is.a('number'); + expect(whoamiResult) + .to.have.property('actorTypeId') + .that.is.a('number'); }); }); - describe('balena.auth.getEmail()', () => { - it('should eventually be the email', async function () { - expect(await balena.auth.getEmail()).to.equal(credentials.email); + describe('balena.auth.getUserInfo()', () => { + it('should be rejected with an error', async function () { + const promise = balena.auth.getUserInfo(); + await expect(promise).to.be.rejected.and.eventually.have.property( + 'message', + 'The authentication credentials in use are not of a user', + ); }); }); - describe('balena.auth.getUserId()', () => { - it('should eventually be a user id', async () => { - const userId = await balena.auth.getUserId(); + describe('balena.auth.getActorId()', () => { + it('should eventually be an actor id', async () => { + const userId = await balena.auth.getActorId(); expect(userId).to.be.a('number'); expect(userId).to.be.greaterThan(0); }); }); - describe('balena.auth.getUserActorId()', () => { - it('should eventually be a user id', async () => { - const userId = await balena.auth.getUserActorId(); + describe('balena.auth.logout()', function () { + it('should logout the user', async () => { + await balena.auth.logout(); + expect(await balena.auth.isLoggedIn()).to.be.false; + }); + + it('...should reset the token on logout', async () => { + const promise = balena.auth.getToken(); + await expect(promise).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaNotLoggedIn', + ); + }); + }); + }); + describe('when logged in with an user API key', function () { + givenLoggedInUserWithApiKey(before); + + describe('balena.auth.isLoggedIn()', () => { + it('should eventually be true', async function () { + expect(await balena.auth.isLoggedIn()).to.be.true; + }); + }); + + describe('balena.auth.whoami()', () => { + it('should eventually be the user whoami response', async function () { + const whoamiResult = + (await balena.auth.whoami()) as UserKeyWhoAmIResponse; + expect(whoamiResult?.actorType).to.equal('user'); + expect(whoamiResult?.username).to.equal(credentials.username); + expect(whoamiResult?.email).to.equal(credentials.email); + expect(whoamiResult).to.have.property('id').that.is.a('number'); + expect(whoamiResult) + .to.have.property('actorTypeId') + .that.is.a('number'); + }); + }); + + describe('balena.auth.getUserInfo()', () => { + it('should be rejected with an error', async function () { + const userInfo = await balena.auth.getUserInfo(); + expect(userInfo.email).to.equal(credentials.email); + expect(userInfo.username).to.equal(credentials.username); + expect(userInfo.id).to.be.a('number'); + expect(userInfo.id).to.be.greaterThan(0); + }); + }); + + describe('balena.auth.getActorId()', () => { + it('should eventually be an actor id', async () => { + const userId = await balena.auth.getActorId(); expect(userId).to.be.a('number'); expect(userId).to.be.greaterThan(0); }); diff --git a/tests/integration/models/api-key.spec.ts b/tests/integration/models/api-key.spec.ts index 512066521..b2358405d 100644 --- a/tests/integration/models/api-key.spec.ts +++ b/tests/integration/models/api-key.spec.ts @@ -61,7 +61,7 @@ describe('API Key model', function () { it('should retrieve an empty array', async function () { const apiKeys = await balena.models.apiKey.getAll({ $filter: { - is_of__actor: await balena.auth.getUserActorId(), + is_of__actor: await balena.auth.getActorId(), name: { $ne: null }, }, }); @@ -81,7 +81,7 @@ describe('API Key model', function () { it('should be able to retrieve all api keys created', async function () { const apiKeys = await balena.models.apiKey.getAll({ $filter: { - is_of__actor: await balena.auth.getUserActorId(), + is_of__actor: await balena.auth.getActorId(), name: { $ne: null }, }, }); diff --git a/tests/integration/models/application.spec.ts b/tests/integration/models/application.spec.ts index 519b70d49..ee3bb07ea 100644 --- a/tests/integration/models/application.spec.ts +++ b/tests/integration/models/application.spec.ts @@ -1122,7 +1122,7 @@ describe('Application Model', function () { describe('given two releases', function () { before(async function () { - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); this.oldRelease = await balena.pine.post({ resource: 'release', body: { @@ -1350,11 +1350,12 @@ describe('Application Model', function () { [ 'draft', async function () { + const { id: userId } = await balena.auth.getUserInfo(); this.testNonLatestRelease = await balena.pine.post({ resource: 'release', body: { belongs_to__application: this.application.id, - is_created_by__user: await balena.auth.getUserId(), + is_created_by__user: userId, commit: 'draft-release-commit', status: 'success', source: 'cloud', @@ -1368,11 +1369,12 @@ describe('Application Model', function () { [ 'invalidated', async function () { + const { id } = await balena.auth.getUserInfo(); this.testNonLatestRelease = await balena.pine.post({ resource: 'release', body: { belongs_to__application: this.application.id, - is_created_by__user: await balena.auth.getUserId(), + is_created_by__user: id, commit: 'invalidated-release-commit', status: 'success', source: 'cloud', diff --git a/tests/integration/models/config.spec.ts b/tests/integration/models/config.spec.ts index cf3181d58..687a7d2d8 100644 --- a/tests/integration/models/config.spec.ts +++ b/tests/integration/models/config.spec.ts @@ -19,47 +19,14 @@ const expectDeviceTypeArray = function ( } }; -const REPLACED_STATES = ['PREVIEW', 'EXPERIMENTAL']; - -const REPLACED_NAME_SUFFIXES = ['(PREVIEW)', '(EXPERIMENTAL)', '(BETA)']; - type ConfigContext = Mocha.Context & { deviceTypes: BalenaSdk.DeviceTypeJson.DeviceType[]; }; const itNormalizesDeviceTypes = function () { - it('changes old device type states', function (this: Mocha.Context) { - for (const deviceType of (this as ConfigContext).deviceTypes) { - expect(deviceType.state).to.satisfy((dtState: string) => - _.every(REPLACED_STATES, (replacedState) => dtState !== replacedState), - ); - } - }); - - it('changes old device type name suffixes', function (this: Mocha.Context) { - for (const deviceType of (this as ConfigContext).deviceTypes) { - expect(deviceType.name).to.satisfy((dtName: string) => - _.every( - REPLACED_NAME_SUFFIXES, - (replacedSuffix) => !_.endsWith(dtName, replacedSuffix), - ), - ); - } - }); - - it('properly replaces the names of device types with old states', function (this: Mocha.Context) { + it('should not have an `instructions` field', function (this: Mocha.Context) { for (const deviceType of (this as ConfigContext).deviceTypes) { - if (deviceType.state === 'PREVIEW') { - expect(deviceType.name).to.satisfy((dtName: string) => - _.endsWith(dtName, '(ALPHA)'), - ); - } - - if (deviceType.state === 'BETA') { - expect(deviceType.name).to.satisfy((dtName: string) => - _.endsWith(dtName, '(NEW)'), - ); - } + expect(deviceType).to.not.have.property('instructions'); } }); }; diff --git a/tests/integration/models/organization-membership.spec.ts b/tests/integration/models/organization-membership.spec.ts index 8c7a3deda..c72217079 100644 --- a/tests/integration/models/organization-membership.spec.ts +++ b/tests/integration/models/organization-membership.spec.ts @@ -43,8 +43,10 @@ describe('Organization Membership Model', function () { let ctx: Mocha.Context; before(async function () { ctx = this; - this.username = await balena.auth.whoami(); - this.userId = await balena.auth.getUserId(); + const userInfoResult = await balena.auth.getUserInfo(); + this.username = userInfoResult.username; + this.userId = userInfoResult.id; + const roles = await balena.pine.get({ resource: 'organization_membership_role', options: { $select: ['id', 'name'] }, diff --git a/tests/integration/models/release.spec.ts b/tests/integration/models/release.spec.ts index bd50cf144..a9ead85a9 100644 --- a/tests/integration/models/release.spec.ts +++ b/tests/integration/models/release.spec.ts @@ -265,7 +265,7 @@ describe('Release Model', function () { const testReleaseByField: Dictionary = {}; before(async function () { - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); await Promise.all( uniquePropertyNames.map(async (field, i) => { const fieldKey = getFieldLabel(field); @@ -633,7 +633,7 @@ describe('Release Model', function () { describe('balena.models.release.getLatestByApplication()', function () { before(async function () { ctx = this; - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); for (const body of [ { @@ -693,7 +693,7 @@ describe('Release Model', function () { describe('given two releases that share the same commit root', function () { before(async function () { const { application } = this; - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); await balena.pine.post({ resource: 'release', body: { diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 2e7e7cdbc..ff64cd6e5 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -137,7 +137,7 @@ export async function resetUser() { // only delete named user api keys options: { $filter: { - is_of__actor: await balena.auth.getUserActorId(), + is_of__actor: await balena.auth.getActorId(), name: { $ne: null, }, @@ -179,7 +179,10 @@ export function givenLoggedInUserWithApiKey(beforeFn: Mocha.HookFunction) { afterFn(() => resetUser()); } -export function givenLoggedInUser(beforeFn: Mocha.HookFunction) { +export function givenLoggedInUser( + beforeFn: Mocha.HookFunction, + forceRelogin = false, +) { beforeFn(async () => { await balena.auth.login({ email: credentials.email, @@ -189,7 +192,16 @@ export function givenLoggedInUser(beforeFn: Mocha.HookFunction) { }); const afterFn = beforeFn === beforeEach ? afterEach : after; - afterFn(() => resetUser()); + afterFn(async () => { + if (forceRelogin) { + await balena.auth.logout(); + await balena.auth.login({ + email: credentials.email, + password: credentials.password, + }); + } + return resetUser(); + }); } export function loginUserWith2FA() { @@ -207,7 +219,7 @@ export function loginPaidUser() { } async function resetInitialOrganization() { - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); const initialOrg = await getInitialOrganization(); await balena.pine.delete({ resource: 'organization_membership', @@ -349,6 +361,33 @@ export const testDeviceOsInfo = { supervisor_version: '10.8.0', }; +export function givenLoggedInWithAnApplicationApiKey( + beforeFn: Mocha.HookFunction, +) { + givenLoggedInUser(beforeFn, true); + givenAnApplication(beforeFn); + + beforeFn(async function () { + const key = await balena.models.application.generateProvisioningKey( + this.application.slug, + ); + await balena.auth.logout(); + await balena.auth.loginWithToken(key); + }); +} + +export function givenLoggedInWithADeviceApiKey(beforeFn: Mocha.HookFunction) { + givenLoggedInUser(beforeFn, true); + givenAnApplication(beforeFn); + givenADevice(beforeFn); + + beforeFn(async function () { + const key = await balena.models.device.generateDeviceKey(this.device.id); + await balena.auth.logout(); + await balena.auth.loginWithToken(key); + }); +} + export function givenADevice( beforeFn: Mocha.HookFunction, extraDeviceProps?: BalenaSdk.PineSubmitBody, @@ -459,7 +498,7 @@ export function givenMulticontainerApplication(beforeFn: Mocha.HookFunction) { givenAnApplication(beforeFn); beforeFn(async function () { - const userId = await balena.auth.getUserId(); + const { id: userId } = await balena.auth.getUserInfo(); const oldDate = new Date('2017-01-01').toISOString(); const now = new Date().toISOString(); const [webService, dbService, [oldRelease, newRelease]] = await Promise.all( diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index d21030e7d..40fcadbd1 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -2,17 +2,23 @@ import { Dictionary } from '../../typings/utils'; import { balena } from './setup'; export const getInitialOrganization = async () => { - const [org] = await balena.pine.get({ - resource: 'organization', - options: { - $select: ['id', 'handle'], - $filter: { - handle: await balena.auth.whoami(), + const whoamiResult = await balena.auth.whoami(); + + if (whoamiResult?.actorType === 'user') { + const [org] = await balena.pine.get({ + resource: 'organization', + options: { + $select: ['id', 'handle'], + $filter: { + handle: whoamiResult.username, + }, }, - }, - }); + }); + + return org; + } - return org; + throw new Error('Organization can only be filtered with user api key'); }; export const getFieldLabel = (field: string | { [key: string]: string }) =>