Files
bruno/tests/request/generate-code/url-encoding-off.spec.ts
2026-06-09 12:54:24 +05:30

381 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from '../../../playwright';
import { closeGenerateCodeDialog, getGeneratedSnippet, openCollection, openRequestInFolder, setUrlEncoding } from '../../utils/page';
const COLLECTION = 'generate-code-encoding';
const FOLDER = 'requests';
test.describe('Generate Code URL Encoding OFF', () => {
test.describe('Query preservation', () => {
test('preserves literal spaces in query values (no %20)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-spaces');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?name=John Doe&age=25');
await closeGenerateCodeDialog(page);
});
test('preserves pre-encoded %20 / %40 without double-encoding', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-preencoded');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?name=John%20Doe&email=john%40example.com');
await closeGenerateCodeDialog(page);
});
test('preserves equals signs in query values (token=abc123==)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-equals');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?token=abc123==&type=test');
await closeGenerateCodeDialog(page);
});
test('preserves redirect URL with colons and slashes', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-redirect-url');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?path=/users/123&redirect=https://other.com');
await closeGenerateCodeDialog(page);
});
test('preserves email with + alias and @ (test+alias@example.com)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-email-plus');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/invite?email=test+alias@example.com');
await closeGenerateCodeDialog(page);
});
test('preserves comma-separated values and colon (tags=a,b,c&time=10:30)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-commas-colons');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/filter?tags=a,b,c&time=10:30');
await closeGenerateCodeDialog(page);
});
test('preserves pipe operator in query values (no %7C)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-pipe');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?filter=status|active&sort=name|asc');
await closeGenerateCodeDialog(page);
});
test('preserves unicode in query values (no %C3%A9 / %C3%BC)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-unicode');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?name=José&city=München');
await closeGenerateCodeDialog(page);
});
test('preserves already-double-encoded values verbatim (mirror of ON canonical case)', async ({ pageWithUserData: page }) => {
// Same fixture URL the ON spec uses — but here the toggle is OFF, so
// the user-typed bytes round-trip unchanged. This is the contract a
// user expects when they've already encoded their redirect URL
// themselves and don't want Bruno to touch it.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-double-encode');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain(
'http://localhost:8081/api/echo/anything/login?redirect=https%3A%2F%2Fother.com%2Fcb&token=abc%2520xyz'
);
await closeGenerateCodeDialog(page);
});
test('preserves # literal in query value (no encoding)', async ({ pageWithUserData: page }) => {
// `?query=aaa#bbb` stays as `?query=aaa#bbb`. OFF mode is the only mode
// that retains fragment semantics — the `#` survives as a literal byte
// in the displayed snippet (curl/fetch will treat it as a fragment on
// the wire and drop it, but that's outside the snippet's concern).
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-hash');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?query=aaa#bbb');
await closeGenerateCodeDialog(page);
});
test('preserves JSON-shaped array query values verbatim (no encoding, no validator error)', async ({ pageWithUserData: page }) => {
// Literal `[` `]` `"` `,` and space would be rejected by HTTPSnippet's
// HAR validator without the pre-encode-then-replace-back path. The
// snippet still ends up containing the raw form.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'query-arrays');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).not.toBe('Error generating code snippet');
expect(snippet).toContain(
'http://localhost:8081/api/echo/anything/api?empty=[]&nums=[1, 2, 3]&strs=["string", "string"]&nested=[[1, 2, 3], ["string", "string"]]'
);
await closeGenerateCodeDialog(page);
});
});
test.describe('Path preservation', () => {
test('preserves literal spaces in path segments', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-spaces');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api/path with spaces/users');
await closeGenerateCodeDialog(page);
});
test('preserves square brackets in path segments (no Error generating code snippet)', async ({ pageWithUserData: page }) => {
// HTTPSnippet's HAR validator rejects literal `[` `]` — without the
// pre-encode step in snippet-generator this returns the error string.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-brackets');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).not.toBe('Error generating code snippet');
expect(snippet).toContain('http://localhost:8081/api/echo/anything/list[123]');
await closeGenerateCodeDialog(page);
});
test('preserves unicode in path segments (no %C3%A9)', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-unicode');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/users/José/profile');
await closeGenerateCodeDialog(page);
});
test('preserves pre-encoded %20 in path verbatim (no decode, no re-encode)', async ({ pageWithUserData: page }) => {
// Mirror of the idempotency check ON does — OFF should leave the
// already-encoded `%20` exactly as the user typed it.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-idempotent');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api/path%20with%20spaces/users');
await closeGenerateCodeDialog(page);
});
test('preserves OData-style parenthesized path params and $ filters', async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-odata');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain(
'http://localhost:8081/api/echo/anything/odata/Products(123)/Categories(456)?$expand=Items&$filter=Price gt 10'
);
await closeGenerateCodeDialog(page);
});
test('preserves URL fragment in snippet (intentional asymmetry vs ON)', async ({ pageWithUserData: page }) => {
// Raw mode honors user intent — fragment is kept verbatim. ON mode
// encodes `#` to %23 as data. See snippet-generator.spec.js for the
// designed-behavior comment.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'fragment-preserved');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/api?name=john doe#section1');
await closeGenerateCodeDialog(page);
});
test('preserves # directly in path (no query ahead of it)', async ({ pageWithUserData: page }) => {
// Variant of fragment-preserved where the `#` sits in the path with no
// `?` before it. OFF mode keeps the URL byte-for-byte in the snippet
// (`hash#tag`). On the wire, downstream HTTP clients treat `#tag` as a
// fragment and strip it — so the server receives `/hash` only. The
// snippet still shows the user's literal input; that's the OFF contract.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-fragment');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/hash#tag');
await closeGenerateCodeDialog(page);
});
test('preserves /issues/#1234 verbatim (semantic-path fragment)', async ({ pageWithUserData: page }) => {
// Scenario 4: GitHub/GitLab-style issue link with `#` after a trailing
// slash. OFF mode keeps the user's typed form intact in the snippet.
// Note: on the wire, HTTP clients strip everything from `#` onwards,
// so the server only ever sees `/issues/` — that's the deliberate cost
// of OFF mode for fragment-style URLs.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-issues-fragment');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/issues#1234');
await closeGenerateCodeDialog(page);
});
test('preserves SPA hash-router URL (/#/dashboard/settings)', async ({ pageWithUserData: page }) => {
// Scenario 5: legacy SPA hash-routing. OFF mode keeps the literal `#`
// so the snippet matches what the user typed. The wire only sees `/path/`
// (axios strips the fragment) — fine for SPAs since the JS reads
// location.hash to decide what to render.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-spa-route');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/spa#/dashboard/settings');
await closeGenerateCodeDialog(page);
});
test('preserves OAuth callback fragment verbatim', async ({ pageWithUserData: page }) => {
// Scenario 8: OAuth implicit-flow callback. The fragment is the entire
// point — it keeps the access_token out of server logs. OFF mode
// preserves the literal `#access_token=…` in the snippet so users can
// see/copy the URL form they typed.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'oauth-callback-fragment');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain(
'http://localhost:8081/api/echo/anything/callback#access_token=abc123&token_type=Bearer'
);
await closeGenerateCodeDialog(page);
});
});
test.describe('Path-params table (params:path)', () => {
test('does not throw for path-param value with literal space (regression: user-reported `aaa bbb`)', async ({ pageWithUserData: page }) => {
// Repro: URL `http://localhost:8081/api/echo/anything/users/:id` with `id = aaa bbb`.
// After interpolateUrlPathParams (raw mode) the URL has a literal space:
// `http://localhost:8081/api/echo/anything/users/aaa bbb`. HTTPSnippet's HAR validator
// rejects it → "Error generating code snippet". The pre-encode-then-
// replace-back path in snippet-generator preserves the raw form.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'params-path-space');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).not.toBe('Error generating code snippet');
expect(snippet).toContain('http://localhost:8081/api/echo/anything/users/aaa bbb');
await closeGenerateCodeDialog(page);
});
test('path-param value with # — /profile suffix kept in snippet (but lost on wire)', async ({ pageWithUserData: page }) => {
// Scenario 3 from the # encoding decision tree: when `:id = john#doe`
// and the URL template has `/profile` after `:id`, OFF mode keeps
// `/profile` visible in the snippet (so the user sees what they typed).
// On the wire, downstream HTTP clients strip `#doe/profile` as a
// fragment — that's the cost of OFF for this shape. To send the full
// path to the server, toggle ON (encodes `#` to `%23`).
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'path-param-hash-trailing');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).toContain('http://localhost:8081/api/echo/anything/users/john#doe/profile');
await closeGenerateCodeDialog(page);
});
test('OData-style path with literal Tags("tag test") is preserved verbatim', async ({ pageWithUserData: page }) => {
// Mirror of the ON canonical regression. In OFF mode all the literal
// `"`, space, and quoted `:CategoryID` characters survive in the
// displayed snippet exactly as the path-param substitution emits them.
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, 'params-path-odata');
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).not.toBe('Error generating code snippet');
expect(snippet).toContain(
'http://localhost:8081/api/echo/anything/odata/Category(\'category123\')/Item(item456)/foobar/Tags("tag test")'
);
await closeGenerateCodeDialog(page);
});
// Path-param substitution matrix — OFF mirror of url-encoding-on.spec.ts.
// In raw mode the snippet preserves the user-typed value byte-for-byte
// (the pre-encode-then-replace-back path in snippet-generator restores
// the form after HAR validation). Caveats on the / # ? rows are the same
// as in the ON spec — the literal-in-value semantic is lost as soon as
// the char lands in the URL string, but OFF mode at least keeps those
// bytes visible (including the `#bbb` fragment, intentional per the
// designed-behavior comment in snippet-generator).
const pathParamCases: Array<{ name: string; file: string; expected: string }> = [
{ name: '/ in value preserved (looks like two segments)', file: 'path-param-slash', expected: 'http://localhost:8081/api/echo/anything/users/aaa/bbb' },
{ name: '# in value preserved as fragment marker (intentional asymmetry vs ON)', file: 'path-param-hash', expected: 'http://localhost:8081/api/echo/anything/users/aaa#bbb' },
{ name: 'space in value preserved (John Doe stays literal)', file: 'path-param-space', expected: 'http://localhost:8081/api/echo/anything/users/John Doe' },
{ name: '& in value preserved (a&b stays literal)', file: 'path-param-ampersand', expected: 'http://localhost:8081/api/echo/anything/users/a&b' },
{ name: '= in value preserved (a=b stays literal)', file: 'path-param-equals', expected: 'http://localhost:8081/api/echo/anything/users/a=b' },
{ name: '+ in value preserved (a+b stays literal)', file: 'path-param-plus', expected: 'http://localhost:8081/api/echo/anything/users/a+b' },
{ name: '? in value preserved (becomes query separator — literal lost)', file: 'path-param-question', expected: 'http://localhost:8081/api/echo/anything/users/a?b' },
{ name: '@ in value preserved (user@host stays literal)', file: 'path-param-at', expected: 'http://localhost:8081/api/echo/anything/users/user@host' },
{ name: ': in value preserved (ISO timestamp stays literal)', file: 'path-param-colon', expected: 'http://localhost:8081/api/echo/anything/users/2026-01-15T10:30:00' },
{ name: ', in value preserved (a,b,c stays literal)', file: 'path-param-comma', expected: 'http://localhost:8081/api/echo/anything/users/a,b,c' },
{ name: 'unicode in value preserved (José stays literal)', file: 'path-param-unicode', expected: 'http://localhost:8081/api/echo/anything/users/José' },
{ name: '[ ] in value preserved (list[1] stays literal, no validator error)', file: 'path-param-brackets', expected: 'http://localhost:8081/api/echo/anything/users/list[1]' },
{ name: '{ } in value preserved ({x} stays literal)', file: 'path-param-braces', expected: 'http://localhost:8081/api/echo/anything/users/{x}' },
{ name: '| in value preserved (a|b stays literal)', file: 'path-param-pipe', expected: 'http://localhost:8081/api/echo/anything/users/a|b' }
];
for (const c of pathParamCases) {
test(c.name, async ({ pageWithUserData: page }) => {
await openCollection(page, COLLECTION);
await openRequestInFolder(page, FOLDER, c.file);
await setUrlEncoding(page, false);
const snippet = await getGeneratedSnippet(page);
expect(snippet).not.toBe('Error generating code snippet');
expect(snippet).toContain(c.expected);
await closeGenerateCodeDialog(page);
});
}
});
});