Coverage for tests/test_get_model_definitions_formfeed_escape.py: 100%

42 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-12-04 08:29 +0000

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

2 

3import fastapi._compat 1abcdefg

4import fastapi.openapi.utils 1abcdefg

5import pydantic.schema 1abcdefg

6import pytest 1abcdefg

7from fastapi import FastAPI 1abcdefg

8from pydantic import BaseModel 1abcdefg

9from starlette.testclient import TestClient 1abcdefg

10 

11from .utils import needs_pydanticv1 1abcdefg

12 

13 

14class Address(BaseModel): 1abcdefg

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 1abcdefg

22 city: str 1abcdefg

23 state_province: str 1abcdefg

24 

25 

26class Facility(BaseModel): 1abcdefg

27 id: str 1abcdefg

28 address: Address 1abcdefg

29 

30 

31app = FastAPI() 1abcdefg

32 

33client = TestClient(app) 1abcdefg

34 

35 

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

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

38 

39 

40openapi_schema = { 1abcdefg

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(): 1abcdefg

133 """ 

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

135 """ 

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

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

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

139 

140 

141class SortedTypeSet(set): 1abcdefg

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): 1abcdefg

147 super().__init__(seq) 1hijklm

148 self.sort_reversed = sort_reversed 1hijklm

149 

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

151 members_sorted = sorted( 1hijklm

152 super().__iter__(), 

153 key=lambda type_: type_.__name__, 

154 reverse=self.sort_reversed, 

155 ) 

156 yield from members_sorted 1hijklm

157 

158 

159@needs_pydanticv1 1abcdefg

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

161def test_model_description_escaped_with_formfeed(sort_reversed: bool): 1abcdefg

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 from fastapi._compat import v1 1hijklm

168 

169 all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes) 1hijklm

170 

171 flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set()) 1hijklm

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

173 

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

175 

176 models = v1.get_model_definitions( 1hijklm

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 1hijklm