diff --git a/packages/test/test-drivers/src/index.ts b/packages/test/test-drivers/src/index.ts index b667ea7692d4..076a5c9949ae 100644 --- a/packages/test/test-drivers/src/index.ts +++ b/packages/test/test-drivers/src/index.ts @@ -17,7 +17,13 @@ export { OdspDriverApi, OdspDriverApiType, } from "./odspDriverApi.js"; -export { assertOdspEndpoint, getOdspCredentials, OdspTestDriver } from "./odspTestDriver.js"; +export { + assertOdspEndpoint, + getOdspCredentials, + OdspTestDriver, + TenantSetupResult, + UserPassCredentials, +} from "./odspTestDriver.js"; export { RouterliciousDriverApi, RouterliciousDriverApiType, diff --git a/packages/test/test-drivers/src/odspTestDriver.ts b/packages/test/test-drivers/src/odspTestDriver.ts index f33719f9c6c6..645eaefbdac2 100644 --- a/packages/test/test-drivers/src/odspTestDriver.ts +++ b/packages/test/test-drivers/src/odspTestDriver.ts @@ -53,27 +53,28 @@ interface IOdspTestDriverConfig extends TokenConfig { options: HostStoragePolicy | undefined; } -// specific a range of user name from to all having the same password -interface LoginTenantRange { - prefix: string; - start: number; - count: number; - password: string; -} - -interface LoginTenants { - [tenant: string]: { - range: LoginTenantRange; - // add different format here - }; -} +// // specific a range of user name from to all having the same password +// interface LoginTenantRange { +// prefix: string; +// start: number; +// count: number; +// password: string; +// } + +// interface LoginTenants { +// [tenant: string]: { +// range: LoginTenantRange; +// // add different format here +// }; +// } /** * A simplified version of the credentials returned by the tenant pool containing only username and password values. + * @internal */ export interface UserPassCredentials { - UserPrincipalName: string; - Password: string; + username: string; + password: string; } /** @@ -88,101 +89,173 @@ export function assertOdspEndpoint( throw new TypeError("Not a odsp endpoint"); } +/** + * @internal + */ +export interface TenantSetupResult { + userPass: UserPassCredentials[]; + reservationId: string; + appClientId: string; +} + +interface SetupArgs { + waitTime?: number; + accessToken?: string; + odspEndpoint?: "prod" | "dogfood" | "df"; +} + +interface CleanupArgs { + reservationId?: string; + odspEndpoint?: "prod" | "dogfood" | "df"; +} + +const importTenantManagerPackage = async ( + args: SetupArgs | CleanupArgs, + setup: boolean, +): Promise => { + if (process.env.FLUID_TENANT_SETUP_PKG_SPECIFIER !== undefined) { + if (setup) { + // We expect that the specified package provides a setupTenants function. + const { setupTenants } = await import(process.env.FLUID_TENANT_SETUP_PKG_SPECIFIER); + assert( + typeof setupTenants === "function", + "A setupTenants function was not provided from the specified package", + ); + assert("waitTime" in args, "waitTime must be provided for tenant setup"); + return setupTenants(args) as Promise; + } else { + // We expect that the specified package provides a releaseTenants function. + const { releaseTenants } = await import(process.env.FLUID_TENANT_SETUP_PKG_SPECIFIER); + assert( + typeof releaseTenants === "function", + "A releaseTenants function was not provided from the specified package", + ); + assert("reservationId" in args, "reservationId must be provided for tenant cleanup"); + return releaseTenants(args) as Promise; + } + } + return undefined; +}; + /** * Get from the env a set of credentials to use from a single tenant * @param tenantIndex - integer to choose the tenant from array of options (if multiple tenants are available) * @param requestedUserName - specific user name to filter to * @internal */ -export function getOdspCredentials( +export async function getOdspCredentials( odspEndpointName: OdspEndpoint, tenantIndex: number, requestedUserName?: string, -): { username: string; password: string }[] { - const creds: { username: string; password: string }[] = []; - const loginTenants = - odspEndpointName === "odsp" - ? process.env.login__odsp__test__tenants - : process.env.login__odspdf__test__tenants; - - if (loginTenants !== undefined) { - /** - * Parse login credentials using the new tenant format for e2e tests. - * For the expected format of loginTenants, see {@link UserPassCredentials} - */ - if (loginTenants.includes("UserPrincipalName")) { - const output: UserPassCredentials[] = JSON.parse(loginTenants); - if (output?.[tenantIndex] === undefined) { - throw new Error("No resources found in the login tenants"); - } - - // Return the set of accounts to choose from a single tenant - for (const account of output) { - const username = account.UserPrincipalName; - const password = account.Password; - if (requestedUserName === undefined || requestedUserName === username) { - creds.push({ username, password }); - } - } - } else { - /** - * Parse login credentials using the tenant format for stress tests. - * For the expected format of loginTenants, see {@link LoginTenants} - */ - const tenants: LoginTenants = JSON.parse(loginTenants); - const tenantNames = Object.keys(tenants); - const tenant = tenantNames[tenantIndex % tenantNames.length]; - if (tenant === undefined) { - throw new Error("tenant should not be undefined when getting odsp credentials"); - } - const tenantInfo = tenants[tenant]; - if (tenantInfo === undefined) { - throw new Error("tenantInfo should not be undefined when getting odsp credentials"); - } - // Translate all the user from that user to the full user principal name by appending the tenant domain - const range = tenantInfo.range; - - // Return the set of account to choose from a single tenant - for (let i = 0; i < range.count; i++) { - const username = `${range.prefix}${range.start + i}@${tenant}`; - if (requestedUserName === undefined || requestedUserName === username) { - creds.push({ username, password: range.password }); - } - } - } - } else { - const loginAccounts = - odspEndpointName === "odsp" - ? process.env.login__odsp__test__accounts - : process.env.login__odspdf__test__accounts; - if (loginAccounts === undefined) { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - const inCi = !!process.env.TF_BUILD; - const odspOrOdspdf = odspEndpointName === "odsp" ? "odsp" : "odspdf"; - assert.fail( - `Missing secrets from environment. At least one of login__${odspOrOdspdf}__test__tenants or login__${odspOrOdspdf}__test__accounts must be set.${ - inCi ? "" : "\n\nRun getkeys to populate these environment variables." - }`, - ); - } - - // Expected format of login__odsp__test__accounts is simply string key-value pairs of username and password - const passwords: { [user: string]: string } = JSON.parse(loginAccounts); - - // Need to choose one out of the set as these account might be from different tenant - const username = requestedUserName ?? Object.keys(passwords)[0]; - if (username === undefined) { - throw new Error("username should not be undefined when getting odsp credentials"); - } - const userPass = passwords[username]; - if (userPass === undefined) { +): Promise { + const creds: UserPassCredentials[] = []; + const odspEndpoint = odspEndpointName === "odsp" ? "prod" : "dogfood"; + + const result = await importTenantManagerPackage( + { + waitTime: 3600, + accessToken: process.env.SYSTEM_ACCESSTOKEN, + odspEndpoint, + }, + true, + ); + assert(result !== undefined, "Tenant setup result is undefined."); + const { userPass, reservationId, appClientId } = result; + + // Return the set of accounts to choose from a single tenant + for (const account of userPass) { + const { username, password } = account; + if (username === undefined || password === undefined) { throw new Error( - "password for username should not be undefined when getting odsp credentials", + `username or password should not be undefined when getting odsp credentials - user: ${username}, pass: ${password}`, ); } - creds.push({ username, password: userPass }); + if (requestedUserName === undefined || requestedUserName === username) { + creds.push({ username, password }); + } } - return creds; + + const tenantSetupResult: TenantSetupResult = { + userPass: creds, + reservationId, + appClientId, + }; + return tenantSetupResult; + + // const loginTenants = + // odspEndpointName === "odsp" + // ? process.env.login__odsp__test__tenants + // : process.env.login__odspdf__test__tenants; + + // if (loginTenants !== undefined) { + // /** + // * Parse login credentials using the new tenant format for e2e tests. + // * For the expected format of loginTenants, see {@link UserPassCredentials} + // */ + // if (loginTenants.includes("UserPrincipalName")) { + // const output: UserPassCredentials[] = JSON.parse(loginTenants); + // if (output?.[tenantIndex] === undefined) { + // throw new Error("No resources found in the login tenants"); + // } + // } else { + // /** + // * Parse login credentials using the tenant format for stress tests. + // * For the expected format of loginTenants, see {@link LoginTenants} + // */ + // const tenants: LoginTenants = JSON.parse(loginTenants); + // const tenantNames = Object.keys(tenants); + // const tenant = tenantNames[tenantIndex % tenantNames.length]; + // if (tenant === undefined) { + // throw new Error("tenant should not be undefined when getting odsp credentials"); + // } + // const tenantInfo = tenants[tenant]; + // if (tenantInfo === undefined) { + // throw new Error("tenantInfo should not be undefined when getting odsp credentials"); + // } + // // Translate all the user from that user to the full user principal name by appending the tenant domain + // const range = tenantInfo.range; + + // // Return the set of account to choose from a single tenant + // for (let i = 0; i < range.count; i++) { + // const username = `${range.prefix}${range.start + i}@${tenant}`; + // if (requestedUserName === undefined || requestedUserName === username) { + // creds.push({ username, password: range.password }); + // } + // } + // } + // } else { + // const loginAccounts = + // odspEndpointName === "odsp" + // ? process.env.login__odsp__test__accounts + // : process.env.login__odspdf__test__accounts; + // if (loginAccounts === undefined) { + // // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + // const inCi = !!process.env.TF_BUILD; + // const odspOrOdspdf = odspEndpointName === "odsp" ? "odsp" : "odspdf"; + // assert.fail( + // `Missing secrets from environment. At least one of login__${odspOrOdspdf}__test__tenants or login__${odspOrOdspdf}__test__accounts must be set.${ + // inCi ? "" : "\n\nRun getkeys to populate these environment variables." + // }`, + // ); + // } + + // // Expected format of login__odsp__test__accounts is simply string key-value pairs of username and password + // const passwords: { [user: string]: string } = JSON.parse(loginAccounts); + + // // Need to choose one out of the set as these account might be from different tenant + // const username = requestedUserName ?? Object.keys(passwords)[0]; + // if (username === undefined) { + // throw new Error("username should not be undefined when getting odsp credentials"); + // } + // const userPass = passwords[username]; + // if (userPass === undefined) { + // throw new Error( + // "password for username should not be undefined when getting odsp credentials", + // ); + // } + // creds.push({ username, password: userPass }); + // } + // return creds; } /** @@ -199,8 +272,16 @@ export class OdspTestDriver implements ITestDriver { try { return await getDriveId(siteUrl, "", undefined, { accessToken: await this.getStorageToken({ siteUrl, refresh: false }, tokenConfig), - refreshTokenFn: async () => - this.getStorageToken({ siteUrl, refresh: true }, tokenConfig), + refreshTokenFn: async () => { + await importTenantManagerPackage( + { + reservationId: tokenConfig.reservationId, + odspEndpoint: tokenConfig.endpointName === "odsp" ? "prod" : "dogfood", + }, + false, + ); + return this.getStorageToken({ siteUrl, refresh: true }, tokenConfig); + }, }); } catch (ex) { if (tokenConfig.supportsBrowserAuth !== true) { @@ -231,7 +312,12 @@ export class OdspTestDriver implements ITestDriver { const tenantIndex = config?.tenantIndex ?? 0; assertOdspEndpoint(config?.odspEndpointName); const endpointName = config?.odspEndpointName ?? "odsp"; - const creds = getOdspCredentials(endpointName, tenantIndex, config?.username); + // DEAL WITH THIS + const { + reservationId, + appClientId, + userPass: creds, + } = await getOdspCredentials(endpointName, tenantIndex, config?.username); // Pick a random one on the list (only supported for >= 0.46) const randomUserIndex = compare(api.version, "0.46.0") >= 0 @@ -273,6 +359,8 @@ export class OdspTestDriver implements ITestDriver { tenantName, userIndex, endpointName, + reservationId, + appClientId, ); } @@ -300,10 +388,14 @@ export class OdspTestDriver implements ITestDriver { tenantName?: string, userIndex?: number, endpointName?: string, + reservationId?: string, + appClientId?: string, ) { const tokenConfig: TokenConfig = { ...loginConfig, - ...getMicrosoftConfiguration(), + clientId: appClientId ?? getMicrosoftConfiguration().clientId, + endpointName, + reservationId, }; const driveId = await this.getDriveId(loginConfig.siteUrl, tokenConfig); @@ -354,10 +446,20 @@ export class OdspTestDriver implements ITestDriver { } // This function can handle token request for any multiple sites. // Where the test driver is for a specific site. + const result = await importTenantManagerPackage( + { + waitTime: 3600, + accessToken: process.env.SYSTEM_ACCESSTOKEN, + odspEndpoint: config.endpointName === "odsp" ? "prod" : "dogfood", + }, + true, + ); + assert(result !== undefined, "Tenant setup result is undefined."); + const { userPass, reservationId } = result; const tokens = await this.odspTokenManager.getOdspTokens( host, - config, - passwordTokenConfig(config.username, config.password), + { reservationId, ...config }, + passwordTokenConfig(userPass[0]?.username, userPass[0]?.password), options.refresh, ); return tokens.accessToken; diff --git a/packages/utils/odsp-doclib-utils/src/odspAuth.ts b/packages/utils/odsp-doclib-utils/src/odspAuth.ts index 5b29bcc706d5..693a3b3d434e 100644 --- a/packages/utils/odsp-doclib-utils/src/odspAuth.ts +++ b/packages/utils/odsp-doclib-utils/src/odspAuth.ts @@ -26,6 +26,8 @@ export interface IOdspTokens { */ export interface IPublicClientConfig { clientId: string; + endpointName?: string; + reservationId?: string; } /** @@ -171,7 +173,7 @@ export async function fetchTokens( try { throwOdspNetworkError( // pre-0.58 error message: unableToGetAccessToken - "Unable to get access token.", + `Unable to get access token. response: ${JSON.stringify(parsedResponse)}`, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access parsedResponse.error === "invalid_grant" ? 401 : response.status, response, @@ -221,6 +223,14 @@ function isAccessTokenError(parsedResponse: unknown): parsedResponse is AadOauth ); } +/** + * A simplified version of the credentials returned by the tenant pool containing only username and password values. + */ +export interface UserPassCredentials { + username: string; + password: string; +} + /** * Fetch fresh tokens. * @param server - The server to auth against diff --git a/tools/pipelines/templates/include-test-real-service.yml b/tools/pipelines/templates/include-test-real-service.yml index 3579bdcb38ef..46fc05b87827 100644 --- a/tools/pipelines/templates/include-test-real-service.yml +++ b/tools/pipelines/templates/include-test-real-service.yml @@ -383,38 +383,92 @@ stages: az login --service-principal -u $servicePrincipalId -p $idToken --tenant $tenantId - - task: Bash@3 - displayName: 'Run tenant setup script' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - inputs: - targetType: 'inline' - script: | - set -eu -o pipefail - - # Increase the maximum time to wait for a tenant to 1 hour to accommodate multiple test runs at the same time. - pnpm exec trips-setup --waitTime=3600 --accessToken=$SYSTEM_ACCESSTOKEN --odspEndpoint=${{ parameters.odspTenantType }} - echo "##vso[task.setvariable variable=tenantSetupSuccess;]true" + # - task: Bash@3 + # displayName: 'Run tenant setup script' + # env: + # SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # inputs: + # targetType: 'inline' + # script: | + # set -eu -o pipefail + + # # Increase the maximum time to wait for a tenant to 1 hour to accommodate multiple test runs at the same time. + # pnpm exec trips-setup --waitTime=3600 --accessToken=$SYSTEM_ACCESSTOKEN --odspEndpoint=${{ parameters.odspTenantType }} + # echo "##vso[task.setvariable variable=tenantSetupSuccess;]true" # run the test - - task: Npm@1 + - task: Bash@3 displayName: '[test] ${{ parameters.testCommand }} ${{ variant.flags }}' continueOnError: ${{ parameters.continueOnError }} env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) ${{ each pair in parameters.env }}: ${{ pair.key }}: ${{ pair.value }} # ODSP tenant setup script injects the values here (appClientId, tenantCreds). # The e2e test workload references the keys (login__*). - ${{ if eq(parameters.odspTenantType, 'dogfood') }}: - login__microsoft__clientId: $(appClientId) - login__odspdf__test__tenants: $(tenantCreds) - ${{ elseif eq(parameters.odspTenantType, 'prod') }}: - login__microsoft__clientId: $(appClientId) - login__odsp__test__tenants: $(tenantCreds) + # login__microsoft__clientId: $(appClientId) + # ${{ if eq(parameters.odspTenantType, 'dogfood') }}: + # login__odspdf__test__tenants: $(tenantCreds) + # ${{ elseif eq(parameters.odspTenantType, 'prod') }}: + # login__microsoft__clientId: $(appClientId) + # login__odsp__test__tenants: $(tenantCreds) inputs: - command: 'custom' - workingDir: $(Build.SourcesDirectory)/node_modules/${{ parameters.testPackage }} - customCommand: 'run ${{ parameters.testCommand }} -- ${{ variant.flags }}' + targetType: 'inline' + workingDirectory: $(Build.SourcesDirectory)/node_modules/${{ parameters.testPackage }} + script: | + set -eu -o pipefail + + # if [[ "${{ parameters.odspTenantType }}" != "none" ]]; then + # SETUP_OUTPUT=$(node -e " + # (async () => { + # try { + # const tenantSetupPackage = '${{ parameters.tenantSetupPackage }}'; + # const { setupTenants } = await import(tenantSetupPackage); + # const result = await setupTenants({ + # waitTime: 3600, + # accessToken: process.env.SYSTEM_ACCESSTOKEN, + # odspEndpoint: '${{ parameters.odspTenantType }}' + # }); + # fs.writeFileSync('/tmp/setup_result.json', result); + # } catch (e) { + # console.error('Error during tenant setup: ', e); + # fs.unlinkSync('/tmp/setup_result.json'); + # process.exit(1); + # } + # })(); + # ") + + # # Read JSON from temp file + # JSON_OUTPUT=$(cat /tmp/setup_result.json) + + # # Now safely parse the JSON + # RESERVATION_ID=$(echo "$JSON_OUTPUT" | jq -r '.reservationId // "null"') + # USER_PASS=$(echo "$JSON_OUTPUT" | jq '.userPass // "null"') + + # # Only set variables if parsing succeeded + # if [[ "$RESERVATION_ID" != "null" && "$RESERVATION_ID" != "" ]]; then + # export tenant__reservationId="$RESERVATION_ID" + + # # Set Azure DevOps variables for cleanup task + # echo "##vso[task.setvariable variable=stringBearerToken]$RESERVATION_ID" + # echo "##vso[task.setvariable variable=tenantCreds]$USER_PASS" + + # if [[ "${{ parameters.odspTenantType }}" == "dogfood" ]]; then + # export login__odspdf__test__tenants="$USER_PASS" + # elif [[ "${{ parameters.odspTenantType }}" == "prod" ]]; then + # export login__odsp__test__tenants="$USER_PASS" + # fi + + # else + # echo "ERROR: Failed to parse reservationId from setup output" + # exit 1 + # fi + # # Clean up temp file + # rm -f /tmp/setup_result.json + # fi + + pnpm run ${{ parameters.testCommand }} -- ${{ variant.flags }} + - ${{ if eq(parameters.skipTestResultPublishing, false) }}: # filter report diff --git a/tools/pipelines/test-real-service.yml b/tools/pipelines/test-real-service.yml index 4cf6a7ca02ca..2a7000c0583e 100644 --- a/tools/pipelines/test-real-service.yml +++ b/tools/pipelines/test-real-service.yml @@ -192,3 +192,4 @@ stages: env: FLUID_TEST_LOGGER_PKG_SPECIFIER: '@ff-internal/aria-logger' # Contains createTestLogger impl to inject FLUID_LOGGER_PROPS: '{ "displayName": "${{variables.pipelineIdentifierForTelemetry}}"}' + FLUID_TENANT_SETUP_PKG_SPECIFIER: '@ff-internal/trips-setup'