Skip to content

Commit

Permalink
test: simplify endpoint integration tests (#6373)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Aug 12, 2024
1 parent 7867ee1 commit 88d384c
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 125 deletions.
204 changes: 79 additions & 125 deletions tests/endpoints-2.0/endpoints-integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,136 +1,98 @@
import { resolveParams } from "@smithy/middleware-endpoint";
import { EndpointParameters, EndpointV2 } from "@smithy/types";
import * as fs from "fs";
import * as path from "path";
import { EndpointV2 } from "@smithy/types";
import { resolveEndpoint, EndpointParams } from "@smithy/util-endpoints";
import { existsSync, readdirSync } from "fs";
import { join } from "path";

import { EndpointExpectation, EndpointTestCase, ErrorExpectation, ServiceNamespace } from "./integration-test-types";

const clientList: string[] = [];
const root = path.join(__dirname, "..", "..");
const clients = fs.readdirSync(path.join(root, "clients"));
clientList.push(...clients);
import { EndpointExpectation, ServiceModel, ServiceNamespace } from "./integration-test-types";

describe("client list", () => {
const root = join(__dirname, "..", "..");
const clientPackageNameList = readdirSync(join(root, "clients"));

it("should be at least 300 clients", () => {
expect(clientList.length).toBeGreaterThan(300);
expect(clientPackageNameList.length).toBeGreaterThan(300);
});
});

for (const client of clientList) {
const serviceName = client.slice(7);
describe.each(clientPackageNameList)(`%s endpoint test cases`, (clientPackageName) => {
const serviceName = clientPackageName.slice(7);

let defaultEndpointResolver: any;
let namespace: any;
let model: any;
// since client package name list is populated from clients folder, we know it exists.
const namespace = require(`@aws-sdk/${clientPackageName}`);
const modelPath = join(root, "codegen", "sdk-codegen", "aws-models", serviceName + ".json");

// this may also work with dynamic async import() in a beforeAll() block,
// but needs more effort than using synchronous require().
try {
defaultEndpointResolver =
require(`@aws-sdk/client-${serviceName}/src/endpoint/endpointResolver`).defaultEndpointResolver;
namespace = require(`@aws-sdk/client-${serviceName}`);
model = require(path.join(root, "codegen", "sdk-codegen", "aws-models", serviceName + ".json"));
} catch (e) {
defaultEndpointResolver = null;
namespace = null;
model = null;
if (e.code !== "MODULE_NOT_FOUND") {
console.error(e);
}
}

describe(`client-${serviceName} endpoint test cases`, () => {
if (defaultEndpointResolver && namespace && model) {
const [, service] = Object.entries(model.shapes).find(
([k, v]) => typeof v === "object" && v !== null && "type" in v && v.type === "service"
) as any;
const [, tests] = Object.entries(service.traits).find(([k, v]) => k === "smithy.rules#endpointTests") as any;
if (tests?.testCases) {
runTestCases(tests, service, defaultEndpointResolver, "");
} else {
it.skip("has no test cases", () => {});
if (existsSync(modelPath)) {
const model = require(modelPath);
for (const value of Object.values(model.shapes)) {
if (typeof value === "object" && value !== null && "type" in value && value.type === "service") {
const service = value as ServiceModel;
runTestCases(service, namespace);
break;
}
}
} else {
it.skip("unable to load endpoint resolver, namespace, or test cases", () => {});
}
});
}

function runTestCases(
{ testCases }: { testCases: EndpointTestCase[] },
service: ServiceNamespace,
defaultEndpointResolver: (endpointParams: EndpointParameters) => EndpointV2,
serviceId: string
) {
for (const testCase of testCases) {
runTestCase(testCase, service, defaultEndpointResolver, serviceId);
}
}

async function runTestCase(
testCase: EndpointTestCase,
service: ServiceNamespace,
defaultEndpointResolver: (endpointParams: EndpointParameters) => EndpointV2,
serviceId: string
) {
const { documentation, params = {}, expect: expectation, operationInputs } = testCase;

for (const key of Object.keys(params)) {
// e.g. S3Control::UseArnRegion as a param key indicates
// an error with the test case, it will be ignored.
if (key.includes(":")) {
delete params[key];
}
}

if (params.UseGlobalEndpoint || params.Region === "aws-global") {
it.skip(documentation || "undocumented testcase", () => {});
return;
}

params.serviceId = serviceId;
});

it(documentation || "undocumented testcase", async () => {
if (isEndpointExpectation(expectation)) {
const { endpoint } = expectation;
if (operationInputs) {
for (const operationInput of operationInputs) {
const { operationName, operationParams = {} } = operationInput;
const endpointParams = await resolveParams(operationParams, service[`${operationName}Command`], params);
const observed = defaultEndpointResolver(endpointParams as any);
assertEndpointResolvedCorrectly(endpoint, observed);
function runTestCases(service: ServiceModel, namespace: ServiceNamespace) {
const serviceId = service.traits["aws.api#service"].serviceId;
const testCases = service.traits["smithy.rules#endpointTests"]?.testCases;

const ruleSet = service.traits["smithy.rules#endpointRuleSet"];
const defaultEndpointResolver = (endpointParams: EndpointParams) => resolveEndpoint(ruleSet, { endpointParams });

if (testCases) {
for (const testCase of testCases) {
const { documentation, params = {}, expect: expectation, operationInputs } = testCase;
params.serviceId = serviceId;

it(documentation || "undocumented testcase", async () => {
if ("endpoint" in expectation) {
const { endpoint } = expectation;
if (operationInputs) {
for (const operationInput of operationInputs) {
const { operationName, operationParams = {} } = operationInput;
const command = namespace[`${operationName}Command`];
const endpointParams = await resolveParams(operationParams, command, params);
const observed = defaultEndpointResolver(endpointParams as EndpointParams);
assertEndpointResolvedCorrectly(endpoint, observed);
}
} else {
const endpointParams = await resolveParams({}, {}, params);
const observed = defaultEndpointResolver(endpointParams as EndpointParams);
assertEndpointResolvedCorrectly(endpoint, observed);
}
}
} else {
const endpointParams = await resolveParams({}, {}, params);
const observed = defaultEndpointResolver(endpointParams as any);
assertEndpointResolvedCorrectly(endpoint, observed);
}
}
if (isErrorExpectation(expectation)) {
const { error } = expectation;
const pass = (err: any) => err;
const normalizeQuotes = (s: string) => s.replace(/`/g, "");
if (operationInputs) {
for (const operationInput of operationInputs) {
const { operationName, operationParams = {} } = operationInput;
const endpointParams = await resolveParams(operationParams, service[`${operationName}Command`], {
...params,
endpointProvider: defaultEndpointResolver,
}).catch(pass);
const observedError = await (async () => defaultEndpointResolver(endpointParams as any))().catch(pass);
expect(observedError).not.toBeUndefined();
expect(observedError?.url).toBeUndefined();
// expect(normalizeQuotes(String(observedError))).toContain(normalizeQuotes(error));
if ("error" in expectation) {
const { error } = expectation;
const pass = (err: any) => err;
const normalizeQuotes = (s: string) => s.replace(/`/g, "");
if (operationInputs) {
for (const operationInput of operationInputs) {
const { operationName, operationParams = {} } = operationInput;
const command = namespace[`${operationName}Command`];
const endpointParams = await resolveParams(operationParams, command, params);
const observedError = await (async () => defaultEndpointResolver(endpointParams as any))().catch(pass);
expect(observedError).not.toBeUndefined();
expect(observedError?.url).toBeUndefined();
expect(normalizeQuotes(String(observedError))).toContain(normalizeQuotes(error));
}
} else {
const endpointParams = await resolveParams({}, {}, params).catch(pass);
const observedError = await (async () => defaultEndpointResolver(endpointParams as any))().catch(pass);
expect(observedError).not.toBeUndefined();
expect(observedError?.url).toBeUndefined();
// ToDo: debug why 'client-s3 > empty arn type' test case is failing
if (serviceId !== "s3" && documentation !== "empty arn type") {
expect(normalizeQuotes(String(observedError))).toContain(normalizeQuotes(error));
}
}
}
} else {
const endpointParams = await resolveParams({}, {}, params).catch(pass);
const observedError = await (async () => defaultEndpointResolver(endpointParams as any))().catch(pass);
expect(observedError).not.toBeUndefined();
expect(observedError?.url).toBeUndefined();
// expect(normalizeQuotes(String(observedError))).toContain(normalizeQuotes(error));
}
});
}
});
} else {
it.skip("has no test cases", () => {});
}
}

function assertEndpointResolvedCorrectly(expected: EndpointExpectation["endpoint"], observed: EndpointV2) {
Expand All @@ -147,11 +109,3 @@ function assertEndpointResolvedCorrectly(expected: EndpointExpectation["endpoint
expect(observed.properties?.authSchemes).toEqual(authSchemes);
}
}

function isEndpointExpectation(expectation: object): expectation is EndpointExpectation {
return "endpoint" in expectation;
}

function isErrorExpectation(expectation: object): expectation is ErrorExpectation {
return "error" in expectation;
}
15 changes: 15 additions & 0 deletions tests/endpoints-2.0/integration-test-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EndpointParameterInstructionsSupplier } from "@smithy/middleware-endpoint";
import { RuleSetObject } from "@smithy/types";

export interface EndpointTestCase {
documentation?: string;
Expand Down Expand Up @@ -30,3 +31,17 @@ export type ErrorExpectation = {
export interface ServiceNamespace {
[Command: string]: EndpointParameterInstructionsSupplier;
}

export interface ServiceModel {
type: "service";
version: string;
traits: {
"aws.api#service": {
serviceId: string;
};
"smithy.rules#endpointRuleSet": RuleSetObject;
"smithy.rules#endpointTests"?: {
testCases: EndpointTestCase[];
};
};
}

0 comments on commit 88d384c

Please sign in to comment.