1import * as AWS from "@/AWS";
2import { Role } from "@/AWS/IAM";
3import { Server, User } from "@/AWS/Transfer";
4import * as Test from "@/Test/Alchemy";
5import * as transfer from "@distilled.cloud/aws/transfer";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Ungated typed-error probe: proves the distilled error union carries the
13// not-found tag this provider's read/delete paths depend on.
14test.provider(
15 "describeServer on a nonexistent server fails with ResourceNotFoundException",
16 () =>
17 Effect.gen(function* () {
18 const error = yield* Effect.flip(
19 transfer.describeServer({ ServerId: "s-00000000000000000" }),
20 );
21 expect(error._tag).toBe("ResourceNotFoundException");
22 }),
23 { tags: ["provider:aws", "provider:aws:transfer", "live"] },
24);
25
26const describeServer = (serverId: string) =>
27 transfer.describeServer({ ServerId: serverId }).pipe(
28 Effect.map((r) => r.Server),
29 Effect.catchTag("ResourceNotFoundException", () =>
30 Effect.succeed(undefined),
31 ),
32 );
33
34const assertServerGone = (serverId: string) =>
35 describeServer(serverId).pipe(
36 Effect.flatMap((server) =>
37 server === undefined
38 ? Effect.void
39 : Effect.fail(new Error(`server '${serverId}' still exists`)),
40 ),
41 Effect.retry({
42 schedule: Schedule.max([
43 Schedule.fixed("5 seconds"),
44 Schedule.recurs(24),
45 ]),
46 }),
47 );
48
49// A running Transfer server is billed hourly. Provisioning to ONLINE takes a
50// few minutes. The full lifecycle is gated behind AWS_TEST_SLOW=1 and always
51// destroys what it created.
52test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
53 "create SFTP server + service-managed user, destroy, verify gone",
54 (stack) =>
55 Effect.gen(function* () {
56 yield* stack.destroy();
57
58 const { server, user } = yield* stack.deploy(
59 Effect.gen(function* () {
60 const server = yield* Server("Sftp", {
61 protocols: ["SFTP"],
62 domain: "S3",
63 endpointType: "PUBLIC",
64 identityProviderType: "SERVICE_MANAGED",
65 tags: { env: "test" },
66 });
67 const role = yield* Role("TransferUserRole", {
68 assumeRolePolicyDocument: {
69 Version: "2012-10-17",
70 Statement: [
71 {
72 Effect: "Allow",
73 Principal: { Service: "transfer.amazonaws.com" },
74 Action: ["sts:AssumeRole"],
75 },
76 ],
77 },
78 inlinePolicies: {
79 s3: {
80 Version: "2012-10-17",
81 Statement: [
82 {
83 Effect: "Allow",
84 Action: ["s3:ListBucket", "s3:GetObject", "s3:PutObject"],
85 Resource: ["*"],
86 },
87 ],
88 },
89 },
90 });
91 const user = yield* User("Alice", {
92 serverId: server.serverId,
93 userName: "alice",
94 role: role.roleArn,
95 homeDirectory: "/example-bucket/alice",
96 tags: { env: "test" },
97 });
98 return { server, user };
99 }),
100 );
101
102 expect(server.arn).toContain(":server/");
103 expect(server.protocols).toContain("SFTP");
104 expect(server.domain).toBe("S3");
105 expect(["ONLINE", "OFFLINE", "STARTING"]).toContain(server.state);
106 expect(server.tags.env).toBe("test");
107
108 // Out-of-band verification via distilled.
109 const observedServer = yield* describeServer(server.serverId);
110 expect(observedServer?.IdentityProviderType).toBe("SERVICE_MANAGED");
111
112 expect(user.userName).toBe("alice");
113 expect(user.arn).toContain(":user/");
114 const observedUser = yield* transfer.describeUser({
115 ServerId: server.serverId,
116 UserName: "alice",
117 });
118 expect(observedUser.User.HomeDirectory).toBe("/example-bucket/alice");
119 expect(observedUser.User.Role).toBe(user.role);
120
121 yield* stack.destroy();
122 yield* assertServerGone(server.serverId);
123 }),
124 {
125 tags: ["provider:aws", "provider:aws:iam", "provider:aws:transfer", "live"],
126 timeout: 900_000,
127 },
128);