From 5ffa0bb7708a9bb58c725bd888fa2869a2250151 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Fri, 30 May 2025 14:16:24 +1000 Subject: [PATCH 01/12] [credential-cache] Support custom cache paths The `CredentialCache` does not rely on the Rush user configuration. It can be used outside of `@rush/rush-sdk` by directly importing it from `@microsoft/rush-lib`. To support such use cases, we should allow the consumer to specify a file path for their cache, rather than the fixed `~/.rush-user/credentials.json` path. Non-breaking change because the default path remains the same, and we add support for additional options in `ICredentialCacheOptions`. | `ICredentialCacheOptions.cacheDirectory` | `.cacheName` | Resulting Path | |-|-|-| | `undefined` | `undefined` | `~/.rush-user/credentials.json` (default) | | `undefined` | `custom-name` | `~/.rush-user/custom-name.json` | | `/custom-dir` | `undefined` | `/custom-dir/credentials.json` | | `/custom-dir` | `custom-name` | `/custom-dir/custom-name.json` | --- .../rush-lib/src/logic/CredentialCache.ts | 25 +- .../src/logic/test/CredentialCache.test.ts | 507 ++++++++++-------- 2 files changed, 304 insertions(+), 228 deletions(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index de2bb123967..6c4296dba62 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -8,7 +8,7 @@ import { RushUserConfiguration } from '../api/RushUserConfiguration'; import schemaJson from '../schemas/credentials.schema.json'; import { objectsAreDeepEqual } from '../utilities/objectUtilities'; -const CACHE_FILENAME: string = 'credentials.json'; +const DEFAULT_CACHE_NAME: string = 'credentials'; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; interface ICredentialCacheJson { @@ -38,6 +38,8 @@ export interface ICredentialCacheEntry { */ export interface ICredentialCacheOptions { supportEditing: boolean; + cacheDirectory?: string; + cacheName?: string; } /** @@ -66,9 +68,22 @@ export class CredentialCache /* implements IDisposable */ { this._lockfile = lockfile; } - public static async initializeAsync(options: ICredentialCacheOptions): Promise { - const rushUserFolderPath: string = RushUserConfiguration.getRushUserFolderPath(); - const cacheFilePath: string = `${rushUserFolderPath}/${CACHE_FILENAME}`; + public static initializeAsync(options: ICredentialCacheOptions): Promise { + return CredentialCache.initializeAsyncFromResolvedOptions(CredentialCache.resolveOptions(options)); + } + + private static resolveOptions(options: ICredentialCacheOptions): Required { + return { + ...options, + cacheDirectory: options.cacheDirectory || RushUserConfiguration.getRushUserFolderPath(), + cacheName: options.cacheName || DEFAULT_CACHE_NAME + }; + } + + private static async initializeAsyncFromResolvedOptions( + options: Required + ): Promise { + const cacheFilePath: string = `${options.cacheDirectory}/${options.cacheName}.json`; const jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); let loadedJson: ICredentialCacheJson | undefined; @@ -82,7 +97,7 @@ export class CredentialCache /* implements IDisposable */ { let lockfile: LockFile | undefined; if (options.supportEditing) { - lockfile = await LockFile.acquireAsync(rushUserFolderPath, `${CACHE_FILENAME}.lock`); + lockfile = await LockFile.acquireAsync(options.cacheDirectory, `${options.cacheName}.lock`); } const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index b2d8ac49a43..cbfc4af4894 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -3,10 +3,16 @@ import { LockFile, Async, FileSystem } from '@rushstack/node-core-library'; import { RushUserConfiguration } from '../../api/RushUserConfiguration'; -import { CredentialCache } from '../CredentialCache'; +import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCache'; const FAKE_RUSH_USER_FOLDER: string = '~/.rush-user'; -const FAKE_CREDENTIALS_CACHE_FILE: string = `${FAKE_RUSH_USER_FOLDER}/credentials.json`; +const FAKE_DEFAULT_CREDENTIALS_CACHE_FILE: string = `${FAKE_RUSH_USER_FOLDER}/credentials.json`; + +interface PathsTestCase { + testCaseName: string; + partialOptions: Pick; + expectedCacheFilePath: string; +} describe(CredentialCache.name, () => { let fakeFilesystem: { [key: string]: string }; @@ -56,6 +62,7 @@ describe(CredentialCache.name, () => { jest .spyOn(FileSystem, 'writeFileAsync') .mockImplementation(async (filePath: string, data: Buffer | string) => { + console.log(`Writing to fake filesystem: ${filePath}`); fakeFilesystem[filePath] = data.toString(); }); @@ -85,92 +92,128 @@ describe(CredentialCache.name, () => { jest.restoreAllMocks(); }); - it("initializes a credential cache correctly when one doesn't exist on disk", async () => { - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false }); - expect(credentialCache).toBeDefined(); - credentialCache.dispose(); - }); + describe.each([ + { + testCaseName: 'default cache directory with default cache name', + partialOptions: {}, + expectedCacheFilePath: FAKE_DEFAULT_CREDENTIALS_CACHE_FILE + }, + { + testCaseName: 'default cache directory with custom cache name', + partialOptions: { cacheName: 'my-cache-name' }, + expectedCacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` + }, + { + testCaseName: 'custom cache directory with default cache name', + partialOptions: { cacheDirectory: '~/custom-dir' }, + expectedCacheFilePath: '~/custom-dir/credentials.json' + }, + { + testCaseName: 'custom cache directory with custom cache name', + partialOptions: { cacheDirectory: '~/custom-dir', cacheName: 'my-cache-name' }, + expectedCacheFilePath: '~/custom-dir/my-cache-name.json' + } + ])('cache paths [$testCaseName]', ({ partialOptions, expectedCacheFilePath }) => { + it("initializes a credential cache correctly when one doesn't exist on disk", async () => { + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ + supportEditing: false + }); + expect(credentialCache).toBeDefined(); + credentialCache.dispose(); + }); - it('initializes a credential cache correctly when one exists on disk', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 0, - credential: credentialValue + it('initializes a credential cache correctly when one exists on disk', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 0, + credential: credentialValue + } } - } - }); + }); - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false }); - expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); - expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); - credentialCache.dispose(); - }); + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); + expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); + credentialCache.dispose(); + }); - it('initializes a credential cache correctly when one exists on disk with a expired credential', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 100, // Expired - credential: credentialValue + it('initializes a credential cache correctly when one exists on disk with a expired credential', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 100, // Expired + credential: credentialValue + } } - } - }); + }); - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false }); - expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); - expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toMatchInlineSnapshot( - `1970-01-01T00:00:00.100Z` - ); - credentialCache.dispose(); - }); + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); + expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toMatchInlineSnapshot( + `1970-01-01T00:00:00.100Z` + ); + credentialCache.dispose(); + }); - it('correctly trims expired credentials', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 100, // Expired - credential: credentialValue + it('correctly trims expired credentials', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 100, // Expired + credential: credentialValue + } } - } - }); + }); - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache.trimExpiredEntries(); - expect(credentialCache.tryGetCacheEntry(credentialId)).toBeUndefined(); - await credentialCache.saveIfModifiedAsync(); - credentialCache.dispose(); + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache.trimExpiredEntries(); + expect(credentialCache.tryGetCacheEntry(credentialId)).toBeUndefined(); + await credentialCache.saveIfModifiedAsync(); + credentialCache.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": {} } " `); - }); + }); - it('correctly adds a new credential', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; + it('correctly adds a new credential', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; - const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache1.setCacheEntry(credentialId, { credential: credentialValue }); - expect(credentialCache1.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); - expect(credentialCache1.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); - await credentialCache1.saveIfModifiedAsync(); - credentialCache1.dispose(); + const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache1.setCacheEntry(credentialId, { credential: credentialValue }); + expect(credentialCache1.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); + expect(credentialCache1.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); + await credentialCache1.saveIfModifiedAsync(); + credentialCache1.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -183,36 +226,40 @@ describe(CredentialCache.name, () => { " `); - const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - supportEditing: false + const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); + expect(credentialCache2.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); + credentialCache2.dispose(); }); - expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); - expect(credentialCache2.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); - credentialCache2.dispose(); - }); - it('correctly updates an existing credential', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - const newCredentialValue: string = 'new-test-value'; - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 0, - credential: credentialValue + it('correctly updates an existing credential', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + const newCredentialValue: string = 'new-test-value'; + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 0, + credential: credentialValue + } } - } - }); + }); - const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache1.setCacheEntry(credentialId, { credential: newCredentialValue }); - expect(credentialCache1.tryGetCacheEntry(credentialId)?.credential).toEqual(newCredentialValue); - expect(credentialCache1.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); - await credentialCache1.saveIfModifiedAsync(); - credentialCache1.dispose(); + const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache1.setCacheEntry(credentialId, { credential: newCredentialValue }); + expect(credentialCache1.tryGetCacheEntry(credentialId)?.credential).toEqual(newCredentialValue); + expect(credentialCache1.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); + await credentialCache1.saveIfModifiedAsync(); + credentialCache1.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -225,33 +272,37 @@ describe(CredentialCache.name, () => { " `); - const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - supportEditing: false + const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(newCredentialValue); + expect(credentialCache2.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); + credentialCache2.dispose(); }); - expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(newCredentialValue); - expect(credentialCache2.tryGetCacheEntry(credentialId)?.expires).toBeUndefined(); - credentialCache2.dispose(); - }); - it('correctly deletes an existing credential', async () => { - const credentialId: string = 'test-credential'; - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 0, - credential: 'test-value' + it('correctly deletes an existing credential', async () => { + const credentialId: string = 'test-credential'; + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 0, + credential: 'test-value' + } } - } - }); + }); - const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache1.deleteCacheEntry(credentialId); - expect(credentialCache1.tryGetCacheEntry(credentialId)).toBeUndefined(); - await credentialCache1.saveIfModifiedAsync(); - credentialCache1.dispose(); + const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache1.deleteCacheEntry(credentialId); + expect(credentialCache1.tryGetCacheEntry(credentialId)).toBeUndefined(); + await credentialCache1.saveIfModifiedAsync(); + credentialCache1.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": {} @@ -259,69 +310,35 @@ describe(CredentialCache.name, () => { " `); - const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - supportEditing: false + const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache2.tryGetCacheEntry(credentialId)).toBeUndefined(); + credentialCache2.dispose(); }); - expect(credentialCache2.tryGetCacheEntry(credentialId)).toBeUndefined(); - credentialCache2.dispose(); - }); - - it('does not allow interaction if already disposed', async () => { - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache.dispose(); - - expect(() => credentialCache.deleteCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache has been disposed."` - ); - await expect(() => credentialCache.saveIfModifiedAsync()).rejects.toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache has been disposed."` - ); - expect(() => - credentialCache.setCacheEntry('test', { credential: 'test' }) - ).toThrowErrorMatchingInlineSnapshot(`"This instance of CredentialCache has been disposed."`); - expect(() => credentialCache.trimExpiredEntries()).toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache has been disposed."` - ); - expect(() => credentialCache.tryGetCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache has been disposed."` - ); - }); - it("does not allow modification if initialized with 'supportEditing': false", async () => { - const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false }); - - expect(() => credentialCache.deleteCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache does not support editing."` - ); - await expect(() => credentialCache.saveIfModifiedAsync()).rejects.toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache does not support editing."` - ); - expect(() => - credentialCache.setCacheEntry('test', { credential: 'test' }) - ).toThrowErrorMatchingInlineSnapshot(`"This instance of CredentialCache does not support editing."`); - expect(() => credentialCache.trimExpiredEntries()).toThrowErrorMatchingInlineSnapshot( - `"This instance of CredentialCache does not support editing."` - ); - }); - - it('correctly sets credentialMetadata', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - const credentialMetadata: object = { - a: 1, - b: true - }; - - const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache1.setCacheEntry(credentialId, { credential: credentialValue, credentialMetadata }); - expect(credentialCache1.tryGetCacheEntry(credentialId)).toEqual({ - credential: credentialValue, - credentialMetadata - }); - await credentialCache1.saveIfModifiedAsync(); - credentialCache1.dispose(); + it('correctly sets credentialMetadata', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + const credentialMetadata: object = { + a: 1, + b: true + }; + + const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache1.setCacheEntry(credentialId, { credential: credentialValue, credentialMetadata }); + expect(credentialCache1.tryGetCacheEntry(credentialId)).toEqual({ + credential: credentialValue, + credentialMetadata + }); + await credentialCache1.saveIfModifiedAsync(); + credentialCache1.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -338,51 +355,55 @@ describe(CredentialCache.name, () => { " `); - const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - supportEditing: false - }); - expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ - credential: credentialValue, - credentialMetadata + const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ + credential: credentialValue, + credentialMetadata + }); + credentialCache2.dispose(); }); - credentialCache2.dispose(); - }); - it('correctly updates credentialMetadata', async () => { - const credentialId: string = 'test-credential'; - const credentialValue: string = 'test-value'; - const oldCredentialMetadata: object = { - a: 1, - b: true - }; - const newCredentialMetadata: object = { - c: ['a', 'b', 'c'] - }; - - fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE] = JSON.stringify({ - version: '0.1.0', - cacheEntries: { - [credentialId]: { - expires: 0, - credential: 'test-value', - credentialMetadata: oldCredentialMetadata + it('correctly updates credentialMetadata', async () => { + const credentialId: string = 'test-credential'; + const credentialValue: string = 'test-value'; + const oldCredentialMetadata: object = { + a: 1, + b: true + }; + const newCredentialMetadata: object = { + c: ['a', 'b', 'c'] + }; + + fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + version: '0.1.0', + cacheEntries: { + [credentialId]: { + expires: 0, + credential: 'test-value', + credentialMetadata: oldCredentialMetadata + } } - } - }); + }); - const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); - credentialCache1.setCacheEntry(credentialId, { - credential: credentialValue, - credentialMetadata: newCredentialMetadata - }); - expect(credentialCache1.tryGetCacheEntry(credentialId)).toEqual({ - credential: credentialValue, - credentialMetadata: newCredentialMetadata - }); - await credentialCache1.saveIfModifiedAsync(); - credentialCache1.dispose(); + const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: true + }); + credentialCache1.setCacheEntry(credentialId, { + credential: credentialValue, + credentialMetadata: newCredentialMetadata + }); + expect(credentialCache1.tryGetCacheEntry(credentialId)).toEqual({ + credential: credentialValue, + credentialMetadata: newCredentialMetadata + }); + await credentialCache1.saveIfModifiedAsync(); + credentialCache1.dispose(); - expect(fakeFilesystem[FAKE_CREDENTIALS_CACHE_FILE]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -402,13 +423,53 @@ describe(CredentialCache.name, () => { " `); - const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - supportEditing: false - }); - expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ - credential: credentialValue, - credentialMetadata: newCredentialMetadata + const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ + ...partialOptions, + supportEditing: false + }); + expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ + credential: credentialValue, + credentialMetadata: newCredentialMetadata + }); + credentialCache2.dispose(); }); - credentialCache2.dispose(); + }); + + it('does not allow interaction if already disposed', async () => { + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: true }); + credentialCache.dispose(); + + expect(() => credentialCache.deleteCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache has been disposed."` + ); + await expect(() => credentialCache.saveIfModifiedAsync()).rejects.toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache has been disposed."` + ); + expect(() => + credentialCache.setCacheEntry('test', { credential: 'test' }) + ).toThrowErrorMatchingInlineSnapshot(`"This instance of CredentialCache has been disposed."`); + expect(() => credentialCache.trimExpiredEntries()).toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache has been disposed."` + ); + expect(() => credentialCache.tryGetCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache has been disposed."` + ); + }); + + it("does not allow modification if initialized with 'supportEditing': false", async () => { + const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false }); + + expect(() => credentialCache.deleteCacheEntry('test')).toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache does not support editing."` + ); + await expect(() => credentialCache.saveIfModifiedAsync()).rejects.toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache does not support editing."` + ); + expect(() => + credentialCache.setCacheEntry('test', { credential: 'test' }) + ).toThrowErrorMatchingInlineSnapshot(`"This instance of CredentialCache does not support editing."`); + expect(() => credentialCache.trimExpiredEntries()).toThrowErrorMatchingInlineSnapshot( + `"This instance of CredentialCache does not support editing."` + ); }); }); From f9c9c470a834659933ea37de680fdc633dcbd904 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Fri, 30 May 2025 14:18:57 +1000 Subject: [PATCH 02/12] chore: update extracted api --- common/reviews/api/rush-lib.api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 76e081722d1..2abb0ddafe2 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -437,6 +437,10 @@ export interface ICredentialCacheEntry { // @beta (undocumented) export interface ICredentialCacheOptions { + // (undocumented) + cacheDirectory?: string; + // (undocumented) + cacheName?: string; // (undocumented) supportEditing: boolean; } From 10c69347da7f93ad06593b1f5d7e44c675c50ebe Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Fri, 30 May 2025 14:26:13 +1000 Subject: [PATCH 03/12] chore: rush change (add changelog entry) --- .../feat-credcache-custom-paths_2025-05-30-04-23.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json diff --git a/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json b/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json new file mode 100644 index 00000000000..0e4a18901af --- /dev/null +++ b/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for custom `CredentialCache` paths", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file From 7906d506336ebb2717782a8ff8e7b0f73d8348b5 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Fri, 30 May 2025 15:08:12 +1000 Subject: [PATCH 04/12] chore: fix linting issues --- libraries/rush-lib/src/logic/CredentialCache.ts | 6 +++--- libraries/rush-lib/src/logic/test/CredentialCache.test.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index 6c4296dba62..5e6e8737e3f 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -69,10 +69,10 @@ export class CredentialCache /* implements IDisposable */ { } public static initializeAsync(options: ICredentialCacheOptions): Promise { - return CredentialCache.initializeAsyncFromResolvedOptions(CredentialCache.resolveOptions(options)); + return CredentialCache._initializeAsyncFromResolvedOptions(CredentialCache._resolveOptions(options)); } - private static resolveOptions(options: ICredentialCacheOptions): Required { + private static _resolveOptions(options: ICredentialCacheOptions): Required { return { ...options, cacheDirectory: options.cacheDirectory || RushUserConfiguration.getRushUserFolderPath(), @@ -80,7 +80,7 @@ export class CredentialCache /* implements IDisposable */ { }; } - private static async initializeAsyncFromResolvedOptions( + private static async _initializeAsyncFromResolvedOptions( options: Required ): Promise { const cacheFilePath: string = `${options.cacheDirectory}/${options.cacheName}.json`; diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index cbfc4af4894..1080c8adea5 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -62,7 +62,6 @@ describe(CredentialCache.name, () => { jest .spyOn(FileSystem, 'writeFileAsync') .mockImplementation(async (filePath: string, data: Buffer | string) => { - console.log(`Writing to fake filesystem: ${filePath}`); fakeFilesystem[filePath] = data.toString(); }); From 7466500a6b02c24c427835248be90879a0f7b065 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Fri, 30 May 2025 15:41:01 +1000 Subject: [PATCH 05/12] chore: fix more linting issues --- libraries/rush-lib/src/logic/CredentialCache.ts | 4 ++-- libraries/rush-lib/src/logic/test/CredentialCache.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index 5e6e8737e3f..a9e273fce54 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -69,7 +69,7 @@ export class CredentialCache /* implements IDisposable */ { } public static initializeAsync(options: ICredentialCacheOptions): Promise { - return CredentialCache._initializeAsyncFromResolvedOptions(CredentialCache._resolveOptions(options)); + return CredentialCache._initializeFromResolvedOptionsAsync(CredentialCache._resolveOptions(options)); } private static _resolveOptions(options: ICredentialCacheOptions): Required { @@ -80,7 +80,7 @@ export class CredentialCache /* implements IDisposable */ { }; } - private static async _initializeAsyncFromResolvedOptions( + private static async _initializeFromResolvedOptionsAsync( options: Required ): Promise { const cacheFilePath: string = `${options.cacheDirectory}/${options.cacheName}.json`; diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index 1080c8adea5..5391d34aa61 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -8,7 +8,7 @@ import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCach const FAKE_RUSH_USER_FOLDER: string = '~/.rush-user'; const FAKE_DEFAULT_CREDENTIALS_CACHE_FILE: string = `${FAKE_RUSH_USER_FOLDER}/credentials.json`; -interface PathsTestCase { +interface IPathsTestCase { testCaseName: string; partialOptions: Pick; expectedCacheFilePath: string; @@ -91,7 +91,7 @@ describe(CredentialCache.name, () => { jest.restoreAllMocks(); }); - describe.each([ + describe.each([ { testCaseName: 'default cache directory with default cache name', partialOptions: {}, From 7901e467b07c1c78a1a31a5e4518e9e3fde6bb29 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Wed, 4 Jun 2025 11:26:39 +1000 Subject: [PATCH 06/12] chore: Use cachePath instead of separate dirName+cacheName options --- common/reviews/api/rush-lib.api.md | 4 +- .../rush-lib/src/logic/CredentialCache.ts | 32 ++++---- .../src/logic/test/CredentialCache.test.ts | 81 +++++++++---------- 3 files changed, 54 insertions(+), 63 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 2abb0ddafe2..f4cad01bcf7 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -438,9 +438,7 @@ export interface ICredentialCacheEntry { // @beta (undocumented) export interface ICredentialCacheOptions { // (undocumented) - cacheDirectory?: string; - // (undocumented) - cacheName?: string; + cacheFilePath?: string; // (undocumented) supportEditing: boolean; } diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index a9e273fce54..4d904b05b75 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import * as path from 'path'; import { FileSystem, JsonFile, JsonSchema, LockFile } from '@rushstack/node-core-library'; import { Utilities } from '../utilities/Utilities'; @@ -38,8 +39,7 @@ export interface ICredentialCacheEntry { */ export interface ICredentialCacheOptions { supportEditing: boolean; - cacheDirectory?: string; - cacheName?: string; + cacheFilePath?: string; } /** @@ -68,22 +68,18 @@ export class CredentialCache /* implements IDisposable */ { this._lockfile = lockfile; } - public static initializeAsync(options: ICredentialCacheOptions): Promise { - return CredentialCache._initializeFromResolvedOptionsAsync(CredentialCache._resolveOptions(options)); - } - - private static _resolveOptions(options: ICredentialCacheOptions): Required { - return { - ...options, - cacheDirectory: options.cacheDirectory || RushUserConfiguration.getRushUserFolderPath(), - cacheName: options.cacheName || DEFAULT_CACHE_NAME - }; - } + public static async initializeAsync(options: ICredentialCacheOptions): Promise { + let cacheDirectory: string; + let cacheName: string; + if (options.cacheFilePath) { + cacheDirectory = path.dirname(options.cacheFilePath); + cacheName = path.basename(options.cacheFilePath, '.json'); + } else { + cacheDirectory = RushUserConfiguration.getRushUserFolderPath(); + cacheName = DEFAULT_CACHE_NAME; + } + const cacheFilePath: string = `${cacheDirectory}/${cacheName}.json`; - private static async _initializeFromResolvedOptionsAsync( - options: Required - ): Promise { - const cacheFilePath: string = `${options.cacheDirectory}/${options.cacheName}.json`; const jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); let loadedJson: ICredentialCacheJson | undefined; @@ -97,7 +93,7 @@ export class CredentialCache /* implements IDisposable */ { let lockfile: LockFile | undefined; if (options.supportEditing) { - lockfile = await LockFile.acquireAsync(options.cacheDirectory, `${options.cacheName}.lock`); + lockfile = await LockFile.acquireAsync(cacheDirectory, `${cacheName}.lock`); } const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index 5391d34aa61..6d9fa9f32bc 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -6,12 +6,10 @@ import { RushUserConfiguration } from '../../api/RushUserConfiguration'; import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCache'; const FAKE_RUSH_USER_FOLDER: string = '~/.rush-user'; -const FAKE_DEFAULT_CREDENTIALS_CACHE_FILE: string = `${FAKE_RUSH_USER_FOLDER}/credentials.json`; -interface IPathsTestCase { +interface IPathsTestCase extends Pick { testCaseName: string; - partialOptions: Pick; - expectedCacheFilePath: string; + expectedCachePath: string; } describe(CredentialCache.name, () => { @@ -93,26 +91,25 @@ describe(CredentialCache.name, () => { describe.each([ { - testCaseName: 'default cache directory with default cache name', - partialOptions: {}, - expectedCacheFilePath: FAKE_DEFAULT_CREDENTIALS_CACHE_FILE + testCaseName: 'default cache path', + expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/credentials.json` }, { - testCaseName: 'default cache directory with custom cache name', - partialOptions: { cacheName: 'my-cache-name' }, - expectedCacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` + testCaseName: 'custom cache path with no suffix', + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name`, + expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` }, { - testCaseName: 'custom cache directory with default cache name', - partialOptions: { cacheDirectory: '~/custom-dir' }, - expectedCacheFilePath: '~/custom-dir/credentials.json' + testCaseName: 'custom cache path with json suffix', + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json`, + expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` }, { - testCaseName: 'custom cache directory with custom cache name', - partialOptions: { cacheDirectory: '~/custom-dir', cacheName: 'my-cache-name' }, - expectedCacheFilePath: '~/custom-dir/my-cache-name.json' + testCaseName: 'custom cache path with custom suffix should append a json suffix', + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.staging`, + expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.staging.json` } - ])('cache paths [$testCaseName]', ({ partialOptions, expectedCacheFilePath }) => { + ])('cache paths [$testCaseName]', ({ cacheFilePath, expectedCachePath }) => { it("initializes a credential cache correctly when one doesn't exist on disk", async () => { const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false @@ -124,7 +121,7 @@ describe(CredentialCache.name, () => { it('initializes a credential cache correctly when one exists on disk', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -135,7 +132,7 @@ describe(CredentialCache.name, () => { }); const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); @@ -146,7 +143,7 @@ describe(CredentialCache.name, () => { it('initializes a credential cache correctly when one exists on disk with a expired credential', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -157,7 +154,7 @@ describe(CredentialCache.name, () => { }); const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); @@ -170,7 +167,7 @@ describe(CredentialCache.name, () => { it('correctly trims expired credentials', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -181,7 +178,7 @@ describe(CredentialCache.name, () => { }); const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache.trimExpiredEntries(); @@ -189,7 +186,7 @@ describe(CredentialCache.name, () => { await credentialCache.saveIfModifiedAsync(); credentialCache.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": {} @@ -203,7 +200,7 @@ describe(CredentialCache.name, () => { const credentialValue: string = 'test-value'; const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache1.setCacheEntry(credentialId, { credential: credentialValue }); @@ -212,7 +209,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -226,7 +223,7 @@ describe(CredentialCache.name, () => { `); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); @@ -238,7 +235,7 @@ describe(CredentialCache.name, () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; const newCredentialValue: string = 'new-test-value'; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -249,7 +246,7 @@ describe(CredentialCache.name, () => { }); const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache1.setCacheEntry(credentialId, { credential: newCredentialValue }); @@ -258,7 +255,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -272,7 +269,7 @@ describe(CredentialCache.name, () => { `); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache2.tryGetCacheEntry(credentialId)?.credential).toEqual(newCredentialValue); @@ -282,7 +279,7 @@ describe(CredentialCache.name, () => { it('correctly deletes an existing credential', async () => { const credentialId: string = 'test-credential'; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -293,7 +290,7 @@ describe(CredentialCache.name, () => { }); const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache1.deleteCacheEntry(credentialId); @@ -301,7 +298,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": {} @@ -310,7 +307,7 @@ describe(CredentialCache.name, () => { `); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache2.tryGetCacheEntry(credentialId)).toBeUndefined(); @@ -326,7 +323,7 @@ describe(CredentialCache.name, () => { }; const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache1.setCacheEntry(credentialId, { credential: credentialValue, credentialMetadata }); @@ -337,7 +334,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -355,7 +352,7 @@ describe(CredentialCache.name, () => { `); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ @@ -376,7 +373,7 @@ describe(CredentialCache.name, () => { c: ['a', 'b', 'c'] }; - fakeFilesystem[expectedCacheFilePath] = JSON.stringify({ + fakeFilesystem[expectedCachePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -388,7 +385,7 @@ describe(CredentialCache.name, () => { }); const credentialCache1: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: true }); credentialCache1.setCacheEntry(credentialId, { @@ -402,7 +399,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCacheFilePath]).toMatchInlineSnapshot(` + expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` "{ \\"version\\": \\"0.1.0\\", \\"cacheEntries\\": { @@ -423,7 +420,7 @@ describe(CredentialCache.name, () => { `); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ - ...partialOptions, + cacheFilePath: cacheFilePath, supportEditing: false }); expect(credentialCache2.tryGetCacheEntry(credentialId)).toEqual({ From aa089090634b94efe51669c44c42f449d4a9cf42 Mon Sep 17 00:00:00 2001 From: Delilah Wu Date: Wed, 4 Jun 2025 11:38:10 +1000 Subject: [PATCH 07/12] fix: Keep using `credentials.json.lock` instead of `credentials.lock` Reverts unintended change in lockfile path. --- libraries/rush-lib/src/logic/CredentialCache.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index 4d904b05b75..ebdd3c7c4bc 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -9,7 +9,8 @@ import { RushUserConfiguration } from '../api/RushUserConfiguration'; import schemaJson from '../schemas/credentials.schema.json'; import { objectsAreDeepEqual } from '../utilities/objectUtilities'; -const DEFAULT_CACHE_NAME: string = 'credentials'; +const CACHE_FILE_EXTENSION: string = '.json'; +const DEFAULT_CACHE_FILENAME: string = `credentials${CACHE_FILE_EXTENSION}`; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; interface ICredentialCacheJson { @@ -59,7 +60,7 @@ export class CredentialCache /* implements IDisposable */ { lockfile: LockFile | undefined ) { if (loadedJson && loadedJson.version !== LATEST_CREDENTIALS_JSON_VERSION) { - throw new Error(`Unexpected credentials.json file version: ${loadedJson.version}`); + throw new Error(`Unexpected ${cacheFilePath} file version: ${loadedJson.version}`); } this._cacheFilePath = cacheFilePath; @@ -70,15 +71,16 @@ export class CredentialCache /* implements IDisposable */ { public static async initializeAsync(options: ICredentialCacheOptions): Promise { let cacheDirectory: string; - let cacheName: string; + let cacheFileName: string; if (options.cacheFilePath) { cacheDirectory = path.dirname(options.cacheFilePath); - cacheName = path.basename(options.cacheFilePath, '.json'); + // Ensure .json extension + cacheFileName = path.basename(options.cacheFilePath, CACHE_FILE_EXTENSION) + CACHE_FILE_EXTENSION; } else { cacheDirectory = RushUserConfiguration.getRushUserFolderPath(); - cacheName = DEFAULT_CACHE_NAME; + cacheFileName = DEFAULT_CACHE_FILENAME; } - const cacheFilePath: string = `${cacheDirectory}/${cacheName}.json`; + const cacheFilePath: string = `${cacheDirectory}/${cacheFileName}`; const jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); @@ -93,7 +95,7 @@ export class CredentialCache /* implements IDisposable */ { let lockfile: LockFile | undefined; if (options.supportEditing) { - lockfile = await LockFile.acquireAsync(cacheDirectory, `${cacheName}.lock`); + lockfile = await LockFile.acquireAsync(cacheDirectory, `${cacheFileName}.lock`); } const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); From 66a867134b6ca95b5f76479d39dceb6520246ab8 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:08:41 -0400 Subject: [PATCH 08/12] Rush change. --- .../rush/feat-credcache-custom-paths_2025-05-30-04-23.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json b/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json index 0e4a18901af..2d208ba14fb 100644 --- a/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json +++ b/common/changes/@microsoft/rush/feat-credcache-custom-paths_2025-05-30-04-23.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Add support for custom `CredentialCache` paths", + "comment": "Update the `CredentialCache` options object to add support for custom cache file paths. This is useful if `CredentialCache` is used outside of Rush.", "type": "none" } ], From 3e5ebede8110f8d90f06535963a514f861c5d560 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:15:27 -0400 Subject: [PATCH 09/12] Make snapshots non-inline. --- .../src/logic/test/CredentialCache.test.ts | 83 +---- .../CredentialCache.test.ts.snap | 325 ++++++++++++++++++ 2 files changed, 332 insertions(+), 76 deletions(-) create mode 100644 libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index 6d9fa9f32bc..d4f8aefe109 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -158,9 +158,7 @@ describe(CredentialCache.name, () => { supportEditing: false }); expect(credentialCache.tryGetCacheEntry(credentialId)?.credential).toEqual(credentialValue); - expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toMatchInlineSnapshot( - `1970-01-01T00:00:00.100Z` - ); + expect(credentialCache.tryGetCacheEntry(credentialId)?.expires).toMatchSnapshot('expiration'); credentialCache.dispose(); }); @@ -186,13 +184,7 @@ describe(CredentialCache.name, () => { await credentialCache.saveIfModifiedAsync(); credentialCache.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": {} -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); }); it('correctly adds a new credential', async () => { @@ -209,18 +201,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\" - } - } -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -255,18 +236,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"new-test-value\\" - } - } -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -298,13 +268,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": {} -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -334,22 +298,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\", - \\"credentialMetadata\\": { - \\"a\\": 1, - \\"b\\": true - } - } - } -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -399,25 +348,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchInlineSnapshot(` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\", - \\"credentialMetadata\\": { - \\"c\\": [ - \\"a\\", - \\"b\\", - \\"c\\" - ] - } - } - } -} -" -`); + expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap new file mode 100644 index 00000000000..25f8b1d6823 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap @@ -0,0 +1,325 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly adds a new credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly deletes an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly sets credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"a\\": 1, + \\"b\\": true + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly trims expired credentials: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly updates an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"new-test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly updates credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"c\\": [ + \\"a\\", + \\"b\\", + \\"c\\" + ] + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] initializes a credential cache correctly when one exists on disk with a expired credential: expiration 1`] = `1970-01-01T00:00:00.100Z`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly adds a new credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly deletes an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly sets credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"a\\": 1, + \\"b\\": true + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly trims expired credentials: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly updates an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"new-test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] correctly updates credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"c\\": [ + \\"a\\", + \\"b\\", + \\"c\\" + ] + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with json suffix] initializes a credential cache correctly when one exists on disk with a expired credential: expiration 1`] = `1970-01-01T00:00:00.100Z`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly adds a new credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly deletes an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly sets credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"a\\": 1, + \\"b\\": true + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly trims expired credentials: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly updates an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"new-test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] correctly updates credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"c\\": [ + \\"a\\", + \\"b\\", + \\"c\\" + ] + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [custom cache path with no suffix] initializes a credential cache correctly when one exists on disk with a expired credential: expiration 1`] = `1970-01-01T00:00:00.100Z`; + +exports[`CredentialCache cache paths [default cache path] correctly adds a new credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [default cache path] correctly deletes an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [default cache path] correctly sets credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"a\\": 1, + \\"b\\": true + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [default cache path] correctly trims expired credentials: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": {} +} +" +`; + +exports[`CredentialCache cache paths [default cache path] correctly updates an existing credential: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"new-test-value\\" + } + } +} +" +`; + +exports[`CredentialCache cache paths [default cache path] correctly updates credentialMetadata: credential cache file 1`] = ` +"{ + \\"version\\": \\"0.1.0\\", + \\"cacheEntries\\": { + \\"test-credential\\": { + \\"expires\\": 0, + \\"credential\\": \\"test-value\\", + \\"credentialMetadata\\": { + \\"c\\": [ + \\"a\\", + \\"b\\", + \\"c\\" + ] + } + } + } +} +" +`; + +exports[`CredentialCache cache paths [default cache path] initializes a credential cache correctly when one exists on disk with a expired credential: expiration 1`] = `1970-01-01T00:00:00.100Z`; From 27751d17cf25469599490c8b0fb90b171d6ba131 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:22:28 -0400 Subject: [PATCH 10/12] Remove the necessity of a JSON extension. --- .../rush-lib/src/logic/CredentialCache.ts | 6 +- .../src/logic/test/CredentialCache.test.ts | 42 ++++------ .../CredentialCache.test.ts.snap | 81 ------------------- 3 files changed, 19 insertions(+), 110 deletions(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index ebdd3c7c4bc..74c045ad713 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -9,8 +9,7 @@ import { RushUserConfiguration } from '../api/RushUserConfiguration'; import schemaJson from '../schemas/credentials.schema.json'; import { objectsAreDeepEqual } from '../utilities/objectUtilities'; -const CACHE_FILE_EXTENSION: string = '.json'; -const DEFAULT_CACHE_FILENAME: string = `credentials${CACHE_FILE_EXTENSION}`; +const DEFAULT_CACHE_FILENAME: string = `credentials.json`; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; interface ICredentialCacheJson { @@ -74,8 +73,7 @@ export class CredentialCache /* implements IDisposable */ { let cacheFileName: string; if (options.cacheFilePath) { cacheDirectory = path.dirname(options.cacheFilePath); - // Ensure .json extension - cacheFileName = path.basename(options.cacheFilePath, CACHE_FILE_EXTENSION) + CACHE_FILE_EXTENSION; + cacheFileName = options.cacheFilePath.slice(cacheDirectory.length + 1); } else { cacheDirectory = RushUserConfiguration.getRushUserFolderPath(); cacheFileName = DEFAULT_CACHE_FILENAME; diff --git a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts index d4f8aefe109..fe7e92fae97 100644 --- a/libraries/rush-lib/src/logic/test/CredentialCache.test.ts +++ b/libraries/rush-lib/src/logic/test/CredentialCache.test.ts @@ -7,9 +7,8 @@ import { CredentialCache, type ICredentialCacheOptions } from '../CredentialCach const FAKE_RUSH_USER_FOLDER: string = '~/.rush-user'; -interface IPathsTestCase extends Pick { +interface IPathsTestCase extends Required> { testCaseName: string; - expectedCachePath: string; } describe(CredentialCache.name, () => { @@ -92,24 +91,17 @@ describe(CredentialCache.name, () => { describe.each([ { testCaseName: 'default cache path', - expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/credentials.json` + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/credentials.json` }, { testCaseName: 'custom cache path with no suffix', - cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name`, - expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name` }, { testCaseName: 'custom cache path with json suffix', - cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json`, - expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` - }, - { - testCaseName: 'custom cache path with custom suffix should append a json suffix', - cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.staging`, - expectedCachePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.staging.json` + cacheFilePath: `${FAKE_RUSH_USER_FOLDER}/my-cache-name.json` } - ])('cache paths [$testCaseName]', ({ cacheFilePath, expectedCachePath }) => { + ])('cache paths [$testCaseName]', ({ cacheFilePath }) => { it("initializes a credential cache correctly when one doesn't exist on disk", async () => { const credentialCache: CredentialCache = await CredentialCache.initializeAsync({ supportEditing: false @@ -121,7 +113,7 @@ describe(CredentialCache.name, () => { it('initializes a credential cache correctly when one exists on disk', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -143,7 +135,7 @@ describe(CredentialCache.name, () => { it('initializes a credential cache correctly when one exists on disk with a expired credential', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -165,7 +157,7 @@ describe(CredentialCache.name, () => { it('correctly trims expired credentials', async () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -184,7 +176,7 @@ describe(CredentialCache.name, () => { await credentialCache.saveIfModifiedAsync(); credentialCache.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); }); it('correctly adds a new credential', async () => { @@ -201,7 +193,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -216,7 +208,7 @@ describe(CredentialCache.name, () => { const credentialId: string = 'test-credential'; const credentialValue: string = 'test-value'; const newCredentialValue: string = 'new-test-value'; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -236,7 +228,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -249,7 +241,7 @@ describe(CredentialCache.name, () => { it('correctly deletes an existing credential', async () => { const credentialId: string = 'test-credential'; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -268,7 +260,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -298,7 +290,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, @@ -322,7 +314,7 @@ describe(CredentialCache.name, () => { c: ['a', 'b', 'c'] }; - fakeFilesystem[expectedCachePath] = JSON.stringify({ + fakeFilesystem[cacheFilePath] = JSON.stringify({ version: '0.1.0', cacheEntries: { [credentialId]: { @@ -348,7 +340,7 @@ describe(CredentialCache.name, () => { await credentialCache1.saveIfModifiedAsync(); credentialCache1.dispose(); - expect(fakeFilesystem[expectedCachePath]).toMatchSnapshot('credential cache file'); + expect(fakeFilesystem[cacheFilePath]).toMatchSnapshot('credential cache file'); const credentialCache2: CredentialCache = await CredentialCache.initializeAsync({ cacheFilePath: cacheFilePath, diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap index 25f8b1d6823..f0c2c1e488b 100644 --- a/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap +++ b/libraries/rush-lib/src/logic/test/__snapshots__/CredentialCache.test.ts.snap @@ -1,86 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly adds a new credential: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\" - } - } -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly deletes an existing credential: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": {} -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly sets credentialMetadata: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\", - \\"credentialMetadata\\": { - \\"a\\": 1, - \\"b\\": true - } - } - } -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly trims expired credentials: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": {} -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly updates an existing credential: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"new-test-value\\" - } - } -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] correctly updates credentialMetadata: credential cache file 1`] = ` -"{ - \\"version\\": \\"0.1.0\\", - \\"cacheEntries\\": { - \\"test-credential\\": { - \\"expires\\": 0, - \\"credential\\": \\"test-value\\", - \\"credentialMetadata\\": { - \\"c\\": [ - \\"a\\", - \\"b\\", - \\"c\\" - ] - } - } - } -} -" -`; - -exports[`CredentialCache cache paths [custom cache path with custom suffix should append a json suffix] initializes a credential cache correctly when one exists on disk with a expired credential: expiration 1`] = `1970-01-01T00:00:00.100Z`; - exports[`CredentialCache cache paths [custom cache path with json suffix] correctly adds a new credential: credential cache file 1`] = ` "{ \\"version\\": \\"0.1.0\\", From 1b490645da1c34163bcb977508fb044dfda3a274 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:24:54 -0400 Subject: [PATCH 11/12] fixup! Remove the necessity of a JSON extension. --- libraries/rush-lib/src/logic/CredentialCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index 74c045ad713..a17f3558576 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -9,7 +9,7 @@ import { RushUserConfiguration } from '../api/RushUserConfiguration'; import schemaJson from '../schemas/credentials.schema.json'; import { objectsAreDeepEqual } from '../utilities/objectUtilities'; -const DEFAULT_CACHE_FILENAME: string = `credentials.json`; +const DEFAULT_CACHE_FILENAME: 'credentials.json' = 'credentials.json'; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; interface ICredentialCacheJson { From b1bed0329d10e4a297eaf2466c0449e0af2160c7 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:25:33 -0400 Subject: [PATCH 12/12] Add a doc comment for cacheFilePath --- common/reviews/api/rush-lib.api.md | 1 - libraries/rush-lib/src/logic/CredentialCache.ts | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index f4cad01bcf7..f997aa22e05 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -437,7 +437,6 @@ export interface ICredentialCacheEntry { // @beta (undocumented) export interface ICredentialCacheOptions { - // (undocumented) cacheFilePath?: string; // (undocumented) supportEditing: boolean; diff --git a/libraries/rush-lib/src/logic/CredentialCache.ts b/libraries/rush-lib/src/logic/CredentialCache.ts index a17f3558576..5c1c775821e 100644 --- a/libraries/rush-lib/src/logic/CredentialCache.ts +++ b/libraries/rush-lib/src/logic/CredentialCache.ts @@ -39,6 +39,9 @@ export interface ICredentialCacheEntry { */ export interface ICredentialCacheOptions { supportEditing: boolean; + /** + * If specified, use the specified path instead of the default path of `~/.rush-user/credentials.json` + */ cacheFilePath?: string; }