1import * as AWS from "@/AWS";
2import { Connection } from "@/AWS/CodeConnections/Connection.ts";
3import { RepositoryLink } from "@/AWS/CodeConnections/RepositoryLink.ts";
4import * as Test from "@/Test/Alchemy";
5import * as codeconnections from "@distilled.cloud/aws/codeconnections";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// A repository link requires a connection in the AVAILABLE state, and
12// completing a connection's OAuth handshake is a manual console step with no
13// API. The ungated probe below proves the request wiring end-to-end against
14// the real service (the call reaches CodeConnections and returns its typed
15// domain error for a PENDING connection); the full lifecycle is gated on a
16// manually-completed connection.
17const AVAILABLE_CONNECTION_ARN =
18 process.env.AWS_TEST_CODECONNECTIONS_CONNECTION_ARN;
19const OWNER_ID = process.env.AWS_TEST_CODECONNECTIONS_OWNER_ID;
20const REPOSITORY_NAME = process.env.AWS_TEST_CODECONNECTIONS_REPOSITORY_NAME;
21
22test.provider(
23 "probe: creating a link on a PENDING connection surfaces a typed error",
24 (stack) =>
25 Effect.gen(function* () {
26 yield* stack.destroy();
27
28 const connection = yield* stack.deploy(
29 Effect.gen(function* () {
30 return yield* Connection("RepoLinkProbeConn", {
31 connectionName: "alchemy-test-repolink-conn",
32 providerType: "GitHub",
33 });
34 }),
35 );
36 expect(connection.connectionStatus).toBe("PENDING");
37
38 // The connection has not completed its OAuth handshake. Whichever way
39 // the service answers, the request wiring is proven end-to-end: either
40 // the link is rejected with a TYPED tag (not an unmatched catch-all),
41 // or it is created — in which case we clean it up out-of-band.
42 const result = yield* codeconnections
43 .createRepositoryLink({
44 ConnectionArn: connection.connectionArn,
45 OwnerId: "alchemy-test",
46 RepositoryName: "alchemy-test-repo",
47 })
48 .pipe(
49 Effect.map(
50 (res) =>
51 ({ kind: "created", link: res.RepositoryLinkInfo }) as const,
52 ),
53 Effect.catchTag(
54 [
55 "InvalidInputException",
56 "AccessDeniedException",
57 "ConcurrentModificationException",
58 "ResourceAlreadyExistsException",
59 ],
60 (e) => Effect.succeed({ kind: "rejected", tag: e._tag } as const),
61 ),
62 );
63 if (result.kind === "created") {
64 expect(result.link.RepositoryLinkId).toBeTruthy();
65 yield* codeconnections
66 .deleteRepositoryLink({
67 RepositoryLinkId: result.link.RepositoryLinkId,
68 })
69 .pipe(
70 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
71 );
72 } else {
73 expect(result.tag).toBeTruthy();
74 }
75
76 yield* stack.destroy();
77 }),
78 {
79 tags: ["provider:aws", "provider:aws:codeconnections", "live"],
80 timeout: 120_000,
81 },
82);
83
84test.provider.skipIf(
85 !AVAILABLE_CONNECTION_ARN || !OWNER_ID || !REPOSITORY_NAME,
86)(
87 "lifecycle: create link on an AVAILABLE connection, destroy (gated: AWS_TEST_CODECONNECTIONS_CONNECTION_ARN)",
88 (stack) =>
89 Effect.gen(function* () {
90 yield* stack.destroy();
91
92 const deployed = yield* stack.deploy(
93 Effect.gen(function* () {
94 return yield* RepositoryLink("Repo", {
95 connectionArn: AVAILABLE_CONNECTION_ARN!,
96 ownerId: OWNER_ID!,
97 repositoryName: REPOSITORY_NAME!,
98 });
99 }),
100 );
101 expect(deployed.repositoryLinkArn).toContain(":repository-link/");
102 expect(deployed.repositoryLinkId).toBeTruthy();
103 expect(deployed.ownerId).toBe(OWNER_ID);
104 expect(deployed.repositoryName).toBe(REPOSITORY_NAME);
105
106 // Out-of-band verification via distilled.
107 const created = yield* codeconnections.getRepositoryLink({
108 RepositoryLinkId: deployed.repositoryLinkId,
109 });
110 expect(created.RepositoryLinkInfo.ConnectionArn).toBe(
111 AVAILABLE_CONNECTION_ARN,
112 );
113
114 // Destroy — link is deleted; verify it is gone out-of-band.
115 yield* stack.destroy();
116 const after = yield* codeconnections
117 .getRepositoryLink({ RepositoryLinkId: deployed.repositoryLinkId })
118 .pipe(
119 Effect.map((res) => res.RepositoryLinkInfo),
120 Effect.catchTag("ResourceNotFoundException", () =>
121 Effect.succeed(undefined),
122 ),
123 );
124 expect(after).toBeUndefined();
125 }),
126 {
127 tags: ["provider:aws", "provider:aws:codeconnections", "live"],
128 timeout: 120_000,
129 },
130);