-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support npm access command (#436)
> Supports partial npm access query commands. #64 * The following commands are supported: * `npm access list packages [<user>|<scope>|<scope:team> [<package>]` * `npm access list collaborators [<package> [<user>]]` * Added `/-/package/:fullname/collaborators` and `/-/org/:username/package` interfaces. * Error code logic is consistent with the npm registry. -------------- > 支持部分 npm access 查询命令 #64 * 支持如下命令: * `npm access list packages [<user>|<scope>|<scope:team> [<package>]` * `npm access list collaborators [<package> [<user>]]` * 新增 `/-/package/:fullname/collaborators` 及 `/-/org/:username/package` 接口 * 错误码逻辑和 npm registry 保持一致
- Loading branch information
Showing
7 changed files
with
304 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { | ||
HTTPController, | ||
HTTPMethod, | ||
HTTPMethodEnum, | ||
HTTPParam, | ||
} from '@eggjs/tegg'; | ||
import { AbstractController } from './AbstractController'; | ||
import { FULLNAME_REG_STRING, getFullname, getScopeAndName } from '../../common/PackageUtil'; | ||
import { PackageAccessLevel } from '../../common/constants'; | ||
import { ForbiddenError, NotFoundError } from 'egg-errors'; | ||
|
||
@HTTPController() | ||
export class AccessController extends AbstractController { | ||
@HTTPMethod({ | ||
path: `/-/package/:fullname(${FULLNAME_REG_STRING})/collaborators`, | ||
method: HTTPMethodEnum.GET, | ||
}) | ||
async listCollaborators(@HTTPParam() fullname: string) { | ||
const [ scope, name ] = getScopeAndName(fullname); | ||
const pkg = await this.packageRepository.findPackage(scope, name); | ||
// return 403 if pkg not exists | ||
if (!pkg) { | ||
throw new ForbiddenError('Forbidden'); | ||
} | ||
|
||
const maintainers = await this.packageRepository.listPackageMaintainers(pkg!.packageId); | ||
const res: Record<string, string> = {}; | ||
maintainers.forEach(maintainer => { | ||
res[maintainer.displayName] = PackageAccessLevel.write; | ||
}); | ||
|
||
return res; | ||
} | ||
|
||
@HTTPMethod({ | ||
path: '/-/org/:username/package', | ||
method: HTTPMethodEnum.GET, | ||
}) | ||
async listPackagesByUser(@HTTPParam() username: string) { | ||
const user = await this.userRepository.findUserByName(username); | ||
if (!user) { | ||
throw new NotFoundError(`User "${username}" not found`); | ||
} | ||
|
||
const pkgs = await this.packageRepository.listPackagesByUserId(user.userId); | ||
const res: Record<string, string> = {}; | ||
pkgs.forEach(pkg => { | ||
res[getFullname(pkg.scope, pkg.name)] = PackageAccessLevel.write; | ||
}); | ||
|
||
return res; | ||
} | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// import assert from 'assert'; | ||
import path from 'path'; | ||
import { app } from 'egg-mock/bootstrap'; | ||
import coffee from 'coffee'; | ||
import semver from 'semver'; | ||
import { TestUtil } from 'test/TestUtil'; | ||
import { npmLogin } from '../CliUtil'; | ||
|
||
describe('test/cli/npm/access.test.ts', () => { | ||
let server; | ||
let registry; | ||
let fooPkgDir; | ||
let demoDir; | ||
let userconfig; | ||
let cacheDir; | ||
let useLegacyCommands; | ||
before(async () => { | ||
cacheDir = TestUtil.mkdtemp(); | ||
fooPkgDir = TestUtil.getFixtures('@cnpm/foo'); | ||
demoDir = TestUtil.getFixtures('demo'); | ||
userconfig = path.join(fooPkgDir, '.npmrc'); | ||
await TestUtil.rm(userconfig); | ||
await TestUtil.rm(path.join(demoDir, 'node_modules')); | ||
const npmVersion = await TestUtil.getNpmVersion(); | ||
useLegacyCommands = semver.lt(String(npmVersion), '9.0.0'); | ||
await new Promise(resolve => { | ||
server = app.listen(0, () => { | ||
registry = `http://localhost:${server.address().port}`; | ||
console.log(`registry ${registry} ready`); | ||
resolve(void 0); | ||
}); | ||
}); | ||
}); | ||
|
||
after(async () => { | ||
await TestUtil.rm(userconfig); | ||
await TestUtil.rm(cacheDir); | ||
await TestUtil.rm(path.join(demoDir, 'node_modules')); | ||
server && server.close(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
await npmLogin(registry, userconfig); | ||
await coffee | ||
.spawn('npm', [ | ||
'publish', | ||
`--registry=${registry}`, | ||
`--userconfig=${userconfig}`, | ||
`--cache=${cacheDir}`, | ||
], { | ||
cwd: fooPkgDir, | ||
}) | ||
.debug() | ||
.expect('code', 0) | ||
.end(); | ||
}); | ||
|
||
describe('npm access', () => { | ||
|
||
it('should work for list collaborators', async () => { | ||
const subCommands = useLegacyCommands ? [ 'ls-collaborators' ] : [ 'list', 'collaborators' ]; | ||
await coffee | ||
.spawn('npm', [ | ||
'access', | ||
...subCommands, | ||
'@cnpm/foo', | ||
`--registry=${registry}`, | ||
`--userconfig=${userconfig}`, | ||
`--cache=${cacheDir}`, | ||
// '--json', | ||
], { | ||
cwd: demoDir, | ||
}) | ||
.debug() | ||
.expect('stdout', /testuser:\sread-write|\"testuser\":\s\"read-write\"/) | ||
.expect('code', 0) | ||
.end(); | ||
|
||
}); | ||
|
||
it('should work for list all packages', async () => { | ||
const subCommands = useLegacyCommands ? [ 'ls-packages' ] : [ 'list', 'packages' ]; | ||
await coffee | ||
.spawn('npm', [ | ||
'access', | ||
...subCommands, | ||
'testuser', | ||
`--registry=${registry}`, | ||
`--userconfig=${userconfig}`, | ||
`--cache=${cacheDir}`, | ||
// '--json', | ||
], { | ||
cwd: demoDir, | ||
}) | ||
.debug() | ||
.expect('stdout', /@cnpm\/foo: read-write|\"@cnpm\/foo\":\s\"read-write"/) | ||
.expect('code', 0) | ||
.end(); | ||
|
||
}); | ||
|
||
it('should work for list single package', async () => { | ||
|
||
// not support in npm7 * 8 | ||
if (useLegacyCommands) { | ||
console.log('npm list packages user package not implement lt 9.0.0, just skip'); | ||
return; | ||
} | ||
await coffee | ||
.spawn('npm', [ | ||
'access', | ||
'list', | ||
'packages', | ||
'testuser', | ||
'@cnpm/foo', | ||
`--registry=${registry}`, | ||
`--userconfig=${userconfig}`, | ||
`--cache=${cacheDir}`, | ||
// '--json', | ||
], { | ||
cwd: demoDir, | ||
}) | ||
.debug() | ||
.expect('stdout', /@cnpm\/foo: read-write/) | ||
.expect('code', 0) | ||
.end(); | ||
|
||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
test/port/controller/AccessController/listCollaborators.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import assert from 'assert'; | ||
import { app } from 'egg-mock/bootstrap'; | ||
import { TestUtil } from 'test/TestUtil'; | ||
|
||
describe('test/port/controller/AccessController/listCollaborators.test.ts', () => { | ||
describe('[GET /-/package/:fullname/collaborators] listCollaborators()', () => { | ||
|
||
it('should work', async () => { | ||
const { pkg } = await TestUtil.createPackage({ version: '1.0.0' }, { name: 'banana-owner' }); | ||
const res = await app.httpRequest() | ||
.get(`/-/package/${pkg.name}/collaborators`) | ||
.expect(200); | ||
|
||
assert(res.body['banana-owner'] === 'write'); | ||
}); | ||
|
||
it('should 403 when pkg not exists', async () => { | ||
const res = await app.httpRequest() | ||
.get('/-/package/banana/collaborators') | ||
.expect(403); | ||
assert.equal(res.body.error, '[FORBIDDEN] Forbidden'); | ||
}); | ||
|
||
it('should refresh when maintainer updated', async () => { | ||
const owner = await TestUtil.createUser({ name: 'banana-owner' }); | ||
const maintainer = await TestUtil.createUser({ name: 'banana-maintainer' }); | ||
|
||
|
||
// create pkg | ||
const pkg = await TestUtil.getFullPackage({ name: '@cnpm/banana' }); | ||
const createRes = await app.httpRequest() | ||
.put(`/${pkg.name}`) | ||
.set('authorization', owner.authorization) | ||
.set('user-agent', owner.ua) | ||
.send(pkg) | ||
.expect(201); | ||
assert.equal(createRes.body.ok, true); | ||
assert.match(createRes.body.rev, /^\d+\-\w{24}$/); | ||
const rev = createRes.body.rev; | ||
|
||
// updateMaintainers | ||
await app.httpRequest() | ||
.put(`/${pkg.name}/-rev/${rev}`) | ||
.set('authorization', owner.authorization) | ||
.set('user-agent', owner.ua) | ||
.set('npm-command', 'owner') | ||
.send({ | ||
...pkg, | ||
maintainers: [{ name: maintainer.name, email: maintainer.email }, { name: owner.name, email: owner.email }], | ||
}) | ||
.expect(200); | ||
|
||
const res = await app.httpRequest() | ||
.get(`/-/package/${pkg.name}/collaborators`) | ||
.expect(200); | ||
|
||
assert(res.body['banana-owner'] === 'write'); | ||
assert(res.body['banana-maintainer'] === 'write'); | ||
}); | ||
|
||
}); | ||
}); |
25 changes: 25 additions & 0 deletions
25
test/port/controller/AccessController/listPackagesByUser.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import assert from 'assert'; | ||
import { app } from 'egg-mock/bootstrap'; | ||
import { TestUtil } from 'test/TestUtil'; | ||
|
||
describe('test/port/controller/AccessController/listPackagesByUser.test.ts', () => { | ||
describe('[GET /-/org/:username/package] listPackagesByUser()', () => { | ||
|
||
it('should work', async () => { | ||
const { pkg } = await TestUtil.createPackage({ version: '1.0.0' }, { name: 'banana' }); | ||
const res = await app.httpRequest() | ||
.get('/-/org/banana/package') | ||
.expect(200); | ||
|
||
assert.equal(res.body[pkg.name], 'write'); | ||
}); | ||
|
||
it('should 404 when user not exists', async () => { | ||
const res = await app.httpRequest() | ||
.get('/-/org/banana-disappear/package') | ||
.expect(404); | ||
assert.equal(res.body.error, '[NOT_FOUND] User "banana-disappear" not found'); | ||
}); | ||
|
||
}); | ||
}); |