Skip to content

Commit

Permalink
Add HttpRequestMethodArb
Browse files Browse the repository at this point in the history
  • Loading branch information
dansteren committed Dec 27, 2023
1 parent f7df4ba commit c7879ee
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ export function Float32DefinitionArb(): fc.Arbitrary<FloatCandidDefinition> {
// TODO the agent should encode and decode -0 correctly
export function Float32ValueArb(): fc.Arbitrary<CandidValues<number>> {
return SimpleCandidValuesArb(
fc.float().map((sample) => (sample === 0 ? sample * 0 : sample)),
fc
.float32Array({
maxLength: 1,
minLength: 1,
noDefaultInfinity: true,
noNaN: true
})
.map(([sample]) => (sample === 0 ? sample * 0 : sample)),
floatToSrcLiteral
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function QueryMethodArb<
ReturnTypeAgentResponseValue
>;
callbackLocation?: CallbackLocation;
name?: string;
}
) {
return fc
Expand All @@ -66,14 +67,15 @@ export function QueryMethodArb<
)
.map(
([
functionName,
defaultFunctionName,
paramTypes,
returnType,
defaultCallbackLocation,
callbackName
]): QueryMethod => {
const callbackLocation =
constraints.callbackLocation ?? defaultCallbackLocation;
const functionName = constraints.name ?? defaultFunctionName;

const imports = new Set([
'query',
Expand Down
5 changes: 5 additions & 0 deletions property_tests/arbitraries/http/body_arb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import fc from 'fast-check';

export function BodyArb() {
return fc.json().map((json) => new Uint8Array(Buffer.from(json, 'utf-8')));
}
15 changes: 15 additions & 0 deletions property_tests/arbitraries/http/headers_arb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// See https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/

import fc from 'fast-check';

export function HttpHeadersArb() {
const HeaderNameArb = fc.stringMatching(/^[a-zA-Z0-9_-]+$/);

const HeaderValueArb = fc
.stringMatching(/^[a-zA-Z0-9_ :;.,\/"'?!(){}[\]@<>=\-+*#$&`|~^%]+$/)
.map((value) => value.trim());

const HeaderArb = fc.tuple(HeaderNameArb, HeaderValueArb);

return fc.array(HeaderArb);
}
103 changes: 103 additions & 0 deletions property_tests/arbitraries/http/request_arb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import fc from 'fast-check';
import { HttpHeadersArb } from './headers_arb';
import { BodyArb } from './body_arb';
import { HttpRequest, None, Some } from '../../../src/lib';
import { CandidValueAndMeta } from '../candid/candid_value_and_meta_arb';
import { blobToSrcLiteral } from '../candid/to_src_literal/blob';
import { stringToSrcLiteral } from '../candid/to_src_literal/string';

type RequestMethod =
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'TRACE'
| 'PATCH';

const RequestMethodArb = fc.constantFrom<RequestMethod>(
'GET',
'HEAD',
'POST',
'PUT',
'DELETE',
'CONNECT',
'OPTIONS',
'TRACE',
'PATCH'
);

const UrlArb = fc.webUrl({ withQueryParameters: true }).map((url) => {
const parsedUrl = new URL(url);
return parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
});

function HttpRequestValueArb() {
return fc
.tuple(
RequestMethodArb,
UrlArb,
HttpHeadersArb(),
BodyArb(),
fc
.option(fc.integer({ min: 0, max: 2 ** 16 - 1 }))
.map((optCertVer) => {
return optCertVer === null ? None : Some(optCertVer);
})
)
.map(([method, url, headers, body, certificate_version]) => {
return {
method,
url,
headers,
body,
certificate_version
};
});
}

export function HttpRequestArb(): fc.Arbitrary<
CandidValueAndMeta<HttpRequest>
> {
return HttpRequestValueArb().map((httpRequest) => {
const headerStrings = httpRequest.headers
.map(
([name, value]) =>
`[${stringToSrcLiteral(name)},${stringToSrcLiteral(value)}]`
)
.join(',');

const bodySrc = blobToSrcLiteral(httpRequest.body);

const certificateVersion =
'Some' in httpRequest.certificate_version
? `Some(${httpRequest.certificate_version.Some})`
: `None`;

const optImport =
'Some' in httpRequest.certificate_version ? 'Some' : 'None';

return {
value: {
agentArgumentValue: httpRequest,
agentResponseValue: httpRequest,
candidTypeObject: HttpRequest
},
src: {
candidTypeAnnotation: 'HttpRequest',
candidTypeObject: 'HttpRequest',
variableAliasDeclarations: [],
imports: new Set(['HttpRequest', optImport]),
valueLiteral: `{
method: '${httpRequest.method}',
url: '${httpRequest.url}',
headers: [${headerStrings}],
body: ${bodySrc},
certificate_version: ${certificateVersion}
}`
}
};
});
}
84 changes: 84 additions & 0 deletions property_tests/arbitraries/http/response_arb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fc from 'fast-check';
import { HttpResponse, None } from '../../../src/lib';
import { HttpHeadersArb } from './headers_arb';
import { BodyArb } from './body_arb';
import { CandidValueAndMeta } from '../candid/candid_value_and_meta_arb';
import { CorrespondingJSType } from '../candid/corresponding_js_type';
import { blobToSrcLiteral } from '../candid/to_src_literal/blob';
import { stringToSrcLiteral } from '../candid/to_src_literal/string';

export type HttpResponseAgentResponseValue = {
status: number;
headers: [string, string][];
body: string;
};

// The ic replica doesn't support returning status codes in the 1xx range.
const StatusCodeArb = fc.integer({ min: 200, max: 599 });

export function HttpResponseValueArb<T>() {
return fc
.tuple(StatusCodeArb, HttpHeadersArb(), BodyArb())
.map(([status_code, headers, body]): HttpResponse<T> => {
const thing: HttpResponse<T> = {
status_code,
headers,
body,
upgrade: None,
streaming_strategy: None
};
return thing;
});
}
export function HttpResponseArb<T extends CorrespondingJSType = any>(
token: CandidValueAndMeta<CorrespondingJSType>
): fc.Arbitrary<
CandidValueAndMeta<HttpResponse<T>, HttpResponseAgentResponseValue>
> {
return HttpResponseValueArb<T>().map((response) => {
const lowerCasedHeaders = response.headers.map<[string, string]>(
([name, value]) => [name.toLowerCase(), value]
);

const agentResponseValue = {
status: response.status_code,
headers: lowerCasedHeaders,
body: new TextDecoder().decode(response.body)
};

const headerStrings = response.headers
.map(
([name, value]) =>
`[${stringToSrcLiteral(name)},${stringToSrcLiteral(value)}]`
)
.join(',');

const bodySrc = blobToSrcLiteral(response.body);

return {
value: {
agentArgumentValue: response,
agentResponseValue: agentResponseValue,
candidTypeObject: HttpResponse(token.value.candidTypeObject)
},
src: {
candidTypeAnnotation: `HttpResponse<${token.src.candidTypeAnnotation}>`,
candidTypeObject: `HttpResponse(${token.src.candidTypeObject})`,
variableAliasDeclarations: token.src.variableAliasDeclarations,
imports: new Set([
'HttpResponse',
'bool',
'None',
...token.src.imports
]),
valueLiteral: `{
status_code: ${response.status_code},
headers: [${headerStrings}],
body: ${bodySrc},
upgrade: None,
streaming_strategy: None
}`
}
};
});
}
53 changes: 53 additions & 0 deletions property_tests/tests/canister_methods/http_request/test/fletch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { execSync } from 'child_process';
import { HttpRequest } from 'azle';

/**
* A synchronous "fetch" for canisters.
*/
export function fletch(canisterName: string, options: HttpRequest) {
const canisterId = getCanisterId(canisterName);

const requestHeaders = options.headers
.map(
([name, value]) =>
`-H "${escapeForBash(name)}: ${escapeForBash(value)}"`
)
.join(' ');

const curlCommand = `curl\
--silent\
--include\
-X ${options.method}\
${requestHeaders}\
--data "${options.body.join(',')}"\
"${canisterId}.localhost:8000${options.url}" \
--resolve "${canisterId}.localhost:8000:127.0.0.1"`;

const responseBytes = execSync(curlCommand);
const decoder = new TextDecoder();
const response = decoder.decode(responseBytes);
const [statusCodeAndHeaders, body] = response.split('\r\n\r\n');
const [statusCodeString, ...responseHeaderStrings] =
statusCodeAndHeaders.split('\r\n');
const statusCode = Number(statusCodeString.split(' ')[1]);
const responseHeaders = responseHeaderStrings.map((header) =>
header.split(': ')
);
return {
status: statusCode,
headers: responseHeaders,
body
};
}

function getCanisterId(canisterName: string): string {
return execSync(`dfx canister id ${canisterName}`).toString().trim();
}

function escapeForBash(input: string) {
return input
.replace(/\\/g, '\\\\') // Escape backslashes
.replace(/`/g, '\\`') // Escape backticks
.replace(/'/g, "'\\''") // Escape single quotes
.replace(/"/g, '\\"'); // Escape double quotes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { HttpRequest } from 'azle';
import { CandidReturnType } from 'azle/property_tests/arbitraries/candid/candid_return_type_arb';
import { CandidValueAndMeta } from 'azle/property_tests/arbitraries/candid/candid_value_and_meta_arb';
import { Named } from 'azle/property_tests';

export function generateBody(
namedParams: Named<CandidValueAndMeta<HttpRequest>>[],
returnType: CandidValueAndMeta<CandidReturnType>
): string {
const { name: requestParamName, el: requestValueObject } = namedParams[0];
const request = requestValueObject.value.agentArgumentValue;

const headersMap =
request.headers.length === 0
? ''
: `const headers = (${requestParamName}.headers as [string, string][]).reduce<{
[key: string]: string}
>((prev, [name, value]) => ({[name]: value, ...prev}), {});`;

const headerChecks = request.headers
.map(([name, value]) => {
return `
if (headers['${escape(name).toLowerCase()}'] !== '${escape(
value
)}') {
throw new Error(
\`Unexpected value for header '${escape(
name
)}'. Expected '${escape(
value
)}' but received '\${headers['${escape(
name
).toLowerCase()}']}'\`
);
}
`;
})
.join('\n');

return `
if (${requestParamName}.method !== '${request.method}') {
throw new Error(
\`Unexpected req.method. Expected ${
request.method
} but received \${${requestParamName}.method}\`
);
}
if (${requestParamName}.url !== '${escape(request.url)}') {
throw new Error(
\`Unexpected req.url. Expected '${escape(
request.url
)}' but received \${${requestParamName}.url}\`
);
}
${headersMap}
${headerChecks}
return ${returnType.src.valueLiteral};
`;
}

function escape(input: string) {
return input
.replace(/\\/g, '\\\\') // Escape backslashes
.replace(/`/g, '\\`') // Escape backticks
.replace(/'/g, "\\'") // Escape single quotes
.replace(/"/g, '\\"'); // Escape double quotes
}
Loading

0 comments on commit c7879ee

Please sign in to comment.