diff --git a/tests/endpoints-2.0/endpoints-integration.spec.ts b/tests/endpoints-2.0/endpoints-integration.spec.ts index 1b5c77411e38..2b6376043245 100644 --- a/tests/endpoints-2.0/endpoints-integration.spec.ts +++ b/tests/endpoints-2.0/endpoints-integration.spec.ts @@ -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) { @@ -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; -} diff --git a/tests/endpoints-2.0/integration-test-types.ts b/tests/endpoints-2.0/integration-test-types.ts index 9c9d33b75e29..1e5949961dd8 100644 --- a/tests/endpoints-2.0/integration-test-types.ts +++ b/tests/endpoints-2.0/integration-test-types.ts @@ -1,4 +1,5 @@ import { EndpointParameterInstructionsSupplier } from "@smithy/middleware-endpoint"; +import { RuleSetObject } from "@smithy/types"; export interface EndpointTestCase { documentation?: string; @@ -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[]; + }; + }; +}