Complete OpenAPI 3.0/3.1 generation, diagnostics, lifecycle safety, enum and naming mappings, bilingual documentation, examples, and pub.dev release validation.
215 lines
6.1 KiB
Dart
215 lines
6.1 KiB
Dart
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('servers and security metadata', () {
|
|
test('解析 server variables、operation override 与鉴权元数据', () async {
|
|
final tempDir = await Directory.systemTemp.createTemp(
|
|
'koi_swagger_parser_server_security_',
|
|
);
|
|
addTearDown(() => tempDir.delete(recursive: true));
|
|
final spec = File(p.join(tempDir.path, 'openapi.json'));
|
|
await spec.writeAsString(r'''
|
|
{
|
|
"openapi": "3.1.0",
|
|
"info": {"title": "servers and auth", "version": "1"},
|
|
"servers": [
|
|
{
|
|
"url": "https://{environment}.example.com/{version}",
|
|
"variables": {
|
|
"environment": {
|
|
"default": "api",
|
|
"enum": ["api", "staging"]
|
|
},
|
|
"version": {"default": "v1"}
|
|
}
|
|
}
|
|
],
|
|
"security": [
|
|
{"bearerAuth": []},
|
|
{"apiKeyAuth": []}
|
|
],
|
|
"paths": {
|
|
"/accounts": {
|
|
"get": {
|
|
"tags": ["accounts"],
|
|
"operationId": "list_accounts",
|
|
"responses": {"204": {"description": "ok"}}
|
|
}
|
|
},
|
|
"/upload": {
|
|
"post": {
|
|
"tags": ["accounts"],
|
|
"operationId": "upload_account",
|
|
"servers": [{"url": "https://upload.example.com"}],
|
|
"security": [],
|
|
"responses": {"204": {"description": "ok"}}
|
|
}
|
|
}
|
|
},
|
|
"components": {
|
|
"securitySchemes": {
|
|
"bearerAuth": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"bearerFormat": "JWT"
|
|
},
|
|
"basicAuth": {
|
|
"type": "http",
|
|
"scheme": "basic"
|
|
},
|
|
"apiKeyAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-Key"
|
|
},
|
|
"oauth": {
|
|
"type": "oauth2",
|
|
"flows": {
|
|
"clientCredentials": {
|
|
"tokenUrl": "https://auth.example.com/token",
|
|
"scopes": {
|
|
"accounts:read": "read accounts"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
''');
|
|
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_wrapper:
|
|
mode: passthrough
|
|
formatting:
|
|
run_dart_format: false
|
|
''');
|
|
|
|
await const KoiSwaggerParser().generate(configPath: config.path);
|
|
|
|
final api = File(
|
|
p.join(outputDir, 'api', 'v1', 'accounts_api.dart'),
|
|
).readAsStringSync();
|
|
expect(
|
|
api,
|
|
contains(
|
|
"@RestApi(baseUrl: 'https://api.example.com/v1', "
|
|
'parser: Parser.JsonSerializable)',
|
|
),
|
|
);
|
|
expect(api, contains('Security: bearerAuth OR apiKeyAuth'));
|
|
expect(api, contains("@POST('https://upload.example.com/upload')"));
|
|
|
|
final client = File(
|
|
p.join(outputDir, 'api', 'api_client.dart'),
|
|
).readAsStringSync();
|
|
expect(
|
|
client,
|
|
contains("baseUrl = baseUrl ?? 'https://api.example.com/v1'"),
|
|
);
|
|
expect(client, contains("'bearerAuth': <String, Object?>{"));
|
|
expect(client, contains("'scheme': 'bearer'"));
|
|
expect(client, contains("'apiKeyAuth': <String, Object?>{"));
|
|
expect(client, contains("'in': 'header'"));
|
|
expect(client, contains("'scopes': <String>['accounts:read']"));
|
|
expect(client, contains("'GET /accounts':"));
|
|
expect(client, contains("'POST /upload': <Map<String, List<String>>>[]"));
|
|
});
|
|
|
|
test('不存在的 security scheme 会结构化拒绝', () {
|
|
final config = _testConfig();
|
|
expect(
|
|
() => OpenApiDocumentParser(config).parse(
|
|
LoadedOpenApiDocument(
|
|
document: <String, dynamic>{
|
|
'openapi': '3.1.0',
|
|
'info': <String, dynamic>{
|
|
'title': 'invalid auth',
|
|
'version': '1',
|
|
},
|
|
'security': <dynamic>[
|
|
<String, dynamic>{'missing': <String>[]},
|
|
],
|
|
'paths': <String, dynamic>{
|
|
'/value': <String, dynamic>{
|
|
'get': <String, dynamic>{
|
|
'responses': <String, dynamic>{
|
|
'204': <String, dynamic>{'description': 'ok'},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
sources: const <String>['memory://invalid-auth'],
|
|
),
|
|
),
|
|
throwsA(
|
|
isA<KoiDiagnosticException>().having(
|
|
(error) => error.diagnostics.map((item) => item.code),
|
|
'codes',
|
|
contains('openapi.unknown_security_scheme'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
KoiSwaggerConfig _testConfig() {
|
|
return KoiSwaggerConfig(
|
|
configPath: null,
|
|
workingDirectory: Directory.current.path,
|
|
input: const InputConfig(swaggerUrls: <String>[]),
|
|
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,
|
|
),
|
|
),
|
|
);
|
|
}
|