Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: introduce beacon api test ignore list #6171

Merged
merged 27 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
91cd2af
Fix operationId of light_client routes
jeluard Dec 8, 2023
ad53c2d
Fix operationId of bls_to_execution_changes routes
jeluard Dec 8, 2023
3608df8
Update beacon api spec version to 2.4.0
jeluard Dec 8, 2023
429a783
Push case change
jeluard Dec 8, 2023
d23ffe2
Remove now useless file
jeluard Dec 8, 2023
32f9388
fix: lints
jeluard Dec 8, 2023
bb424b4
fix: filter broken test
jeluard Dec 8, 2023
9f3b06a
Revert "Fix operationId of light_client routes"
jeluard Dec 11, 2023
0dfdcab
Revert "Fix operationId of bls_to_execution_changes routes"
jeluard Dec 11, 2023
87f354f
test: ignore missing routes
jeluard Dec 11, 2023
7352a56
test: allow to filter required properties from testing
jeluard Dec 12, 2023
12c7ef4
fix: incorrect case
jeluard Dec 12, 2023
9cc40ca
test: fixed incorrect test filtering
jeluard Dec 12, 2023
0c48269
fix: lints
jeluard Dec 12, 2023
7f3bcde
fix: cleanup
jeluard Dec 12, 2023
e471e68
test: allow more fine grain API tests filtering
jeluard Dec 13, 2023
6c2a15f
fix: lints
jeluard Dec 13, 2023
20267c1
test: increase JSON schema validation strictness
jeluard Dec 13, 2023
b85a4c6
fix: restore removed keyword implementation
jeluard Dec 13, 2023
5d3ce8c
test: improve filtering semantic
jeluard Dec 13, 2023
73eec2b
test: add support for JSONPath syntax to filtering
jeluard Dec 14, 2023
ff673d8
fix: typo
jeluard Dec 14, 2023
4fa3ff9
fix: wording
jeluard Dec 14, 2023
49e757c
test: improve semantic
jeluard Dec 14, 2023
a3e0eb6
test: added issue for context
jeluard Dec 14, 2023
7787236
fix: improved issues references
jeluard Dec 19, 2023
25e8aa5
fix: incorrect dotted property parsing
jeluard Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 103 additions & 8 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {OpenApiFile} from "../../utils/parseOpenApiSpec.js";
import {routes} from "../../../src/beacon/index.js";
import {ReqSerializers} from "../../../src/utils/types.js";
import {Schema} from "../../../src/utils/schema.js";
import {runTestCheckAgainstSpec} from "../../utils/checkAgainstSpec.js";
import {IgnoredProperty, runTestCheckAgainstSpec} from "../../utils/checkAgainstSpec.js";
import {fetchOpenApiSpec} from "../../utils/fetchOpenApiSpec.js";
// Import all testData and merge below
import {testData as beaconTestData} from "./testData/beacon.js";
Expand All @@ -23,7 +23,7 @@ import {testData as validatorTestData} from "./testData/validator.js";
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const version = "v2.3.0";
const version = "v2.4.2";
const openApiFile: OpenApiFile = {
url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`,
filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"),
Expand Down Expand Up @@ -84,11 +84,105 @@ const testDatas = {
...validatorTestData,
};

const ignoredOperations = [
jeluard marked this conversation as resolved.
Show resolved Hide resolved
/* missing route */
/* #5694 */
"getSyncCommitteeRewards",
"getBlockRewards",
"getAttestationsRewards",
"getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697
"getBlindedBlock", // #5699
"getNextWithdrawals", // #5696
"getDebugForkChoice", // #5700
/* #6080 */
"getLightClientBootstrap",
"getLightClientUpdatesByRange",
"getLightClientFinalityUpdate",
"getLightClientOptimisticUpdate",
"getPoolBLSToExecutionChanges",
"submitPoolBLSToExecutionChange",
];

const ignoredProperties: Record<string, IgnoredProperty> = {
/*
#5693
missing finalized
*/
getStateRoot: {response: ["finalized"]},
getStateFork: {response: ["finalized"]},
getStateFinalityCheckpoints: {response: ["finalized"]},
getStateValidators: {response: ["finalized"]},
getStateValidator: {response: ["finalized"]},
getStateValidatorBalances: {response: ["finalized"]},
getEpochCommittees: {response: ["finalized"]},
getEpochSyncCommittees: {response: ["finalized"]},
getStateRandao: {response: ["finalized"]},
getBlockHeaders: {response: ["finalized"]},
getBlockHeader: {response: ["finalized"]},
getBlockV2: {response: ["finalized"]},
getBlockRoot: {response: ["finalized"]},
getBlockAttestations: {response: ["finalized"]},
getStateV2: {response: ["finalized"]},

/*
#6168
/query/syncing_status - must be integer
*/
getHealth: {request: ["query.syncing_status"]},

/**
* #6185
* - must have required property 'query'
*/
getBlobSidecars: {request: ["query"]},

/*
#4638
jeluard marked this conversation as resolved.
Show resolved Hide resolved
/query - must have required property 'skip_randao_verification'
*/
produceBlockV2: {request: ["query.skip_randao_verification"]},
produceBlindedBlock: {request: ["query.skip_randao_verification"]},
};

const openApiJson = await fetchOpenApiSpec(openApiFile);
runTestCheckAgainstSpec(openApiJson, routesData, reqSerializers, returnTypes, testDatas, {
// TODO: Investigate why schema validation fails otherwise
routesDropOneOf: ["produceBlockV2", "produceBlindedBlock", "publishBlindedBlock"],
});
runTestCheckAgainstSpec(
openApiJson,
routesData,
reqSerializers,
returnTypes,
testDatas,
{
// TODO: Investigate why schema validation fails otherwise (see #6187)
routesDropOneOf: [
"produceBlockV2",
"produceBlockV3",
"produceBlindedBlock",
"publishBlindedBlock",
"publishBlindedBlockV2",
],
jeluard marked this conversation as resolved.
Show resolved Hide resolved
},
ignoredOperations,
ignoredProperties
);

const ignoredTopics = [
/*
#6167
eventTestData[bls_to_execution_change] does not match spec's example
*/
"bls_to_execution_change",
/*
#6170
Error: Invalid slot=0 fork=phase0 for lightclient fork types
*/
"light_client_finality_update",
"light_client_optimistic_update",
/*
https://github.com/ethereum/beacon-APIs/pull/379
SyntaxError: Unexpected non-whitespace character after JSON at position 629 (line 1 column 630)
*/
"payload_attributes",
];

// eventstream types are defined as comments in the description of "examples".
// The function runTestCheckAgainstSpec() can't handle those, so the custom code before:
Expand All @@ -113,7 +207,9 @@ describe("eventstream event data", () => {
const eventSerdes = routes.events.getEventSerdes(config);
const knownTopics = new Set<string>(Object.values(routes.events.eventTypes));

for (const [topic, {value}] of Object.entries(eventstreamExamples ?? {})) {
for (const [topic, {value}] of Object.entries(eventstreamExamples ?? {}).filter(
([topic]) => !ignoredTopics.includes(topic)
)) {
it(topic, () => {
if (!knownTopics.has(topic)) {
throw Error(`topic ${topic} not implemented`);
Expand All @@ -130,7 +226,6 @@ describe("eventstream event data", () => {
if (testEvent == null) {
throw Error(`No eventTestData for ${topic}`);
}

const testEventJson = eventSerdes.toJson({
type: topic as routes.events.EventType,
message: testEvent,
Expand Down
75 changes: 69 additions & 6 deletions packages/api/test/utils/checkAgainstSpec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Ajv, {ErrorObject} from "ajv";
import {expect} from "chai";
import {ReqGeneric, ReqSerializer, ReturnTypes, RouteDef} from "../../src/utils/types.js";
import {applyRecursively, OpenApiJson, parseOpenApiSpec, ParseOpenApiSpecOpts} from "./parseOpenApiSpec.js";
import {applyRecursively, JsonSchema, OpenApiJson, parseOpenApiSpec, ParseOpenApiSpecOpts} from "./parseOpenApiSpec.js";
import {GenericServerTestCases} from "./genericServerTest.js";

const ajv = new Ajv({
// strict: true,
// strictSchema: true,
jeluard marked this conversation as resolved.
Show resolved Hide resolved
strict: true,
strictTypes: false, // TODO Enable once https://github.com/ethereum/beacon-APIs/pull/391 is released
jeluard marked this conversation as resolved.
Show resolved Hide resolved
allErrors: true,
});

// TODO: Still necessary?
// Ensure embedded schema 'example' do not fail validation
jeluard marked this conversation as resolved.
Show resolved Hide resolved
ajv.addKeyword({
keyword: "example",
validate: () => true,
Expand All @@ -19,17 +19,69 @@ ajv.addKeyword({

ajv.addFormat("hex", /^0x[a-fA-F0-9]+$/);

/**
* A set of properties that will be ignored during tests execution.
* This allows for a black-list mechanism to have a test pass while some part of the spec is not yet implemented.
*
* Properties can be nested using dot notation, following JSONPath semantic.
*
* Example:
* - query
* - query.skip_randao_verification
*/
export type IgnoredProperty = {
/**
* Properties to ignore in the request schema
*/
request?: string[];
/**
* Properties to ignore in the response schema
*/
response?: string[];
};

/**
* Recursively remove a property from a schema
*
* @param schema Schema to remove a property from
* @param property JSONPath like property to remove from the schema
*/
function deleteNested(schema: JsonSchema | undefined, property: string): void {
const properties = schema?.properties;
if (property.includes(".")) {
// Extract first segment, keep the rest as dotted
const [key, rest] = property.split(".", 2);
jeluard marked this conversation as resolved.
Show resolved Hide resolved
deleteNested(properties?.[key], rest);
} else {
// Remove property from 'required'
if (schema?.required) {
schema.required = schema.required?.filter((e) => property !== e);
}
// Remove property from 'properties'
delete properties?.[property];
}
}

export function runTestCheckAgainstSpec(
openApiJson: OpenApiJson,
routesData: Record<string, RouteDef>,
reqSerializers: Record<string, ReqSerializer<any, any>>,
returnTypes: Record<string, ReturnTypes<any>[string]>,
testDatas: Record<string, GenericServerTestCases<any>[string]>,
opts?: ParseOpenApiSpecOpts
opts?: ParseOpenApiSpecOpts,
ignoredOperations: string[] = [],
ignoredProperties: Record<string, IgnoredProperty> = {}
): void {
const openApiSpec = parseOpenApiSpec(openApiJson, opts);

for (const [operationId, routeSpec] of openApiSpec.entries()) {
const isIgnored = ignoredOperations.some((id) => id === operationId);
if (isIgnored) {
continue;
}

const ignoredProperty = ignoredProperties[operationId];

describe(operationId, () => {
const {requestSchema, responseOkSchema} = routeSpec;
const routeId = operationId;
Expand Down Expand Up @@ -68,7 +120,13 @@ export function runTestCheckAgainstSpec(
stringifyProperties((reqJson as ReqGeneric).params ?? {});
stringifyProperties((reqJson as ReqGeneric).query ?? {});

// Validate response
const ignoredProperties = ignoredProperty?.request;
if (ignoredProperties) {
// Remove ignored properties from schema validation
ignoredProperties.forEach((property) => deleteNested(routeSpec.requestSchema, property));
jeluard marked this conversation as resolved.
Show resolved Hide resolved
}

// Validate request
validateSchema(routeSpec.requestSchema, reqJson, "request");
});
}
Expand All @@ -87,6 +145,11 @@ export function runTestCheckAgainstSpec(
}
}

const ignoredProperties = ignoredProperty?.response;
if (ignoredProperties) {
// Remove ignored properties from schema validation
ignoredProperties.forEach((property) => deleteNested(routeSpec.responseOkSchema, property));
}
// Validate response
validateSchema(responseOkSchema, resJson, "response");
});
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/utils/parseOpenApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type RouteUrl = string;
/** "get" | "post" */
type HttpMethod = string;

type JsonSchema = {
export type JsonSchema = {
type: "object";
properties?: Record<string, JsonSchema>;
required?: string[];
Expand Down
Loading