swagger_generator_flutter/test/comprehensive_generator_tes...

493 lines
16 KiB
Dart

import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
import 'package:test/test.dart';
void main() {
group('Comprehensive Generator Tests', () {
late SwaggerDocument testDocument;
setUp(() {
testDocument = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'A comprehensive test API',
servers: [
const ApiServer(
url: 'https://api.example.com',
description: 'Production server',
),
],
components: const ApiComponents(
securitySchemes: {
'bearerAuth': ApiSecurityScheme(
type: SecuritySchemeType.http,
description: 'Bearer token',
scheme: 'bearer',
bearerFormat: 'JWT',
),
'apiKey': ApiSecurityScheme(
type: SecuritySchemeType.apiKey,
description: 'API Key',
name: 'X-API-Key',
location: ApiKeyLocation.header,
),
},
),
paths: {
const ApiPathKey('/users', HttpMethod.get): const ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get all users',
description: 'Retrieve a list of all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [
ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
),
ApiParameter(
name: 'limit',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Items per page',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'Successful response',
content: {
'application/json': ApiMediaType(
schema: {
'type': 'array',
'items': {
r'$ref': '#/components/schemas/User',
},
},
),
},
),
'400': ApiResponse(
code: '400',
description: 'Bad request',
),
'401': ApiResponse(
code: '401',
description: 'Unauthorized',
),
},
security: [
ApiSecurityRequirement(
requirements: {'bearerAuth': []},
),
],
),
const ApiPathKey('/users/{id}', HttpMethod.get): const ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user by ID',
description: 'Retrieve a specific user by their ID',
operationId: 'getUserById',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'User found',
content: {
'application/json': ApiMediaType(
schema: {
r'$ref': '#/components/schemas/User',
},
),
},
),
'404': ApiResponse(
code: '404',
description: 'User not found',
),
},
),
const ApiPathKey('/users/create', HttpMethod.post): const ApiPath(
path: '/users/create',
method: HttpMethod.post,
summary: 'Create user',
description: 'Create a new user',
operationId: 'createUser',
tags: ['users'],
parameters: [],
requestBody: ApiRequestBody(
description: 'User data',
required: true,
content: {
'application/json': ApiMediaType(
schema: {
r'$ref': '#/components/schemas/CreateUserRequest',
},
),
},
),
responses: {
'201': ApiResponse(
code: '201',
description: 'User created',
content: {
'application/json': ApiMediaType(
schema: {
r'$ref': '#/components/schemas/User',
},
),
},
),
'400': ApiResponse(
code: '400',
description: 'Invalid input',
),
},
),
const ApiPathKey('/files/upload', HttpMethod.post): const ApiPath(
path: '/files/upload',
method: HttpMethod.post,
summary: 'Upload file',
description: 'Upload a file to the server',
operationId: 'uploadFile',
tags: ['files'],
parameters: [],
requestBody: ApiRequestBody(
description: 'File to upload',
required: true,
content: {
'multipart/form-data': ApiMediaType(
schema: {
'type': 'object',
'properties': {
'file': {
'type': 'string',
'format': 'binary',
},
'description': {
'type': 'string',
},
},
},
),
},
),
responses: {
'200': ApiResponse(
code: '200',
description: 'File uploaded successfully',
content: {
'application/json': ApiMediaType(
schema: {
r'$ref': '#/components/schemas/FileUploadResult',
},
),
},
),
},
),
},
models: {
'User': const ApiModel(
name: 'User',
description: 'User model',
properties: {
'id': ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
),
'name': ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
'email': ApiProperty(
name: 'email',
type: PropertyType.string,
description: 'User email',
required: true,
),
'createdAt': ApiProperty(
name: 'createdAt',
type: PropertyType.string,
description: 'Creation timestamp',
required: false,
),
},
required: ['id', 'name', 'email'],
),
'CreateUserRequest': const ApiModel(
name: 'CreateUserRequest',
description: 'Request model for creating a user',
properties: {
'name': ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
'email': ApiProperty(
name: 'email',
type: PropertyType.string,
description: 'User email',
required: true,
),
},
required: ['name', 'email'],
),
'FileUploadResult': const ApiModel(
name: 'FileUploadResult',
description: 'Result of file upload',
properties: {
'url': ApiProperty(
name: 'url',
type: PropertyType.string,
description: 'File URL',
required: true,
),
'filename': ApiProperty(
name: 'filename',
type: PropertyType.string,
description: 'Original filename',
required: true,
),
'size': ApiProperty(
name: 'size',
type: PropertyType.integer,
description: 'File size in bytes',
required: true,
),
},
required: ['url', 'filename', 'size'],
),
},
controllers: {},
security: [
const ApiSecurityRequirement(
requirements: {'bearerAuth': []},
),
],
);
});
group('RetrofitApiGenerator', () {
test('generates basic Retrofit API', () {
final generator = RetrofitApiGenerator(
className: 'TestApiService',
splitByTags: false,
);
final result = generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
expect(result, contains('abstract class TestApiService'));
expect(result, contains('@RestApi'));
expect(result, contains('factory TestApiService('));
expect(result, contains('Dio dio'));
expect(result, contains("@GET('/users')"));
expect(result, contains("@POST('/users/create')"));
expect(result, contains("@Path('id')"));
expect(result, contains("@Query('page')"));
expect(result, contains('@Body()'));
});
test('generates split APIs by tags', () {
final generator = RetrofitApiGenerator(
className: 'ApiService',
);
final result = generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
// The main file should contain the aggregator class
expect(result, contains('class ApiService'));
// It should have getters for the individual API services
expect(result, contains('UsersApi get users;'));
expect(result, contains('FilesApi get files;'));
// It should import the tag-based API files
expect(result, contains("import 'users_api.dart';"));
expect(result, contains("import 'files_api.dart';"));
});
test('handles file upload endpoints', () {
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(testDocument);
expect(result, contains("@POST('/files/upload')"));
// MultiPart handling is optional based on implementation
// expect(result, contains('@MultiPart()'));
// expect(result, contains('MultipartFile'));
});
test('generates proper parameter annotations', () {
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(testDocument);
// Path parameters
expect(result, contains("@Path('id') int id"));
// Query parameters
expect(result, contains("@Query('page') int? page"));
expect(result, contains("@Query('limit') int? limit"));
// Body parameters
expect(result, contains('@Body() CreateUserRequest request'));
});
test('generates security annotations', () {
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(testDocument);
// Should include headers for authentication
expect(
result,
anyOf([contains('Authorization'), contains('X-API-Key')]),
);
});
});
group('Code Quality', () {
test('generated code is valid Dart syntax', () {
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(testDocument);
// Basic syntax checks
expect(result, isNot(contains(';;'))); // No double semicolons
expect(result, isNot(contains(',,'))); // No double commas
// Check for proper imports (at least one import statement)
expect(result, contains('import'));
// Check for proper class structure
final classMatches = RegExp(r'class \w+').allMatches(result);
final abstractClassMatches =
RegExp(r'abstract class \w+').allMatches(result);
expect(
classMatches.length + abstractClassMatches.length,
greaterThan(0),
);
});
test('handles special characters in names', () {
final specialDocument = SwaggerDocument(
title: 'API with Special-Characters_and.dots',
version: '1.0.0',
description: 'Test API',
paths: {
const ApiPathKey('/special-endpoint_with.dots', HttpMethod.get):
const ApiPath(
path: '/special-endpoint_with.dots',
method: HttpMethod.get,
summary: 'Special endpoint',
description: 'Endpoint with special characters',
operationId: 'getSpecialEndpoint',
tags: ['special-tag_with.dots'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
);
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(specialDocument);
expect(result, isNotEmpty);
// Should handle special characters in class names
expect(result, contains('class'));
});
test('handles nullable and required fields correctly', () {
final generator = RetrofitApiGenerator(splitByTags: false);
final result = generator.generateFromDocument(testDocument);
// Required path parameters should not be nullable
expect(result, contains("@Path('id') int id"));
// Optional query parameters should be nullable
expect(result, contains("@Query('page') int? page"));
expect(result, contains("@Query('limit') int? limit"));
});
});
group('Error Handling', () {
test('handles empty document gracefully', () {
const emptyDocument = SwaggerDocument(
title: 'Empty API',
version: '1.0.0',
description: 'Empty test API',
paths: {},
models: {},
controllers: {},
);
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(emptyDocument);
expect(result, isNotEmpty);
// Should still generate basic structure even with no paths
});
test('handles missing operation IDs', () {
final documentWithoutOperationIds = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
paths: {
const ApiPathKey('/test', HttpMethod.get): const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test endpoint',
description: 'Test',
operationId: '', // Empty operation ID
tags: [],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
);
final generator = RetrofitApiGenerator(splitByTags: false);
expect(
() => generator.generateFromDocument(documentWithoutOperationIds),
returnsNormally,
);
});
});
});
}