Skip to content

Commit efc23fa

Browse files
committed
test: add tests for InlineModelResolver requestBody with component schema refs
1 parent 3b3ae6e commit efc23fa

File tree

8 files changed

+229
-60
lines changed

8 files changed

+229
-60
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public class CodegenOperation {
3030
hasVersionHeaders = false, hasVersionQueryParams = false,
3131
isResponseBinary = false, isResponseFile = false, isResponseOptional = false, hasReference = false, defaultReturnType = false,
3232
isDeprecated, isCallbackRequest, uniqueItems,
33-
hasErrorResponseObject; // if 4xx, 5xx responses have at least one error object defined
33+
hasErrorResponseObject, // if 4xx, 5xx responses have at least one error object defined
34+
allowsAnonymous = false; // if the operation allows anonymous access (empty security requirement)
3435
public CodegenProperty returnProperty;
3536
public String path, operationId, returnType, returnFormat, httpMethod, returnBaseType,
3637
returnContainer, summary, unescapedNotes, notes, baseName, defaultResponse;
@@ -403,6 +404,7 @@ public String toString() {
403404
final StringBuffer sb = new StringBuffer("CodegenOperation{");
404405
sb.append("responseHeaders=").append(responseHeaders);
405406
sb.append(", hasAuthMethods=").append(hasAuthMethods);
407+
sb.append(", allowsAnonymous=").append(allowsAnonymous);
406408
sb.append(", hasConsumes=").append(hasConsumes);
407409
sb.append(", hasProduces=").append(hasProduces);
408410
sb.append(", hasOptionalParams=").append(hasOptionalParams);
@@ -475,6 +477,7 @@ public boolean equals(Object o) {
475477
if (o == null || getClass() != o.getClass()) return false;
476478
CodegenOperation that = (CodegenOperation) o;
477479
return hasAuthMethods == that.hasAuthMethods &&
480+
allowsAnonymous == that.allowsAnonymous &&
478481
hasConsumes == that.hasConsumes &&
479482
hasProduces == that.hasProduces &&
480483
hasOptionalParams == that.hasOptionalParams &&
@@ -543,7 +546,7 @@ public boolean equals(Object o) {
543546
@Override
544547
public int hashCode() {
545548

546-
return Objects.hash(responseHeaders, hasAuthMethods, hasConsumes, hasProduces, hasOptionalParams,
549+
return Objects.hash(responseHeaders, hasAuthMethods, allowsAnonymous, hasConsumes, hasProduces, hasOptionalParams,
547550
returnTypeIsPrimitive, returnSimpleType, subresourceOperation, isMap,
548551
isArray, isMultipart, isVoid, isResponseBinary, isResponseFile, isResponseOptional, hasReference,
549552
isDeprecated, isCallbackRequest, uniqueItems, path, operationId, returnType, httpMethod,

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,19 @@ private void processOperation(String resourcePath, String httpMethod, Operation
15791579
continue;
15801580
}
15811581

1582+
// Check if anonymous access is allowed (empty SecurityRequirement means anonymous is allowed)
1583+
// This must be checked before processing auth methods to properly handle mixed security requirements
1584+
boolean allowsAnonymous = checkAllowsAnonymous(securities);
1585+
if (!allowsAnonymous && securities == null) {
1586+
// If no operation-level securities, check global securities
1587+
allowsAnonymous = checkAllowsAnonymous(globalSecurities);
1588+
}
1589+
// If no securities at all (neither operation nor global), anonymous is allowed by default
1590+
if (!allowsAnonymous && securities == null && (globalSecurities == null || globalSecurities.isEmpty())) {
1591+
allowsAnonymous = true;
1592+
}
1593+
codegenOperation.allowsAnonymous = allowsAnonymous;
1594+
15821595
Map<String, SecurityScheme> authMethods = getAuthMethods(securities, securitySchemes);
15831596

15841597
if (authMethods != null && !authMethods.isEmpty()) {
@@ -1788,6 +1801,25 @@ private ModelsMap processModels(CodegenConfig config, Map<String, Schema> defini
17881801
return objs;
17891802
}
17901803

1804+
/**
1805+
* Check if the security requirements allow anonymous access.
1806+
* An empty SecurityRequirement (empty map) indicates anonymous access is allowed.
1807+
*
1808+
* @param securities List of security requirements to check
1809+
* @return true if any security requirement is empty (allows anonymous), false otherwise
1810+
*/
1811+
private boolean checkAllowsAnonymous(List<SecurityRequirement> securities) {
1812+
if (securities == null || securities.isEmpty()) {
1813+
return false;
1814+
}
1815+
for (SecurityRequirement req : securities) {
1816+
if (req == null || req.isEmpty()) {
1817+
return true;
1818+
}
1819+
}
1820+
return false;
1821+
}
1822+
17911823
private Map<String, SecurityScheme> getAuthMethods(List<SecurityRequirement> securities, Map<String, SecurityScheme> securitySchemes) {
17921824
if (securities == null || (securitySchemes == null || securitySchemes.isEmpty())) {
17931825
return null;

modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -496,23 +496,7 @@ private void flattenContent(Content content, String name) {
496496
if (schema == null) {
497497
continue;
498498
}
499-
// Check if schema has a $ref pointing to a component schema
500-
// If so, prefer the referenced schema name over operation-based naming
501-
String schemaName;
502-
if (schema.get$ref() != null) {
503-
String refName = ModelUtils.getSimpleRef(schema.get$ref());
504-
if (refName != null && openAPI.getComponents() != null &&
505-
openAPI.getComponents().getSchemas() != null &&
506-
openAPI.getComponents().getSchemas().containsKey(refName)) {
507-
// Use the referenced schema name instead of operation-based name
508-
schemaName = resolveModelName(schema.getTitle(), refName);
509-
} else {
510-
// Fallback to original behavior if ref doesn't point to a component schema
511-
schemaName = resolveModelName(schema.getTitle(), name);
512-
}
513-
} else {
514-
schemaName = resolveModelName(schema.getTitle(), name); // name example: testPost_request
515-
}
499+
String schemaName = resolveModelName(schema.getTitle(), name); // name example: testPost_request
516500
// Recursively gather/make inline models within this schema if any
517501
gatherInlineModels(schema, schemaName);
518502
if (isModelNeeded(schema)) {

modules/openapi-generator/src/main/resources/typescript-axios/apiInner.mustache

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const {{classname}}AxiosParamCreator = function (configuration?: Configur
6767
const localVarHeaderParameter = {} as any;
6868
const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
6969
const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
70+
const allowsAnonymous = {{allowsAnonymous}};
7071
7172
{{#authMethods}}
7273
// authentication {{name}} required
@@ -76,23 +77,23 @@ export const {{classname}}AxiosParamCreator = function (configuration?: Configur
7677
await setAWS4SignatureInterceptor(globalAxios, configuration)
7778
{{/withAWSV4Signature}}
7879
{{#isKeyInHeader}}
79-
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
80+
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration, allowsAnonymous)
8081
{{/isKeyInHeader}}
8182
{{#isKeyInQuery}}
82-
await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration)
83+
await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration, allowsAnonymous)
8384
{{/isKeyInQuery}}
8485
{{/isApiKey}}
8586
{{#isBasicBasic}}
8687
// http basic authentication required
87-
setBasicAuthToObject(localVarRequestOptions, configuration)
88+
setBasicAuthToObject(localVarRequestOptions, configuration, allowsAnonymous)
8889
{{/isBasicBasic}}
8990
{{#isBasicBearer}}
9091
// http bearer authentication required
91-
await setBearerAuthToObject(localVarHeaderParameter, configuration)
92+
await setBearerAuthToObject(localVarHeaderParameter, configuration, allowsAnonymous)
9293
{{/isBasicBearer}}
9394
{{#isOAuth}}
9495
// oauth required
95-
await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration)
96+
await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration, allowsAnonymous)
9697
{{/isOAuth}}
9798
9899
{{/authMethods}}

modules/openapi-generator/src/main/resources/typescript-axios/common.mustache

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,70 @@ export const assertParamExists = function (functionName: string, paramName: stri
2525
}
2626
}
2727

28-
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
28+
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration, allowsAnonymous?: boolean) {
2929
if (configuration && configuration.apiKey) {
30-
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
31-
? await configuration.apiKey(keyParamName)
32-
: await configuration.apiKey;
33-
object[keyParamName] = localVarApiKeyValue;
30+
try {
31+
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
32+
? await configuration.apiKey(keyParamName)
33+
: await configuration.apiKey;
34+
if (localVarApiKeyValue) {
35+
object[keyParamName] = localVarApiKeyValue;
36+
}
37+
} catch (error) {
38+
if (!allowsAnonymous) {
39+
throw error;
40+
}
41+
// If anonymous is allowed, silently ignore authentication failures
42+
}
3443
}
3544
}
3645

37-
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
46+
export const setBasicAuthToObject = function (object: any, configuration?: Configuration, allowsAnonymous?: boolean) {
3847
if (configuration && (configuration.username || configuration.password)) {
39-
object["auth"] = { username: configuration.username, password: configuration.password };
48+
try {
49+
object["auth"] = { username: configuration.username, password: configuration.password };
50+
} catch (error) {
51+
if (!allowsAnonymous) {
52+
throw error;
53+
}
54+
// If anonymous is allowed, silently ignore authentication failures
55+
}
4056
}
4157
}
4258

43-
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
59+
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration, allowsAnonymous?: boolean) {
4460
if (configuration && configuration.accessToken) {
45-
const accessToken = typeof configuration.accessToken === 'function'
46-
? await configuration.accessToken()
47-
: await configuration.accessToken;
48-
object["Authorization"] = "Bearer " + accessToken;
61+
try {
62+
const accessToken = typeof configuration.accessToken === 'function'
63+
? await configuration.accessToken()
64+
: await configuration.accessToken;
65+
if (accessToken) {
66+
object["Authorization"] = "Bearer " + accessToken;
67+
}
68+
} catch (error) {
69+
if (!allowsAnonymous) {
70+
throw error;
71+
}
72+
// If anonymous is allowed, silently ignore authentication failures
73+
}
4974
}
5075
}
5176

52-
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
77+
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration, allowsAnonymous?: boolean) {
5378
if (configuration && configuration.accessToken) {
54-
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
55-
? await configuration.accessToken(name, scopes)
56-
: await configuration.accessToken;
57-
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
79+
try {
80+
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
81+
? await configuration.accessToken(name, scopes)
82+
: await configuration.accessToken;
83+
if (localVarAccessTokenValue) {
84+
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
85+
}
86+
} catch (error) {
87+
if (!allowsAnonymous) {
88+
throw error;
89+
}
90+
// If anonymous is allowed, silently ignore authentication failures
91+
}
5892
}
5993
}
6094

modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,4 +1205,83 @@ public void doNotWrapSingleAllOfRefs() {
12051205
assertNotNull(allOfRefWithDescriptionAndReadonly.getAllOf());
12061206
assertEquals(numberRangeRef, ((Schema) allOfRefWithDescriptionAndReadonly.getAllOf().get(0)).get$ref());
12071207
}
1208+
1209+
@Test
1210+
public void resolveRequestBodyWithRefToComponentSchema() {
1211+
OpenAPI openAPI = new OpenAPI();
1212+
openAPI.setComponents(new Components());
1213+
1214+
// Create a component schema
1215+
ObjectSchema userSchema = new ObjectSchema()
1216+
.title("User")
1217+
.addProperty("id", new IntegerSchema())
1218+
.addProperty("name", new StringSchema());
1219+
openAPI.getComponents().addSchemas("User", userSchema);
1220+
1221+
// Create an operation with requestBody that references the component schema
1222+
openAPI.path("/test", new PathItem()
1223+
.post(new Operation()
1224+
.operationId("testPost")
1225+
.requestBody(new RequestBody()
1226+
.content(new Content()
1227+
.addMediaType("application/json",
1228+
new MediaType()
1229+
.schema(new Schema().$ref("#/components/schemas/User")))))));
1230+
1231+
new InlineModelResolver().flatten(openAPI);
1232+
1233+
// Verify that the requestBody schema uses the referenced schema name "User"
1234+
// instead of operation-based name "testPost_request"
1235+
RequestBody requestBody = openAPI.getPaths().get("/test").getPost().getRequestBody();
1236+
Schema requestBodySchema = requestBody.getContent().get("application/json").getSchema();
1237+
1238+
// When a requestBody schema has a $ref to a component schema, it should reference that schema directly
1239+
// and not create an inline schema with operation-based naming
1240+
assertNotNull(requestBodySchema.get$ref());
1241+
assertEquals("#/components/schemas/User", requestBodySchema.get$ref());
1242+
1243+
// Verify that no operation-based schema was created
1244+
assertNull(openAPI.getComponents().getSchemas().get("testPost_request"));
1245+
1246+
// Verify that the User schema still exists
1247+
assertNotNull(openAPI.getComponents().getSchemas().get("User"));
1248+
}
1249+
1250+
@Test
1251+
public void resolveRequestBodyWithRefToComponentSchemaWithTitle() {
1252+
OpenAPI openAPI = new OpenAPI();
1253+
openAPI.setComponents(new Components());
1254+
1255+
// Create a component schema with a title
1256+
ObjectSchema petSchema = new ObjectSchema()
1257+
.title("Pet")
1258+
.addProperty("id", new IntegerSchema())
1259+
.addProperty("name", new StringSchema());
1260+
openAPI.getComponents().addSchemas("Pet", petSchema);
1261+
1262+
// Create an operation with requestBody that references the component schema
1263+
openAPI.path("/pets", new PathItem()
1264+
.post(new Operation()
1265+
.operationId("createPet")
1266+
.requestBody(new RequestBody()
1267+
.content(new Content()
1268+
.addMediaType("application/json",
1269+
new MediaType()
1270+
.schema(new Schema().$ref("#/components/schemas/Pet")))))));
1271+
1272+
new InlineModelResolver().flatten(openAPI);
1273+
1274+
// Verify that the requestBody schema uses the referenced schema name "Pet"
1275+
RequestBody requestBody = openAPI.getPaths().get("/pets").getPost().getRequestBody();
1276+
Schema requestBodySchema = requestBody.getContent().get("application/json").getSchema();
1277+
1278+
assertNotNull(requestBodySchema.get$ref());
1279+
assertEquals("#/components/schemas/Pet", requestBodySchema.get$ref());
1280+
1281+
// Verify that no operation-based schema was created
1282+
assertNull(openAPI.getComponents().getSchemas().get("createPet_request"));
1283+
1284+
// Verify that the Pet schema still exists
1285+
assertNotNull(openAPI.getComponents().getSchemas().get("Pet"));
1286+
}
12081287
}

samples/client/echo_api/typescript-axios/build/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,11 @@ export const AuthApiAxiosParamCreator = function (configuration?: Configuration)
160160
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
161161
const localVarHeaderParameter = {} as any;
162162
const localVarQueryParameter = {} as any;
163+
const allowsAnonymous = false;
163164

164165
// authentication http_auth required
165166
// http basic authentication required
166-
setBasicAuthToObject(localVarRequestOptions, configuration)
167+
setBasicAuthToObject(localVarRequestOptions, configuration, allowsAnonymous)
167168

168169
localVarHeaderParameter['Accept'] = 'text/plain';
169170

@@ -194,10 +195,11 @@ export const AuthApiAxiosParamCreator = function (configuration?: Configuration)
194195
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
195196
const localVarHeaderParameter = {} as any;
196197
const localVarQueryParameter = {} as any;
198+
const allowsAnonymous = false;
197199

198200
// authentication http_bearer_auth required
199201
// http bearer authentication required
200-
await setBearerAuthToObject(localVarHeaderParameter, configuration)
202+
await setBearerAuthToObject(localVarHeaderParameter, configuration, allowsAnonymous)
201203

202204
localVarHeaderParameter['Accept'] = 'text/plain';
203205

0 commit comments

Comments
 (0)