import 'dart:convert'; import 'dart:typed_data'; import 'package:ikar/core/plugins/plugin_signing.dart'; import 'package:ikar/core/plugins/plugin_models.dart'; import 'package:archive/archive.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ikar/core/plugins/plugin_manifest.dart'; import 'package:ikar/core/plugins/plugin_package.dart'; void main() { test('verifies a signature and rejects modified module contents', () async { final manifest = PluginManifest.fromJson(_manifest()); final files = { 'main.js': Uint8List.fromList(utf8.encode('export function hello() {}')), }; final signature = await PluginSigning.sign( manifest, files, List.generate(32, (index) => index), ); final signed = PluginManifest.fromJson({ ...manifest.toJson(), 'signature': signature.toJson(), }); expect( (await PluginSigning.verify(signed, files)).status, PluginSignatureStatus.verified, ); files['main.js'] = Uint8List.fromList( utf8.encode('export function hello() { return 1; }'), ); await expectLater( PluginSigning.verify(signed, files), throwsFormatException, ); }); test('decodes a valid .kinet package', () { final package = PluginPackage.decode( _kinet( manifest: _manifest(), files: {'main.js': 'export async function hello() {}'}, ), ); expect(package.manifest.id, 'dev.example.synthetic'); expect(package.manifest.commands.single.name, '/hello'); expect(package.manifest.commands.single.arguments.single.name, 'text'); expect(package.manifest.commands.single.arguments.single.rest, isTrue); expect(package.manifest.permissions, {PluginPermission.uiNotify}); expect(package.files, contains('main.js')); }); test('rejects unknown permissions', () { final manifest = _manifest()..['permissions'] = ['system.raw']; expect( () => PluginPackage.decode( _kinet(manifest: manifest, files: {'main.js': ''}), ), throwsFormatException, ); }); test('rejects a newer API version', () { final manifest = _manifest()..['apiVersion'] = kPluginApiVersion + 1; expect( () => PluginPackage.decode( _kinet(manifest: manifest, files: {'main.js': ''}), ), throwsFormatException, ); }); test('rejects archive path traversal', () { expect( () => PluginPackage.decode( _kinet( manifest: _manifest(), files: {'main.js': '', '../escape.js': ''}, ), ), throwsFormatException, ); }); test('rejects unsupported package files', () { expect( () => PluginPackage.decode( _kinet( manifest: _manifest(), files: {'main.js': '', 'payload.bin': 'synthetic'}, ), ), throwsFormatException, ); }); test('rejects a missing entry module', () { expect( () => PluginPackage.decode( _kinet(manifest: _manifest(), files: {'other.js': ''}), ), throwsFormatException, ); }); } Map _manifest() => { 'schemaVersion': 1, 'id': 'dev.example.synthetic', 'name': 'Synthetic Plugin', 'version': '1.2.3', 'apiVersion': 1, 'description': 'Synthetic fixture', 'author': 'Test Author', 'main': 'main.js', 'permissions': ['ui.notify'], 'commands': [ { 'name': '/hello', 'description': 'Synthetic command', 'handler': 'hello', 'arguments': [ {'name': 'text', 'description': 'Synthetic text', 'rest': true}, ], }, ], }; List _kinet({ required Map manifest, required Map files, }) { final archive = Archive() ..addFile(ArchiveFile.string('manifest.json', jsonEncode(manifest))); for (final entry in files.entries) { archive.addFile(ArchiveFile.string(entry.key, entry.value)); } return ZipEncoder().encodeBytes(archive); }