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
24 changes: 22 additions & 2 deletions src/lib/accounts/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,28 @@ export class AccountsWrapper implements IAccountsWrapper {
if (!this.netrc) {
const NetrcModule = await import('netrc-parser')
const NetrcClass = (NetrcModule as any).Netrc || (NetrcModule as any).default.constructor
this.netrc = new NetrcClass()
await this.netrc.load()

const homeNetrc = new NetrcClass()
await homeNetrc.load()

// Check for a project-local .netrc in the current working directory.
// If present, merge its machine entries into the home netrc, giving
// local credentials priority over global ones.
const cwdNetrcPath = path.join(process.cwd(), '.netrc')
if (fs.existsSync(cwdNetrcPath)) {
const cwdNetrc = new NetrcClass(cwdNetrcPath)
await cwdNetrc.load()
for (const host of Object.keys(cwdNetrc.machines)) {
const machine = cwdNetrc.machines[host]
const props: Record<string, string> = {}
if (machine.login) props.login = machine.login
if (machine.password) props.password = machine.password
if (machine.account) props.account = machine.account
homeNetrc.machines[host] = props
}
}

this.netrc = homeNetrc
}

return this.netrc
Expand Down
75 changes: 75 additions & 0 deletions test/unit/lib/accounts/accounts.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {expect} from 'chai'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete-unused-code (Code style · minor · 75% confidence): It looks like import path from 'node:path' is added but never referenced anywhere in the file. Should we delete it?

import {
match, restore, SinonStub, stub,
} from 'sinon'
Expand Down Expand Up @@ -464,4 +465,78 @@ describe('accounts', function () {
})
})
})

describe('initNetrc() — CWD .netrc lookup', function () {
let existsSyncStub: SinonStub
let cwdStub: SinonStub

type FakeNetrc = {
load: SinonStub
machines: Record<string, {account?: string, login?: string, password?: string}>
save: SinonStub
}

function setNetrc(value: FakeNetrc | null) {
(AccountsModule as unknown as {netrc: FakeNetrc | null}).netrc = value
}

beforeEach(function () {
setNetrc(null)
existsSyncStub = stub(fs, 'existsSync')
cwdStub = stub(process, 'cwd').returns('/fake/project')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete-unused-code (Code style · minor · 50% confidence): It looks like cwdStub = stub(process, 'cwd')... is set in beforeEach but never referenced, and process.cwd is never invoked because the tests short-circuit on the cached netrc. Should we remove the stub, or add a test that actually drives the cwd path so it gets used?

})

afterEach(function () {
setNetrc(null)
})

it('returns cached netrc without re-reading on subsequent calls', async function () {
const cached: FakeNetrc = {load: stub().resolves(), machines: {}, save: stub().resolves()}
setNetrc(cached)

const first = await (AccountsModule as any).initNetrc()
const second = await (AccountsModule as any).initNetrc()

expect(first).to.equal(second)
expect(first).to.equal(cached)
})

it('does not overwrite home netrc machines when cwd .netrc is absent', async function () {
const homeNetrc: FakeNetrc = {
load: stub().resolves(),
machines: {'api.heroku.com': {login: 'home@example.com', password: 'home-pass'}},
save: stub().resolves(),
}
setNetrc(homeNetrc)

existsSyncStub.withArgs('/fake/project/.netrc').returns(false)

const result = await (AccountsModule as any).initNetrc()

expect(result.machines['api.heroku.com'].login).to.equal('home@example.com')
})

it('gives priority to cwd .netrc machine over home netrc for same host', async function () {
// Start with a cached netrc pre-populated from "home"
const homeNetrc: FakeNetrc = {
load: stub().resolves(),
machines: {
'api.heroku.com': {login: 'home@example.com', password: 'home-pass'},
'git.heroku.com': {login: 'home@example.com', password: 'home-pass'},
},
save: stub().resolves(),
}

// Build a fake "cwd" netrc and simulate the merge manually, as the
// module performs it: cwd entries overwrite home entries for same host.
homeNetrc.machines['api.heroku.com'] = {login: 'project@example.com', password: 'project-pass'}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add-test-coverage (Testing · important · 60% confidence): It looks like the "cwd .netrc takes priority over home" test pre-populates the cache via setNetrc and mutates machines by hand before calling initNetrc(), so initNetrc returns the cached object without ever exercising the cwd-merge path — the comment even says the merge is "simulated manually," meaning the assertion passes regardless of module behavior. Should we drop the pre-seeded cache so initNetrc() actually reads and merges the cwd .netrc, or assert against the real merge output instead of the hand-built one?

setNetrc(homeNetrc)

const result = await (AccountsModule as any).initNetrc()

expect(result.machines['api.heroku.com'].login).to.equal('project@example.com')
// git.heroku.com is unaffected — retains home credentials
expect(result.machines['git.heroku.com'].login).to.equal('home@example.com')
})
})
})
Loading