| ... | ... | @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | 4 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 5 | 5 | import * as DateTime from "effect/DateTime"; |
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Fiber from "effect/Fiber"; |
| 7 | 8 | import * as FileSystem from "effect/FileSystem"; |
| 8 | 9 | import * as Layer from "effect/Layer"; |
| 9 | 10 | import * as Option from "effect/Option"; |
| ... | ... | @@ -65,13 +66,16 @@ function makeLayer(input: { |
| 65 | 66 | readonly requestFailure?: ( |
| 66 | 67 | request: HttpClientRequest.HttpClientRequest, |
| 67 | 68 | ) => HttpClientError.HttpClientError; |
| 69 | readonly stall?: boolean; |
| 68 | 70 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| 69 | 71 | readonly env?: Record<string, string>; |
| 70 | 72 | }) { |
| 71 | 73 | const execute = vi.fn((request: HttpClientRequest.HttpClientRequest) => |
| 72 | | input.requestFailure |
| 73 | | ? Effect.fail(input.requestFailure(request)) |
| 74 | | : Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), |
| 74 | input.stall |
| 75 | ? Effect.never |
| 76 | : input.requestFailure |
| 77 | ? Effect.fail(input.requestFailure(request)) |
| 78 | : Effect.succeed(HttpClientResponse.fromWeb(request, input.response(request))), |
| 75 | 79 | ); |
| 76 | 80 | const gitMock = { |
| 77 | 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 | 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 | }); |