1+ /* jscpd:ignore-start */
2+ import { createServer , type Server } from "node:http"
3+ import type { AddressInfo } from "node:net"
4+
15import { NodeContext } from "@effect/platform-node"
26import { describe , expect , it } from "@effect/vitest"
37import { Effect } from "effect"
4- import { afterEach , beforeEach , vi } from "vitest"
8+ import { beforeEach , vi } from "vitest"
59
610import { request } from "../../src/docker-git/api-http.js"
11+ /* jscpd:ignore-end */
712
813const resolveApiBaseUrlMock = vi . hoisted ( ( ) => vi . fn < ( ) => string > ( ) )
914const ensureControllerReadyMock = vi . hoisted ( ( ) => vi . fn < ( ) => Effect . Effect < void > > ( ) )
@@ -15,22 +20,42 @@ vi.mock("../../src/docker-git/controller.js", () => ({
1520
1621const joinIp = ( ...octets : ReadonlyArray < string > ) : string => octets . join ( "." )
1722const makeHttpUrl = ( host : string , port : string ) : string => [ "ht" , "tp://" , host , ":" , port ] . join ( "" )
18- const toFetchUrl = ( value : Parameters < typeof globalThis . fetch > [ 0 ] | undefined ) : string => {
19- if ( value === undefined ) {
20- throw new TypeError ( "unexpected undefined fetch request value" )
21- }
22- if ( typeof value === "string" ) {
23- return value
24- }
25- if ( value instanceof URL ) {
26- return value . toString ( )
27- }
28- if ( value instanceof Request ) {
29- return value . url
30- }
31-
32- throw new TypeError ( "unexpected fetch request value" )
33- }
23+
24+ const listen = ( server : Server ) : Effect . Effect < number , Error > =>
25+ Effect . async ( ( resume ) => {
26+ const onError = ( error : Error ) => {
27+ resume ( Effect . fail ( error ) )
28+ }
29+
30+ server . once ( "error" , onError )
31+ server . listen ( 0 , "127.0.0.1" , ( ) => {
32+ server . off ( "error" , onError )
33+ resume ( Effect . succeed ( ( server . address ( ) as AddressInfo ) . port ) )
34+ } )
35+
36+ return Effect . sync ( ( ) => {
37+ server . off ( "error" , onError )
38+ } )
39+ } )
40+
41+ const close = ( server : Server ) : Effect . Effect < void , Error > =>
42+ Effect . async ( ( resume ) => {
43+ server . close ( ( error ) => {
44+ if ( error === undefined ) {
45+ resume ( Effect . void )
46+ return
47+ }
48+ resume ( Effect . fail ( error ) )
49+ } )
50+ } )
51+
52+ const reserveUnusedPort = ( ) =>
53+ Effect . gen ( function * ( _ ) {
54+ const server = createServer ( )
55+ const port = yield * _ ( listen ( server ) )
56+ yield * _ ( close ( server ) )
57+ return port
58+ } )
3459
3560describe ( "api-http request retry" , ( ) => {
3661 beforeEach ( ( ) => {
@@ -39,55 +64,49 @@ describe("api-http request retry", () => {
3964 ensureControllerReadyMock . mockImplementation ( ( ) => Effect . void )
4065 } )
4166
42- afterEach ( ( ) => {
43- vi . unstubAllGlobals ( )
44- } )
45-
4667 it . effect ( "refreshes controller readiness once after a transport failure" , ( ) =>
4768 Effect . gen ( function * ( _ ) {
48- const fetchMock = vi . fn < typeof globalThis . fetch > ( )
49- fetchMock . mockRejectedValueOnce ( new TypeError ( "fetch failed" ) )
50- fetchMock . mockResolvedValueOnce (
51- Response . json ( { ok : true } , {
52- status : 200 ,
53- headers : { "content-type" : "application/json" }
54- } )
55- )
56- vi . stubGlobal ( "fetch" , fetchMock )
57-
58- resolveApiBaseUrlMock . mockReturnValueOnce (
59- makeHttpUrl ( joinIp ( "127" , "0" , "0" , "1" ) , "3334" )
60- )
61- resolveApiBaseUrlMock . mockReturnValueOnce (
62- makeHttpUrl ( joinIp ( "172" , "17" , "0" , "20" ) , "3334" )
69+ const seenUrls : Array < string | undefined > = [ ]
70+ const server = createServer ( ( incoming , response ) => {
71+ seenUrls . push ( incoming . url )
72+ response . writeHead ( 200 , { "content-type" : "application/json" } )
73+ response . end ( JSON . stringify ( { ok : true } ) )
74+ } )
75+ const deadPort = yield * _ ( reserveUnusedPort ( ) )
76+ const port = yield * _ ( listen ( server ) )
77+
78+ yield * _ (
79+ Effect . gen ( function * ( _ ) {
80+ resolveApiBaseUrlMock . mockReturnValueOnce (
81+ makeHttpUrl ( joinIp ( "127" , "0" , "0" , "1" ) , String ( deadPort ) )
82+ )
83+ resolveApiBaseUrlMock . mockReturnValueOnce (
84+ makeHttpUrl ( joinIp ( "127" , "0" , "0" , "1" ) , String ( port ) )
85+ )
86+
87+ const payload = yield * _ ( request ( "GET" , "/health" ) )
88+
89+ expect ( payload ) . toEqual ( { ok : true } )
90+ expect ( ensureControllerReadyMock ) . toHaveBeenCalledTimes ( 1 )
91+ expect ( seenUrls ) . toEqual ( [ "/health" ] )
92+ } ) . pipe (
93+ Effect . ensuring ( close ( server ) . pipe ( Effect . catchAll ( ( ) => Effect . void ) ) )
94+ )
6395 )
64-
65- const payload = yield * _ ( request ( "GET" , "/health" ) )
66-
67- expect ( payload ) . toEqual ( { ok : true } )
68- expect ( ensureControllerReadyMock ) . toHaveBeenCalledTimes ( 1 )
69- expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 )
70-
71- const firstCall = fetchMock . mock . calls [ 0 ] ?. [ 0 ]
72- const secondCall = fetchMock . mock . calls [ 1 ] ?. [ 0 ]
73- expect ( toFetchUrl ( firstCall ) ) . toContain ( `${ joinIp ( "127" , "0" , "0" , "1" ) } :3334/health` )
74- expect ( toFetchUrl ( secondCall ) ) . toContain ( `${ joinIp ( "172" , "17" , "0" , "20" ) } :3334/health` )
7596 } ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
7697
7798 it . effect ( "does not replay mutating requests after a transport failure" , ( ) =>
7899 Effect . gen ( function * ( _ ) {
79- const fetchMock = vi . fn < typeof globalThis . fetch > ( )
80- fetchMock . mockRejectedValueOnce ( new TypeError ( "fetch failed" ) )
81- vi . stubGlobal ( "fetch" , fetchMock )
100+ const deadPort = yield * _ ( reserveUnusedPort ( ) )
82101
83102 resolveApiBaseUrlMock . mockReturnValue (
84- makeHttpUrl ( joinIp ( "127" , "0" , "0" , "1" ) , "3334" )
103+ makeHttpUrl ( joinIp ( "127" , "0" , "0" , "1" ) , String ( deadPort ) )
85104 )
86105
87106 const result = yield * _ ( Effect . either ( request ( "POST" , "/projects" , { outDir : "project-1" } ) ) )
88107
89108 expect ( result . _tag ) . toBe ( "Left" )
90109 expect ( ensureControllerReadyMock ) . not . toHaveBeenCalled ( )
91- expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
110+ expect ( resolveApiBaseUrlMock ) . toHaveBeenCalledTimes ( 1 )
92111 } ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
93112} )
0 commit comments