Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 70 additions & 77 deletions Tasks/NpmAuthenticateV0/Tests/L0.Authentication.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as path from 'path';
import * as assert from 'assert';
import * as ttm from 'azure-pipelines-task-lib/mock-test';
import { TestEnvVars, TestData } from './TestConstants';
import { TestHelpers } from './TestHelpers';

describe('NpmAuthenticate L0 - Authentication', function () {
describe('NpmAuthenticate L0 - Authentication (Integration)', function () {
this.timeout(20000);

beforeEach(function () {
Expand All @@ -17,185 +16,179 @@ describe('NpmAuthenticate L0 - Authentication', function () {

describe('Internal feed authentication', function () {
it('appends auth token for a matching internal registry', async () => {
// Arrange
const npmrcPath = TestHelpers.createTempNpmrc(
`registry=${TestData.internalRegistryUrl}\nalways-auth=true`
);
// The project .npmrc and the target .npmrc are the same file here.
// The registry host (dev.azure.com) matches the collectionUri host.
const internalUrl = `${TestData.collectionUri}_packaging/TestFeed/npm/registry/`;
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${internalUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

const localRegistry = TestHelpers.buildLocalRegistry(TestData.internalRegistryUrl, 'internal-auth-token-abc');
process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = TestData.internalRegistryUrl;
process.env[TestEnvVars.localRegistries] = JSON.stringify([localRegistry]);

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertAuthAppended(tr, 'internal-auth-token-abc');
TestHelpers.assertNpmrcContains(npmrcPath, '_authToken=internal-auth-token-abc');
});

it('appends auth for each registry when multiple internal feeds are listed', async () => {
// Arrange
const url1 = 'https://pkgs.dev.azure.com/testorg/_packaging/Feed1/npm/registry/';
const url2 = 'https://pkgs.dev.azure.com/testorg/_packaging/Feed2/npm/registry/';
const npmrcPath = TestHelpers.createTempNpmrc(
`@scope1:registry=${url1}\n@scope2:registry=${url2}`
);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

const localReg1 = TestHelpers.buildLocalRegistry(url1, 'token-feed1');
const localReg2 = TestHelpers.buildLocalRegistry(url2, 'token-feed2');
process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = [url1, url2].join(';');
process.env[TestEnvVars.localRegistries] = JSON.stringify([localReg1, localReg2]);

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertAuthAppended(tr, 'token-feed1');
TestHelpers.assertAuthAppended(tr, 'token-feed2');
const appended = TestHelpers.getAppendedAuth(tr);
assert.strictEqual(appended.length, 2, 'appendToNpmrc should be called once per matching registry');
TestHelpers.assertNpmrcContains(npmrcPath, '_authToken=token-feed1');
TestHelpers.assertNpmrcContains(npmrcPath, '_authToken=token-feed2');
TestHelpers.assertNpmrcContains(npmrcPath, `_authToken=${TestData.systemAccessToken}`);
});

it('logs that credentials are being added', async () => {
// Arrange
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${TestData.internalRegistryUrl}`);
const internalUrl = `${TestData.collectionUri}_packaging/TestFeed/npm/registry/`;
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${internalUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = TestData.internalRegistryUrl;
process.env[TestEnvVars.localRegistries] = JSON.stringify([
TestHelpers.buildLocalRegistry(TestData.internalRegistryUrl, 'some-token')
]);

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputContains(tr, 'AddingLocalCredentials');
});
});

describe('External service connection authentication', function () {
it('appends auth token for a matching external registry', async () => {
// Arrange
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${TestData.externalRegistryUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = TestData.externalRegistryUrl;
process.env[TestEnvVars.customEndpoint] = TestData.externalEndpointId;
process.env[TestEnvVars.externalRegistryUrl] = TestData.externalRegistryUrl;
process.env[TestEnvVars.externalRegistryToken] = TestData.externalRegistryToken;
// No local registries — the external endpoint is the only auth source

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertAuthAppended(tr, TestData.externalRegistryToken);
TestHelpers.assertNpmrcContains(npmrcPath, `_authToken=${TestData.externalRegistryToken}`);
});

it('logs that endpoint credentials are being added', async () => {
// Arrange
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${TestData.externalRegistryUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = TestData.externalRegistryUrl;
process.env[TestEnvVars.customEndpoint] = TestData.externalEndpointId;
process.env[TestEnvVars.externalRegistryUrl] = TestData.externalRegistryUrl;
process.env[TestEnvVars.externalRegistryToken] = TestData.externalRegistryToken;

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputContains(tr, 'AddingEndpointCredentials');
});
});

describe('Unmatched registry', function () {
it('ignores registry when no auth source matches', async () => {
// Arrange: .npmrc has a registry that has no matching local or external auth source
const unmatchedUrl = 'https://registry.npmjs.org/';
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${unmatchedUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = unmatchedUrl;
// No localRegistries, no customEndpoint — nothing matches

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertNoAuthAppended(tr, 'appendToNpmrc should not be called for unmatched registry');
TestHelpers.assertOutputContains(tr, 'IgnoringRegistry');
TestHelpers.assertNpmrcNotContains(npmrcPath, '_authToken');
});

it('succeeds with no auth when .npmrc has no registries', async () => {
// Arrange: completely empty .npmrc
const npmrcPath = TestHelpers.createTempNpmrc('');
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
// npmrcRegistries not set → mock returns empty list

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertNoAuthAppended(tr);
TestHelpers.assertNpmrcNotContains(npmrcPath, '_authToken');
});
});

describe('Duplicate endpoint detection', function () {
it('warns when external endpoint was already registered in a prior task run', async () => {
// Arrange: EXISTING_ENDPOINTS already contains the external registry URL,
// simulating a previous NpmAuthenticate task run in the same pipeline job.
const npmrcPath = TestHelpers.createTempNpmrc(`registry=${TestData.externalRegistryUrl}`);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.npmrcRegistries] = TestData.externalRegistryUrl;
process.env[TestEnvVars.customEndpoint] = TestData.externalEndpointId;
process.env[TestEnvVars.externalRegistryUrl] = TestData.externalRegistryUrl;
process.env[TestEnvVars.externalRegistryToken] = TestData.externalRegistryToken;
// Seed the already-seen endpoints list so the task sees a duplicate
process.env[TestEnvVars.existingEndpoints] = TestData.externalRegistryUrl;

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertWarningIssue(tr, 'DuplicateCredentials',
'Task should warn when the same endpoint is registered twice in the same job');
});
});

describe('Checked-in credentials', function () {
it('warns and replaces when .npmrc has checked-in credentials', async () => {
// Arrange: .npmrc has an external registry with pre-existing auth lines
// (a common user mistake — committing tokens to source control).
// The task should warn about overriding them and write fresh auth.
const npmrcContent = [
`registry=${TestData.externalRegistryUrl}`,
`//registry.example.com/npm/:_authToken=old-checked-in-token`,
`//registry.example.com/npm/:always-auth=true`
].join('\n');
const npmrcPath = TestHelpers.createTempNpmrc(npmrcContent);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.customEndpoint] = TestData.externalEndpointId;
process.env[TestEnvVars.externalRegistryUrl] = TestData.externalRegistryUrl;
process.env[TestEnvVars.externalRegistryToken] = TestData.externalRegistryToken;

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
// Fresh token should be written, replacing the checked-in one
TestHelpers.assertNpmrcContains(npmrcPath, TestData.externalRegistryToken);
});
});

describe('Mixed internal and external registries', function () {
it('authenticates both internal and external registries in the same .npmrc', async () => {
// Arrange: .npmrc has two registries — one internal (matches collectionUri host)
// and one external (resolved via customEndpoint mock)
const internalUrl = `${TestData.collectionUri}_packaging/InternalFeed/npm/registry/`;
const npmrcContent = [
`@internal:registry=${internalUrl}`,
`@external:registry=${TestData.externalRegistryUrl}`
].join('\n');
const npmrcPath = TestHelpers.createTempNpmrc(npmrcContent);
const tp = path.join(__dirname, 'TestSetup.js');
const tr = new ttm.MockTestRunner(tp);

process.env[TestEnvVars.npmrcPath] = npmrcPath;
process.env[TestEnvVars.customEndpoint] = TestData.externalEndpointId;
process.env[TestEnvVars.externalRegistryUrl] = TestData.externalRegistryUrl;
process.env[TestEnvVars.externalRegistryToken] = TestData.externalRegistryToken;

// Act
await tr.runAsync();

// Assert
TestHelpers.assertSuccess(tr);
// Internal feed should get System.AccessToken
TestHelpers.assertNpmrcContains(npmrcPath, TestData.systemAccessToken);
// External feed should get the endpoint token
TestHelpers.assertNpmrcContains(npmrcPath, TestData.externalRegistryToken);
// Both credential sources should be logged
TestHelpers.assertOutputContains(tr, 'AddingLocalCredentials');
TestHelpers.assertOutputContains(tr, 'AddingEndpointCredentials');
});
});
});
36 changes: 21 additions & 15 deletions Tasks/NpmAuthenticateV0/Tests/L0.Cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import { TestHelpers } from './TestHelpers';
// ── Helpers ───────────────────────────────────────────────────────────────────

/**
* Create a SAVE_NPMRC_PATH directory containing an index.json whose entry for
* `npmrcPath` maps to a placeholder original content string.
* The directory is tracked via TestHelpers so afterEach() cleans it up.
* Create a SAVE_NPMRC_PATH directory containing an index.json and a backup file
* in the format NpmrcBackupManager expects: { "index": 1, "<npmrcPath>": 0 }
* with a file named "0" holding the original .npmrc content.
*/
function createSaveDir(npmrcPath: string): string {
function createSaveDir(npmrcPath: string, originalContent: string = 'original=https://registry.npmjs.org/\n'): string {
const saveDir = TestHelpers.createTempDir('npm-auth-save-');
const indexContent: { [key: string]: string } = {};
indexContent[npmrcPath] = 'original=https://registry.npmjs.org/\n';
fs.writeFileSync(path.join(saveDir, 'index.json'), JSON.stringify(indexContent), 'utf8');
const index: { [key: string]: number } = { index: 1 };
index[npmrcPath] = 0;
fs.writeFileSync(path.join(saveDir, 'index.json'), JSON.stringify(index), 'utf8');
// Create the backup file that restoreBackedUpFile() will copy back
fs.writeFileSync(path.join(saveDir, '0'), originalContent, 'utf8');
return saveDir;
}

Expand All @@ -34,8 +36,9 @@ describe('NpmAuthenticate L0 - Cleanup', function () {

it('restores the .npmrc when index.json and working file both exist', async () => {
// Arrange
const originalContent = 'registry=https://registry.npmjs.org/\n';
const npmrcPath = TestHelpers.createTempNpmrc('modified-by-task');
const saveDir = createSaveDir(npmrcPath);
const saveDir = createSaveDir(npmrcPath, originalContent);
const tp = path.join(__dirname, 'TestSetupCleanup.js');
const tr = new ttm.MockTestRunner(tp);

Expand All @@ -47,8 +50,10 @@ describe('NpmAuthenticate L0 - Cleanup', function () {

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputContains(tr, 'RESTORE_FILE_CALLED');
TestHelpers.assertOutputContains(tr, 'loc_mock_RevertedChangesToNpmrc');
// Verify the file was physically restored
const restoredContent = fs.readFileSync(npmrcPath, 'utf8');
TestHelpers.assertNpmrcContains(npmrcPath, 'registry=https://registry.npmjs.org/');
});

it('logs NoIndexJsonFile when index.json is missing from SAVE_NPMRC_PATH', async () => {
Expand All @@ -67,7 +72,7 @@ describe('NpmAuthenticate L0 - Cleanup', function () {

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputNotContains(tr, 'RESTORE_FILE_CALLED');
TestHelpers.assertOutputNotContains(tr, 'loc_mock_RevertedChangesToNpmrc');
TestHelpers.assertOutputContains(tr, 'loc_mock_NoIndexJsonFile');
});

Expand All @@ -87,14 +92,16 @@ describe('NpmAuthenticate L0 - Cleanup', function () {

// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputNotContains(tr, 'RESTORE_FILE_CALLED');
TestHelpers.assertOutputNotContains(tr, 'loc_mock_RevertedChangesToNpmrc');
TestHelpers.assertOutputContains(tr, 'loc_mock_NoIndexJsonFile');
});

it('removes the temp directory when SAVE_NPMRC_PATH contains only the index file', async () => {
// Arrange — save dir has exactly 1 file (index.json) so the rmRF branch is reached
// Arrange — after restore, only index.json remains so the rmRF branch triggers.
// createSaveDir adds both index.json and the backup file "0".
// After restoreBackedUpFile runs, it deletes "0", leaving only index.json.
const npmrcPath = TestHelpers.createTempNpmrc('modified-by-task');
const saveDir = createSaveDir(npmrcPath); // 1 file: index.json
const saveDir = createSaveDir(npmrcPath);
const tempDir = TestHelpers.createTempDir('npm-auth-temp-');
const tp = path.join(__dirname, 'TestSetupCleanup.js');
const tr = new ttm.MockTestRunner(tp);
Expand All @@ -107,9 +114,8 @@ describe('NpmAuthenticate L0 - Cleanup', function () {
// Act
await tr.runAsync();

// Assert — task reaches the rmRF branch without throwing
// Assert
TestHelpers.assertSuccess(tr);
TestHelpers.assertOutputContains(tr, 'RESTORE_FILE_CALLED');
TestHelpers.assertOutputContains(tr, 'loc_mock_RevertedChangesToNpmrc');
});
});
Loading