| ... | @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import * as ConfigProvider from "effect/ConfigProvider"; | 4 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 5 | import * as DateTime from "effect/DateTime"; | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| | 7 | import * as Fiber from "effect/Fiber"; |
| 7 | import * as FileSystem from "effect/FileSystem"; | 8 | import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Layer from "effect/Layer"; | 9 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; | 10 | import * as Option from "effect/Option"; |
| ... | @@ -65,13 +66,16 @@ function makeLayer(input: { | ... | @@ -65,13 +66,16 @@ function makeLayer(input: { |
| 65 | readonly requestFailure?: ( | 66 | readonly requestFailure?: ( |
| 66 | request: HttpClientRequest.HttpClientRequest, | 67 | request: HttpClientRequest.HttpClientRequest, |
| 67 | ) => HttpClientError.HttpClientError; | 68 | ) => HttpClientError.HttpClientError; |
| | 69 | readonly stall?: boolean; |
| 68 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; | 70 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 69 | readonly env?: Record<string, string>; | 71 | readonly env?: Record<string, string>; |
| 70 | }) { | 72 | }) { |
| 71 | const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) => | 73 | const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) => |
| 72 | input.requestFailure | 74 | input.stall |
| 73 | ? Effect.fail(input.requestFailure(request)) | 75 | ? Effect.never |
| 74 | : Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), | 76 | : input.requestFailure |
| | 77 | ? Effect.fail(input.requestFailure(request)) |
| | 78 | : Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), |
| 75 | ); | 79 | ); |
| 76 | const gitMock = { | 80 | const gitMock = { |
| 77 | readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() => | 81 | readConfigValue: vi.fn<GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"]>(() => |
| ... | @@ -1099,3 +1103,80 @@ it.effect("cuts a response short rather than reading an unbounded diff into memo | ... | @@ -1099,3 +1103,80 @@ it.effect("cuts a response short rather than reading an unbounded diff into memo |
| 1099 | ), | 1103 | ), |
| 1100 | ), | 1104 | ), |
| 1101 | ); | 1105 | ); |
| | 1106 | |
| | 1107 | const stalledResponseBody = (status: number): Response => |
| | 1108 | new Response( |
| | 1109 | new ReadableStream<Uint8Array>({ |
| | 1110 | start(controller) { |
| | 1111 | controller.enqueue(new TextEncoder().encode("{")); |
| | 1112 | // Deliberately never close: headers arrive before the body stalls. |
| | 1113 | }, |
| | 1114 | }), |
| | 1115 | { status, headers: { "content-type": "application/json" } }, |
| | 1116 | ); |
| | 1117 | |
| | 1118 | it.effect.each([200, 503])( |
| | 1119 | "times out a stalled Bitbucket JSON body after HTTP %s headers", |
| | 1120 | (status) => { |
| | 1121 | const { execute, layer } = makeLayer({ response: () => stalledResponseBody(status) }); |
| | 1122 | return Effect.gen(function* () { |
| | 1123 | const api = yield* BitbucketApi.BitbucketApi; |
| | 1124 | const fiber = yield* Effect.forkChild( |
| | 1125 | Effect.flip(api.getPullRequest({ cwd: "/repo", reference: "42" })), |
| | 1126 | ); |
| | 1127 | while (execute.mock.calls.length === 0) yield* Effect.yieldNow; |
| | 1128 | yield* TestClock.adjust("31 seconds"); |
| | 1129 | const error = yield* Fiber.join(fiber); |
| | 1130 | assert.instanceOf(error, BitbucketApi.BitbucketRequestError); |
| | 1131 | assert.strictEqual(error.operation, "getPullRequest"); |
| | 1132 | }).pipe(Effect.provide(layer)); |
| | 1133 | }, |
| | 1134 | ); |
| | 1135 | |
| | 1136 | it.effect.each([200, 503])( |
| | 1137 | "times out a stalled Bitbucket raw body after HTTP %s headers", |
| | 1138 | (status) => { |
| | 1139 | const { execute, layer } = makeLayer({ response: () => stalledResponseBody(status) }); |
| | 1140 | return Effect.gen(function* () { |
| | 1141 | const api = yield* BitbucketApi.BitbucketApi; |
| | 1142 | const fiber = yield* Effect.forkChild( |
| | 1143 | Effect.flip(api.request({ method: "GET", url: "/user" })), |
| | 1144 | ); |
| | 1145 | while (execute.mock.calls.length === 0) yield* Effect.yieldNow; |
| | 1146 | yield* TestClock.adjust("31 seconds"); |
| | 1147 | const error = yield* Fiber.join(fiber); |
| | 1148 | assert.instanceOf(error, BitbucketApi.BitbucketRequestError); |
| | 1149 | assert.strictEqual(error.operation, "request"); |
| | 1150 | }).pipe(Effect.provide(layer)); |
| | 1151 | }, |
| | 1152 | ); |
| | 1153 | |
| | 1154 | it.effect("times out before JSON headers arrive", () => { |
| | 1155 | const { execute, layer } = makeLayer({ response: () => Response.json({}), stall: true }); |
| | 1156 | return Effect.gen(function* () { |
| | 1157 | const api = yield* BitbucketApi.BitbucketApi; |
| | 1158 | const fiber = yield* Effect.forkChild( |
| | 1159 | Effect.flip(api.getPullRequest({ cwd: "/repo", reference: "42" })), |
| | 1160 | ); |
| | 1161 | while (execute.mock.calls.length === 0) yield* Effect.yieldNow; |
| | 1162 | yield* TestClock.adjust("31 seconds"); |
| | 1163 | const error = yield* Fiber.join(fiber); |
| | 1164 | assert.instanceOf(error, BitbucketApi.BitbucketRequestError); |
| | 1165 | assert.strictEqual(error.operation, "getPullRequest"); |
| | 1166 | }).pipe(Effect.provide(layer)); |
| | 1167 | }); |
| | 1168 | |
| | 1169 | it.effect("times out before raw response headers arrive", () => { |
| | 1170 | const { execute, layer } = makeLayer({ response: () => Response.json({}), stall: true }); |
| | 1171 | return Effect.gen(function* () { |
| | 1172 | const api = yield* BitbucketApi.BitbucketApi; |
| | 1173 | const fiber = yield* Effect.forkChild( |
| | 1174 | Effect.flip(api.request({ method: "GET", url: "/user" })), |
| | 1175 | ); |
| | 1176 | while (execute.mock.calls.length === 0) yield* Effect.yieldNow; |
| | 1177 | yield* TestClock.adjust("31 seconds"); |
| | 1178 | const error = yield* Fiber.join(fiber); |
| | 1179 | assert.instanceOf(error, BitbucketApi.BitbucketRequestError); |
| | 1180 | assert.strictEqual(error.operation, "request"); |
| | 1181 | }).pipe(Effect.provide(layer)); |
| | 1182 | }); |