Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions modules/ensemble/test/upload_path_security_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:ensemble/framework/data_context.dart' as edc;
import 'package:ensemble/util/upload_utils.dart';
import 'package:flutter_test/flutter_test.dart';

Expand All @@ -18,4 +19,23 @@ void main() {
expect(uploadPathContainsParentSegment('../etc/passwd'), true);
});
});

group('UploadUtils.uploadFiles path guard', () {
test('throws before sending when a file path contains parent segments', () async {
final badFile =
edc.File(null, null, null, '/var/tmp/../../etc/passwd', null);
await expectLater(
UploadUtils.uploadFiles(
taskId: 'upload_test_task',
method: 'POST',
url: 'https://example.invalid/upload',
headers: const {},
fields: const {},
files: [badFile],
fieldName: 'file',
),
throwsA(isA<FormatException>()),
);
});
});
}
76 changes: 76 additions & 0 deletions modules/ensemble/test/widget/page_header_lifecycle_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:ensemble/framework/data_context.dart';
import 'package:ensemble/framework/theme_manager.dart';
import 'package:ensemble/framework/view/page.dart';
import 'package:ensemble/page_model.dart';
import 'package:ensemble/util/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:yaml/yaml.dart';

import 'test_utils.dart';

/// Regression coverage for PageState header storage listeners: periodic timers
/// and stream subscriptions must be cancelled on dispose so teardown does not
/// tick after the widget tree is torn down (see fix in page.dart).
void main() {
TestWidgetsFlutterBinding.ensureInitialized();

tearDown(() {
EnsembleThemeManager().reset();
});

SinglePageModel modelWithStorageDrivenHeader() {
final yaml = YamlMap.wrap({
'View': YamlMap.wrap({
'header': YamlMap.wrap({
'titleText': 'T',
'styles': YamlMap.wrap({
'listenTitleBarHeightStorage': true,
'titleBarHeight': 'ensemble.storage.tb_h',
}),
'collapsibleHeader': YamlMap.wrap({
'enabled': true,
'visible': 'ensemble.storage.coll_vis',
}),
}),
'body': YamlMap.wrap({
'Text': YamlMap.wrap({'text': 'body'}),
}),
}),
});
return PageModel.fromYaml(yaml) as SinglePageModel;
}

testWidgets(
'Page with storage-driven header disposes without timer or subscription leaks',
(tester) async {
EnsembleThemeManager().reset();

final pageModel = modelWithStorageDrivenHeader();
final dataContext = DataContext(buildContext: MockBuildContext());

await tester.pumpWidget(
MaterialApp(
navigatorKey: Utils.globalAppKey,
home: Page(
dataContext: dataContext,
pageModel: pageModel,
onRendered: () {},
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 350));

await tester.pumpWidget(
MaterialApp(
navigatorKey: Utils.globalAppKey,
home: const SizedBox.shrink(),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));

expect(tester.takeException(), isNull);
});
}
Loading