mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix(grpc): gRPC request loses all messages except the first on save for yaml collection (#8203)
* fix(grpc): gRPC request loses all messages except the first on save for yaml collection * fix(grpc): enhance gRPC locators and improve message handling in tests
This commit is contained in:
@@ -39,7 +39,7 @@ const MessageToolbar = ({
|
|||||||
</ToolHint>
|
</ToolHint>
|
||||||
|
|
||||||
<ToolHint text="Generate sample" toolhintId={`regenerate-msg-${index}`}>
|
<ToolHint text="Generate sample" toolhintId={`regenerate-msg-${index}`}>
|
||||||
<button onClick={onRegenerateMessage} className="toolbar-btn">
|
<button onClick={onRegenerateMessage} className="toolbar-btn" data-testid={`grpc-regenerate-message-${index}`}>
|
||||||
<IconRefresh size={16} strokeWidth={1.5} />
|
<IconRefresh size={16} strokeWidth={1.5} />
|
||||||
</button>
|
</button>
|
||||||
</ToolHint>
|
</ToolHint>
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initi
|
|||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
key="show-file-format"
|
key="show-file-format"
|
||||||
|
data-testid="show-file-format-toggle"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
dropdownTippyRef.current.hide();
|
dropdownTippyRef.current.hide();
|
||||||
setShowFileFormat(!showFileFormat);
|
setShowFileFormat(!showFileFormat);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
|
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
|
||||||
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
|
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
|
||||||
import type { GrpcRequest, GrpcMetadata } from '@opencollection/types/requests/grpc';
|
import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant } from '@opencollection/types/requests/grpc';
|
||||||
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
|
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
|
||||||
import { toBrunoAuth } from '../common/auth';
|
import { toBrunoAuth } from '../common/auth';
|
||||||
import { toBrunoVariables } from '../common/variables';
|
import { toBrunoVariables } from '../common/variables';
|
||||||
@@ -57,10 +57,16 @@ const parseGrpcRequest = (ocRequest: GrpcRequest): BrunoItem => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// message
|
// message
|
||||||
if (isNonEmptyString(grpc?.message)) {
|
const rawMessage = grpc?.message;
|
||||||
|
if (Array.isArray(rawMessage)) {
|
||||||
|
brunoRequest.body.grpc = (rawMessage as GrpcMessageVariant[]).map(({ title, message }, index) => ({
|
||||||
|
name: title || `message ${index + 1}`,
|
||||||
|
content: ensureString(message)
|
||||||
|
}));
|
||||||
|
} else if (isNonEmptyString(rawMessage)) {
|
||||||
brunoRequest.body.grpc = [{
|
brunoRequest.body.grpc = [{
|
||||||
name: '',
|
name: '',
|
||||||
content: grpc?.message as string
|
content: rawMessage as string
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
|
import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
|
||||||
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
|
import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
|
||||||
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
|
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
|
||||||
import type { GrpcRequest, GrpcMetadata, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
|
import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
|
||||||
import type { Auth } from '@opencollection/types/common/auth';
|
import type { Auth } from '@opencollection/types/common/auth';
|
||||||
import type { Scripts } from '@opencollection/types/common/scripts';
|
import type { Scripts } from '@opencollection/types/common/scripts';
|
||||||
import type { Variable } from '@opencollection/types/common/variables';
|
import type { Variable } from '@opencollection/types/common/variables';
|
||||||
@@ -73,16 +73,11 @@ const stringifyGrpcRequest = (item: BrunoItem): string => {
|
|||||||
|
|
||||||
// message
|
// message
|
||||||
if (brunoRequest.body?.mode === 'grpc' && brunoRequest.body.grpc?.length) {
|
if (brunoRequest.body?.mode === 'grpc' && brunoRequest.body.grpc?.length) {
|
||||||
const messages = brunoRequest.body.grpc;
|
const messages: GrpcMessageVariant[] = brunoRequest.body.grpc.map(({ name, content }, index) => ({
|
||||||
|
title: name || `message ${index + 1}`,
|
||||||
// todo: bruno app supports only one message for now
|
message: content || ''
|
||||||
// update this when bruno app supports multiple messages
|
}));
|
||||||
if (messages.length) {
|
grpc.message = messages;
|
||||||
const message: GrpcMessage = messages[0].content || '';
|
|
||||||
if (message.trim().length) {
|
|
||||||
grpc.message = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|||||||
70
tests/grpc/multi-message-yml/multi-message.spec.ts
Normal file
70
tests/grpc/multi-message-yml/multi-message.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { bruToJsonV2 } from '@usebruno/lang';
|
||||||
|
import { expect, test } from '../../../playwright';
|
||||||
|
import {
|
||||||
|
addGrpcMessage,
|
||||||
|
createCollection,
|
||||||
|
createRequest,
|
||||||
|
generateGrpcSampleMessage,
|
||||||
|
saveRequest,
|
||||||
|
selectGrpcMethod
|
||||||
|
} from '../../utils/page/actions';
|
||||||
|
|
||||||
|
const REQUEST_NAME = 'grpc-multi-msg';
|
||||||
|
const GRPC_URL = 'grpcb.in:9000';
|
||||||
|
const GRPC_METHOD = 'BidiHello';
|
||||||
|
|
||||||
|
type GrpcRequestYml = {
|
||||||
|
grpc?: {
|
||||||
|
message?: { title: string; message: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FORMATS = [
|
||||||
|
{ format: 'yml', collectionName: 'grpc-yml-multi-msg', tmpDirPrefix: 'grpc-yml-collection' },
|
||||||
|
{ format: 'bru', collectionName: 'grpc-bru-multi-msg', tmpDirPrefix: 'grpc-bru-collection' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const { format, collectionName, tmpDirPrefix } of FORMATS) {
|
||||||
|
test.describe.serial(`grpc multi-message (${format} format)`, () => {
|
||||||
|
let collectionPath: string;
|
||||||
|
|
||||||
|
test('creates a gRPC request with multiple messages and saves it', async ({ page, createTmpDir }) => {
|
||||||
|
collectionPath = await createTmpDir(tmpDirPrefix);
|
||||||
|
|
||||||
|
await createCollection(page, collectionName, collectionPath, format);
|
||||||
|
await createRequest(page, REQUEST_NAME, collectionName, { url: GRPC_URL, requestType: 'grpc' });
|
||||||
|
await selectGrpcMethod(page, GRPC_METHOD);
|
||||||
|
|
||||||
|
await addGrpcMessage(page);
|
||||||
|
await addGrpcMessage(page);
|
||||||
|
await generateGrpcSampleMessage(page, 0);
|
||||||
|
await generateGrpcSampleMessage(page, 1);
|
||||||
|
await generateGrpcSampleMessage(page, 2);
|
||||||
|
|
||||||
|
await saveRequest(page);
|
||||||
|
|
||||||
|
const messageContainers = page.getByTestId('grpc-messages-container').locator('.message-container');
|
||||||
|
await expect(messageContainers).toHaveCount(3, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`verifies all messages are saved in the request .${format} file`, async () => {
|
||||||
|
const requestFilePath = path.join(collectionPath, collectionName, `${REQUEST_NAME}.${format}`);
|
||||||
|
expect(fs.existsSync(requestFilePath)).toBe(true);
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(requestFilePath, 'utf8');
|
||||||
|
|
||||||
|
if (format === 'yml') {
|
||||||
|
const parsed = yaml.load(fileContent) as GrpcRequestYml;
|
||||||
|
const messages = parsed.grpc?.message ?? [];
|
||||||
|
expect(messages.length).toBe(3);
|
||||||
|
} else if (format === 'bru') {
|
||||||
|
const parsed = bruToJsonV2(fileContent) as { body?: { grpc?: { name: string; content: string }[] } };
|
||||||
|
const messages = parsed.body?.grpc ?? [];
|
||||||
|
expect(messages.length).toBe(3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright';
|
import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { buildCommonLocators, buildScriptErrorLocators } from './locators';
|
import { buildCommonLocators, buildScriptErrorLocators, buildGrpcCommonLocators } from './locators';
|
||||||
|
|
||||||
type SandboxMode = 'safe' | 'developer';
|
type SandboxMode = 'safe' | 'developer';
|
||||||
|
|
||||||
|
type CollectionFormat = 'bru' | 'yml';
|
||||||
|
|
||||||
type WaitForAppReadyOptions = {
|
type WaitForAppReadyOptions = {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
@@ -101,9 +103,9 @@ const createCollection = async (
|
|||||||
page,
|
page,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
collectionLocation: string,
|
collectionLocation: string,
|
||||||
format?: 'bru' | 'yml'
|
format: CollectionFormat = 'yml'
|
||||||
) => {
|
) => {
|
||||||
await test.step(`Create collection "${collectionName}"`, async () => {
|
await test.step(`Create ${format} collection "${collectionName}"`, async () => {
|
||||||
await page.getByTestId('collections-header-add-menu').click();
|
await page.getByTestId('collections-header-add-menu').click();
|
||||||
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
|
||||||
|
|
||||||
@@ -1335,6 +1337,44 @@ const saveRequest = async (page: Page) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the gRPC "Add Message" button to append a new message to the request
|
||||||
|
* @param page - The page object
|
||||||
|
*/
|
||||||
|
const addGrpcMessage = async (page: Page) => {
|
||||||
|
await test.step('Add gRPC message', async () => {
|
||||||
|
const locators = buildGrpcCommonLocators(page);
|
||||||
|
await locators.request.addMessageButton().click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the "Generate sample" button on a gRPC message to populate it with a sample payload
|
||||||
|
* @param page - The page object
|
||||||
|
* @param index - The 0-based index of the message (default: 0)
|
||||||
|
*/
|
||||||
|
const generateGrpcSampleMessage = async (page: Page, index: number = 0) => {
|
||||||
|
await test.step(`Generate sample for gRPC message #${index}`, async () => {
|
||||||
|
const locators = buildGrpcCommonLocators(page);
|
||||||
|
await locators.request.regenerateMessage(index).click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the gRPC method dropdown and select a method by name
|
||||||
|
* @param page - The page object
|
||||||
|
* @param methodName - The name of the gRPC method to select (e.g. "BidiHello")
|
||||||
|
*/
|
||||||
|
const selectGrpcMethod = async (page: Page, methodName: string) => {
|
||||||
|
await test.step(`Select gRPC method "${methodName}"`, async () => {
|
||||||
|
const locators = buildGrpcCommonLocators(page);
|
||||||
|
await locators.method.dropdownTrigger().click();
|
||||||
|
await locators.method.dropdown().waitFor({ state: 'visible', timeout: 5000 });
|
||||||
|
await locators.method.item(methodName).first().click();
|
||||||
|
await expect(locators.method.selectedName()).toContainText(methodName);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close all open request tabs using the right-click context menu
|
* Close all open request tabs using the right-click context menu
|
||||||
* @param page - The page object
|
* @param page - The page object
|
||||||
@@ -1756,6 +1796,9 @@ export {
|
|||||||
editAssertion,
|
editAssertion,
|
||||||
deleteAssertion,
|
deleteAssertion,
|
||||||
saveRequest,
|
saveRequest,
|
||||||
|
addGrpcMessage,
|
||||||
|
generateGrpcSampleMessage,
|
||||||
|
selectGrpcMethod,
|
||||||
closeAllTabs,
|
closeAllTabs,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
switchWorkspace,
|
switchWorkspace,
|
||||||
|
|||||||
@@ -237,13 +237,18 @@ export const buildGrpcCommonLocators = (page: Page) => ({
|
|||||||
...buildCommonLocators(page),
|
...buildCommonLocators(page),
|
||||||
method: {
|
method: {
|
||||||
dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),
|
dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),
|
||||||
indicator: () => page.getByTestId('grpc-method-indicator')
|
indicator: () => page.getByTestId('grpc-method-indicator'),
|
||||||
|
dropdown: () => page.getByTestId('grpc-methods-dropdown'),
|
||||||
|
item: (methodName: string) =>
|
||||||
|
page.getByTestId('grpc-methods-dropdown').getByTestId('grpc-method-item').filter({ hasText: methodName }),
|
||||||
|
selectedName: () => page.getByTestId('selected-grpc-method-name')
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
queryUrlContainer: () => page.getByTestId('grpc-query-url-container'),
|
queryUrlContainer: () => page.getByTestId('grpc-query-url-container'),
|
||||||
sendButton: () => page.getByTestId('grpc-send-request-button'),
|
sendButton: () => page.getByTestId('grpc-send-request-button'),
|
||||||
messagesContainer: () => page.getByTestId('grpc-messages-container'),
|
messagesContainer: () => page.getByTestId('grpc-messages-container'),
|
||||||
addMessageButton: () => page.getByTestId('grpc-add-message-button'),
|
addMessageButton: () => page.getByTestId('grpc-add-message-button'),
|
||||||
|
regenerateMessage: (index: number) => page.getByTestId(`grpc-regenerate-message-${index}`),
|
||||||
sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),
|
sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),
|
||||||
endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),
|
endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),
|
||||||
cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')
|
cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')
|
||||||
|
|||||||
Reference in New Issue
Block a user