@atproto/oauth-scopes
Version:
A library for manipulating and validating ATproto OAuth scopes in TypeScript.
324 lines (295 loc) • 11 kB
text/typescript
import { RpcPermission } from './rpc-permission.js'
describe('RpcPermission', () => {
describe('static', () => {
describe('fromString', () => {
it('should parse positional scope', () => {
const scope = RpcPermission.fromString(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(scope!.aud).toBe('did:web:example.com#service_id')
expect(scope!.lxm).toEqual(['com.example.service'])
})
it('should parse strings correctly', () => {
expect(
RpcPermission.fromString('rpc?lxm=com.example.method1&aud=*'),
).toEqual({
aud: '*',
lxm: ['com.example.method1'],
})
expect(
RpcPermission.fromString('rpc:com.example.method1?aud=*'),
).toEqual({
aud: '*',
lxm: ['com.example.method1'],
})
})
it('should render strings correctly', () => {
expect(
new RpcPermission('did:web:example.com#service_id', [
'com.example.service',
]).toString(),
).toBe('rpc:com.example.service?aud=did:web:example.com%23service_id')
expect(new RpcPermission('*', ['com.example.method1']).toString()).toBe(
'rpc:com.example.method1?aud=*',
)
expect(
new RpcPermission('did:web:example.com#service_id', [
'com.example.method1',
'com.example.method2',
]).toString(),
).toBe(
'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:web:example.com%23service_id',
)
})
it('should reject scopes without lxm', () => {
expect(
RpcPermission.fromString('rpc?aud=did:web:example.com%23service_id'),
).toBeNull()
expect(
RpcPermission.fromString('rpc:?aud=did:web:example.com%23service_id'),
).toBeNull()
})
it('should reject scopes without aud', () => {
expect(
RpcPermission.fromString('rpc?lxm=com.example.method1'),
).toBeNull()
expect(RpcPermission.fromString('rpc:com.example.method1')).toBeNull()
})
it('should reject scopes with lxm in both positional and query form', () => {
expect(
RpcPermission.fromString(
'rpc:com.example.method1?aud=did:web:example.com&lxm=com.example.method2',
),
).toBeNull()
})
it('should parse valid rpc scope with multiple lxm', () => {
const scope = RpcPermission.fromString(
'rpc?aud=*&lxm=com.example.method1&lxm=com.example.method2',
)
expect(scope).not.toBeNull()
expect(scope!.aud).toBe('*')
expect(scope!.lxm).toEqual([
'com.example.method1',
'com.example.method2',
])
})
it('should reject rpc scope without lxm', () => {
const scope = RpcPermission.fromString('rpc?aud=did:web:example.com')
expect(scope).toBeNull()
})
it('should reject rpc scope without aud', () => {
const scope = RpcPermission.fromString('rpc?lxm=com.example.method1')
expect(scope).toBeNull()
})
it('should reject any aud/any lxm', () => {
expect(RpcPermission.fromString('rpc?aud=*&lxm=*')).toBeNull()
expect(RpcPermission.fromString('rpc:*?aud=*')).toBeNull()
})
it('should reject missing aud', () => {
expect(RpcPermission.fromString('rpc:com.example.service')).toBeNull()
})
it('should reject invalid aud', () => {
expect(
RpcPermission.fromString('rpc:com.example.service?aud=invalid'),
).toBeNull()
})
it('should reject invalid lxm', () => {
expect(RpcPermission.fromString('rpc:invalid')).toBeNull()
expect(RpcPermission.fromString('rpc?lxm=invalid')).toBeNull()
})
for (const invalid of [
'rpc:*',
'invalid',
'rpc:invalid',
'rpc:invalid?aud=did:web:example.com',
'rpc:invalid?aud=did:web:example.com%23service_id',
'rpc:foo.bar',
'rpc:com.example.service?aud=did:web:example.com%23service_id&invalid=param',
'rpc:foo.bar.baz?aud=did:web',
'rpc:foo.bar.baz?aud=did:web%23service_id',
'rpc:foo.bar.baz?aud=did:plc:111',
'rpc:foo.bar.baz?aud=did:plc:111%23service_id',
'rpc:foo.bar.baz?aud=did:foo:bar',
'rpc:foo.bar.baz?aud=did:foo:bar%23service_id',
'rpc:foo.bar.baz?aud=did:web:example.com%23service_id&lxm=foo.bar.baz',
'rpc:foo.bar.baz?aud=invalid',
'rpc:foo.bar.baz?aud=invalid',
'rpc:invalid?aud=did:web:example.com',
'rpc:invalid?aud=did:web:example.com%23service_id',
'rpc:com.example.service?aud=invalid',
'notrpc:com.example.service?aud=did:web:example.com%23service_id',
'rpc?lxm=invalid&aud=invalid',
]) {
it(`should return null for invalid rpc scope: ${invalid}`, () => {
expect(RpcPermission.fromString(invalid)).toBeNull()
})
}
})
describe('scopeNeededFor', () => {
it('should return correct scope string for specific lxm and aud', () => {
const scope = RpcPermission.scopeNeededFor({
lxm: 'com.example.service',
aud: 'did:web:example.com#service_id',
})
expect(scope).toBe(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
})
it('should return scope that accepts all aud with specific lxm', () => {
const scope = RpcPermission.scopeNeededFor({
lxm: 'com.example.method1',
aud: '*',
})
expect(scope).toBe('rpc:com.example.method1?aud=*')
})
})
})
describe('instance', () => {
describe('matches', () => {
it('should match exact lxm and aud', () => {
const scope = RpcPermission.fromString(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.service',
aud: 'did:web:example.com#service_id',
}),
).toBe(true)
})
it('should not match different lxm', () => {
const scope = RpcPermission.fromString(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.OtherService',
aud: 'did:web:example.com#service_id',
}),
).toBe(false)
})
it('should not match different aud', () => {
const scope = RpcPermission.fromString(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.service',
aud: 'did:example:456#service_id',
}),
).toBe(false)
})
it('should match wildcard aud', () => {
const scope = RpcPermission.fromString('rpc:com.example.method1?aud=*')
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.method1',
aud: 'did:web:example.com#service_id',
}),
).toBe(true)
})
it('should match wildcard lxm', () => {
const scope = RpcPermission.fromString(
'rpc:*?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.method1',
aud: 'did:web:example.com#service_id',
}),
).toBe(true)
})
it('should not match different lxm with wildcard aud', () => {
const scope = RpcPermission.fromString(
'rpc:*?aud=did:web:example.com%23service_id',
)
expect(scope).not.toBeNull()
expect(
scope!.matches({
lxm: 'com.example.anyMethod',
aud: 'did:web:example.com#service_id',
}),
).toBe(true)
})
})
describe('toString', () => {
it('should format scope with lxm and aud', () => {
const scope = new RpcPermission('did:web:example.com#service_id', [
'com.example.service',
])
expect(scope.toString()).toBe(
'rpc:com.example.service?aud=did:web:example.com%23service_id',
)
})
it('should format scope with wildcard aud', () => {
const scope = new RpcPermission('*', ['com.example.method1'])
expect(scope.toString()).toBe('rpc:com.example.method1?aud=*')
})
it('should format scope with wildcard lxm', () => {
const scope = new RpcPermission('did:web:example.com#service_id', ['*'])
expect(scope.toString()).toBe(
'rpc:*?aud=did:web:example.com%23service_id',
)
})
it('simplifies lxm if one of them is "*"', () => {
const scope = new RpcPermission('did:web:example.com#service_id', [
'*',
'com.example.method1',
])
expect(scope.toString()).toBe(
'rpc:*?aud=did:web:example.com%23service_id',
)
})
})
})
describe('consistency', () => {
const testCases: { input: string; expected: string }[] = [
{
input: 'rpc:com.example.service?aud=did:web:example.com%23service_id',
expected:
'rpc:com.example.service?aud=did:web:example.com%23service_id',
},
{
input: 'rpc:com.example.service?aud=did:web:example.com#service_id',
expected:
'rpc:com.example.service?aud=did:web:example.com%23service_id',
},
{
input: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
expected: 'rpc?lxm=com.example.method1&lxm=com.example.method2&aud=*',
},
{
input:
'rpc?lxm=com.example.method1&lxm=com.example.method2&lxm=*&aud=did:web:example.com%23service_id',
expected: 'rpc:*?aud=did:web:example.com%23service_id',
},
{
input: 'rpc?aud=did:web:example.com%23foo&lxm=com.example.service',
expected: 'rpc:com.example.service?aud=did:web:example.com%23foo',
},
{
input: 'rpc?lxm=com.example.method1&aud=did:web:example.com#foo',
expected: 'rpc:com.example.method1?aud=did:web:example.com%23foo',
},
{
input: 'rpc?lxm=com.example.method1&aud=did:web:example.com%23bar',
expected: 'rpc:com.example.method1?aud=did:web:example.com%23bar',
},
{
input: 'rpc:com.example.method1?&aud=*',
expected: 'rpc:com.example.method1?aud=*',
},
]
for (const { input, expected } of testCases) {
it(`should properly re-format ${input}`, () => {
expect(RpcPermission.fromString(input)?.toString()).toBe(expected)
})
}
})
})