11import { describe , expect , it } from "@effect/vitest"
22import { Effect } from "effect"
3+ import * as fc from "fast-check"
34
45import { buildDockerGitOpenApi } from "../src/api/openapi.js"
56
67const documentedMethods = [ "delete" , "get" , "post" , "put" ] as const
78const commonErrorStatuses = [ "400" , "401" , "404" , "409" , "500" ]
9+ const okOnlyOperations = [
10+ { method : "post" , path : "/projects/apply-all" } ,
11+ { method : "post" , path : "/projects/down-all" } ,
12+ { method : "delete" , path : "/projects/{projectId}" } ,
13+ { method : "post" , path : "/projects/{projectId}/down" } ,
14+ { method : "delete" , path : "/projects/{projectId}/ports/{targetPort}" } ,
15+ { method : "delete" , path : "/projects/{projectId}/databases/profiles/{profileId}" } ,
16+ { method : "delete" , path : "/projects/{projectId}/databases/profiles/{profileId}/expose" } ,
17+ { method : "delete" , path : "/projects/by-key/{projectKey}/terminal-sessions/{sessionId}" } ,
18+ { method : "delete" , path : "/auth/terminal-sessions/{sessionId}" } ,
19+ { method : "post" , path : "/projects/{projectId}/tasks/{pid}/stop" }
20+ ] as const
821
922describe ( "openapi contract" , ( ) => {
1023 it . effect ( "documents generated REST paths from the Effect HttpApi contract" , ( ) =>
@@ -86,22 +99,38 @@ describe("openapi contract", () => {
8699 }
87100 } ) )
88101
102+ it . effect ( "preserves common error status invariant for arbitrary documented operations" , ( ) =>
103+ Effect . sync ( ( ) => {
104+ const spec = buildDockerGitOpenApi ( )
105+ const paths = spec . paths ?? { }
106+ const operations = Object . entries ( paths ) . flatMap ( ( [ path , item ] ) =>
107+ documentedMethods . flatMap ( ( method ) => {
108+ const responses = item [ method ] ?. responses
109+ return responses === undefined ? [ ] : [ { method, path, responses } ]
110+ } )
111+ )
112+
113+ expect ( operations . length ) . toBeGreaterThan ( 0 )
114+
115+ fc . assert (
116+ fc . property ( fc . integer ( { min : 0 , max : operations . length - 1 } ) , ( index ) => {
117+ const operation = operations [ index ]
118+ if ( operation === undefined ) {
119+ return false
120+ }
121+ expect ( Object . keys ( operation . responses ) , `${ operation . method . toUpperCase ( ) } ${ operation . path } ` ) . toEqual (
122+ expect . arrayContaining ( commonErrorStatuses )
123+ )
124+ return true
125+ } ) ,
126+ { numRuns : Math . max ( operations . length * 2 , 50 ) }
127+ )
128+ } ) )
129+
89130 it . effect ( "documents ok-only HTTP handlers as 200 JSON responses" , ( ) =>
90131 Effect . sync ( ( ) => {
91132 const spec = buildDockerGitOpenApi ( )
92133 const paths = spec . paths ?? { }
93- const okOnlyOperations = [
94- { method : "post" , path : "/projects/apply-all" } ,
95- { method : "post" , path : "/projects/down-all" } ,
96- { method : "delete" , path : "/projects/{projectId}" } ,
97- { method : "post" , path : "/projects/{projectId}/down" } ,
98- { method : "delete" , path : "/projects/{projectId}/ports/{targetPort}" } ,
99- { method : "delete" , path : "/projects/{projectId}/databases/profiles/{profileId}" } ,
100- { method : "delete" , path : "/projects/{projectId}/databases/profiles/{profileId}/expose" } ,
101- { method : "delete" , path : "/projects/by-key/{projectKey}/terminal-sessions/{sessionId}" } ,
102- { method : "delete" , path : "/auth/terminal-sessions/{sessionId}" } ,
103- { method : "post" , path : "/projects/{projectId}/tasks/{pid}/stop" }
104- ] as const
105134
106135 for ( const operation of okOnlyOperations ) {
107136 const responses = paths [ operation . path ] ?. [ operation . method ] ?. responses ?? { }
@@ -114,6 +143,25 @@ describe("openapi contract", () => {
114143 }
115144 } ) )
116145
146+ it . effect ( "preserves ok-only handler response invariant for arbitrary ok-only operations" , ( ) =>
147+ Effect . sync ( ( ) => {
148+ const spec = buildDockerGitOpenApi ( )
149+ const paths = spec . paths ?? { }
150+
151+ fc . assert (
152+ fc . property ( fc . constantFrom ( ...okOnlyOperations ) , ( operation ) => {
153+ const responses = paths [ operation . path ] ?. [ operation . method ] ?. responses ?? { }
154+ const serializedSuccessSchema = JSON . stringify ( responses [ "200" ] ?? { } )
155+
156+ expect ( responses [ "200" ] , `${ operation . method . toUpperCase ( ) } ${ operation . path } ` ) . toBeDefined ( )
157+ expect ( responses [ "204" ] , `${ operation . method . toUpperCase ( ) } ${ operation . path } ` ) . toBeUndefined ( )
158+ expect ( serializedSuccessSchema ) . toContain ( "\"required\":[\"ok\"]" )
159+ expect ( serializedSuccessSchema ) . toContain ( "\"ok\":{\"type\":\"boolean\",\"enum\":[true]}" )
160+ } ) ,
161+ { numRuns : okOnlyOperations . length * 2 }
162+ )
163+ } ) )
164+
117165 it . effect ( "documents project auth snapshots without nonexistent totalEntries" , ( ) =>
118166 Effect . sync ( ( ) => {
119167 const spec = buildDockerGitOpenApi ( )
0 commit comments