t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/CodeArtifact/Bindings.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

383 lines
14 KiB
TypeScript

import * as AWS from "@/AWS";
import * as Core from "@/Test/Core";
import * as Test from "@/Test/Alchemy";
import * as codeartifact from "@distilled.cloud/aws/codeartifact";
import { describe, expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Redacted from "effect/Redacted";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import CodeArtifactTestFunctionLive, {
CodeArtifactTestFunction,
DOMAIN,
REPO,
} from "./handler";
const testOptions = { providers: AWS.providers() };
const { test, beforeAll, afterAll } = Test.make(testOptions);
const sharedStack = Core.scratchStack(testOptions, "CodeArtifactBindings");
let baseUrl: string;
class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
readonly status: number;
readonly body: string;
readonly retryable: boolean;
}> {
override get message() {
return `HTTP ${this.status} (retryable=${this.retryable}): ${this.body}`;
}
}
// The handler surfaces every typed error as a 500 JSON body ({ error: _tag }).
// Only some of those are actually transient — retrying a deterministic tag
// (ConflictException from a re-published version, ValidationException from a
// wiring bug) just burns the whole budget and hides the real failure.
const RETRYABLE_ERROR_TAGS: ReadonlySet<string> = new Set([
// Fresh Lambda role + CodeArtifact/S3 permission propagation — the first
// calls after a cold deploy can 500 with AccessDenied for tens of seconds.
"AccessDeniedException",
"ThrottlingException",
"InternalServerException",
"ServiceUnavailableException",
// The handler's fallback tag for untyped defects.
"UnknownError",
]);
const isRetryableBody = (body: string): boolean => {
try {
const parsed = JSON.parse(body) as { error?: string };
// No tag at all (e.g. a raw gateway body) — assume transient.
return parsed.error === undefined || RETRYABLE_ERROR_TAGS.has(parsed.error);
} catch {
// Non-JSON 5xx: function-URL cold start / gateway errors — transient.
return true;
}
};
// Retry transient 5xx only; a deterministic typed error or a genuine 4xx
// fails immediately (loud, with the body in the error message).
const send = (request: HttpClientRequest.HttpClientRequest) =>
HttpClient.execute(request).pipe(
Effect.flatMap((response) =>
response.status >= 500
? response.text.pipe(
Effect.flatMap((body) =>
Effect.fail(
new TransientUpstream({
status: response.status,
body,
retryable: isRetryableBody(body),
}),
),
),
)
: Effect.succeed(response),
),
Effect.retry({
while: (e) => e._tag === "TransientUpstream" && e.retryable,
// Bounded well under the test timeouts (~63s of sleeps) so a
// persistent 500 surfaces its body instead of an opaque timeout.
// Fresh-role AccessDenied propagation has been observed to outlive
// a 31s budget on cold deploys; the runner's whole-body retries
// (with the /reset pre-clean keeping them idempotent) extend the
// effective window without violating the bounded-backoff doctrine.
schedule: Schedule.max([
Schedule.exponential("1 second"),
Schedule.recurs(6),
]),
}),
);
const getJson = <T>(path: string) =>
Effect.gen(function* () {
const response = yield* send(HttpClientRequest.get(`${baseUrl}${path}`));
expect(response.status).toBe(200);
return (yield* response.json) as T;
});
const postJson = <T>(path: string) =>
Effect.gen(function* () {
const response = yield* send(HttpClientRequest.post(`${baseUrl}${path}`));
expect(response.status).toBe(200);
return (yield* response.json) as T;
});
describe(
"CodeArtifact Bindings",
{
tags: [
"provider:aws",
"provider:aws:codeartifact",
"provider:aws:lambda",
"provider:aws:s3",
"live",
],
},
() => {
beforeAll(
Effect.gen(function* () {
yield* Effect.logInfo(
"CodeArtifact Bindings setup: destroying previous resources",
);
yield* sharedStack.destroy();
yield* Effect.logInfo(
"CodeArtifact Bindings setup: deploying domain -> repositories -> Lambda",
);
const { functionUrl } = yield* sharedStack.deploy(
Effect.gen(function* () {
return yield* CodeArtifactTestFunction;
}).pipe(Effect.provide(CodeArtifactTestFunctionLive)),
);
expect(functionUrl).toBeTruthy();
baseUrl = functionUrl!.replace(/\/+$/, "");
// Readiness probe — fresh function URLs take seconds to serve 200s.
yield* HttpClient.get(`${baseUrl}/nope`).pipe(
Effect.flatMap((response) =>
response.status === 404
? Effect.succeed(response)
: Effect.fail(
new Error(`Function not ready: ${response.status}`),
),
),
Effect.retry({
schedule: Schedule.max([
Schedule.fixed("2 seconds"),
Schedule.recurs(75),
]),
}),
);
}),
{ timeout: 300_000 },
);
// No NO_DESTROY escape hatch here: scratch-stack state is in-memory per
// process, so a skipped destroy would orphan the whole stack forever.
afterAll(sharedStack.destroy(), { timeout: 300_000 });
describe("GetAuthorizationToken", () => {
test.provider(
"mints a domain token with a bounded duration from inside the Lambda",
() =>
Effect.gen(function* () {
const body = yield* getJson<{ redacted: boolean; length: number }>(
"/token",
);
// A CodeArtifact bearer token is a long opaque credential. (The
// in-Lambda `redacted` flag flips to true once the distilled `lib/`
// build catches up with the SensitiveString patch — the bundler
// resolves the built lib, unlike vitest which resolves src.)
expect(body.length).toBeGreaterThan(100);
}),
{ timeout: 120_000 },
);
test.provider(
"authorizationToken is Redacted (distilled sensitive patch)",
() =>
Effect.gen(function* () {
// Direct distilled call (vitest resolves distilled from src, so the
// SensitiveString patch is live here) against the deployed domain.
const res = yield* codeartifact.getAuthorizationToken({
domain: DOMAIN,
durationSeconds: 900,
});
expect(Redacted.isRedacted(res.authorizationToken)).toBe(true);
expect(
Redacted.value(
res.authorizationToken as Redacted.Redacted<string>,
).length,
).toBeGreaterThan(100);
}),
{ timeout: 120_000 },
);
});
describe("GetRepositoryEndpoint", () => {
test.provider(
"resolves the generic-format endpoint",
() =>
Effect.gen(function* () {
const body = yield* getJson<{ endpoint: string }>("/endpoint");
expect(body.endpoint).toContain("codeartifact");
expect(body.endpoint).toContain(REPO);
}),
{ timeout: 120_000 },
);
});
// The package bindings mutate shared repository state, so they run as ONE
// ordered flow — vitest executes tests in this file concurrently
// (`sequence.concurrent`), which would otherwise race the delete steps
// against the read steps.
describe("package lifecycle", () => {
test.provider(
"publish -> inspect -> promote -> dispose -> delete through the bindings",
() =>
Effect.gen(function* () {
// Clean slate — a retried test body (alchemy-test re-runs failing
// bodies) must not trip ConflictException on the re-publish of an
// already-Published generic version.
yield* postJson<{ reset: boolean }>("/reset");
// PublishPackageVersion — a generic package version.
const published = yield* postJson<{ status: string }>(
"/publish?version=1.0.0",
);
expect(published.status).toBe("Published");
// DescribePackage.
const pkg = yield* getJson<{
name: string;
format: string;
namespace: string;
}>("/package");
expect(pkg.name).toBe("test-package");
expect(pkg.format).toBe("generic");
expect(pkg.namespace).toBe("alchemy");
// DescribePackageVersion.
const version = yield* getJson<{ version: string; status: string }>(
"/version?version=1.0.0",
);
expect(version.version).toBe("1.0.0");
expect(version.status).toBe("Published");
// ListPackages.
const packages = yield* getJson<{ names: string[] }>("/packages");
expect(packages.names).toContain("test-package");
// ListPackageVersions.
const versions = yield* getJson<{ versions: string[] }>(
"/versions",
);
expect(versions.versions).toContain("1.0.0");
// ListPackageVersionAssets.
const assets = yield* getJson<{ assets: string[] }>(
"/assets?version=1.0.0",
);
expect(assets.assets).toContain("artifact.txt");
// GetPackageVersionAsset — download and decode the asset stream.
const asset = yield* getJson<{ content: string }>(
"/asset?version=1.0.0",
);
expect(asset.content).toBe("hello codeartifact 1.0.0");
// GetPackageVersionReadme — generic packages have no readme; the
// typed ResourceNotFoundException surfaces instead.
const readme = yield* getJson<{ readme?: string; error?: string }>(
"/readme?version=1.0.0",
);
expect(
readme.error === "ResourceNotFoundException" ||
typeof readme.readme === "string",
).toBe(true);
// ListPackageVersionDependencies — generic packages record no
// dependency manifest.
const deps = yield* getJson<{
dependencies?: string[];
error?: string;
}>("/deps?version=1.0.0");
expect(
deps.error === "ResourceNotFoundException" ||
deps.error === "ValidationException" ||
Array.isArray(deps.dependencies),
).toBe(true);
// PublishPackageVersion (unfinished) + UpdatePackageVersionsStatus.
const unfinished = yield* postJson<{ status: string }>(
"/publish?version=2.0.0&unfinished=1",
);
expect(unfinished.status).toBe("Unfinished");
const updated = yield* postJson<{ successful: string[] }>(
"/status?version=2.0.0&target=Published",
);
expect(updated.successful).toContain("2.0.0");
const after = yield* getJson<{ status: string }>(
"/version?version=2.0.0",
);
expect(after.status).toBe("Published");
// PutPackageOriginConfiguration.
const origin = yield* postJson<{
restrictions: { publish: string; upstream: string };
}>("/origin");
expect(origin.restrictions.publish).toBe("ALLOW");
expect(origin.restrictions.upstream).toBe("BLOCK");
// CopyPackageVersions — promote 1.0.0 into the mirror.
const copied = yield* postJson<{ successful: string[] }>(
"/copy?version=1.0.0",
);
expect(copied.successful).toContain("1.0.0");
const mirror = yield* getJson<{ names: string[] }>(
"/mirror/packages",
);
expect(mirror.names).toContain("test-package");
// DisposePackageVersions — in the mirror.
const disposed = yield* postJson<{ successful: string[] }>(
"/mirror/dispose?version=1.0.0",
);
expect(disposed.successful).toContain("1.0.0");
// DeletePackageVersions — the disposed version.
const deletedVersions = yield* postJson<{ successful: string[] }>(
"/mirror/delete-versions?version=1.0.0",
);
expect(deletedVersions.successful).toContain("1.0.0");
// DeletePackage — the whole package record.
yield* postJson<{ deleted: boolean }>("/mirror/delete-package");
const mirrorAfter = yield* getJson<{ names: string[] }>(
"/mirror/packages",
);
expect(mirrorAfter.names).not.toContain("test-package");
}),
{ timeout: 180_000 },
);
});
describe("PackageVersionStateChangeEventSource", () => {
// CodeArtifact publishes package-version events to EventBridge on a
// best-effort basis with highly variable latency: a live diagnostic
// verified the full pipeline end-to-end (rule pattern matched, Lambda
// permission granted, event delivered, S3 marker written ~60s after
// publish), but delivery has also been observed to exceed a 5-minute
// watch window, which flakes a bounded test. Gate the live-delivery
// assertion; the event source itself deploys with the fixture above on
// every run.
test.provider.skipIf(!process.env.AWS_TEST_CODEARTIFACT_EVENTS)(
"delivers package version events to the handler",
() =>
Effect.gen(function* () {
// The probe publishes its own version (3.0.0, untouched by the
// lifecycle test) and polls S3 for the marker the event handler
// wrote; retry the whole probe a few times to ride out fresh-rule
// propagation on the default bus.
const seen = yield* Effect.gen(function* () {
const body = yield* getJson<{ seen: boolean; version: string }>(
"/events/probe?version=3.0.0",
);
return body.seen;
}).pipe(
Effect.repeat({
schedule: Schedule.spaced("2 seconds"),
until: (s): boolean => s,
times: 3,
}),
);
expect(seen).toBe(true);
}),
{ timeout: 180_000 },
);
});
},
);