mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
460 lines
17 KiB
TypeScript
460 lines
17 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { describe, expect, test } from "vite-plus/test";
|
|
import YAML from "yaml";
|
|
import { convertBruno } from "../src";
|
|
import { auth } from "../src/auth";
|
|
import { body, urlAndParams } from "../src/body";
|
|
import { template, variables } from "../src/common";
|
|
|
|
function collection(items: unknown[] = [], extra: Record<string, unknown> = {}) {
|
|
return { opencollection: "1.0.0", info: { name: "Test" }, bundled: true, items, ...extra };
|
|
}
|
|
const fixture = readFileSync(new URL("./fixtures/collection.yaml", import.meta.url), "utf8");
|
|
|
|
describe("Bruno OpenCollection importer", () => {
|
|
test("imports a bundled export with folders, scopes, disabled rows, and all four request types", () => {
|
|
const imported = convertBruno(fixture)!;
|
|
const {
|
|
workspaces: [workspace],
|
|
folders: [folder],
|
|
environments,
|
|
httpRequests,
|
|
grpcRequests: [grpc],
|
|
websocketRequests: [ws],
|
|
} = imported.resources;
|
|
expect(workspace).toMatchObject({
|
|
name: "Example API",
|
|
authenticationType: "bearer",
|
|
authentication: { token: "${[ token ]}" },
|
|
description: "Synthetic migration fixture.",
|
|
});
|
|
expect(folder).toMatchObject({ name: "Users", sortPriority: 2, authenticationType: null });
|
|
const [base, dev, shared, folderEnv] = environments;
|
|
expect(base).toMatchObject({
|
|
parentModel: "workspace",
|
|
parentId: null,
|
|
variables: [
|
|
{ name: "host", value: "https://example.com" },
|
|
{ name: "client", value: "yaak" },
|
|
],
|
|
});
|
|
expect(dev).toMatchObject({
|
|
parentModel: "environment",
|
|
parentId: null,
|
|
variables: [
|
|
{ name: "count", value: "42" },
|
|
{ name: "host" },
|
|
{ name: "token", value: "" },
|
|
{ name: "disabled", enabled: false },
|
|
],
|
|
});
|
|
expect(shared).toMatchObject({ color: "#123456", variables: [{ name: "count", value: "42" }] });
|
|
expect(folderEnv).toMatchObject({ parentModel: "folder", parentId: folder!.id });
|
|
const [get, graphql, publicRequest, upload] = httpRequests;
|
|
expect(get).toMatchObject({
|
|
folderId: folder!.id,
|
|
authenticationType: null,
|
|
sortPriority: 3,
|
|
url: "${[ host ]}/users/:id?keep=1#details",
|
|
urlParameters: [
|
|
{ name: ":id", value: "${[ userId ]}", enabled: true },
|
|
{ name: "expand", value: "profile", enabled: true },
|
|
{ name: "inactive", value: "true", enabled: false },
|
|
],
|
|
headers: [{ name: "X-Disabled", value: "ignored", enabled: false }],
|
|
settingFollowRedirects: { enabled: true, value: false },
|
|
settingRequestTimeout: { enabled: true, value: 2000 },
|
|
});
|
|
expect(graphql).toMatchObject({
|
|
bodyType: "graphql",
|
|
method: "POST",
|
|
authenticationType: "basic",
|
|
authentication: { username: "${[ client ]}" },
|
|
body: {
|
|
query: "query Get($id: ID!) { node(id: $id) { id } }",
|
|
variables: '{"id":"${[ userId ]}"}',
|
|
},
|
|
headers: [{ name: "Content-Type", value: "application/json", enabled: true }],
|
|
});
|
|
expect(publicRequest).toMatchObject({ authenticationType: "none", folderId: null });
|
|
expect(upload?.body?.form).toEqual([
|
|
{ name: "label", value: "${[ client ]}", enabled: true },
|
|
{ name: "files", file: "./first.txt", contentType: "text/plain", enabled: true },
|
|
{ name: "files", file: "./second.txt", contentType: "text/plain", enabled: true },
|
|
]);
|
|
expect(grpc).toMatchObject({
|
|
service: "example.Greeter",
|
|
method: "SayHello",
|
|
message: '{"name":"${[ client ]}"}',
|
|
});
|
|
expect(ws).toMatchObject({
|
|
message: '{"subscribe":"${[ client ]}"}',
|
|
authenticationType: null,
|
|
});
|
|
const all = Object.values(imported.resources).flat();
|
|
expect(new Set(all.map((r) => r.id)).size).toBe(all.length);
|
|
expect(
|
|
all.filter((r) => r.model !== "workspace").every((r) => r.workspaceId === workspace!.id),
|
|
).toBe(true);
|
|
expect(imported.sourceKeys).toBeUndefined();
|
|
expect(convertBruno(fixture)).toEqual(imported);
|
|
});
|
|
|
|
test("accepts JSON serialization and inline collections without a bundled flag", () => {
|
|
expect(convertBruno(JSON.stringify(YAML.parse(fixture)))).toEqual(convertBruno(fixture));
|
|
expect(
|
|
convertBruno(JSON.stringify({ opencollection: "1.0.0", items: [] }))?.resources.workspaces,
|
|
).toHaveLength(1);
|
|
expect(convertBruno(JSON.stringify(collection()))?.resources.httpRequests).toHaveLength(0);
|
|
});
|
|
|
|
test.each([
|
|
"",
|
|
"not: [valid",
|
|
"null",
|
|
"[]",
|
|
"openapi: 3.1.0",
|
|
'{"info":{"name":"Postman"},"item":[]}',
|
|
"meta { name: Test }",
|
|
"opencollection: 1.0.0\nbundled: true\nitems: &items [*items]",
|
|
])("ignores unrelated, malformed, or cyclic input: %s", (input) => {
|
|
expect(convertBruno(input)).toBeNull();
|
|
});
|
|
|
|
test("rejects directory config and unknown format versions with actionable errors", () => {
|
|
expect(() => convertBruno("opencollection: 1.0.0\ninfo:\n name: Directory")).toThrow(
|
|
"Share menu",
|
|
);
|
|
expect(() => convertBruno(JSON.stringify(collection([], { bundled: false })))).toThrow(
|
|
"single YAML file",
|
|
);
|
|
expect(() => convertBruno(JSON.stringify(collection([], { opencollection: "2.0.0" })))).toThrow(
|
|
"Unsupported Bruno collection format version",
|
|
);
|
|
});
|
|
|
|
test("rejects missing and cyclic environment parents", () => {
|
|
for (const environments of [
|
|
[{ name: "A", extends: "missing" }],
|
|
[{ name: "A", extends: "A" }],
|
|
[
|
|
{ name: "A", extends: "B" },
|
|
{ name: "B", extends: "A" },
|
|
],
|
|
]) {
|
|
expect(() =>
|
|
convertBruno(JSON.stringify(collection([], { config: { environments } }))),
|
|
).toThrow(/environment/);
|
|
}
|
|
});
|
|
|
|
test("retains scripts, request variables, unsupported authentication, and unknown items for manual migration", () => {
|
|
const result = convertBruno(
|
|
JSON.stringify(
|
|
collection([
|
|
{
|
|
info: { name: "Scripted", type: "http" },
|
|
http: { url: "{{host}}", auth: { type: "wsse", username: "test" } },
|
|
runtime: {
|
|
scripts: [{ code: "bru.setVar('token', 'example');\n```" }],
|
|
variables: [{ name: "host", value: "https://example.com" }],
|
|
},
|
|
},
|
|
{ info: { name: "App", type: "app" }, code: "example" },
|
|
]),
|
|
),
|
|
)!;
|
|
expect(result.resources.httpRequests[0]).toMatchObject({ authenticationType: "none" });
|
|
const desc = result.resources.httpRequests[0]!.description;
|
|
expect(desc).toContain("not executed");
|
|
expect(desc).toContain("bru.setVar");
|
|
expect(desc).toContain("variables:");
|
|
expect(desc).toContain("wsse");
|
|
expect(desc).toContain("````yaml");
|
|
expect(result.resources.folders[0]?.description).toContain("unsupportedItem:");
|
|
});
|
|
|
|
test("does not override a collection Content-Type with a generated body header", () => {
|
|
const result = convertBruno(
|
|
JSON.stringify(
|
|
collection(
|
|
[{ info: { name: "JSON", type: "http" }, http: { body: { type: "json", data: "{}" } } }],
|
|
{
|
|
request: { headers: [{ name: "content-type", value: "application/vnd.example+json" }] },
|
|
},
|
|
),
|
|
),
|
|
)!;
|
|
expect(result.resources.httpRequests[0]?.headers).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("request conversion", () => {
|
|
test.each([
|
|
["json", "application/json", "application/json"],
|
|
["xml", "application/xml", "application/xml"],
|
|
["text", "other", "text/plain"],
|
|
["sparql", "other", "application/sparql-query"],
|
|
])("imports %s bodies with a default content type", (type, bodyType, contentType) => {
|
|
expect(body({ type, data: "{{value}}" }, false, [])).toEqual({
|
|
bodyType,
|
|
body: { text: "${[ value ]}" },
|
|
headers: [{ name: "Content-Type", value: contentType, enabled: true }],
|
|
});
|
|
});
|
|
test("preserves explicit and disabled Content-Type headers", () => {
|
|
expect(
|
|
body({ type: "json", data: "{}" }, false, [
|
|
{ name: "CONTENT-type", value: "custom", disabled: true },
|
|
]).headers,
|
|
).toEqual([{ name: "CONTENT-type", value: "custom", enabled: false }]);
|
|
});
|
|
test("imports form fields and the selected file or body variant", () => {
|
|
expect(
|
|
body(
|
|
{ type: "form-urlencoded", data: [{ name: "field", value: "{{value}}", disabled: true }] },
|
|
false,
|
|
[],
|
|
).body,
|
|
).toEqual({ form: [{ name: "field", value: "${[ value ]}", enabled: false }] });
|
|
expect(
|
|
body(
|
|
{
|
|
type: "file",
|
|
data: [
|
|
{ filePath: "wrong", selected: false },
|
|
{ filePath: "./correct.bin", selected: true, contentType: "application/octet-stream" },
|
|
],
|
|
},
|
|
false,
|
|
[],
|
|
),
|
|
).toMatchObject({ bodyType: "binary", body: { filePath: "./correct.bin" } });
|
|
expect(
|
|
body(
|
|
[
|
|
{ title: "A", body: { type: "json", data: "a" } },
|
|
{ title: "B", selected: true, body: { type: "json", data: "b" } },
|
|
],
|
|
false,
|
|
[],
|
|
).body,
|
|
).toEqual({ text: "b" });
|
|
expect(
|
|
body({ type: "file", data: [{ filePath: "disabled", selected: false }] }, false, []).bodyType,
|
|
).toBeNull();
|
|
});
|
|
test("imports raw GraphQL variables without parsing expressions and preserves operationName", () => {
|
|
expect(
|
|
body(
|
|
{ query: "query Get { me }", variables: '{"id": {{id}}}', operationName: "Get" },
|
|
true,
|
|
[],
|
|
).body,
|
|
).toEqual({ query: "query Get { me }", variables: '{"id": ${[ id ]}}', operationName: "Get" });
|
|
expect(body({ query: "{ me }", variables: { id: "{{id}}" } }, true, []).body.variables).toBe(
|
|
'{\n "id": "${[ id ]}"\n}',
|
|
);
|
|
});
|
|
test("avoids duplicate encoded query keys without losing unrelated query entries or fragments", () => {
|
|
expect(
|
|
urlAndParams("{{host}}/a?some%20key=old&some+key=old&keep=%2F#part", [
|
|
{ type: "query", name: "some key", value: "new" },
|
|
]),
|
|
).toMatchObject({
|
|
url: "${[ host ]}/a?keep=%2F#part",
|
|
urlParameters: [{ name: "some key", value: "new" }],
|
|
});
|
|
expect(urlAndParams("{{host}}/a?x=1", []).url).toBe("${[ host ]}/a?x=1");
|
|
});
|
|
test("converts plain variables and preserves unsupported expressions", () => {
|
|
expect(
|
|
template(
|
|
"{{ host }} {{user.id}} {{process.env.TOKEN}} {{$randomUUID}} {{a + b}} {{true}} {{null}}",
|
|
),
|
|
).toBe(
|
|
"${[ host ]} ${[ user.id ]} {{process.env.TOKEN}} {{$randomUUID}} {{a + b}} {{true}} {{null}}",
|
|
);
|
|
expect(
|
|
variables([
|
|
{ name: "secret", secret: true, value: "must not import" },
|
|
{
|
|
name: "variant",
|
|
value: [{ value: "a" }, { selected: true, value: { type: "object", data: '{"x":1}' } }],
|
|
},
|
|
]),
|
|
).toEqual([
|
|
{ name: "secret", value: "", enabled: true },
|
|
{ name: "variant", value: '{"x":1}', enabled: true },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("authentication", () => {
|
|
test("distinguishes request no-auth from explicit and folder inheritance", () => {
|
|
expect(auth(undefined).authenticationType).toBe("none");
|
|
expect(auth("inherit").authenticationType).toBeNull();
|
|
expect(auth(undefined, true).authenticationType).toBeNull();
|
|
});
|
|
test.each([
|
|
["basic", "basic"],
|
|
["digest", "digest"],
|
|
["ntlm", "windows"],
|
|
["bearer", "bearer"],
|
|
["awsv4", "awsv4"],
|
|
])("imports %s auth", (type, expected) => {
|
|
expect(auth({ type, username: "{{user}}", token: "{{token}}" }).authenticationType).toBe(
|
|
expected,
|
|
);
|
|
});
|
|
test("maps API key placement and OAuth client credentials/PKCE", () => {
|
|
expect(auth({ type: "apikey", key: "key", value: "{{token}}", placement: "query" })).toEqual({
|
|
authenticationType: "apikey",
|
|
authentication: { key: "key", value: "${[ token ]}", location: "query" },
|
|
});
|
|
expect(
|
|
auth({
|
|
type: "oauth2",
|
|
flow: "authorization_code",
|
|
credentials: { clientId: "{{client}}", placement: "basic_auth_header" },
|
|
callbackUrl: "http://localhost/callback",
|
|
pkce: { method: "S256" },
|
|
tokenConfig: { placement: { header: "Token" } },
|
|
}).authentication,
|
|
).toMatchObject({
|
|
grantType: "authorization_code",
|
|
clientId: "${[ client ]}",
|
|
credentials: "basic",
|
|
redirectUri: "http://localhost/callback",
|
|
usePkce: true,
|
|
pkceChallengeMethod: "S256",
|
|
headerPrefix: "Token",
|
|
});
|
|
expect(
|
|
auth({
|
|
type: "oauth2",
|
|
flow: "resource_owner_password_credentials",
|
|
resourceOwner: { username: "a", password: "b" },
|
|
}).authentication,
|
|
).toMatchObject({ grantType: "password", username: "a", password: "b" });
|
|
expect(auth({ type: "oauth2", flow: "client_credentials" }).authentication).toMatchObject({
|
|
grantType: "client_credentials",
|
|
credentials: "body",
|
|
usePkce: false,
|
|
});
|
|
});
|
|
test.each([
|
|
{ type: "awsv4", profileName: "local" },
|
|
{ type: "oauth2", flow: "client_credentials", tokenConfig: { placement: { query: "token" } } },
|
|
{ type: "oauth2", flow: "unknown" },
|
|
{ type: "wsse" },
|
|
])("does not silently inherit auth for unsupported configurations", (value) => {
|
|
expect(auth(value).authenticationType).toBe("none");
|
|
});
|
|
});
|
|
|
|
describe("scope and unsupported-field regressions", () => {
|
|
test("flattens multi-level environments without disabled overrides hiding inherited values", () => {
|
|
const environments = [
|
|
{
|
|
name: "Child",
|
|
extends: "Middle",
|
|
variables: [
|
|
{ name: "a", value: "disabled", disabled: true },
|
|
{ name: "b", value: "child" },
|
|
],
|
|
},
|
|
{ name: "Middle", extends: "Root", variables: [{ name: "a", value: "middle" }] },
|
|
{
|
|
name: "Root",
|
|
variables: [
|
|
{ name: "a", value: "root" },
|
|
{ name: "b", value: "root" },
|
|
{ name: "off", value: "ignored", disabled: true },
|
|
],
|
|
},
|
|
];
|
|
const result = convertBruno(JSON.stringify(collection([], { config: { environments } })))!;
|
|
const child = result.resources.environments[1]!;
|
|
expect(child.parentId).toBeNull();
|
|
expect(child.variables).toEqual([
|
|
{ name: "a", value: "middle", enabled: true },
|
|
{ name: "a", value: "disabled", enabled: false },
|
|
{ name: "b", value: "child", enabled: true },
|
|
]);
|
|
});
|
|
|
|
test("distinguishes an empty folder from folder request defaults with no auth", () => {
|
|
const result = convertBruno(
|
|
JSON.stringify(
|
|
collection([
|
|
{ info: { type: "folder", name: "Empty" } },
|
|
{ info: { type: "folder", name: "No auth" }, request: { headers: [] } },
|
|
{ info: { type: "folder", name: "Inherited" }, request: { auth: "inherit" } },
|
|
]),
|
|
),
|
|
)!;
|
|
expect(result.resources.folders.map((f) => f.authenticationType)).toEqual([null, "none", null]);
|
|
});
|
|
|
|
test("retains external environment settings and unknown bodies, and does not turn binary WS data into text", () => {
|
|
const result = convertBruno(
|
|
JSON.stringify(
|
|
collection(
|
|
[
|
|
{
|
|
info: { type: "http", name: "Unknown body" },
|
|
http: { body: { type: "future", data: "original" } },
|
|
},
|
|
{
|
|
info: { type: "websocket", name: "Binary" },
|
|
websocket: { message: { type: "binary", data: "AA==" } },
|
|
},
|
|
],
|
|
{
|
|
config: {
|
|
environments: [
|
|
{
|
|
name: "Vault",
|
|
externalSecrets: { type: "aws-secrets-manager" },
|
|
dotEnvFilePath: "./.env",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
),
|
|
),
|
|
)!;
|
|
expect(result.resources.workspaces[0]?.description).toContain("aws-secrets-manager");
|
|
expect(result.resources.workspaces[0]?.description).toContain("./.env");
|
|
expect(result.resources.httpRequests[0]?.description).toContain("original");
|
|
expect(result.resources.websocketRequests[0]?.message).toBe("");
|
|
expect(result.resources.websocketRequests[0]?.description).toContain("AA==");
|
|
});
|
|
|
|
test("maps OAuth 1.0 credentials and keeps unsupported private-key files inactive", () => {
|
|
expect(
|
|
auth({
|
|
type: "oauth1",
|
|
consumerKey: "{{key}}",
|
|
accessToken: "token",
|
|
accessTokenSecret: "secret",
|
|
privateKey: { type: "text", value: "pem" },
|
|
}),
|
|
).toMatchObject({
|
|
authenticationType: "oauth1",
|
|
authentication: {
|
|
consumerKey: "${[ key ]}",
|
|
tokenKey: "token",
|
|
tokenSecret: "secret",
|
|
privateKey: "pem",
|
|
signatureMethod: "HMAC-SHA1",
|
|
},
|
|
});
|
|
expect(
|
|
auth({ type: "oauth1", privateKey: { type: "file", value: "./key.pem" } }).authenticationType,
|
|
).toBe("none");
|
|
expect(auth({ type: "oauth1", placement: "query" }).authenticationType).toBe("none");
|
|
});
|
|
});
|