diff --git a/docs/reuse-auth-and-network-settings-in-sdk-gen.md b/docs/reuse-auth-and-network-settings-in-sdk-gen.md new file mode 100644 index 00000000..7306e5d7 --- /dev/null +++ b/docs/reuse-auth-and-network-settings-in-sdk-gen.md @@ -0,0 +1,226 @@ +# Resuse Authentication and Netowrk Settings from Legacy Node SDK in SDK Gen + +This guide explains three methods that help you migrate from the legacy Box Node.js SDK to the new SDK-Gen package by reusing your existing authentication and network configuration. + +## Quick Start + +If you just want to get a working SDK-Gen client from your legacy client: + +```javascript +const BoxSDK = require('box-node-sdk').default; + +// Your existing legacy SDK setup +const sdk = BoxSDK.getPreconfiguredInstance(config); +const legacyClient = sdk.getAppAuthClient('enterprise'); + +// Get sdk gen client from legacy client +const sdkGenClient = legacyClient.getSdkGenClient(); + +// Use SDK-Gen client +const user = await sdkGenClient.users.getUserMe(); +console.log(`User , ${user.name}!`); +``` + +--- + +## getSdkGenClient() + +The easiest way to get a fully configured SDK-Gen client. Internally calls `getAuthentication()` and `getNetworkSession()`. + +### Signature + +```typescript +getSdkGenClient(options?: { + authOptions?: { tokenStorage?: TokenStorage }; + networkOptions?: { + networkClient?: NetworkClient; + retryStrategy?: RetryStrategy; + dataSanitizer?: DataSanitizer; + interceptors?: Interceptor[]; + additionalHeaders?: { [key: string]: string }; + }; +}): BoxClient +``` + +### Basic Usage + +```javascript +const BoxSDK = require('box-node-sdk').default; + +// Initialize legacy SDK +const sdk = BoxSDK.getPreconfiguredInstance(jwtConfig); +const legacyClient = sdk.getAppAuthClient('enterprise'); + +// Get SDK-Gen client +const sdkGenClient = legacyClient.getSdkGenClient(); + +// Make API calls +const user = await sdkGenClient.users.getUserMe(); +const folder = await sdkGenClient.folders.getFolderById('0'); +``` + +### With Custom Authentication and Network Settings + +```javascript +// Custom token storage +const { InMemoryTokenStorage } = require('box-node-sdk/lib/sdk-gen/box/tokenStorage/inMemoryTokenStorage'); + +const sdkGenClient = legacyClient.getSdkGenClient({ + authOptions: { + tokenStorage: new InMemoryTokenStorage(), + }, + networkOptions: { + additionalHeaders: { + 'X-Request-ID': 'request-id', + }, + }, +}); +``` + +### With Custom Retry Strategy + +```javascript +const { BoxRetryStrategy } = require('box-node-sdk/lib/sdk-gen/networking/retries'); + +const sdkGenClient = legacyClient.getSdkGenClient({ + networkOptions: { + retryStrategy: new BoxRetryStrategy({ + maxAttempts: 10, + retryBaseInterval: 5, + }), + }, +}); +``` + +--- + +## getAuthentication() + +Extracts authentication configuration from the legacy client and returns an SDK-Gen `Authentication` object. + +### Signature + +```typescript +getAuthentication(options?: { + tokenStorage?: TokenStorage +}): Authentication +``` + +### Supported Session Types + +| Legacy Session | SDK-Gen Auth Type | +|---------------|-------------------| +| `BasicAPISession` | `BoxDeveloperTokenAuth` | +| `PersistentAPISession` | `BoxOAuth` | +| `AppAuthSession` | `BoxJwtAuth` | +| `CCGAPISession` | `BoxCcgAuth` | + +### Usage + +```javascript +// Get authentication object +const auth = legacyClient.getAuthentication(); + +// Use with SDK-Gen BoxClient +const { BoxClient } = require('box-node-sdk/lib/sdk-gen/client'); +const sdkGenClient = new BoxClient({ auth }); +``` + +### With Custom Token Storage + +```javascript +const { InMemoryTokenStorage } = require('box-node-sdk/lib/sdk-gen/box/tokenStorage/inMemoryTokenStorage'); + +const customStorage = new InMemoryTokenStorage(); +const auth = legacyClient.getAuthentication({ + tokenStorage: customStorage, +}); +``` + +--- + +## getNetworkSession() + +Extracts network configuration from the legacy client and returns an SDK-Gen `NetworkSession` object. + +### Signature + +```typescript +getNetworkSession(options?: { + networkClient?: NetworkClient; + retryStrategy?: RetryStrategy; + dataSanitizer?: DataSanitizer; + interceptors?: Interceptor[]; + additionalHeaders?: { [key: string]: string }; +}): NetworkSession +``` + +### Configuration Mapping + +| Legacy Config | SDK-Gen NetworkSession | +|--------------|------------------------| +| `apiRootURL` | `baseUrls.baseUrl` | +| `uploadAPIRootURL` | `baseUrls.uploadUrl` | +| `authorizeRootURL` | `baseUrls.oauth2Url` | +| `proxy.url`, `proxy.username`, `proxy.password` | `proxyConfig` | +| `request.headers` | `additionalHeaders` | +| `numMaxRetries` | `retryStrategy.maxAttempts` | +| `retryIntervalMS` | `retryStrategy.retryBaseInterval` (converted to seconds) | +| `request.agentOptions` | `agentOptions` | + +### Usage + +```javascript +// Get network session +const networkSession = legacyClient.getNetworkSession(); + +// Use with SDK-Gen BoxClient +const { BoxClient } = require('box-node-sdk/lib/sdk-gen/client'); +const auth = legacyClient.getAuthentication(); +const sdkGenClient = new BoxClient({ auth, networkSession }); +``` + +### With Custom Headers + +```javascript +const networkSession = legacyClient.getNetworkSession({ + additionalHeaders: { + 'X-Custom-Header': 'custom-value', + 'X-Request-ID': 'tracking-123', + }, +}); +``` + +### With Custom Retry Strategy + +```javascript +const { BoxRetryStrategy } = require('box-node-sdk/lib/sdk-gen/networking/retries'); + +const networkSession = legacyClient.getNetworkSession({ + retryStrategy: new BoxRetryStrategy({ + maxAttempts: 15, + retryBaseInterval: 3, + }), +}); +``` + +### With Request Interceptors + +```javascript +const networkSession = legacyClient.getNetworkSession({ + interceptors: [ + { + beforeRequest: (options) => { + console.log('Request:', options.url); + return options; + }, + afterResponse: (response) => { + console.log('Response:', response.status); + return response; + }, + }, + ], +}); +``` + + diff --git a/src/box-client.ts b/src/box-client.ts index 3c25f4fa..b706e091 100644 --- a/src/box-client.ts +++ b/src/box-client.ts @@ -79,6 +79,30 @@ import merge from 'merge-options'; import PagingIterator from './util/paging-iterator'; const pkg = require('../package.json'); +// ------------------------------------------------------------------------------ +// SDK-Gen Imports (for getAuthentication, getNetworkSession, getSdkGenClient) +// ------------------------------------------------------------------------------ +import { BoxDeveloperTokenAuth } from './sdk-gen/box/developerTokenAuth'; +import { BoxOAuth, OAuthConfig } from './sdk-gen/box/oauth'; +import { BoxJwtAuth, JwtConfig } from './sdk-gen/box/jwtAuth'; +import { BoxCcgAuth, CcgConfig } from './sdk-gen/box/ccgAuth'; +import { InMemoryTokenStorage } from './sdk-gen/box/tokenStorage'; +import { + wrapLegacyTokenStore, + convertLegacyTokenInfoToAccessToken, +} from './util/token-storage-adapter'; +import BasicAPISession from './sessions/basic-session'; +import PersistentAPISession from './sessions/persistent-session'; +import AppAuthSession from './sessions/app-auth-session'; +import CCGAPISession from './sessions/ccg-session'; +import { NetworkSession } from './sdk-gen/networking/network'; +import { BaseUrls } from './sdk-gen/networking/baseUrls'; +import { BoxRetryStrategy } from './sdk-gen/networking/retries'; +import { BoxNetworkClient } from './sdk-gen/networking/boxNetworkClient'; +import { DataSanitizer } from './sdk-gen/internal/logging'; +import { createAgent } from './sdk-gen/internal/utils'; +import { BoxClient as SdkGenBoxClient } from './sdk-gen/client'; + // ------------------------------------------------------------------------------ // Private // ------------------------------------------------------------------------------ @@ -746,6 +770,249 @@ class BoxClient { // Create plugin and export plugin onto client. (this as any)[name] = plugin(this, options); } + + /** + * Get an sdk-gen Authentication object configured with the current client's authentication settings. + * This allows reusing authentication configuration between the legacy SDK and sdk-gen SDK. + * + * @param {Object} [options] Optional configuration + * @param {TokenStorage} [options.tokenStorage] Optional custom token storage (sdk-gen format) + * @returns {Authentication} A configured sdk-gen Authentication object + * @throws {Error} If the authentication type cannot be determined or is not supported + */ + getAuthentication(options?: { tokenStorage?: any }): any { + + let tokenStorage = options?.tokenStorage; + + if (this._session instanceof BasicAPISession) { + + if (!tokenStorage) { + tokenStorage = new InMemoryTokenStorage({ + token: { accessToken: (this._session as any)._accessToken }, + }); + } + + return new BoxDeveloperTokenAuth({ + token: (this._session as any)._accessToken, + config: { + clientId: (this._session as any)._tokenManager?._config?.clientID || '', + clientSecret: (this._session as any)._tokenManager?._config?.clientSecret || '', + }, + }); + } + + if (this._session instanceof PersistentAPISession) { + const session = this._session as any; + + if (!tokenStorage) { + if (session._tokenStore) { + tokenStorage = wrapLegacyTokenStore(session._tokenStore); + } else { + const currentToken = convertLegacyTokenInfoToAccessToken( + session._tokenInfo + ); + tokenStorage = new InMemoryTokenStorage({ + token: currentToken, + }); + } + } + + const config = new OAuthConfig({ + clientId: session._config.clientID, + clientSecret: session._config.clientSecret, + tokenStorage: tokenStorage, + }); + + return new BoxOAuth({ config }); + } + + if (this._session instanceof AppAuthSession) { + const session = this._session as any; + const appAuthConfig = session._config.appAuth; + + if (!appAuthConfig) { + throw new Error( + 'AppAuth configuration not found. Cannot create Authentication object.' + ); + } + + if (!tokenStorage) { + if (session._tokenStore) { + tokenStorage = wrapLegacyTokenStore(session._tokenStore); + } else { + const existingToken = session._tokenInfo + ? convertLegacyTokenInfoToAccessToken(session._tokenInfo) + : undefined; + tokenStorage = new InMemoryTokenStorage({ + token: existingToken, + }); + } + } + + const jwtConfig = new JwtConfig({ + clientId: session._config.clientID, + clientSecret: session._config.clientSecret, + jwtKeyId: appAuthConfig.keyID, + privateKey: appAuthConfig.privateKey, + privateKeyPassphrase: appAuthConfig.passphrase, + enterpriseId: + session._type === 'enterprise' ? session._id : undefined, + userId: session._type === 'user' ? session._id : undefined, + tokenStorage: tokenStorage, + }); + + return new BoxJwtAuth({ config: jwtConfig }); + } + if (this._session instanceof CCGAPISession) { + const session = this._session as any; + + if (!tokenStorage) { + const existingToken = session._tokenInfo + ? convertLegacyTokenInfoToAccessToken(session._tokenInfo) + : undefined; + tokenStorage = new InMemoryTokenStorage({ + token: existingToken, + }); + } + + const config = session._config; + const ccgConfig = new CcgConfig({ + clientId: config.clientID, + clientSecret: config.clientSecret, + enterpriseId: + config.boxSubjectType === 'enterprise' + ? config.boxSubjectId + : undefined, + userId: + config.boxSubjectType === 'user' ? config.boxSubjectId : undefined, + tokenStorage: tokenStorage, + }); + + return new BoxCcgAuth({ config: ccgConfig }); + } + + // Unknown session type + throw new Error( + `Unknown session type. Cannot create Authentication object from ${this._session?.constructor?.name || 'unknown session'}.` + ); + } + + /** + * Get an sdk-gen NetworkSession configured with the current SDK's network settings. + * This allows reusing network configuration between the legacy SDK and sdk-gen SDK. + * @param {Object} [options] Optional configuration for SDK-Gen-only properties + * @param {NetworkClient} [options.networkClient] Custom network client + * @param {RetryStrategy} [options.retryStrategy] Custom retry strategy + * @param {DataSanitizer} [options.dataSanitizer] Custom data sanitizer + * @param {Interceptor[]} [options.interceptors] Request interceptors + * @param {Object} [options.additionalHeaders] Additional headers + * @returns {NetworkSession} Configured NetworkSession for sdk-gen + */ + getNetworkSession(options?: { + networkClient?: any; + retryStrategy?: any; + dataSanitizer?: any; + interceptors?: any[]; + additionalHeaders?: { [key: string]: string }; + }): any { + + const session = this._session as any; + const config = session._config || {}; + + // Build BaseUrls from legacy config + // Legacy authorizeRootURL is 'https://account.box.com/api' + // SDK-Gen oauth2Url should be 'https://account.box.com/api/oauth2' + let oauth2Url = 'https://account.box.com/api/oauth2'; + if (config.authorizeRootURL) { + oauth2Url = config.authorizeRootURL.endsWith('/oauth2') + ? config.authorizeRootURL + : `${config.authorizeRootURL}/oauth2`; + } + + const baseUrls = new BaseUrls({ + baseUrl: config.apiRootURL || 'https://api.box.com', + uploadUrl: config.uploadAPIRootURL || 'https://upload.box.com/api', + oauth2Url: oauth2Url, + }); + + let proxyConfig: + | { url: string; username?: string; password?: string } + | undefined; + if (config.proxy?.url) { + proxyConfig = { + url: config.proxy.url, + username: config.proxy.username || undefined, + password: config.proxy.password || undefined, + }; + } + + const legacyHeaders = config.request?.headers || {}; + const additionalHeaders = { + ...legacyHeaders, + ...(options?.additionalHeaders || {}), + }; + + const retryStrategy = + options?.retryStrategy || + new BoxRetryStrategy({ + maxAttempts: config.numMaxRetries || 5, + retryBaseInterval: (config.retryIntervalMS || 2000) / 1000, + }); + + const agentOptions = config.request?.agentOptions || { keepAlive: true }; + + return new NetworkSession({ + additionalHeaders: additionalHeaders, + baseUrls: baseUrls, + interceptors: options?.interceptors || [], + agent: createAgent(agentOptions, proxyConfig), + agentOptions: agentOptions, + proxyConfig: proxyConfig, + networkClient: options?.networkClient || new BoxNetworkClient({}), + retryStrategy: retryStrategy, + dataSanitizer: options?.dataSanitizer || new DataSanitizer({}), + }); + } + + /** + * Get a fully configured sdk-gen BoxClient that shares authentication and + * network settings with this legacy client. + * + * @param {Object} [options] Optional configuration + * @param {Object} [options.authOptions] Options to pass to getAuthentication() + * @param {TokenStorage} [options.authOptions.tokenStorage] Custom token storage + * @param {Object} [options.networkOptions] Options to pass to getNetworkSession() + * @param {NetworkClient} [options.networkOptions.networkClient] Custom network client + * @param {RetryStrategy} [options.networkOptions.retryStrategy] Custom retry strategy + * @param {DataSanitizer} [options.networkOptions.dataSanitizer] Custom data sanitizer + * @param {Interceptor[]} [options.networkOptions.interceptors] Request interceptors + * @param {Object} [options.networkOptions.additionalHeaders] Additional headers + * @returns {BoxClient} A fully configured sdk-gen BoxClient + */ + getSdkGenClient(options?: { + authOptions?: { + tokenStorage?: any; + }; + networkOptions?: { + networkClient?: any; + retryStrategy?: any; + dataSanitizer?: any; + interceptors?: any[]; + additionalHeaders?: { [key: string]: string }; + }; + }): any { + // Get authentication using getAuthentication() method + const auth = this.getAuthentication(options?.authOptions); + + // Get network session using getNetworkSession() method + const networkSession = this.getNetworkSession(options?.networkOptions); + + // Create and return the fully configured sdk-gen BoxClient + return new SdkGenBoxClient({ + auth: auth, + networkSession: networkSession, + }); + } } // ------------------------------------------------------------------------------ diff --git a/src/util/token-storage-adapter.ts b/src/util/token-storage-adapter.ts new file mode 100644 index 00000000..e07b7da3 --- /dev/null +++ b/src/util/token-storage-adapter.ts @@ -0,0 +1,145 @@ +/** + * @fileoverview Token Storage Adapter + * + * Provides the method to convert between: + * 1. Legacy TokenStore (callback-based) to SDK-Gen TokenStorage (promise-based) + * 2. Legacy TokenInfo structure to sdk-gen AccessToken structure + */ + +import { AccessToken } from '../sdk-gen/schemas/accessToken'; +import { TokenStorage } from '../sdk-gen/box/tokenStorage'; + +export interface LegacyTokenInfo { + accessToken: string; + refreshToken?: string; + accessTokenTTLMS: number; + acquiredAtMS: number; +} + +export interface LegacyTokenStore { + read(callback: (err: Error | null, tokenInfo: LegacyTokenInfo | null) => void): void; + write(tokenInfo: LegacyTokenInfo, callback: (err: Error | null) => void): void; + clear(callback: (err: Error | null) => void): void; +} + +export function convertLegacyTokenInfoToAccessToken( + legacyTokenInfo: LegacyTokenInfo | null +): AccessToken | undefined { + if (!legacyTokenInfo) { + return undefined; + } + + // Calculate expiresIn (seconds) from TTL and acquired time + const currentTimeMS = Date.now(); + const expiresAtMS = legacyTokenInfo.acquiredAtMS + legacyTokenInfo.accessTokenTTLMS; + const remainingMS = expiresAtMS - currentTimeMS; + const expiresInSeconds = Math.max(0, Math.floor(remainingMS / 1000)); + + return { + accessToken: legacyTokenInfo.accessToken, + refreshToken: legacyTokenInfo.refreshToken, + expiresIn: expiresInSeconds, + tokenType: 'bearer', + }; +} + +/** + * Convert sdk-gen AccessToken to legacy TokenInfo + * + * @param accessToken The sdk-gen AccessToken + * @param acquiredAtMS Optional timestamp of when token was acquired (defaults to now) + */ +export function convertAccessTokenToLegacyTokenInfo( + accessToken: AccessToken | null | undefined, + acquiredAtMS?: number +): LegacyTokenInfo | null { + if (!accessToken || !accessToken.accessToken) { + return null; + } + + const acquired = acquiredAtMS || Date.now(); + // Convert expiresIn (seconds) to accessTokenTTLMS (milliseconds) + const ttlMS = (accessToken.expiresIn || 3600) * 1000; + + return { + accessToken: accessToken.accessToken, + refreshToken: accessToken.refreshToken, + accessTokenTTLMS: ttlMS, + acquiredAtMS: acquired, + }; +} + +/** + * Adapter class that wraps a legacy callback-based TokenStore + * and makes it compatible with sdk-gen's promise-based TokenStorage interface + */ +export class LegacyTokenStoreAdapter implements TokenStorage { + private legacyStore: LegacyTokenStore; + + constructor(legacyStore: LegacyTokenStore) { + this.legacyStore = legacyStore; + } + + /** + * Store an AccessToken by converting it to legacy TokenInfo format + */ + async store(token: AccessToken): Promise { + const legacyTokenInfo = convertAccessTokenToLegacyTokenInfo(token); + + if (!legacyTokenInfo) { + throw new Error('Cannot store invalid token'); + } + + return new Promise((resolve, reject) => { + this.legacyStore.write(legacyTokenInfo, (err) => { + if (err) { + reject(err); + } else { + resolve(undefined); + } + }); + }); + } + + /** + * Retrieve an AccessToken by reading legacy TokenInfo and converting it + */ + async get(): Promise { + return new Promise((resolve, reject) => { + this.legacyStore.read((err, legacyTokenInfo) => { + if (err) { + reject(err); + } else { + const accessToken = convertLegacyTokenInfoToAccessToken(legacyTokenInfo); + resolve(accessToken); + } + }); + }); + } + + /** + * Clear the token store + */ + async clear(): Promise { + return new Promise((resolve, reject) => { + this.legacyStore.clear((err) => { + if (err) { + reject(err); + } else { + resolve(undefined); + } + }); + }); + } +} + +/** + * Convenience function to wrap a legacy TokenStore as sdk-gen TokenStorage + */ +export function wrapLegacyTokenStore( + legacyStore: LegacyTokenStore +): TokenStorage { + return new LegacyTokenStoreAdapter(legacyStore); +} + + diff --git a/tests/manual-sdk/integration_test/__tests__/sdk-gen-client.test.js b/tests/manual-sdk/integration_test/__tests__/sdk-gen-client.test.js new file mode 100644 index 00000000..7aff8041 --- /dev/null +++ b/tests/manual-sdk/integration_test/__tests__/sdk-gen-client.test.js @@ -0,0 +1,149 @@ +'use strict'; + +/** + * @fileoverview Integration tests for getSdkGenClient() + * + * These tests verify that the SDK-Gen client created via getSdkGenClient() + * properly reuses authentication and network settings from the legacy SDK + * and can successfully make API calls. + */ + +const path = require('path'); +const uuid = require('uuid'); +const { getAppClient, getUserClient } = require('../context'); +const { createBoxTestFile } = require('../objects/box-test-file'); +const { createBoxTestFolder } = require('../objects/box-test-folder'); +const { + createBoxTestUser, + clearUserContent, +} = require('../objects/box-test-user'); + +const context = {}; + +beforeAll(async () => { + let appClient = getAppClient(); + let user = await createBoxTestUser(appClient); + let userClient = getUserClient(user.id); + let folder = await createBoxTestFolder(userClient); + + context.user = user; + context.appClient = appClient; + context.legacyClient = userClient; + context.folder = folder; + + // Create SDK-Gen client from legacy client + context.sdkGenClient = userClient.getSdkGenClient(); +}); + +afterAll(async () => { + await context.folder.dispose(); + await clearUserContent(context.legacyClient); + await context.user.dispose(); + context.folder = null; + context.user = null; + context.legacyClient = null; + context.sdkGenClient = null; +}); + +describe('getSdkGenClient() Integration Tests', () => { + test('SDK-Gen client should be created successfully', () => { + expect(context.sdkGenClient).toBeDefined(); + expect(context.sdkGenClient.users).toBeDefined(); + expect(context.sdkGenClient.folders).toBeDefined(); + expect(context.sdkGenClient.files).toBeDefined(); + }); + + test('SDK-Gen client should have auth configured', () => { + expect(context.sdkGenClient.auth).toBeDefined(); + expect(typeof context.sdkGenClient.auth.retrieveToken).toBe('function'); + }); + + test('SDK-Gen client should have networkSession configured', () => { + expect(context.sdkGenClient.networkSession).toBeDefined(); + expect(context.sdkGenClient.networkSession.baseUrls).toBeDefined(); + }); + + test('SDK-Gen client can get current user', async () => { + const user = await context.sdkGenClient.users.getUserMe(); + + expect(user).toBeDefined(); + expect(user.id).toBe(context.user.id); + expect(user.type).toBe('user'); + }); + + test('Legacy and SDK-Gen clients return same user', async () => { + const legacyUser = await context.legacyClient.users.get('me'); + const sdkGenUser = await context.sdkGenClient.users.getUserMe(); + + expect(legacyUser.id).toBe(sdkGenUser.id); + expect(legacyUser.login).toBe(sdkGenUser.login); + expect(legacyUser.name).toBe(sdkGenUser.name); + }); + + test('SDK-Gen client can get folder by ID', async () => { + const folder = await context.sdkGenClient.folders.getFolderById( + context.folder.id + ); + + expect(folder).toBeDefined(); + expect(folder.id).toBe(context.folder.id); + expect(folder.type).toBe('folder'); + }); + + test('SDK-Gen client can upload and download file', async () => { + // Create a test file using legacy client helper + const testFile = await createBoxTestFile( + context.legacyClient, + path.join(__dirname, '../resources/blank.pdf'), + `sdk-gen-test-${uuid.v4()}.pdf`, + context.folder.id + ); + + try { + const fileInfo = await context.sdkGenClient.files.getFileById( + testFile.id + ); + + expect(fileInfo).toBeDefined(); + expect(fileInfo.id).toBe(testFile.id); + expect(fileInfo.type).toBe('file'); + expect(fileInfo.name).toContain('sdk-gen-test-'); + } finally { + await context.legacyClient.files.delete(testFile.id); + } + }); + + test('Multiple SDK-Gen clients from same legacy client work independently', async () => { + const sdkGenClient1 = context.legacyClient.getSdkGenClient(); + const sdkGenClient2 = context.legacyClient.getSdkGenClient(); + + // They should be different instances + expect(sdkGenClient1).not.toBe(sdkGenClient2); + + // But both should work + const user1 = await sdkGenClient1.users.getUserMe(); + const user2 = await sdkGenClient2.users.getUserMe(); + + expect(user1.id).toBe(user2.id); + expect(user1.id).toBe(context.user.id); + }); + + test('SDK-Gen client with custom headers works', async () => { + const sdkGenClientWithHeaders = context.legacyClient.getSdkGenClient({ + networkOptions: { + additionalHeaders: { + 'X-Box-Test-Header': 'integration-test-value', + }, + }, + }); + + // Should still work with custom headers + const user = await sdkGenClientWithHeaders.users.getUserMe(); + + expect(user).toBeDefined(); + expect(user.id).toBe(context.user.id); + }); + +}); + + diff --git a/tests/manual-sdk/lib/get-sdk-gen-client-test.js b/tests/manual-sdk/lib/get-sdk-gen-client-test.js new file mode 100644 index 00000000..a4793dc9 --- /dev/null +++ b/tests/manual-sdk/lib/get-sdk-gen-client-test.js @@ -0,0 +1,756 @@ +/** + * @fileoverview Unit tests for getSdkGenClient() method + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ +var assert = require('chai').assert, + sinon = require('sinon'), + mockery = require('mockery'), + leche = require('leche'); + +var TokenManager = require('@/lib/token-manager').default, + Config = require('@/lib/util/config').default; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ +var sandbox = sinon.createSandbox(), + BasicClient, + BasicAPISession, + PersistentAPISession, + AppAuthSession, + CCGAPISession, + basicClient; + +var MODULE_FILE_PATH = '@/lib/box-client'; +var BASIC_SESSION_PATH = '@/lib/sessions/basic-session'; +var PERSISTENT_SESSION_PATH = '@/lib/sessions/persistent-session'; +var APP_AUTH_SESSION_PATH = '@/lib/sessions/app-auth-session'; +var CCG_SESSION_PATH = '@/lib/sessions/ccg-session'; + +// Default config for tests +var defaultParams = { + clientID: 'abc123', + clientSecret: 'xyz456', + apiRootURL: 'https://api.box.com', + uploadAPIRootURL: 'https://upload.box.com/api', + authorizeRootURL: 'https://account.box.com/api', + apiVersion: '2.0', + numMaxRetries: 5, + retryIntervalMS: 2000, + request: { + headers: { + 'User-Agent': 'Box Node SDK', + }, + agentOptions: { + keepAlive: true, + }, + }, +}; + +// Config with app auth (JWT) +var jwtParams = { + ...defaultParams, + appAuth: { + keyID: 'key123', + privateKey: + '-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----', + passphrase: 'password', + }, + enterpriseID: 'ent123', +}; + +// Config for CCG +var ccgParams = { + ...defaultParams, + boxSubjectType: 'enterprise', + boxSubjectId: 'ent456', +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +describe('getSdkGenClient()', function () { + var tokenManagerFake, requestManagerFake; + + // Load modules once before all tests (faster CI) + before(function () { + mockery.enable({ + warnOnUnregistered: false, + }); + + mockery.registerAllowable(MODULE_FILE_PATH, true); + mockery.registerAllowable(BASIC_SESSION_PATH, true); + mockery.registerAllowable(PERSISTENT_SESSION_PATH, true); + mockery.registerAllowable(APP_AUTH_SESSION_PATH, true); + mockery.registerAllowable(CCG_SESSION_PATH, true); + + BasicClient = require(MODULE_FILE_PATH).default; + BasicAPISession = require(BASIC_SESSION_PATH).default; + PersistentAPISession = require(PERSISTENT_SESSION_PATH).default; + AppAuthSession = require(APP_AUTH_SESSION_PATH).default; + CCGAPISession = require(CCG_SESSION_PATH).default; + }); + + after(function () { + mockery.deregisterAll(); + mockery.disable(); + }); + + // Reset fakes before each test + beforeEach(function () { + tokenManagerFake = leche.fake(TokenManager.prototype); + requestManagerFake = {}; + }); + + afterEach(function () { + sandbox.verifyAndRestore(); + }); + + describe('Basic SDK-Gen client creation (Developer token)', function () { + it('should return an SDK-Gen BoxClient instance for BasicAPISession', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + assert.isObject(sdkGenClient); + // Verify auth and networkSession are configured + assert.property(sdkGenClient, 'auth'); + assert.property(sdkGenClient, 'networkSession'); + // Verify resource managers are present + assert.property(sdkGenClient, 'users'); + assert.property(sdkGenClient, 'folders'); + assert.property(sdkGenClient, 'files'); + }); + + it('should return an SDK-Gen BoxClient instance for PersistentAPISession', function () { + var config = new Config(defaultParams); + var tokenInfo = { + accessToken: 'oauth-access-token', + refreshToken: 'oauth-refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + assert.isObject(sdkGenClient); + assert.property(sdkGenClient, 'auth'); + assert.property(sdkGenClient, 'networkSession'); + assert.property(sdkGenClient, 'users'); + assert.property(sdkGenClient, 'folders'); + assert.property(sdkGenClient, 'files'); + }); + + it('should return an SDK-Gen BoxClient instance for AppAuthSession', function () { + var config = new Config(jwtParams); + var session = new AppAuthSession( + 'enterprise', + 'ent123', + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + assert.isObject(sdkGenClient); + assert.property(sdkGenClient, 'auth'); + assert.property(sdkGenClient, 'networkSession'); + assert.property(sdkGenClient, 'users'); + assert.property(sdkGenClient, 'folders'); + assert.property(sdkGenClient, 'files'); + }); + + it('should return an SDK-Gen BoxClient instance for CCGAPISession', function () { + var config = new Config(ccgParams); + var session = new CCGAPISession(config, tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + assert.isObject(sdkGenClient); + assert.property(sdkGenClient, 'auth'); + assert.property(sdkGenClient, 'networkSession'); + assert.property(sdkGenClient, 'users'); + assert.property(sdkGenClient, 'folders'); + assert.property(sdkGenClient, 'files'); + }); + }); + + describe('Integration with getAuthentication()', function () { + it('should call getAuthentication() internally', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getAuthSpy = sandbox.spy(basicClient, 'getAuthentication'); + + basicClient.getSdkGenClient(); + + assert.isTrue(getAuthSpy.calledOnce); + }); + + it('should pass authOptions to getAuthentication()', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getAuthSpy = sandbox.spy(basicClient, 'getAuthentication'); + + var customTokenStorage = { + store: function () {}, + get: function () {}, + clear: function () {}, + }; + + basicClient.getSdkGenClient({ + authOptions: { + tokenStorage: customTokenStorage, + }, + }); + + assert.isTrue(getAuthSpy.calledOnce); + assert.deepEqual(getAuthSpy.firstCall.args[0], { + tokenStorage: customTokenStorage, + }); + }); + + it('should call getAuthentication() with undefined when no authOptions provided', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getAuthSpy = sandbox.spy(basicClient, 'getAuthentication'); + + basicClient.getSdkGenClient(); + + assert.isTrue(getAuthSpy.calledOnce); + assert.isUndefined(getAuthSpy.firstCall.args[0]); + }); + }); + + describe('Integration with getNetworkSession()', function () { + it('should call getNetworkSession() internally', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getNetworkSpy = sandbox.spy(basicClient, 'getNetworkSession'); + + basicClient.getSdkGenClient(); + + assert.isTrue(getNetworkSpy.calledOnce); + }); + + it('should pass networkOptions to getNetworkSession()', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getNetworkSpy = sandbox.spy(basicClient, 'getNetworkSession'); + + var customHeaders = { 'X-Custom': 'value' }; + + basicClient.getSdkGenClient({ + networkOptions: { + additionalHeaders: customHeaders, + }, + }); + + assert.isTrue(getNetworkSpy.calledOnce); + assert.deepEqual(getNetworkSpy.firstCall.args[0], { + additionalHeaders: customHeaders, + }); + }); + + it('should call getNetworkSession() with undefined when no networkOptions provided', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getNetworkSpy = sandbox.spy(basicClient, 'getNetworkSession'); + + basicClient.getSdkGenClient(); + + assert.isTrue(getNetworkSpy.calledOnce); + assert.isUndefined(getNetworkSpy.firstCall.args[0]); + }); + + it('should pass custom retry strategy through networkOptions', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getNetworkSpy = sandbox.spy(basicClient, 'getNetworkSession'); + + var customRetryStrategy = { + maxAttempts: 10, + shouldRetry: function () { + return Promise.resolve(true); + }, + retryAfter: function () { + return 5; + }, + }; + + basicClient.getSdkGenClient({ + networkOptions: { + retryStrategy: customRetryStrategy, + }, + }); + + assert.isTrue(getNetworkSpy.calledOnce); + assert.strictEqual( + getNetworkSpy.firstCall.args[0].retryStrategy, + customRetryStrategy + ); + }); + }); + + describe('Combined options', function () { + it('should pass both authOptions and networkOptions when both provided', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var getAuthSpy = sandbox.spy(basicClient, 'getAuthentication'); + var getNetworkSpy = sandbox.spy(basicClient, 'getNetworkSession'); + + var customTokenStorage = { + store: function () {}, + get: function () {}, + clear: function () {}, + }; + var customHeaders = { 'X-Custom': 'value' }; + + basicClient.getSdkGenClient({ + authOptions: { + tokenStorage: customTokenStorage, + }, + networkOptions: { + additionalHeaders: customHeaders, + }, + }); + + assert.isTrue(getAuthSpy.calledOnce); + assert.isTrue(getNetworkSpy.calledOnce); + + assert.deepEqual(getAuthSpy.firstCall.args[0], { + tokenStorage: customTokenStorage, + }); + + assert.deepEqual(getNetworkSpy.firstCall.args[0], { + additionalHeaders: customHeaders, + }); + }); + }); + + describe('Multiple calls', function () { + it('should create independent SDK-Gen clients on each call', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient1 = basicClient.getSdkGenClient(); + var sdkGenClient2 = basicClient.getSdkGenClient(); + + assert.notStrictEqual(sdkGenClient1, sdkGenClient2); + }); + + it('should allow different options for each call', function () { + var config = new Config(defaultParams); + var session = new BasicAPISession('test-token', tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + // First call with custom headers + var sdkGenClient1 = basicClient.getSdkGenClient({ + networkOptions: { + additionalHeaders: { 'X-Header-1': 'value1' }, + }, + }); + + // Second call with different headers + var sdkGenClient2 = basicClient.getSdkGenClient({ + networkOptions: { + additionalHeaders: { 'X-Header-2': 'value2' }, + }, + }); + + assert.notStrictEqual(sdkGenClient1, sdkGenClient2); + }); + }); + + describe('Configuration transfer from legacy to SDK-Gen', function () { + it('should transfer base URLs from legacy config to SDK-Gen networkSession', function () { + var config = new Config(defaultParams); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify base URLs match the ones from legacy sdk config + assert.equal( + sdkGenClient.networkSession.baseUrls.baseUrl, + defaultParams.apiRootURL + ); + assert.equal( + sdkGenClient.networkSession.baseUrls.uploadUrl, + defaultParams.uploadAPIRootURL + ); + }); + + it('should transfer retry strategy settings from legacy config to SDK-Gen networkSession', function () { + var config = new Config(defaultParams); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify retry settings match legacy config + assert.equal( + sdkGenClient.networkSession.retryStrategy.maxAttempts, + defaultParams.numMaxRetries + ); + assert.equal( + sdkGenClient.networkSession.retryStrategy.retryBaseInterval, + defaultParams.retryIntervalMS/1000 + ); + }); + + it('should transfer headers from legacy config to SDK-Gen networkSession', function () { + var config = new Config(defaultParams); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify headers from legacy config are transferred + assert.equal( + sdkGenClient.networkSession.additionalHeaders['User-Agent'], + defaultParams.request.headers['User-Agent'] + ); + }); + + it('should transfer client credentials from legacy to SDK-Gen auth (OAuth)', function () { + var config = new Config(defaultParams); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify auth has the correct client credentials + assert.equal(sdkGenClient.auth.config.clientId, defaultParams.clientID); + assert.equal( + sdkGenClient.auth.config.clientSecret, + defaultParams.clientSecret + ); + }); + + it('should transfer JWT config from legacy to SDK-Gen auth (JWT)', function () { + var config = new Config(jwtParams); + var session = new AppAuthSession( + 'enterprise', + 'ent123', + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify JWT auth has correct config + assert.equal(sdkGenClient.auth.config.clientId, jwtParams.clientID); + assert.equal(sdkGenClient.auth.config.jwtKeyId, jwtParams.appAuth.keyID); + assert.equal(sdkGenClient.auth.config.enterpriseId, 'ent123'); + }); + + it('should transfer CCG config from legacy to SDK-Gen auth (CCG)', function () { + var config = new Config(ccgParams); + var session = new CCGAPISession(config, tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify CCG auth has correct config + assert.equal(sdkGenClient.auth.config.clientId, ccgParams.clientID); + assert.equal( + sdkGenClient.auth.config.enterpriseId, + ccgParams.boxSubjectId + ); + }); + + it('should transfer proxy config from legacy to SDK-Gen networkSession', function () { + var configWithProxy = { + ...defaultParams, + proxy: { + url: 'http://proxy.example.com:8080', + username: 'proxyuser', + password: 'proxypass', + }, + }; + var config = new Config(configWithProxy); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify proxy config is transferred + assert.isObject(sdkGenClient.networkSession.proxyConfig); + assert.equal( + sdkGenClient.networkSession.proxyConfig.url, + 'http://proxy.example.com:8080' + ); + assert.equal( + sdkGenClient.networkSession.proxyConfig.username, + 'proxyuser' + ); + assert.equal( + sdkGenClient.networkSession.proxyConfig.password, + 'proxypass' + ); + }); + + it('should transfer custom URLs from legacy to SDK-Gen networkSession', function () { + var customUrlConfig = { + ...defaultParams, + apiRootURL: 'https://custom-api.box.com', + uploadAPIRootURL: 'https://custom-upload.box.com/api', + authorizeRootURL: 'https://custom-account.box.com/api', + }; + var config = new Config(customUrlConfig); + var tokenInfo = { + accessToken: 'oauth-token', + refreshToken: 'refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Verify custom URLs are transferred + assert.equal( + sdkGenClient.networkSession.baseUrls.baseUrl, + 'https://custom-api.box.com' + ); + assert.equal( + sdkGenClient.networkSession.baseUrls.uploadUrl, + 'https://custom-upload.box.com/api' + ); + assert.equal( + sdkGenClient.networkSession.baseUrls.oauth2Url, + 'https://custom-account.box.com/api/oauth2' + ); + }); + }); + + describe('Token sharing between legacy and SDK-Gen', function () { + it('should share access token from legacy OAuth session to SDK-Gen auth', async function () { + var config = new Config(defaultParams); + var legacyAccessToken = 'shared-oauth-access-token-12345'; + var tokenInfo = { + accessToken: legacyAccessToken, + refreshToken: 'shared-refresh-token', + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Get token from SDK-Gen auth's token storage + var sdkGenToken = await sdkGenClient.auth.config.tokenStorage.get(); + + assert.isObject(sdkGenToken); + assert.equal(sdkGenToken.accessToken, legacyAccessToken); + }); + + it('should share access token from legacy BasicAPISession to SDK-Gen auth', function () { + var config = new Config(defaultParams); + var legacyAccessToken = 'shared-developer-token'; + var session = new BasicAPISession(legacyAccessToken, tokenManagerFake); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + assert.equal(sdkGenClient.auth.token, legacyAccessToken); + }); + + it('should share access token from legacy JWT session to SDK-Gen auth', async function () { + var config = new Config(jwtParams); + var session = new AppAuthSession( + 'enterprise', + 'ent123', + config, + tokenManagerFake + ); + var legacyAccessToken = 'shared-jwt-access-token'; + session._tokenInfo = { + accessToken: legacyAccessToken, + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Get token from SDK-Gen auth's token storage + var sdkGenToken = await sdkGenClient.auth.config.tokenStorage.get(); + + assert.isObject(sdkGenToken); + assert.equal(sdkGenToken.accessToken, legacyAccessToken); + }); + + it('should share access token from legacy CCG session to SDK-Gen auth', async function () { + var config = new Config(ccgParams); + var session = new CCGAPISession(config, tokenManagerFake); + var legacyAccessToken = 'shared-ccg-access-token'; + session._tokenInfo = { + accessToken: legacyAccessToken, + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Get token from SDK-Gen auth's token storage + var sdkGenToken = await sdkGenClient.auth.config.tokenStorage.get(); + assert.isObject(sdkGenToken); + assert.equal(sdkGenToken.accessToken, legacyAccessToken); + }); + + it('should share refresh token from legacy OAuth session to SDK-Gen auth', async function () { + var config = new Config(defaultParams); + var legacyRefreshToken = 'shared-refresh-token'; + var tokenInfo = { + accessToken: 'access-token', + refreshToken: legacyRefreshToken, + accessTokenTTLMS: 3600000, + acquiredAtMS: Date.now(), + }; + var session = new PersistentAPISession( + tokenInfo, + null, + config, + tokenManagerFake + ); + + basicClient = new BasicClient(session, config, requestManagerFake); + + var sdkGenClient = basicClient.getSdkGenClient(); + + // Get token from SDK-Gen auth's token storage + var sdkGenToken = await sdkGenClient.auth.config.tokenStorage.get(); + assert.isObject(sdkGenToken); + assert.equal(sdkGenToken.refreshToken, legacyRefreshToken); + }); + }); +});