import 'dart:io'; import 'package:koi_swagger_parser/koi_swagger_parser.dart'; import 'package:koi_swagger_parser/src/openapi/openapi.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; void main() { group('response semantics', () { test('default 不作为成功响应,并保留多个 2xx 与响应头元数据', () { final parsed = OpenApiDocumentParser(_testConfig()).parse( LoadedOpenApiDocument( document: { 'openapi': '3.1.0', 'info': {'title': 'responses', 'version': '1'}, 'paths': { '/api/v1/items': { 'get': { 'tags': ['items'], 'operationId': 'list_items', 'responses': { '200': { 'description': 'items', 'headers': { 'X-Rate-Limit': { 'description': 'remaining requests', 'schema': {'type': 'integer'}, }, }, 'content': { 'application/json': { 'schema': { 'type': 'array', 'items': {'type': 'string'}, }, }, }, }, '204': {'description': 'empty'}, 'default': { 'description': 'error', 'content': { 'application/problem+json': { 'schema': { 'type': 'object', 'properties': { 'detail': {'type': 'string'}, }, }, }, }, }, }, }, }, }, }, sources: const ['memory://responses'], ), ); final operation = parsed.operations.single; expect(operation.returnType.render(), 'List'); expect(operation.primaryResponseStatusCode, '200'); expect(operation.responses, hasLength(3)); expect( operation.responses .singleWhere((response) => response.statusCode == 'default') .isSuccess, isFalse, ); final success = operation.responses.singleWhere( (response) => response.statusCode == '200', ); expect(success.headers.single.name, 'X-Rate-Limit'); expect(success.headers.single.type.render(), 'int?'); expect( parsed.models.values.any( (model) => model.originalName.contains('defaultError'), ), isTrue, ); }); test('binary response 可生成 bytes + HttpResponse + Accept 元数据', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_binary_response_', ); addTearDown(() => tempDir.delete(recursive: true)); final spec = File(p.join(tempDir.path, 'openapi.json')); await spec.writeAsString(r''' { "openapi": "3.1.0", "info": {"title": "download", "version": "1"}, "paths": { "/api/v1/files/{id}": { "get": { "tags": ["files"], "operationId": "download_file", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} } ], "responses": { "200": { "description": "file", "headers": { "Content-Disposition": { "schema": {"type": "string"} } }, "content": { "application/octet-stream": { "schema": {"type": "string", "format": "binary"} } } } } } } } } '''); final outputDir = p.join(tempDir.path, 'lib', 'src'); final config = File(p.join(tempDir.path, 'koi_swagger_parser.yaml')); await config.writeAsString(''' input: swagger_urls: - url: ${spec.path} output: base_dir: $outputDir api_dir: ${p.join(outputDir, 'api')} models_dir: ${p.join(outputDir, 'models')} generation: api: response_mode: http_response binary_response_mode: bytes response_wrapper: mode: passthrough formatting: run_dart_format: false '''); await const KoiSwaggerParser().generate(configPath: config.path); final apiFile = File(p.join(outputDir, 'api', 'v1', 'files_api.dart')); final api = await apiFile.readAsString(); expect(api, contains("import 'dart:typed_data';")); expect(api, contains("@Headers({'Accept':")); expect(api, contains("'application/octet-stream'")); expect(api, contains('@DioResponseType(ResponseType.bytes)')); expect(api, contains('Future> downloadFile')); expect(api, contains('Header Content-Disposition: String?')); }); test('binary 与 JSON 成功响应混用时结构化拒绝', () { expect( () => OpenApiDocumentParser(_testConfig()).parse( LoadedOpenApiDocument( document: { 'openapi': '3.1.0', 'info': {'title': 'mixed', 'version': '1'}, 'paths': { '/api/v1/export': { 'get': { 'tags': ['export'], 'responses': { '200': { 'description': 'json', 'content': { 'application/json': { 'schema': {'type': 'string'}, }, }, }, '206': { 'description': 'bytes', 'content': { 'application/octet-stream': { 'schema': { 'type': 'string', 'format': 'binary', }, }, }, }, }, }, }, }, }, sources: const ['memory://mixed'], ), ), throwsA( isA().having( (error) => error.diagnostics.map((item) => item.code), 'codes', contains('openapi.mixed_success_media_types'), ), ), ); }); test('XML/Protobuf 等未实现 decoder 的 media type 会明确拒绝', () { expect( () => OpenApiDocumentParser(_testConfig()).parse( LoadedOpenApiDocument( document: { 'openapi': '3.1.0', 'info': {'title': 'xml', 'version': '1'}, 'paths': { '/legacy': { 'get': { 'responses': { '200': { 'description': 'xml', 'content': { 'application/xml': { 'schema': {'type': 'object'}, }, }, }, }, }, }, }, }, sources: const ['memory://xml'], ), ), throwsA( isA().having( (error) => error.diagnostics.map((item) => item.code), 'codes', contains('openapi.unsupported_response_content_type'), ), ), ); }); test('Link Object 不静默丢失,产生明确 warning', () { final parsed = OpenApiDocumentParser(_testConfig()).parse( LoadedOpenApiDocument( document: { 'openapi': '3.1.0', 'info': {'title': 'links', 'version': '1'}, 'paths': { '/orders': { 'get': { 'responses': { '200': { 'description': 'order', 'content': { 'application/json': { 'schema': {'type': 'string'}, }, }, 'links': { 'customer': { 'operationId': 'getCustomer', }, }, }, }, }, }, }, }, sources: const ['memory://links'], ), ); expect( parsed.diagnostics.map((item) => item.code), contains('openapi.links_not_supported'), ); }); }); } KoiSwaggerConfig _testConfig() { return KoiSwaggerConfig( configPath: null, workingDirectory: Directory.current.path, input: const InputConfig(swaggerUrls: []), output: OutputConfig( baseDir: Directory.current.path, apiDir: p.join(Directory.current.path, 'api'), modelsDir: p.join(Directory.current.path, 'models'), splitByTags: true, extractVersionFromUrl: false, ), generation: GenerationConfig( preset: GeneratorPresetRegistry.koiFlutter, api: const ApiGenerationConfig( enabled: true, useRetrofit: true, cancelTokenSupport: false, client: ClientConfig(className: 'ApiClient', fileName: 'api_client'), responseWrapper: ResponseWrapperConfig( mode: ResponseWrapperMode.passthrough, successDataField: 'data', detectStandardEnvelope: false, ), ), models: const ModelGenerationConfig( enabled: true, classPrefix: 'Koi', defaultNonNullableListToEmpty: true, jsonSerializable: JsonSerializableConfig( checked: true, explicitToJson: true, includeIfNull: false, ), ), formatting: const FormattingConfig( runDartFormat: false, runDartFix: false, ), ), ); }