Coverage for tests/test_get_model_definitions_formfeed_escape.py: 100%

41 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-09-22 00:03 +0000

1from typing import Any, Iterator, Set, Type 1abcdef

2 

3import fastapi._compat 1abcdef

4import fastapi.openapi.utils 1abcdef

5import pydantic.schema 1abcdef

6import pytest 1abcdef

7from fastapi import FastAPI 1abcdef

8from pydantic import BaseModel 1abcdef

9from starlette.testclient import TestClient 1abcdef

10 

11from .utils import needs_pydanticv1 1abcdef

12 

13 

14class Address(BaseModel): 1abcdef

15 """ 

16 This is a public description of an Address 

17 \f 

18 You can't see this part of the docstring, it's private! 

19 """ 

20 

21 line_1: str 1abcdef

22 city: str 1abcdef

23 state_province: str 1abcdef

24 

25 

26class Facility(BaseModel): 1abcdef

27 id: str 1abcdef

28 address: Address 1abcdef

29 

30 

31app = FastAPI() 1abcdef

32 

33client = TestClient(app) 1abcdef

34 

35 

36@app.get("/facilities/{facility_id}") 1abcdef

37def get_facility(facility_id: str) -> Facility: ... 1abcdef

38 

39 

40openapi_schema = { 1abcdef

41 "components": { 

42 "schemas": { 

43 "Address": { 

44 # NOTE: the description of this model shows only the public-facing text, before the `\f` in docstring 

45 "description": "This is a public description of an Address\n", 

46 "properties": { 

47 "city": {"title": "City", "type": "string"}, 

48 "line_1": {"title": "Line 1", "type": "string"}, 

49 "state_province": {"title": "State Province", "type": "string"}, 

50 }, 

51 "required": ["line_1", "city", "state_province"], 

52 "title": "Address", 

53 "type": "object", 

54 }, 

55 "Facility": { 

56 "properties": { 

57 "address": {"$ref": "#/components/schemas/Address"}, 

58 "id": {"title": "Id", "type": "string"}, 

59 }, 

60 "required": ["id", "address"], 

61 "title": "Facility", 

62 "type": "object", 

63 }, 

64 "HTTPValidationError": { 

65 "properties": { 

66 "detail": { 

67 "items": {"$ref": "#/components/schemas/ValidationError"}, 

68 "title": "Detail", 

69 "type": "array", 

70 } 

71 }, 

72 "title": "HTTPValidationError", 

73 "type": "object", 

74 }, 

75 "ValidationError": { 

76 "properties": { 

77 "loc": { 

78 "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, 

79 "title": "Location", 

80 "type": "array", 

81 }, 

82 "msg": {"title": "Message", "type": "string"}, 

83 "type": {"title": "Error Type", "type": "string"}, 

84 }, 

85 "required": ["loc", "msg", "type"], 

86 "title": "ValidationError", 

87 "type": "object", 

88 }, 

89 } 

90 }, 

91 "info": {"title": "FastAPI", "version": "0.1.0"}, 

92 "openapi": "3.1.0", 

93 "paths": { 

94 "/facilities/{facility_id}": { 

95 "get": { 

96 "operationId": "get_facility_facilities__facility_id__get", 

97 "parameters": [ 

98 { 

99 "in": "path", 

100 "name": "facility_id", 

101 "required": True, 

102 "schema": {"title": "Facility Id", "type": "string"}, 

103 } 

104 ], 

105 "responses": { 

106 "200": { 

107 "content": { 

108 "application/json": { 

109 "schema": {"$ref": "#/components/schemas/Facility"} 

110 } 

111 }, 

112 "description": "Successful Response", 

113 }, 

114 "422": { 

115 "content": { 

116 "application/json": { 

117 "schema": { 

118 "$ref": "#/components/schemas/HTTPValidationError" 

119 } 

120 } 

121 }, 

122 "description": "Validation Error", 

123 }, 

124 }, 

125 "summary": "Get Facility", 

126 } 

127 } 

128 }, 

129} 

130 

131 

132def test_openapi_schema(): 1abcdef

133 """ 

134 Sanity check to ensure our app's openapi schema renders as we expect 

135 """ 

136 response = client.get("/openapi.json") 1mnopqr

137 assert response.status_code == 200, response.text 1mnopqr

138 assert response.json() == openapi_schema 1mnopqr

139 

140 

141class SortedTypeSet(set): 1abcdef

142 """ 

143 Set of Types whose `__iter__()` method yields results sorted by the type names 

144 """ 

145 

146 def __init__(self, seq: Set[Type[Any]], *, sort_reversed: bool): 1abcdef

147 super().__init__(seq) 1ghijkl

148 self.sort_reversed = sort_reversed 1ghijkl

149 

150 def __iter__(self) -> Iterator[Type[Any]]: 1abcdef

151 members_sorted = sorted( 1ghijkl

152 super().__iter__(), 

153 key=lambda type_: type_.__name__, 

154 reverse=self.sort_reversed, 

155 ) 

156 yield from members_sorted 1ghijkl

157 

158 

159@needs_pydanticv1 1abcdef

160@pytest.mark.parametrize("sort_reversed", [True, False]) 1abcdef

161def test_model_description_escaped_with_formfeed(sort_reversed: bool): 1abcdef

162 """ 

163 Regression test for bug fixed by https://github.com/fastapi/fastapi/pull/6039. 

164 

165 Test `get_model_definitions` with models passed in different order. 

166 """ 

167 all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) 1ghijkl

168 

169 flat_models = fastapi._compat.get_flat_models_from_fields( 1ghijkl

170 all_fields, known_models=set() 

171 ) 

172 model_name_map = pydantic.schema.get_model_name_map(flat_models) 1ghijkl

173 

174 expected_address_description = "This is a public description of an Address\n" 1ghijkl

175 

176 models = fastapi._compat.get_model_definitions( 1ghijkl

177 flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed), 

178 model_name_map=model_name_map, 

179 ) 

180 assert models["Address"]["description"] == expected_address_description 1ghijkl