mirror of
https://github.com/foss42/apidash.git
synced 2025-12-05 20:40:02 +08:00
Merge branch 'main' into copilot/find-security-vulnerabilities
This commit is contained in:
13
README.md
13
README.md
@@ -1,9 +1,7 @@
|
|||||||
# API Dash ⚡️
|
# API Dash ⚡️
|
||||||
|
|
||||||
[](https://discord.com/invite/bBeSdtJ6Ue)
|
<a href="https://discord.com/invite/bBeSdtJ6Ue" target="_blank"><img src="https://img.shields.io/badge/DISCORD-JOIN%20SERVER-5663F7?style=for-the-badge&logo=discord&logoColor=white" alt="Discord Server Invite"></a><br>
|
||||||
|
<img src="https://trendshift.io/api/badge/repositories/9734" alt="foss42%2Fapidash | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/> <a href="https://summerofcode.withgoogle.com/programs/2025/organizations/api-dash" target="_blank"><img src="https://github.com/foss42/apidash/assets/615622/493ce57f-06c3-4789-b7ae-9fa63bca8183" alt="GSoC" width="400"></a>
|
||||||
<a href="https://summerofcode.withgoogle.com/programs/2025/organizations/api-dash" target="_blank"><img src="https://github.com/foss42/apidash/assets/615622/493ce57f-06c3-4789-b7ae-9fa63bca8183" alt="GSoC" width="400"></a>
|
|
||||||
|
|
||||||
|
|
||||||
### Please support this initiative by giving this project a Star ⭐️
|
### Please support this initiative by giving this project a Star ⭐️
|
||||||
|
|
||||||
@@ -138,6 +136,13 @@ API Dash can be downloaded from the links below:
|
|||||||
- Customize various options using a dedicated Settings screen.
|
- Customize various options using a dedicated Settings screen.
|
||||||
- Window Configuration (Size & Position) is persisted and restored on app start. (Only macOS & Windows)
|
- Window Configuration (Size & Position) is persisted and restored on app start. (Only macOS & Windows)
|
||||||
|
|
||||||
|
|
||||||
|
## Supported by ❤️
|
||||||
|
|
||||||
|
<a href="https://www.lambdatest.com/?utm_source=apidash&utm_medium=sponsor" target="_blank">
|
||||||
|
<img width="250" height="48" alt="lambdatest-logo" src="https://github.com/user-attachments/assets/c1d9263a-e60e-4161-8d65-fc5ff82fe9f7" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Code Generators
|
## Code Generators
|
||||||
|
|
||||||
API Dash currently supports API integration code generation for the following languages/libraries.
|
API Dash currently supports API integration code generation for the following languages/libraries.
|
||||||
|
|||||||
111
doc/dev_guide/openapi.md
Normal file
111
doc/dev_guide/openapi.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# OpenAPI
|
||||||
|
|
||||||
|
The OpenAPI import feature was failing when trying to import specifications from URLs like https://catfact.ninja/docs?api-docs.json. The error "The fetched content does not look like a valid OpenAPI spec (JSON or YAML)" was shown even though the content was a valid OpenAPI 3.0 specification. This was caused by a bug in the openapi_spec package (v0.15.0) that cannot parse OpenAPI specs containing "security": [[]] (empty security arrays), which is valid according to the OpenAPI 3.0 specification.
|
||||||
|
|
||||||
|
> Fix
|
||||||
|
|
||||||
|
Added a workaround in OpenApiImportService.tryParseSpec() that detects parsing failures and automatically removes problematic security fields containing empty arrays before retrying the parse operation. This is a temporary workaround until the upstream package is fixed.
|
||||||
|
|
||||||
|
- [APIDash](https://drive.google.com/file/d/1CWocxCVW99-bEWkZlwInGq0JykHalv9a/view?usp=sharing) - Works without any fix
|
||||||
|
- [Cat Fact API](https://drive.google.com/file/d/1ox71b3tT4Lv-9jw7zV1ronWQR_uW3K25/view?usp=drive_link) - Works with this fix
|
||||||
|
- [DigitalOcean Droplet Metadata API](https://drive.google.com/file/d/1XKZXJvrwvAVm3OVBEZFhScOuCMjPJBZh/view?usp=drive_link) - Works without any fix
|
||||||
|
- [GitHub v3 REST API](https://drive.google.com/file/d/1WcJXSosHPD0uiybJrqpJSknM5FA0De02/view?usp=drive_link) - Doesn't Work
|
||||||
|
- [Swagger Petstore](https://drive.google.com/file/d/1LBqBrlcsXo7Clr7VKn7CYe75c_H4U8zQ/view?usp=drive_link) - Doesn't Work
|
||||||
|
- [RailwayStations REST API](https://drive.google.com/file/d/1jVFk-hNf_gb_VeBuAomOgh6tWByU9Fyi/view?usp=drive_link) - Doesn't Work
|
||||||
|
- [UniProt REST API Server](https://drive.google.com/file/d/1KTIqKC7SludxsyCYN6kXWQySve4GpbhD/view?usp=drive_link) - Doesn't Work
|
||||||
|
- [VIT-AP VTOP API](https://drive.google.com/file/d/1B5Mh3IK2uUBoRSocEKQd2Dvf7SZWm03M/view?usp=drive_link) - Works without any fix
|
||||||
|
|
||||||
|
It’s not our parser that causes the issue. The failures come from the documents themselves and how the openapi_spec package (correctly) enforces OpenAPI shapes. Valid security fields work fine as per the package docs; the broken cases are due to invalid spec content.
|
||||||
|
|
||||||
|
### Findings per document
|
||||||
|
|
||||||
|
- cat_facts.json (also the Cat Facts URL)
|
||||||
|
|
||||||
|
- Problem: Top-level security is malformed: security: [[]]
|
||||||
|
- Why it fails: In OpenAPI 3.0, top-level security must be an array of SecurityRequirement objects (maps). Examples:
|
||||||
|
- Valid: security: [] (no requirements) or security: [ { api_key: [] } ]
|
||||||
|
- Invalid: security: [[]] (array of arrays)
|
||||||
|
- openapi_spec error: type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
|
||||||
|
- Conclusion: The document is invalid. This is not a general “security field” issue, just this malformed shape.
|
||||||
|
|
||||||
|
- railway-stations.yaml
|
||||||
|
|
||||||
|
- Problem: Component parameter reference points to a Parameter missing required fields (e.g., 'in').
|
||||||
|
- Error: CheckedFromJsonException: Could not create Parameter. There is a problem with "in". Invalid union type "null"!
|
||||||
|
- The stack/message points at $ref: #/components/parameters/Authorization.
|
||||||
|
- Conclusion: Not related to security. The referenced Parameter definition is incomplete (missing in: header|query|path|cookie) or otherwise invalid.
|
||||||
|
|
||||||
|
- travel.yaml
|
||||||
|
|
||||||
|
- Problem: Same class of failure as railway-stations.yaml, with a parameter ref like $ref: #/components/parameters/page.
|
||||||
|
- Error: CheckedFromJsonException... problem with "in" (Invalid union type "null").
|
||||||
|
- Note: components.securitySchemes is present here and is not the cause.
|
||||||
|
- Conclusion: Also a spec issue with parameter component definitions/references.
|
||||||
|
|
||||||
|
- digitalocean.yaml
|
||||||
|
- Result: Parses successfully with openapi_spec.
|
||||||
|
- Note: No top-level security; nothing problematic here.
|
||||||
|
- Conclusion: Confirms the parser handles valid documents correctly.
|
||||||
|
|
||||||
|
Steps to reproduce failures from local files,
|
||||||
|
|
||||||
|
```
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:openapi_spec/openapi_spec.dart';
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
|
||||||
|
// Pass file paths as args below.
|
||||||
|
final paths = args.isNotEmpty
|
||||||
|
? args
|
||||||
|
: <String>[
|
||||||
|
'./cat_facts.json',
|
||||||
|
'./railway-stations.yaml',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final p in paths) {
|
||||||
|
stdout.writeln('\n=== Parsing: $p ===');
|
||||||
|
final f = File(p);
|
||||||
|
if (!await f.exists()) {
|
||||||
|
stdout.writeln('Skip: file not found');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final content = await f.readAsString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final spec = OpenApi.fromString(source: content, format: null);
|
||||||
|
stdout.writeln('SUCCESS: title="${spec.info.title}", version="${spec.info.version}"');
|
||||||
|
stdout.writeln('Paths: ${spec.paths?.length ?? 0}');
|
||||||
|
} catch (e, st) {
|
||||||
|
final err = e.toString();
|
||||||
|
stdout.writeln('FAIL: ${err.substring(0, err.length.clamp(0, 400))}...');
|
||||||
|
// Stack Trace
|
||||||
|
final stStr = st.toString();
|
||||||
|
if (stStr.isNotEmpty) {
|
||||||
|
stdout.writeln('Stack:\n$stStr');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to run
|
||||||
|
|
||||||
|
- Create a new dart project, put the openapi spec file and this script there.
|
||||||
|
- Add the depndency, `dart pub add openapi_spec: ^0.15.0`
|
||||||
|
- Run:
|
||||||
|
- `dart run path/to/this/file`
|
||||||
|
|
||||||
|
### Expected outcomes
|
||||||
|
|
||||||
|
- `cat_facts.json`
|
||||||
|
|
||||||
|
- FAIL with an error like:
|
||||||
|
- type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
|
||||||
|
- This is triggered by the invalid top-level security shape: security: [[]]
|
||||||
|
|
||||||
|
- `railway-stations.yaml`
|
||||||
|
- FAIL with an error like:
|
||||||
|
- CheckedFromJsonException: Could not create `Parameter`. There is a problem with "in". Invalid union type "null"!
|
||||||
|
- This points to a components/parameters reference missing required “in”.
|
||||||
@@ -132,15 +132,53 @@ class OpenApiImportService {
|
|||||||
|
|
||||||
/// Try to parse a JSON or YAML OpenAPI spec string.
|
/// Try to parse a JSON or YAML OpenAPI spec string.
|
||||||
/// Returns null if parsing fails.
|
/// Returns null if parsing fails.
|
||||||
|
///
|
||||||
|
/// NOTE: There's a known issue with the openapi_spec package where
|
||||||
|
/// security fields containing empty arrays (e.g., "security": [[]])
|
||||||
|
/// cause parsing failures. This method includes a workaround.
|
||||||
static OpenApi? tryParseSpec(String source) {
|
static OpenApi? tryParseSpec(String source) {
|
||||||
try {
|
try {
|
||||||
// Let the library infer JSON/YAML
|
|
||||||
return OpenApi.fromString(source: source, format: null);
|
return OpenApi.fromString(source: source, format: null);
|
||||||
|
} catch (e) {
|
||||||
|
// Try workaround for security field parsing issues
|
||||||
|
try {
|
||||||
|
final processedSource = _removeProblematicSecurityField(source);
|
||||||
|
if (processedSource != source) {
|
||||||
|
return OpenApi.fromString(source: processedSource, format: null);
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
// Workaround failed, fall through to return null
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes problematic security fields that cause parsing issues.
|
||||||
|
/// TODO: Remove this workaround once openapi_spec package fixes
|
||||||
|
/// the issue with security fields containing empty arrays.
|
||||||
|
static String _removeProblematicSecurityField(String source) {
|
||||||
|
try {
|
||||||
|
final spec = jsonDecode(source) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
if (spec.containsKey('security')) {
|
||||||
|
final security = spec['security'];
|
||||||
|
if (security is List && _hasEmptySecurityArrays(security)) {
|
||||||
|
spec.remove('security');
|
||||||
|
return jsonEncode(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
} catch (e) {
|
||||||
|
throw FormatException('Failed to preprocess OpenAPI spec: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if security list contains empty arrays that cause parsing issues.
|
||||||
|
static bool _hasEmptySecurityArrays(List<dynamic> security) {
|
||||||
|
return security.any((item) => item is List && item.isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a single request payload from a path + method operation.
|
/// Build a single request payload from a path + method operation.
|
||||||
/// The payload mirrors CurlImportService payload shape for reuse.
|
/// The payload mirrors CurlImportService payload shape for reuse.
|
||||||
static Map<String, dynamic> _payloadForOperation({
|
static Map<String, dynamic> _payloadForOperation({
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ void main() async {
|
|||||||
await Stac.initialize();
|
await Stac.initialize();
|
||||||
|
|
||||||
//Load all LLMs
|
//Load all LLMs
|
||||||
// await LLMManager.fetchAvailableLLMs();
|
|
||||||
await ModelManager.fetchAvailableModels();
|
await ModelManager.fetchAvailableModels();
|
||||||
|
|
||||||
var settingsModel = await getSettingsFromSharedPrefs();
|
var settingsModel = await getSettingsFromSharedPrefs();
|
||||||
@@ -30,9 +29,6 @@ void main() async {
|
|||||||
settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null);
|
settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Load all models at init
|
|
||||||
// await ModelManager.loadAvailableLLMs();
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|||||||
@@ -250,12 +250,15 @@ class CollectionStateNotifier
|
|||||||
newModel = switch (apiType) {
|
newModel = switch (apiType) {
|
||||||
APIType.rest || APIType.graphql => currentModel.copyWith(
|
APIType.rest || APIType.graphql => currentModel.copyWith(
|
||||||
apiType: apiType,
|
apiType: apiType,
|
||||||
|
requestTabIndex: 0,
|
||||||
name: name ?? currentModel.name,
|
name: name ?? currentModel.name,
|
||||||
description: description ?? currentModel.description,
|
description: description ?? currentModel.description,
|
||||||
httpRequestModel: const HttpRequestModel(),
|
httpRequestModel: const HttpRequestModel(),
|
||||||
aiRequestModel: null),
|
aiRequestModel: null,
|
||||||
|
),
|
||||||
APIType.ai => currentModel.copyWith(
|
APIType.ai => currentModel.copyWith(
|
||||||
apiType: apiType,
|
apiType: apiType,
|
||||||
|
requestTabIndex: 0,
|
||||||
name: name ?? currentModel.name,
|
name: name ?? currentModel.name,
|
||||||
description: description ?? currentModel.description,
|
description: description ?? currentModel.description,
|
||||||
httpRequestModel: null,
|
httpRequestModel: null,
|
||||||
@@ -350,8 +353,20 @@ class CollectionStateNotifier
|
|||||||
executionRequestModel.httpRequestModel!);
|
executionRequestModel.httpRequestModel!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal: start network log
|
// Terminal
|
||||||
final terminal = ref.read(terminalStateProvider.notifier);
|
final terminal = ref.read(terminalStateProvider.notifier);
|
||||||
|
|
||||||
|
var valRes = getValidationResult(substitutedHttpRequestModel);
|
||||||
|
if (valRes != null) {
|
||||||
|
terminal.logSystem(
|
||||||
|
category: 'validation',
|
||||||
|
message: valRes,
|
||||||
|
level: TerminalLevel.error,
|
||||||
|
);
|
||||||
|
ref.read(showTerminalBadgeProvider.notifier).state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal: start network log
|
||||||
final logId = terminal.startNetwork(
|
final logId = terminal.startNetwork(
|
||||||
apiType: executionRequestModel.apiType,
|
apiType: executionRequestModel.apiType,
|
||||||
method: substitutedHttpRequestModel.method,
|
method: substitutedHttpRequestModel.method,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ final historyCodePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
|||||||
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final hasUnsavedChangesProvider = StateProvider<bool>((ref) => false);
|
final hasUnsavedChangesProvider = StateProvider<bool>((ref) => false);
|
||||||
|
final showTerminalBadgeProvider = StateProvider<bool>((ref) => false);
|
||||||
|
|
||||||
// final nameTextFieldControllerProvider =
|
// final nameTextFieldControllerProvider =
|
||||||
// StateProvider.autoDispose<TextEditingController>((ref) {
|
// StateProvider.autoDispose<TextEditingController>((ref) {
|
||||||
|
|||||||
@@ -76,14 +76,22 @@ class Dashboard extends ConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
kVSpacer10,
|
kVSpacer10,
|
||||||
IconButton(
|
Badge(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
isLabelVisible:
|
||||||
|
ref.watch(showTerminalBadgeProvider) && railIdx != 3,
|
||||||
|
child: IconButton(
|
||||||
isSelected: railIdx == 3,
|
isSelected: railIdx == 3,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(navRailIndexStateProvider.notifier).state = 3;
|
ref.read(navRailIndexStateProvider.notifier).state =
|
||||||
|
3;
|
||||||
|
ref.read(showTerminalBadgeProvider.notifier).state =
|
||||||
|
false;
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.terminal_outlined),
|
icon: const Icon(Icons.terminal_outlined),
|
||||||
selectedIcon: const Icon(Icons.terminal),
|
selectedIcon: const Icon(Icons.terminal),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'Logs',
|
'Logs',
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class EditGraphQLRequestPane extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedId = ref.watch(selectedIdStateProvider);
|
final selectedId = ref.watch(selectedIdStateProvider);
|
||||||
var tabIndex = ref.watch(
|
final tabIndex = ref.watch(
|
||||||
selectedRequestModelProvider.select((value) => value?.requestTabIndex));
|
selectedRequestModelProvider.select((value) => value?.requestTabIndex));
|
||||||
final codePaneVisible = ref.watch(codePaneVisibleStateProvider);
|
final codePaneVisible = ref.watch(codePaneVisibleStateProvider);
|
||||||
final headerLength = ref.watch(selectedRequestModelProvider
|
final headerLength = ref.watch(selectedRequestModelProvider
|
||||||
@@ -34,9 +34,6 @@ class EditGraphQLRequestPane extends ConsumerWidget {
|
|||||||
final hasAuth = ref.watch(selectedRequestModelProvider.select((value) =>
|
final hasAuth = ref.watch(selectedRequestModelProvider.select((value) =>
|
||||||
value?.httpRequestModel?.authModel?.type != APIAuthType.none));
|
value?.httpRequestModel?.authModel?.type != APIAuthType.none));
|
||||||
|
|
||||||
if (tabIndex >= 3) {
|
|
||||||
tabIndex = 0;
|
|
||||||
}
|
|
||||||
return RequestPane(
|
return RequestPane(
|
||||||
selectedId: selectedId,
|
selectedId: selectedId,
|
||||||
codePaneVisible: codePaneVisible,
|
codePaneVisible: codePaneVisible,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||||
import 'secure_credential_storage.dart';
|
import 'secure_credential_storage.dart';
|
||||||
|
|
||||||
enum HiveBoxType { normal, lazy }
|
enum HiveBoxType { normal, lazy }
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export 'http_utils.dart';
|
|||||||
export 'js_utils.dart';
|
export 'js_utils.dart';
|
||||||
export 'save_utils.dart';
|
export 'save_utils.dart';
|
||||||
export 'ui_utils.dart';
|
export 'ui_utils.dart';
|
||||||
|
export 'validation_utils.dart';
|
||||||
export 'window_utils.dart';
|
export 'window_utils.dart';
|
||||||
|
|||||||
18
lib/utils/validation_utils.dart
Normal file
18
lib/utils/validation_utils.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:apidash_core/apidash_core.dart';
|
||||||
|
|
||||||
|
String? getValidationResult(HttpRequestModel requestModel) {
|
||||||
|
if (requestModel.url.trim().isEmpty) {
|
||||||
|
return 'Request URL is empty. Please provide a valid URL.';
|
||||||
|
}
|
||||||
|
if (requestModel.method == HTTPVerb.get && requestModel.hasAnyBody) {
|
||||||
|
return 'GET request contains a body. This is not supported.';
|
||||||
|
}
|
||||||
|
if (requestModel.hasJsonData) {
|
||||||
|
try {
|
||||||
|
kJsonDecoder.convert(requestModel.body!);
|
||||||
|
} catch (e) {
|
||||||
|
return 'Invalid JSON in request body: ${e.toString()}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -47,6 +47,10 @@ class HttpRequestModel with _$HttpRequestModel {
|
|||||||
bool get hasTextContentType => bodyContentType == ContentType.text;
|
bool get hasTextContentType => bodyContentType == ContentType.text;
|
||||||
int get contentLength => utf8.encode(body ?? "").length;
|
int get contentLength => utf8.encode(body ?? "").length;
|
||||||
bool get hasBody => hasJsonData || hasTextData || hasFormData;
|
bool get hasBody => hasJsonData || hasTextData || hasFormData;
|
||||||
|
bool get hasAnyBody =>
|
||||||
|
(hasJsonContentType && contentLength > 0) ||
|
||||||
|
(hasTextContentType && contentLength > 0) ||
|
||||||
|
(hasFormDataContentType && formDataMapList.isNotEmpty);
|
||||||
bool get hasJsonData =>
|
bool get hasJsonData =>
|
||||||
kMethodsWithBody.contains(method) &&
|
kMethodsWithBody.contains(method) &&
|
||||||
hasJsonContentType &&
|
hasJsonContentType &&
|
||||||
|
|||||||
26
pubspec.lock
26
pubspec.lock
@@ -844,14 +844,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.3"
|
||||||
hive_flutter:
|
hive_ce:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hive_flutter
|
name: hive_ce
|
||||||
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "2.15.1"
|
||||||
|
hive_ce_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive_ce_flutter
|
||||||
|
sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.3"
|
||||||
hooks_riverpod:
|
hooks_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -944,6 +952,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
isolate_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: isolate_channel
|
||||||
|
sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
jaspr:
|
jaspr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ dependencies:
|
|||||||
fvp: ^0.32.1
|
fvp: ^0.32.1
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
highlighter: ^0.1.1
|
highlighter: ^0.1.1
|
||||||
hive_flutter: ^1.1.0
|
hive_ce_flutter: ^2.3.3
|
||||||
hooks_riverpod: ^2.5.2
|
hooks_riverpod: ^2.5.2
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
jinja: ^0.6.1
|
jinja: ^0.6.1
|
||||||
|
|||||||
@@ -31,6 +31,86 @@ void main() {
|
|||||||
expect(summary, contains('POST'));
|
expect(summary, contains('POST'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tryParseSpec handles problematic security field with empty arrays',
|
||||||
|
() {
|
||||||
|
const specWithEmptySecurityArray = '''
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Cat Fact API",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/fact": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [[]]
|
||||||
|
}''';
|
||||||
|
|
||||||
|
final result =
|
||||||
|
OpenApiImportService.tryParseSpec(specWithEmptySecurityArray);
|
||||||
|
expect(result, isNotNull);
|
||||||
|
expect(result!.info.title, equals('Cat Fact API'));
|
||||||
|
expect(result.info.version, equals('1.0.0'));
|
||||||
|
expect(result.paths, isNotNull);
|
||||||
|
expect(result.paths!.keys, contains('/fact'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryParseSpec handles valid security field with actual requirements',
|
||||||
|
() {
|
||||||
|
const specWithRealSecurity = '''
|
||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Secured API",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/secured": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}''';
|
||||||
|
|
||||||
|
final result = OpenApiImportService.tryParseSpec(specWithRealSecurity);
|
||||||
|
expect(result, isNotNull);
|
||||||
|
expect(result!.info.title, equals('Secured API'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryParseSpec returns null for invalid JSON', () {
|
||||||
|
const invalidSpec = 'not valid json';
|
||||||
|
final result = OpenApiImportService.tryParseSpec(invalidSpec);
|
||||||
|
expect(result, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tryParseSpec returns null for non-OpenAPI JSON', () {
|
||||||
|
const nonOpenApiSpec = '''
|
||||||
|
{
|
||||||
|
"notOpenApi": true,
|
||||||
|
"someField": "value"
|
||||||
|
}''';
|
||||||
|
final result = OpenApiImportService.tryParseSpec(nonOpenApiSpec);
|
||||||
|
expect(result, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
test('extractSpecMeta includes endpoints & baseUrl', () {
|
test('extractSpecMeta includes endpoints & baseUrl', () {
|
||||||
final spec = OpenApiImportService.tryParseSpec(_specJson)!;
|
final spec = OpenApiImportService.tryParseSpec(_specJson)!;
|
||||||
final meta = OpenApiImportService.extractSpecMeta(spec);
|
final meta = OpenApiImportService.extractSpecMeta(spec);
|
||||||
|
|||||||
Reference in New Issue
Block a user