Skip to content

Commit bd1efc5

Browse files
authored
[FEATURE] WIP handle content length (#20)
1 parent 2fa7039 commit bd1efc5

File tree

11 files changed

+202
-30
lines changed

11 files changed

+202
-30
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Build and Deploy Preview Web UI
2+
concurrency: preview-${{ github.ref }}
3+
4+
on:
5+
workflow_dispatch:
6+
pull_request:
7+
types:
8+
- opened
9+
- reopened
10+
- synchronize
11+
- closed
12+
paths:
13+
- 'ui/**'
14+
15+
permissions:
16+
contents: write
17+
pull-requests: write
18+
19+
defaults:
20+
run:
21+
working-directory: ./ui
22+
23+
jobs:
24+
build-and-deploy-preview-web-ui:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout Code
28+
uses: actions/checkout@v3
29+
30+
- name: Cache node_modules
31+
uses: actions/setup-node@v3
32+
with:
33+
node-version: 18
34+
35+
- name: Install Dependencies
36+
run: npm ci
37+
38+
- name: Run unit tests
39+
run: npm run test
40+
41+
- name: Build App & Run e2e tests
42+
run: npm run build
43+
44+
- name: Deploy preview 🚀
45+
uses: rossjrw/pr-preview-action@v1
46+
with:
47+
source-dir: ui/build
48+
preview-branch: gh-pages
49+
umbrella-dir: pr-preview
50+
action: auto

.github/workflows/ui.yml renamed to .github/workflows/build-and-deploy-prod-web-ui.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build and Deploy UI
1+
name: Build and Deploy Web UI
22

33
on:
44
workflow_dispatch:
@@ -16,7 +16,7 @@ defaults:
1616
working-directory: ./ui
1717

1818
jobs:
19-
build-and-deploy-ui:
19+
build-and-deploy-web-ui:
2020
runs-on: ubuntu-latest
2121
steps:
2222
- name: Checkout Code
@@ -47,3 +47,4 @@ jobs:
4747
with:
4848
folder: ui/build
4949
branch: gh-pages
50+
clean-exclude: pr-preview
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export const simulationWithContentType = {
2+
meta: {
3+
schemaVersion: 'v5',
4+
hoverflyVersion: 'v1.6.0',
5+
timeExported: '2023-04-10T12:00:00Z'
6+
},
7+
data: {
8+
pairs: [
9+
{
10+
request: {
11+
method: [
12+
{
13+
matcher: 'exact',
14+
value: ''
15+
}
16+
]
17+
},
18+
response: {
19+
status: 200,
20+
body: 'Hello World',
21+
headers: {
22+
'content-Length': ['11']
23+
},
24+
encodedBody: false
25+
}
26+
}
27+
]
28+
}
29+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from '@playwright/test';
2+
3+
export const expectContentTypeUpdated = {
4+
meta: expect.objectContaining({
5+
schemaVersion: 'v5',
6+
hoverflyVersion: 'v1.6.0',
7+
timeExported: expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
8+
}),
9+
data: expect.objectContaining({
10+
pairs: [
11+
{
12+
request: {
13+
method: [
14+
{
15+
matcher: 'exact',
16+
value: ''
17+
}
18+
]
19+
},
20+
response: {
21+
status: 200,
22+
body: 'Hello World, how is it going ?',
23+
headers: {
24+
'content-Length': ['30']
25+
},
26+
encodedBody: false
27+
}
28+
}
29+
]
30+
})
31+
};

ui/e2e/web-ui.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import empty from './test-data/input/empty';
33
import { WebUiSimulationPage } from './utils/WebUiSimulationPage';
44
import expectedComplete from './test-data/output/expected-complete';
55
import expectedStartFromScratch from './test-data/output/expected-start-from-scratch';
6+
import { simulationWithContentType } from './test-data/input/simulation-with-content-type';
7+
import { expectContentTypeUpdated } from './test-data/output/expected-content-type-updated';
68

79
test('should display a simulation example on first app launch', async ({ page }) => {
810
const simulationPage = new WebUiSimulationPage(page);
@@ -139,3 +141,19 @@ test('should create a full simulation', async ({ page }) => {
139141
expect(JSON.parse(textEditorContent)).toMatchObject(expectedComplete);
140142
});
141143
});
144+
145+
test('content-type headers should update when body change', async ({ page }) => {
146+
const simulationPage = new WebUiSimulationPage(page);
147+
await simulationPage.goto(JSON.stringify(simulationWithContentType));
148+
149+
await page.locator('.card-header').click();
150+
await simulationPage.setTextEditorContent(
151+
simulationPage.responseBodyEditor,
152+
', how is it going ?'
153+
);
154+
155+
const textEditorContent = await simulationPage.getTextEditorContent(
156+
simulationPage.simulationTextEditor
157+
);
158+
expect(JSON.parse(textEditorContent)).toMatchObject(expectContentTypeUpdated);
159+
});

ui/src/components/form-simulation/ResponseBodyEditor.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useMemo, useRef, useState } from 'react';
22
import MonacoEditor, { Monaco, OnMount } from '@monaco-editor/react';
33
import { isJSON, prettify } from '../../services/json-service';
4-
import { editor, IScrollEvent } from 'monaco-editor';
4+
import { editor } from 'monaco-editor';
55
import { Button } from 'react-bootstrap';
66

77
type Props = {
@@ -15,28 +15,12 @@ const MAX_EDITOR_LINE = 50;
1515
const ResponseBodyEditor = ({ value = '', onChange }: Props) => {
1616
const editorRef = useRef<editor.IStandaloneCodeEditor>();
1717
const monacoRef = useRef<Monaco>();
18-
const [isBodyJson] = useState(isJSON(value));
1918
const [editorHeightPx, setEditorHeightPx] = useState(100);
20-
21-
useEffect(() => {
22-
if (!editorRef.current || editorRef.current.hasTextFocus()) {
23-
return;
24-
}
25-
26-
loadPropValueIntoEditor();
27-
}, [value]);
19+
const isBodyJson = useMemo(() => isJSON(value), [value]);
2820

2921
const onEditorMount: OnMount = (editor, monaco) => {
3022
editorRef.current = editor;
3123
monacoRef.current = monaco;
32-
loadPropValueIntoEditor();
33-
};
34-
35-
const loadPropValueIntoEditor = () => {
36-
if (!editorRef.current) {
37-
return;
38-
}
39-
editorRef.current.setValue(value);
4024
updateEditorHeight();
4125
};
4226

@@ -81,8 +65,9 @@ const ResponseBodyEditor = ({ value = '', onChange }: Props) => {
8165
<MonacoEditor
8266
width="100%"
8367
height={`${editorHeightPx}px`}
84-
defaultLanguage={isBodyJson ? 'json' : ''}
68+
language={isBodyJson ? 'json' : ''}
8569
theme="vs-dark"
70+
value={value}
8671
onMount={onEditorMount}
8772
onChange={onEditorValueChange}
8873
options={{

ui/src/components/form-simulation/ResponseMatcherForm.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import React from 'react';
2-
import { Response } from '../../types/hoverfly';
2+
import { RequestHeaders, Response, ResponseHeaders } from '../../types/hoverfly';
33
import ArrowCollapse from '../utilities/ArrowCollapse';
44
import ResponseBodyEditor from './ResponseBodyEditor';
55
import { Form } from 'react-bootstrap';
66
import SelectHttpStatus from '../utilities/SelectHttpStatus';
7+
import { byteLengthUtf8, updateContentLengthAccordingToBody } from '../../services/headers-service';
78

89
type Props = {
910
response?: Response;
1011
onChange: (response: Response) => void;
1112
};
1213

1314
const ResponseMatcherForm = ({ response = {}, onChange }: Props) => {
15+
const onResponseBodyChange = (newBody: string) => {
16+
const newHeaders = updateContentLengthAccordingToBody(newBody, response?.headers);
17+
onChange({ ...response, body: newBody, headers: newHeaders });
18+
};
19+
1420
return (
1521
<div data-testid="response-form">
1622
<legend>Response</legend>
@@ -27,10 +33,7 @@ const ResponseMatcherForm = ({ response = {}, onChange }: Props) => {
2733
</Form.Group>
2834
<Form.Group>
2935
<Form.Label htmlFor="body">Body:</Form.Label>
30-
<ResponseBodyEditor
31-
value={response?.body}
32-
onChange={(value) => onChange({ ...response, body: value })}
33-
/>
36+
<ResponseBodyEditor value={response?.body} onChange={onResponseBodyChange} />
3437
</Form.Group>
3538
<ArrowCollapse visibleByDefault={false}>
3639
<>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { updateContentLengthAccordingToBody } from './headers-service';
2+
import { ResponseHeaders } from '../types/hoverfly';
3+
4+
describe('updateContentLengthAccordingToBody', () => {
5+
it.each([
6+
['do nothing on empty headers', undefined, undefined],
7+
[
8+
'do nothing when no Content-Length header',
9+
{ 'Other-Header': ['value'] },
10+
{ 'Other-Header': ['value'] }
11+
],
12+
[
13+
'do nothing when headers are malformed (not an array)',
14+
{ 'content-Length': '0' },
15+
{ 'content-Length': '0' }
16+
],
17+
['do nothing when headers array is empty', { 'content-Length': [] }, { 'content-Length': [] }],
18+
[
19+
'update Content-Length header',
20+
{ 'Other-header': 'Value', 'content-Length': ['0'] },
21+
{ 'Other-header': 'Value', 'content-Length': ['13'] }
22+
]
23+
])('should %s', (_, headers, expected) => {
24+
expect(updateContentLengthAccordingToBody('Hello, world!', headers as ResponseHeaders)).toEqual(
25+
expected
26+
);
27+
});
28+
});

ui/src/services/headers-service.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ResponseHeaders } from '../types/hoverfly';
2+
3+
export const byteLengthUtf8 = (str: string) => new Blob([str]).size;
4+
5+
export const updateContentLengthAccordingToBody = (
6+
body: string,
7+
headers: ResponseHeaders | undefined
8+
): ResponseHeaders | undefined => {
9+
if (!headers) {
10+
return undefined;
11+
}
12+
13+
const contentLengthKey = Object.keys(headers).find(
14+
(key) => key.toLowerCase() === 'content-length'
15+
);
16+
if (
17+
!contentLengthKey ||
18+
!Array.isArray(headers[contentLengthKey]) ||
19+
headers[contentLengthKey].length == 0
20+
) {
21+
return headers;
22+
}
23+
24+
const updatedHeaders = { ...headers };
25+
updatedHeaders[contentLengthKey][0] = String(byteLengthUtf8(body)); // Assume that response is UTF-8 encoded, that may not be the case, may be more precise by using Encoding header
26+
return updatedHeaders;
27+
};

ui/src/types/hoverfly.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type FieldMatcher = {
2424
doMatch?: FieldMatcher;
2525
};
2626

27-
export type Headers = Record<string, string[]>;
27+
export type ResponseHeaders = Record<string, string[]>;
2828

2929
export type RequestQueries = Record<string, FieldMatcher[]>;
3030

@@ -46,7 +46,7 @@ export type Response = {
4646
bodyFile?: string;
4747
encodedBody?: boolean;
4848
fixedDelay?: number;
49-
headers?: Headers;
49+
headers?: ResponseHeaders;
5050
logNormalDelay?: LogNormalDelay;
5151
removesState?: string[];
5252
status?: number;

0 commit comments

Comments
 (0)