1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as oslogin from "@distilled.cloud/gcp/oslogin_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16const KEY1 =
17 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6Ot81wrURgF58/jKCFQgEzJFjD39ibwfpeC7JLoS6d";
18const KEY2 =
19 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBpVICT7tXAjpo6pXw/44Wm+DYcQRexT7J8nwS9/XtnL";
20
21const EXPIRY_A = "4102444800000000";
22const EXPIRY_B = "4133980800000000";
23
24// OS Login keys belong to the authenticated account. A service account
25// must name itself (`users/me` fails with UserCredentialMismatch), so set
26// GCP_TEST_OSLOGIN_USER to the test credentials' email.
27const user = process.env.GCP_TEST_OSLOGIN_USER?.trim();
28
29const waitUntilGone = (name: string) =>
30 oslogin.getUsersSshPublicKeys({ name }).pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("1 second"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider(
41 "users/me with service-account credentials fails with UserCredentialMismatch",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 oslogin.getUsersSshPublicKeys({
48 name: "users/me/sshPublicKeys/alchemy-missing-fingerprint",
49 }),
50 );
51 expect(error._tag).toEqual("UserCredentialMismatch");
52
53 yield* stack.destroy();
54 }).pipe(logLevel),
55 { tags: ["provider:gcp", "provider:gcp:oslogin", "live"], timeout: 90_000 },
56);
57
58test.provider.skipIf(!user || !!process.env.FAST)(
59 "create, update, and delete an SSH public key",
60 (stack) =>
61 Effect.gen(function* () {
62 yield* stack.destroy();
63
64 const created = yield* stack.deploy(
65 Effect.gen(function* () {
66 return yield* GCP.OSLogin.UsersSshPublicKey("Laptop", {
67 user,
68 key: KEY1,
69 expirationTimeUsec: EXPIRY_A,
70 });
71 }),
72 );
73
74 expect(created.name.length).toBeGreaterThan(0);
75 expect(created.fingerprint.length).toBeGreaterThan(0);
76 expect(created.user).toEqual(user);
77 expect(created.key).toEqual(KEY1);
78 expect(created.expirationTimeUsec).toEqual(EXPIRY_A);
79
80 const fetched = yield* oslogin.getUsersSshPublicKeys({
81 name: created.name,
82 });
83 expect(fetched.name).toEqual(created.name);
84 expect(fetched.fingerprint).toEqual(created.fingerprint);
85 expect(fetched.key).toEqual(KEY1);
86 expect(fetched.expirationTimeUsec).toEqual(EXPIRY_A);
87
88 const updated = yield* stack.deploy(
89 Effect.gen(function* () {
90 return yield* GCP.OSLogin.UsersSshPublicKey("Laptop", {
91 user,
92 key: KEY1,
93 expirationTimeUsec: EXPIRY_B,
94 });
95 }),
96 );
97
98 expect(updated.name).toEqual(created.name);
99 expect(updated.fingerprint).toEqual(created.fingerprint);
100 expect(updated.expirationTimeUsec).toEqual(EXPIRY_B);
101
102 const fetchedUpdate = yield* oslogin.getUsersSshPublicKeys({
103 name: updated.name,
104 });
105 expect(fetchedUpdate.expirationTimeUsec).toEqual(EXPIRY_B);
106 expect(fetchedUpdate.key).toEqual(KEY1);
107
108 const replaced = yield* stack.deploy(
109 Effect.gen(function* () {
110 return yield* GCP.OSLogin.UsersSshPublicKey("Laptop", {
111 user,
112 key: KEY2,
113 expirationTimeUsec: EXPIRY_B,
114 });
115 }),
116 );
117
118 expect(replaced.fingerprint).not.toEqual(created.fingerprint);
119 expect(replaced.key).toEqual(KEY2);
120
121 const fetchedReplace = yield* oslogin.getUsersSshPublicKeys({
122 name: replaced.name,
123 });
124 expect(fetchedReplace.fingerprint).toEqual(replaced.fingerprint);
125 expect(fetchedReplace.key).toEqual(KEY2);
126
127 yield* stack.destroy();
128
129 const gone = yield* waitUntilGone(replaced.name);
130 expect(gone).toEqual("gone");
131 }).pipe(logLevel),
132 {
133 tags: ["provider:gcp", "provider:gcp:oslogin", "live"],
134 timeout: 90_000,
135 exclusive: true,
136 },
137);