| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- import { createPathTagFunction, encodeURIPath } from '@opencode-ai/sdk/internal/utils/path';
- import { inspect } from 'node:util';
- import { runInNewContext } from 'node:vm';
- describe('path template tag function', () => {
- test('validates input', () => {
- const testParams = ['', '.', '..', 'x', '%2e', '%2E', '%2e%2e', '%2E%2e', '%2e%2E', '%2E%2E'];
- const testCases = [
- ['/path_params/', '/a'],
- ['/path_params/', '/'],
- ['/path_params/', ''],
- ['', '/a'],
- ['', '/'],
- ['', ''],
- ['a'],
- [''],
- ['/path_params/', ':initiate'],
- ['/path_params/', '.json'],
- ['/path_params/', '?beta=true'],
- ['/path_params/', '.?beta=true'],
- ['/path_params/', '/', '/download'],
- ['/path_params/', '-', '/download'],
- ['/path_params/', '', '/download'],
- ['/path_params/', '.', '/download'],
- ['/path_params/', '..', '/download'],
- ['/plain/path'],
- ];
- function paramPermutations(len: number): string[][] {
- if (len === 0) return [];
- if (len === 1) return testParams.map((e) => [e]);
- const rest = paramPermutations(len - 1);
- return testParams.flatMap((e) => rest.map((r) => [e, ...r]));
- }
- // We need to test how %2E is handled, so we use a custom encoder that does no escaping.
- const rawPath = createPathTagFunction((s) => s);
- const emptyObject = {};
- const mathObject = Math;
- const numberObject = new Number();
- const stringObject = new String();
- const basicClass = new (class {})();
- const classWithToString = new (class {
- toString() {
- return 'ok';
- }
- })();
- // Invalid values
- expect(() => rawPath`/a/${null}/b`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Null is not a valid path parameter\n' +
- '/a/null/b\n' +
- ' ^^^^',
- );
- expect(() => rawPath`/a/${undefined}/b`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Undefined is not a valid path parameter\n' +
- '/a/undefined/b\n' +
- ' ^^^^^^^^^',
- );
- expect(() => rawPath`/a/${emptyObject}/b`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Object is not a valid path parameter\n' +
- '/a/[object Object]/b\n' +
- ' ^^^^^^^^^^^^^^^',
- );
- expect(() => rawPath`?${mathObject}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Math is not a valid path parameter\n' +
- '?[object Math]\n' +
- ' ^^^^^^^^^^^^^',
- );
- expect(() => rawPath`/${basicClass}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Object is not a valid path parameter\n' +
- '/[object Object]\n' +
- ' ^^^^^^^^^^^^^^',
- );
- expect(() => rawPath`/../${''}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value ".." can\'t be safely passed as a path parameter\n' +
- '/../\n' +
- ' ^^',
- );
- expect(() => rawPath`/../${{}}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value ".." can\'t be safely passed as a path parameter\n' +
- 'Value of type Object is not a valid path parameter\n' +
- '/../[object Object]\n' +
- ' ^^ ^^^^^^^^^^^^^^',
- );
- // Valid values
- expect(rawPath`/${0}`).toBe('/0');
- expect(rawPath`/${''}`).toBe('/');
- expect(rawPath`/${numberObject}`).toBe('/0');
- expect(rawPath`${stringObject}/`).toBe('/');
- expect(rawPath`/${classWithToString}`).toBe('/ok');
- // We need to check what happens with cross-realm values, which we might get from
- // Jest or other frames in a browser.
- const newRealm = runInNewContext('globalThis');
- expect(newRealm.Object).not.toBe(Object);
- const crossRealmObject = newRealm.Object();
- const crossRealmMathObject = newRealm.Math;
- const crossRealmNumber = new newRealm.Number();
- const crossRealmString = new newRealm.String();
- const crossRealmClass = new (class extends newRealm.Object {})();
- const crossRealmClassWithToString = new (class extends newRealm.Object {
- toString() {
- return 'ok';
- }
- })();
- // Invalid cross-realm values
- expect(() => rawPath`/a/${crossRealmObject}/b`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Object is not a valid path parameter\n' +
- '/a/[object Object]/b\n' +
- ' ^^^^^^^^^^^^^^^',
- );
- expect(() => rawPath`?${crossRealmMathObject}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Math is not a valid path parameter\n' +
- '?[object Math]\n' +
- ' ^^^^^^^^^^^^^',
- );
- expect(() => rawPath`/${crossRealmClass}`).toThrow(
- 'Path parameters result in path with invalid segments:\n' +
- 'Value of type Object is not a valid path parameter\n' +
- '/[object Object]\n' +
- ' ^^^^^^^^^^^^^^^',
- );
- // Valid cross-realm values
- expect(rawPath`/${crossRealmNumber}`).toBe('/0');
- expect(rawPath`${crossRealmString}/`).toBe('/');
- expect(rawPath`/${crossRealmClassWithToString}`).toBe('/ok');
- const results: {
- [pathParts: string]: {
- [params: string]: { valid: boolean; result?: string; error?: string };
- };
- } = {};
- for (const pathParts of testCases) {
- const pathResults: Record<string, { valid: boolean; result?: string; error?: string }> = {};
- results[JSON.stringify(pathParts)] = pathResults;
- for (const params of paramPermutations(pathParts.length - 1)) {
- const stringRaw = String.raw({ raw: pathParts }, ...params);
- const plainString = String.raw(
- { raw: pathParts.map((e) => e.replace(/\./g, 'x')) },
- ...params.map((e) => 'X'.repeat(e.length)),
- );
- const normalizedStringRaw = new URL(stringRaw, 'https://example.com').href;
- const normalizedPlainString = new URL(plainString, 'https://example.com').href;
- const pathResultsKey = JSON.stringify(params);
- try {
- const result = rawPath(pathParts, ...params);
- expect(result).toBe(stringRaw);
- // there are no special segments, so the length of the normalized path is
- // equal to the length of the normalized plain path.
- expect(normalizedStringRaw.length).toBe(normalizedPlainString.length);
- pathResults[pathResultsKey] = {
- valid: true,
- result,
- };
- } catch (e) {
- const error = String(e);
- expect(error).toMatch(/Path parameters result in path with invalid segment/);
- // there are special segments, so the length of the normalized path is
- // different than the length of the normalized plain path.
- expect(normalizedStringRaw.length).not.toBe(normalizedPlainString.length);
- pathResults[pathResultsKey] = {
- valid: false,
- error,
- };
- }
- }
- }
- expect(results).toMatchObject({
- '["/path_params/","/a"]': {
- '["x"]': { valid: true, result: '/path_params/x/a' },
- '[""]': { valid: true, result: '/path_params//a' },
- '["%2E%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E%2e/a\n' +
- ' ^^^^^^',
- },
- '["%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E/a\n' +
- ' ^^^',
- },
- },
- '["/path_params/","/"]': {
- '["x"]': { valid: true, result: '/path_params/x/' },
- '[""]': { valid: true, result: '/path_params//' },
- '["%2e%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2e%2E/\n' +
- ' ^^^^^^',
- },
- '["%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2e" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2e/\n' +
- ' ^^^',
- },
- },
- '["/path_params/",""]': {
- '[""]': { valid: true, result: '/path_params/' },
- '["x"]': { valid: true, result: '/path_params/x' },
- '["%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E\n' +
- ' ^^^',
- },
- '["%2E%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E%2e\n' +
- ' ^^^^^^',
- },
- },
- '["","/a"]': {
- '[""]': { valid: true, result: '/a' },
- '["x"]': { valid: true, result: 'x/a' },
- '["%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E" can\'t be safely passed as a path parameter\n%2E/a\n^^^',
- },
- '["%2e%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
- '%2e%2E/a\n' +
- '^^^^^^',
- },
- },
- '["","/"]': {
- '["x"]': { valid: true, result: 'x/' },
- '[""]': { valid: true, result: '/' },
- '["%2E%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
- '%2E%2e/\n' +
- '^^^^^^',
- },
- '["."]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "." can\'t be safely passed as a path parameter\n' +
- './\n^',
- },
- },
- '["",""]': {
- '[""]': { valid: true, result: '' },
- '["x"]': { valid: true, result: 'x' },
- '[".."]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value ".." can\'t be safely passed as a path parameter\n' +
- '..\n^^',
- },
- '["."]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "." can\'t be safely passed as a path parameter\n' +
- '.\n^',
- },
- },
- '["a"]': {},
- '[""]': {},
- '["/path_params/",":initiate"]': {
- '[""]': { valid: true, result: '/path_params/:initiate' },
- '["."]': { valid: true, result: '/path_params/.:initiate' },
- },
- '["/path_params/",".json"]': {
- '["x"]': { valid: true, result: '/path_params/x.json' },
- '["."]': { valid: true, result: '/path_params/..json' },
- },
- '["/path_params/","?beta=true"]': {
- '["x"]': { valid: true, result: '/path_params/x?beta=true' },
- '[""]': { valid: true, result: '/path_params/?beta=true' },
- '["%2E%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E%2E?beta=true\n' +
- ' ^^^^^^',
- },
- '["%2e%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2e%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2e%2E?beta=true\n' +
- ' ^^^^^^',
- },
- },
- '["/path_params/",".?beta=true"]': {
- '[".."]': { valid: true, result: '/path_params/...?beta=true' },
- '["x"]': { valid: true, result: '/path_params/x.?beta=true' },
- '[""]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "." can\'t be safely passed as a path parameter\n' +
- '/path_params/.?beta=true\n' +
- ' ^',
- },
- '["%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2e." can\'t be safely passed as a path parameter\n' +
- '/path_params/%2e.?beta=true\n' +
- ' ^^^^',
- },
- },
- '["/path_params/","/","/download"]': {
- '["",""]': { valid: true, result: '/path_params///download' },
- '["","x"]': { valid: true, result: '/path_params//x/download' },
- '[".","%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "." can\'t be safely passed as a path parameter\n' +
- 'Value "%2e" can\'t be safely passed as a path parameter\n' +
- '/path_params/./%2e/download\n' +
- ' ^ ^^^',
- },
- '["%2E%2e","%2e"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E%2e" can\'t be safely passed as a path parameter\n' +
- 'Value "%2e" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E%2e/%2e/download\n' +
- ' ^^^^^^ ^^^',
- },
- },
- '["/path_params/","-","/download"]': {
- '["","%2e"]': { valid: true, result: '/path_params/-%2e/download' },
- '["%2E",".."]': { valid: true, result: '/path_params/%2E-../download' },
- },
- '["/path_params/","","/download"]': {
- '["%2E%2e","%2e%2E"]': { valid: true, result: '/path_params/%2E%2e%2e%2E/download' },
- '["%2E",".."]': { valid: true, result: '/path_params/%2E../download' },
- '["","%2E"]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E" can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E/download\n' +
- ' ^^^',
- },
- '["%2E","."]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "%2E." can\'t be safely passed as a path parameter\n' +
- '/path_params/%2E./download\n' +
- ' ^^^^',
- },
- },
- '["/path_params/",".","/download"]': {
- '["%2e%2e",""]': { valid: true, result: '/path_params/%2e%2e./download' },
- '["","%2e%2e"]': { valid: true, result: '/path_params/.%2e%2e/download' },
- '["",""]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value "." can\'t be safely passed as a path parameter\n' +
- '/path_params/./download\n' +
- ' ^',
- },
- '["","."]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value ".." can\'t be safely passed as a path parameter\n' +
- '/path_params/../download\n' +
- ' ^^',
- },
- },
- '["/path_params/","..","/download"]': {
- '["","%2E"]': { valid: true, result: '/path_params/..%2E/download' },
- '["","x"]': { valid: true, result: '/path_params/..x/download' },
- '["",""]': {
- valid: false,
- error:
- 'Error: Path parameters result in path with invalid segments:\n' +
- 'Value ".." can\'t be safely passed as a path parameter\n' +
- '/path_params/../download\n' +
- ' ^^',
- },
- },
- });
- });
- });
- describe('encodeURIPath', () => {
- const testCases: string[] = [
- '',
- // Every ASCII character
- ...Array.from({ length: 0x7f }, (_, i) => String.fromCharCode(i)),
- // Unicode BMP codepoint
- 'å',
- // Unicode supplementary codepoint
- '😃',
- ];
- for (const param of testCases) {
- test('properly encodes ' + inspect(param), () => {
- const encoded = encodeURIPath(param);
- const naiveEncoded = encodeURIComponent(param);
- // we should never encode more characters than encodeURIComponent
- expect(naiveEncoded.length).toBeGreaterThanOrEqual(encoded.length);
- expect(decodeURIComponent(encoded)).toBe(param);
- });
- }
- test("leaves ':' intact", () => {
- expect(encodeURIPath(':')).toBe(':');
- });
- test("leaves '@' intact", () => {
- expect(encodeURIPath('@')).toBe('@');
- });
- });
|