1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as transcoder from "@distilled.cloud/gcp/transcoder_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16const location = "us-central1";
17
18const sdConfig: GCP.Transcoder.JobConfig = {
19 elementaryStreams: [
20 {
21 key: "video-stream0",
22 videoStream: {
23 h264: {
24 heightPixels: 360,
25 widthPixels: 640,
26 bitrateBps: 550000,
27 frameRate: 30,
28 },
29 },
30 },
31 {
32 key: "audio-stream0",
33 audioStream: { codec: "aac", bitrateBps: 64000 },
34 },
35 ],
36 muxStreams: [
37 {
38 key: "sd",
39 container: "mp4",
40 elementaryStreams: ["video-stream0", "audio-stream0"],
41 },
42 ],
43};
44
45const hdConfig: GCP.Transcoder.JobConfig = {
46 elementaryStreams: [
47 {
48 key: "video-stream0",
49 videoStream: {
50 h264: {
51 heightPixels: 720,
52 widthPixels: 1280,
53 bitrateBps: 2500000,
54 frameRate: 30,
55 },
56 },
57 },
58 {
59 key: "audio-stream0",
60 audioStream: { codec: "aac", bitrateBps: 64000 },
61 },
62 ],
63 muxStreams: [
64 {
65 key: "hd",
66 container: "mp4",
67 elementaryStreams: ["video-stream0", "audio-stream0"],
68 },
69 ],
70};
71
72const waitUntilGone = (name: string) =>
73 transcoder.getProjectsLocationsJobTemplates({ name }).pipe(
74 Effect.as("found" as const),
75 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
76 Effect.repeat({
77 schedule: Schedule.spaced("1 second"),
78 until: (status) => status === "gone",
79 times: 10,
80 }),
81 );
82
83test.provider(
84 "getProjectsLocationsJobTemplates on a missing template fails with a typed tag",
85 (stack) =>
86 Effect.gen(function* () {
87 const { project } = yield* GcpEnvironment.current;
88 const parent = `projects/${project}/locations/${location}`;
89 const missingName = `${parent}/jobTemplates/alchemy-missing-template`;
90
91 yield* stack.destroy();
92
93 const error = yield* Effect.flip(
94 transcoder.getProjectsLocationsJobTemplates({ name: missingName }),
95 );
96 expect(error._tag).toEqual("NotFound");
97
98 yield* stack.destroy();
99 }).pipe(logLevel),
100 {
101 tags: ["provider:gcp", "provider:gcp:transcoder", "live"],
102 timeout: 90_000,
103 },
104);
105
106test.provider(
107 "create, replace, and delete a job template",
108 (stack) =>
109 Effect.gen(function* () {
110 yield* stack.destroy();
111
112 const created = yield* stack.deploy(
113 Effect.gen(function* () {
114 return yield* GCP.Transcoder.JobTemplate("WebHd", {
115 location,
116 labels: { env: "test" },
117 config: sdConfig,
118 });
119 }),
120 );
121
122 expect(created.jobTemplateId).toEqual(expect.any(String));
123 expect(created.jobTemplateId.length).toBeGreaterThanOrEqual(4);
124 expect(created.name).toContain("/jobTemplates/");
125 expect(created.location).toEqual(location);
126 expect(created.labels).toMatchObject({ env: "test" });
127 expect(created.config?.muxStreams?.[0]?.key).toEqual("sd");
128
129 const fetched = yield* transcoder.getProjectsLocationsJobTemplates({
130 name: created.name,
131 });
132 expect(fetched.name).toEqual(created.name);
133 expect(fetched.labels?.env).toEqual("test");
134 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
135 expect(fetched.config?.muxStreams?.[0]?.key).toEqual("sd");
136
137 const listed = yield* transcoder.listProjectsLocationsJobTemplates({
138 parent: created.parent,
139 pageSize: 1000,
140 });
141 expect(
142 (listed.jobTemplates ?? []).some(
143 (template) => template.name === created.name,
144 ),
145 ).toEqual(true);
146
147 const updated = yield* stack.deploy(
148 Effect.gen(function* () {
149 return yield* GCP.Transcoder.JobTemplate("WebHd", {
150 jobTemplateId: created.jobTemplateId,
151 location,
152 labels: { env: "prod", role: "transcode" },
153 config: hdConfig,
154 });
155 }),
156 );
157
158 expect(updated.jobTemplateId).toEqual(created.jobTemplateId);
159 expect(updated.location).toEqual(location);
160 expect(updated.labels).toMatchObject({ env: "prod", role: "transcode" });
161 expect(updated.config?.muxStreams?.[0]?.key).toEqual("hd");
162
163 const fetchedUpdate = yield* transcoder.getProjectsLocationsJobTemplates({
164 name: updated.name,
165 });
166 expect(fetchedUpdate.labels?.env).toEqual("prod");
167 expect(fetchedUpdate.labels?.role).toEqual("transcode");
168 expect(fetchedUpdate.config?.muxStreams?.[0]?.key).toEqual("hd");
169
170 const replaced = yield* stack.deploy(
171 Effect.gen(function* () {
172 return yield* GCP.Transcoder.JobTemplate("WebHd", {
173 jobTemplateId: created.jobTemplateId,
174 location: "us-east1",
175 labels: { env: "test" },
176 config: sdConfig,
177 });
178 }),
179 );
180
181 expect(replaced.jobTemplateId).toEqual(created.jobTemplateId);
182 expect(replaced.location).toEqual("us-east1");
183 expect(replaced.name).toContain("/locations/us-east1/");
184 expect(replaced.name).not.toEqual(created.name);
185
186 const oldGone = yield* waitUntilGone(updated.name);
187 expect(oldGone).toEqual("gone");
188
189 const fetchedReplace = yield* transcoder.getProjectsLocationsJobTemplates(
190 {
191 name: replaced.name,
192 },
193 );
194 expect(fetchedReplace.name).toEqual(replaced.name);
195 expect(fetchedReplace.config?.muxStreams?.[0]?.key).toEqual("sd");
196
197 yield* stack.destroy();
198
199 const gone = yield* waitUntilGone(replaced.name);
200 expect(gone).toEqual("gone");
201 }).pipe(logLevel),
202 {
203 tags: ["provider:gcp", "provider:gcp:transcoder", "live"],
204 timeout: 90_000,
205 },
206);