atproto/packages/syntax/tests/aturi.test.ts
Matthieu Sieben 61dc0d60e1
Add linting rule to sort imports (#3220)
* Add linting rule to sort imports

* remove spacing between import groups

* changeset

* changeset

* prettier config fine tuning

* forbid use of deprecated imports

* tidy
2025-02-05 15:06:58 +01:00

526 lines
17 KiB
TypeScript

import * as fs from 'node:fs'
import * as readline from 'node:readline'
import { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from '../src/index'
describe('At Uris', () => {
it('parses valid at uris', () => {
// input host path query hash
type AtUriTest = [string, string, string, string, string]
const TESTS: AtUriTest[] = [
['foo.com', 'foo.com', '', '', ''],
['at://foo.com', 'foo.com', '', '', ''],
['at://foo.com/', 'foo.com', '/', '', ''],
['at://foo.com/foo', 'foo.com', '/foo', '', ''],
['at://foo.com/foo/', 'foo.com', '/foo/', '', ''],
['at://foo.com/foo/bar', 'foo.com', '/foo/bar', '', ''],
['at://foo.com?foo=bar', 'foo.com', '', 'foo=bar', ''],
['at://foo.com?foo=bar&baz=buux', 'foo.com', '', 'foo=bar&baz=buux', ''],
['at://foo.com/?foo=bar', 'foo.com', '/', 'foo=bar', ''],
['at://foo.com/foo?foo=bar', 'foo.com', '/foo', 'foo=bar', ''],
['at://foo.com/foo/?foo=bar', 'foo.com', '/foo/', 'foo=bar', ''],
['at://foo.com#hash', 'foo.com', '', '', '#hash'],
['at://foo.com/#hash', 'foo.com', '/', '', '#hash'],
['at://foo.com/foo#hash', 'foo.com', '/foo', '', '#hash'],
['at://foo.com/foo/#hash', 'foo.com', '/foo/', '', '#hash'],
['at://foo.com?foo=bar#hash', 'foo.com', '', 'foo=bar', '#hash'],
[
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo/',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/bar',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo/bar',
'',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'foo=bar',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar&baz=buux',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'foo=bar&baz=buux',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/?foo=bar',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/',
'foo=bar',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo?foo=bar',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo',
'foo=bar',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/?foo=bar',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo/',
'foo=bar',
'',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw#hash',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'',
'#hash',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/#hash',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/',
'',
'#hash',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo#hash',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo',
'',
'#hash',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/#hash',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'/foo/',
'',
'#hash',
],
[
'at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar#hash',
'did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw',
'',
'foo=bar',
'#hash',
],
['did:web:localhost%3A1234', 'did:web:localhost%3A1234', '', '', ''],
['at://did:web:localhost%3A1234', 'did:web:localhost%3A1234', '', '', ''],
[
'at://did:web:localhost%3A1234/',
'did:web:localhost%3A1234',
'/',
'',
'',
],
[
'at://did:web:localhost%3A1234/foo',
'did:web:localhost%3A1234',
'/foo',
'',
'',
],
[
'at://did:web:localhost%3A1234/foo/',
'did:web:localhost%3A1234',
'/foo/',
'',
'',
],
[
'at://did:web:localhost%3A1234/foo/bar',
'did:web:localhost%3A1234',
'/foo/bar',
'',
'',
],
[
'at://did:web:localhost%3A1234?foo=bar',
'did:web:localhost%3A1234',
'',
'foo=bar',
'',
],
[
'at://did:web:localhost%3A1234?foo=bar&baz=buux',
'did:web:localhost%3A1234',
'',
'foo=bar&baz=buux',
'',
],
[
'at://did:web:localhost%3A1234/?foo=bar',
'did:web:localhost%3A1234',
'/',
'foo=bar',
'',
],
[
'at://did:web:localhost%3A1234/foo?foo=bar',
'did:web:localhost%3A1234',
'/foo',
'foo=bar',
'',
],
[
'at://did:web:localhost%3A1234/foo/?foo=bar',
'did:web:localhost%3A1234',
'/foo/',
'foo=bar',
'',
],
[
'at://did:web:localhost%3A1234#hash',
'did:web:localhost%3A1234',
'',
'',
'#hash',
],
[
'at://did:web:localhost%3A1234/#hash',
'did:web:localhost%3A1234',
'/',
'',
'#hash',
],
[
'at://did:web:localhost%3A1234/foo#hash',
'did:web:localhost%3A1234',
'/foo',
'',
'#hash',
],
[
'at://did:web:localhost%3A1234/foo/#hash',
'did:web:localhost%3A1234',
'/foo/',
'',
'#hash',
],
[
'at://did:web:localhost%3A1234?foo=bar#hash',
'did:web:localhost%3A1234',
'',
'foo=bar',
'#hash',
],
[
'at://4513echo.bsky.social/app.bsky.feed.post/3jsrpdyf6ss23',
'4513echo.bsky.social',
'/app.bsky.feed.post/3jsrpdyf6ss23',
'',
'',
],
]
for (const [uri, hostname, pathname, search, hash] of TESTS) {
const urip = new AtUri(uri)
expect(urip.protocol).toBe('at:')
expect(urip.host).toBe(hostname)
expect(urip.hostname).toBe(hostname)
expect(urip.origin).toBe(`at://${hostname}`)
expect(urip.pathname).toBe(pathname)
expect(urip.search).toBe(search)
expect(urip.hash).toBe(hash)
}
})
it('handles ATP-specific parsing', () => {
{
const urip = new AtUri('at://foo.com')
expect(urip.collection).toBe('')
expect(urip.rkey).toBe('')
}
{
const urip = new AtUri('at://foo.com/com.example.foo')
expect(urip.collection).toBe('com.example.foo')
expect(urip.rkey).toBe('')
}
{
const urip = new AtUri('at://foo.com/com.example.foo/123')
expect(urip.collection).toBe('com.example.foo')
expect(urip.rkey).toBe('123')
}
})
it('supports modifications', () => {
const urip = new AtUri('at://foo.com')
expect(urip.toString()).toBe('at://foo.com/')
urip.host = 'bar.com'
expect(urip.toString()).toBe('at://bar.com/')
urip.host = 'did:web:localhost%3A1234'
expect(urip.toString()).toBe('at://did:web:localhost%3A1234/')
urip.host = 'foo.com'
urip.pathname = '/'
expect(urip.toString()).toBe('at://foo.com/')
urip.pathname = '/foo'
expect(urip.toString()).toBe('at://foo.com/foo')
urip.pathname = 'foo'
expect(urip.toString()).toBe('at://foo.com/foo')
urip.collection = 'com.example.foo'
urip.rkey = '123'
expect(urip.toString()).toBe('at://foo.com/com.example.foo/123')
urip.rkey = '124'
expect(urip.toString()).toBe('at://foo.com/com.example.foo/124')
urip.collection = 'com.other.foo'
expect(urip.toString()).toBe('at://foo.com/com.other.foo/124')
urip.pathname = ''
urip.rkey = '123'
expect(urip.toString()).toBe('at://foo.com/undefined/123')
urip.pathname = 'foo'
urip.search = '?foo=bar'
expect(urip.toString()).toBe('at://foo.com/foo?foo=bar')
urip.searchParams.set('baz', 'buux')
expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux')
urip.hash = '#hash'
expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux#hash')
urip.hash = 'hash'
expect(urip.toString()).toBe('at://foo.com/foo?foo=bar&baz=buux#hash')
})
it('supports relative URIs', () => {
// input path query hash
type AtUriTest = [string, string, string, string]
const TESTS: AtUriTest[] = [
// input hostname pathname query hash
['', '', '', ''],
['/', '/', '', ''],
['/foo', '/foo', '', ''],
['/foo/', '/foo/', '', ''],
['/foo/bar', '/foo/bar', '', ''],
['?foo=bar', '', 'foo=bar', ''],
['?foo=bar&baz=buux', '', 'foo=bar&baz=buux', ''],
['/?foo=bar', '/', 'foo=bar', ''],
['/foo?foo=bar', '/foo', 'foo=bar', ''],
['/foo/?foo=bar', '/foo/', 'foo=bar', ''],
['#hash', '', '', '#hash'],
['/#hash', '/', '', '#hash'],
['/foo#hash', '/foo', '', '#hash'],
['/foo/#hash', '/foo/', '', '#hash'],
['?foo=bar#hash', '', 'foo=bar', '#hash'],
]
const BASES: string[] = [
'did:web:localhost%3A1234',
'at://did:web:localhost%3A1234',
'at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash',
'did:web:localhost%3A1234',
'at://did:web:localhost%3A1234',
'at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash',
]
for (const base of BASES) {
const basep = new AtUri(base)
for (const [relative, pathname, search, hash] of TESTS) {
const urip = new AtUri(relative, base)
expect(urip.protocol).toBe('at:')
expect(urip.host).toBe(basep.host)
expect(urip.hostname).toBe(basep.hostname)
expect(urip.origin).toBe(basep.origin)
expect(urip.pathname).toBe(pathname)
expect(urip.search).toBe(search)
expect(urip.hash).toBe(hash)
}
}
})
})
describe('AtUri validation', () => {
const expectValid = (h: string) => {
ensureValidAtUri(h)
ensureValidAtUriRegex(h)
}
const expectInvalid = (h: string) => {
expect(() => ensureValidAtUri(h)).toThrow()
expect(() => ensureValidAtUriRegex(h)).toThrow()
}
it('enforces spec basics', () => {
expectValid('at://did:plc:asdf123')
expectValid('at://user.bsky.social')
expectValid('at://did:plc:asdf123/com.atproto.feed.post')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/record')
expectValid('at://did:plc:asdf123#/frag')
expectValid('at://user.bsky.social#/frag')
expectValid('at://did:plc:asdf123/com.atproto.feed.post#/frag')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/record#/frag')
expectInvalid('a://did:plc:asdf123')
expectInvalid('at//did:plc:asdf123')
expectInvalid('at:/a/did:plc:asdf123')
expectInvalid('at:/did:plc:asdf123')
expectInvalid('AT://did:plc:asdf123')
expectInvalid('http://did:plc:asdf123')
expectInvalid('://did:plc:asdf123')
expectInvalid('at:did:plc:asdf123')
expectInvalid('at:/did:plc:asdf123')
expectInvalid('at:///did:plc:asdf123')
expectInvalid('at://:/did:plc:asdf123')
expectInvalid('at:/ /did:plc:asdf123')
expectInvalid('at://did:plc:asdf123 ')
expectInvalid('at://did:plc:asdf123/ ')
expectInvalid(' at://did:plc:asdf123')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post ')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post# ')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/ ')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#/frag ')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post#fr ag')
expectInvalid('//did:plc:asdf123')
expectInvalid('at://name')
expectInvalid('at://name.0')
expectInvalid('at://diD:plc:asdf123')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.p@st')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.p$st')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.p%st')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.p&st')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.p()t')
expectInvalid('at://did:plc:asdf123/com.atproto.feed_post')
expectInvalid('at://did:plc:asdf123/-com.atproto.feed.post')
expectInvalid('at://did:plc:asdf@123/com.atproto.feed.post')
expectInvalid('at://DID:plc:asdf123')
expectInvalid('at://user.bsky.123')
expectInvalid('at://bsky')
expectInvalid('at://did:plc:')
expectInvalid('at://did:plc:')
expectInvalid('at://frag')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(800))
expectInvalid(
'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200),
)
})
it('has specified behavior on edge cases', () => {
expectInvalid('at://user.bsky.social//')
expectInvalid('at://user.bsky.social//com.atproto.feed.post')
expectInvalid('at://user.bsky.social/com.atproto.feed.post//')
expectInvalid(
'at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more',
)
expectInvalid('at://did:plc:asdf123/short/stuff')
expectInvalid('at://did:plc:asdf123/12345')
})
it('enforces no trailing slashes', () => {
expectValid('at://did:plc:asdf123')
expectInvalid('at://did:plc:asdf123/')
expectValid('at://user.bsky.social')
expectInvalid('at://user.bsky.social/')
expectValid('at://did:plc:asdf123/com.atproto.feed.post')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/record')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/record/#/frag')
})
it('enforces strict paths', () => {
expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123')
expectInvalid('at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf')
})
it('is very permissive about record keys', () => {
expectValid('at://did:plc:asdf123/com.atproto.feed.post/asdf123')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/a')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%23')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123')
expectValid("at://did:plc:asdf123/com.atproto.feed.post/~'sdf123")
expectValid('at://did:plc:asdf123/com.atproto.feed.post/$')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/@')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/!')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/*')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/(')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/,')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/;')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/abc%30123')
})
it('is probably too permissive about URL encoding', () => {
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%30')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%3')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%zz')
expectValid('at://did:plc:asdf123/com.atproto.feed.post/%%%')
})
it('is very permissive about fragments', () => {
expectValid('at://did:plc:asdf123#/frac')
expectInvalid('at://did:plc:asdf123#')
expectInvalid('at://did:plc:asdf123##')
expectInvalid('#at://did:plc:asdf123')
expectInvalid('at://did:plc:asdf123#/asdf#/asdf')
expectValid('at://did:plc:asdf123#/com.atproto.feed.post')
expectValid('at://did:plc:asdf123#/com.atproto.feed.post/')
expectValid('at://did:plc:asdf123#/asdf/')
expectValid('at://did:plc:asdf123/com.atproto.feed.post#/$@!*():,;~.sdf123')
expectValid('at://did:plc:asdf123#/[asfd]')
expectValid('at://did:plc:asdf123#/$')
expectValid('at://did:plc:asdf123#/*')
expectValid('at://did:plc:asdf123#/;')
expectValid('at://did:plc:asdf123#/,')
})
it('conforms to interop valid ATURIs', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/aturi_syntax_valid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length === 0) {
return
}
expectValid(line)
})
})
// NOTE: this package is currently more permissive than spec about AT URIs, so invalid cases are not errors
})