Add runtime networking APIs
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flame_lua_runtime/runtime/lifecycle/runtime_session.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/game_diff.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/runtime_script_services.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/script_engine.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -150,7 +151,10 @@ class _FakeScriptEngine implements ScriptEngine {
|
||||
bool failNext = false;
|
||||
|
||||
@override
|
||||
Future<void> loadPackage(GamePackage package) async {}
|
||||
Future<void> loadPackage(
|
||||
GamePackage package, {
|
||||
RuntimeScriptServices services = const RuntimeScriptServices(),
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
bool smokeTest(Map<String, Object?> context) => true;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_repository.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/runtime_script_services.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/script_engine.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -34,7 +35,10 @@ void main() {
|
||||
|
||||
class _FakeScriptEngine implements ScriptEngine {
|
||||
@override
|
||||
Future<void> loadPackage(GamePackage package) {
|
||||
Future<void> loadPackage(
|
||||
GamePackage package, {
|
||||
RuntimeScriptServices services = const RuntimeScriptServices(),
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
195
test/runtime/network/runtime_network_manager_test.dart
Normal file
195
test/runtime/network/runtime_network_manager_test.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
void main() {
|
||||
group('RuntimeNetworkManager', () {
|
||||
test('supports http and https requests', () async {
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeNetworkManager(
|
||||
eventSink: events.add,
|
||||
httpClient: MockClient((request) async {
|
||||
return http.Response(
|
||||
'ok',
|
||||
201,
|
||||
headers: {'content-type': 'text/plain'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await manager.httpRequest(
|
||||
RuntimeHttpRequest(
|
||||
id: 'http_1',
|
||||
method: 'GET',
|
||||
uri: Uri.parse('http://example.com/ping'),
|
||||
),
|
||||
);
|
||||
await manager.httpRequest(
|
||||
RuntimeHttpRequest(
|
||||
id: 'https_1',
|
||||
method: 'POST',
|
||||
uri: Uri.parse('https://example.com/ping'),
|
||||
body: 'body',
|
||||
),
|
||||
);
|
||||
|
||||
expect(events.map((event) => event.type), [
|
||||
RuntimeNetworkEventType.http,
|
||||
RuntimeNetworkEventType.http,
|
||||
]);
|
||||
expect(events.first.data['ok'], isTrue);
|
||||
expect(events.first.data['status'], 201);
|
||||
expect(events.first.data['body'], 'ok');
|
||||
expect(events.last.data['id'], 'https_1');
|
||||
});
|
||||
|
||||
test('rejects unsupported HTTP schemes with error event', () async {
|
||||
final diagnostics = RuntimeDiagnostics();
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeNetworkManager(
|
||||
eventSink: events.add,
|
||||
diagnostics: diagnostics,
|
||||
httpClient: MockClient((_) async => http.Response('no', 500)),
|
||||
);
|
||||
|
||||
await manager.httpRequest(
|
||||
RuntimeHttpRequest(
|
||||
id: 'bad',
|
||||
method: 'GET',
|
||||
uri: Uri.parse('ftp://example.com/file'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(events.single.type, RuntimeNetworkEventType.http);
|
||||
expect(events.single.data['ok'], isFalse);
|
||||
expect(events.single.data['error'], contains('http/https'));
|
||||
expect(
|
||||
diagnostics.entries.single.type,
|
||||
RuntimeDiagnosticType.networkError,
|
||||
);
|
||||
});
|
||||
|
||||
test('supports ws and wss connections plus send and close', () async {
|
||||
final events = <RuntimeEvent>[];
|
||||
final controllers = <StreamChannelController<Object?>>[];
|
||||
addTearDown(() {
|
||||
for (final controller in controllers) {
|
||||
controller.local.sink.close();
|
||||
}
|
||||
});
|
||||
final manager = RuntimeNetworkManager(
|
||||
eventSink: events.add,
|
||||
webSocketFactory: (_, {protocols}) {
|
||||
final controller = StreamChannelController<Object?>();
|
||||
controllers.add(controller);
|
||||
return _FakeWebSocketChannel(controller.foreign);
|
||||
},
|
||||
);
|
||||
|
||||
manager.wsConnect(
|
||||
RuntimeWebSocketConnectRequest(
|
||||
id: 'ws_1',
|
||||
uri: Uri.parse('ws://example.com/socket'),
|
||||
),
|
||||
);
|
||||
manager.wsConnect(
|
||||
RuntimeWebSocketConnectRequest(
|
||||
id: 'wss_1',
|
||||
uri: Uri.parse('wss://example.com/socket'),
|
||||
protocols: const ['game.v1'],
|
||||
),
|
||||
);
|
||||
expect(manager.wsSend('wss_1', 'hello'), isTrue);
|
||||
controllers.last.local.sink.add('server');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(manager.closeWebSocket('wss_1'), isTrue);
|
||||
|
||||
expect(
|
||||
events.map((event) => event.type),
|
||||
contains(RuntimeNetworkEventType.wsOpen),
|
||||
);
|
||||
expect(
|
||||
events.where((event) => event.type == RuntimeNetworkEventType.wsOpen),
|
||||
hasLength(2),
|
||||
);
|
||||
expect(
|
||||
events
|
||||
.where((event) => event.type == RuntimeNetworkEventType.wsMessage)
|
||||
.single
|
||||
.data['message'],
|
||||
'server',
|
||||
);
|
||||
expect(events.last.type, RuntimeNetworkEventType.wsClose);
|
||||
});
|
||||
|
||||
test('rejects unsupported WebSocket schemes with error event', () {
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeNetworkManager(eventSink: events.add);
|
||||
|
||||
manager.wsConnect(
|
||||
RuntimeWebSocketConnectRequest(
|
||||
id: 'bad',
|
||||
uri: Uri.parse('http://example.com/socket'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(events.single.type, RuntimeNetworkEventType.wsError);
|
||||
expect(events.single.data['error'], contains('ws/wss'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeWebSocketChannel extends StreamChannelMixin
|
||||
implements WebSocketChannel {
|
||||
_FakeWebSocketChannel(StreamChannel<Object?> channel) : _channel = channel;
|
||||
|
||||
final StreamChannel<Object?> _channel;
|
||||
|
||||
@override
|
||||
Stream get stream => _channel.stream;
|
||||
|
||||
@override
|
||||
WebSocketSink get sink => _FakeWebSocketSink(_channel.sink);
|
||||
|
||||
@override
|
||||
int? get closeCode => null;
|
||||
|
||||
@override
|
||||
String? get closeReason => null;
|
||||
|
||||
@override
|
||||
String? get protocol => null;
|
||||
|
||||
@override
|
||||
Future<void> get ready => Future<void>.value();
|
||||
}
|
||||
|
||||
class _FakeWebSocketSink implements WebSocketSink {
|
||||
_FakeWebSocketSink(this._sink);
|
||||
|
||||
final StreamSink<Object?> _sink;
|
||||
|
||||
@override
|
||||
void add(Object? event) => _sink.add(event);
|
||||
|
||||
@override
|
||||
void addError(Object error, [StackTrace? stackTrace]) {
|
||||
_sink.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addStream(Stream stream) => _sink.addStream(stream);
|
||||
|
||||
@override
|
||||
Future<void> close([int? closeCode, String? closeReason]) => _sink.close();
|
||||
|
||||
@override
|
||||
Future<void> get done => _sink.done;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_repository.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/stable_package_store.dart';
|
||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/runtime_script_services.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/script_engine.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -357,7 +358,10 @@ class _FakeScriptEngine implements ScriptEngine {
|
||||
GamePackage? _package;
|
||||
|
||||
@override
|
||||
Future<void> loadPackage(GamePackage package) async {
|
||||
Future<void> loadPackage(
|
||||
GamePackage package, {
|
||||
RuntimeScriptServices services = const RuntimeScriptServices(),
|
||||
}) async {
|
||||
_package = package;
|
||||
loadedPackages.add(package.rootPath);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
|
||||
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/lua_dardo_script_engine.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/runtime_script_services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<GamePackage> _loadExamplePackage(String gameId) async {
|
||||
@@ -941,6 +943,84 @@ end
|
||||
expect(diagnostics.entries.first.context, {'argumentCount': 2});
|
||||
});
|
||||
|
||||
test('exposes async HTTP runtime API to Lua', () async {
|
||||
final package = await _createPackage(
|
||||
mainScript: '''
|
||||
function smoke_test(ctx) return true end
|
||||
function init(ctx)
|
||||
local id = runtime.http_request({
|
||||
id = "login",
|
||||
method = "post",
|
||||
url = "https://example.com/login",
|
||||
headers = { Authorization = "Bearer token" },
|
||||
body = "{}",
|
||||
timeout = 2,
|
||||
})
|
||||
return { commands = { { type = "toast", text = id } } }
|
||||
end
|
||||
function on_event(event) return {} end
|
||||
''',
|
||||
);
|
||||
final network = _RecordingNetworkManager();
|
||||
final engine = LuaDardoScriptEngine();
|
||||
|
||||
await engine.loadPackage(
|
||||
package,
|
||||
services: RuntimeScriptServices(network: network),
|
||||
);
|
||||
final diff = engine.init({'runtimeApiVersion': 1});
|
||||
|
||||
expect(diff.commands.single.payload['text'], 'login');
|
||||
expect(network.httpRequests, hasLength(1));
|
||||
expect(network.httpRequests.single.id, 'login');
|
||||
expect(network.httpRequests.single.method, 'POST');
|
||||
expect(network.httpRequests.single.uri.scheme, 'https');
|
||||
expect(
|
||||
network.httpRequests.single.headers['Authorization'],
|
||||
'Bearer token',
|
||||
);
|
||||
expect(network.httpRequests.single.body, '{}');
|
||||
expect(network.httpRequests.single.timeout, const Duration(seconds: 2));
|
||||
});
|
||||
|
||||
test('exposes WebSocket runtime API to Lua', () async {
|
||||
final package = await _createPackage(
|
||||
mainScript: '''
|
||||
function smoke_test(ctx) return true end
|
||||
function init(ctx)
|
||||
local id = runtime.ws_connect({
|
||||
id = "chat",
|
||||
url = "wss://example.com/socket",
|
||||
protocols = { "game.v1" },
|
||||
})
|
||||
local sent = runtime.ws_send(id, "hello")
|
||||
local closed = runtime.ws_close(id)
|
||||
return {
|
||||
commands = {
|
||||
{ type = "toast", text = id .. ":" .. tostring(sent) .. ":" .. tostring(closed) },
|
||||
},
|
||||
}
|
||||
end
|
||||
function on_event(event) return {} end
|
||||
''',
|
||||
);
|
||||
final network = _RecordingNetworkManager();
|
||||
final engine = LuaDardoScriptEngine();
|
||||
|
||||
await engine.loadPackage(
|
||||
package,
|
||||
services: RuntimeScriptServices(network: network),
|
||||
);
|
||||
final diff = engine.init({'runtimeApiVersion': 1});
|
||||
|
||||
expect(diff.commands.single.payload['text'], 'chat:true:true');
|
||||
expect(network.wsConnectRequests.single.id, 'chat');
|
||||
expect(network.wsConnectRequests.single.uri.scheme, 'wss');
|
||||
expect(network.wsConnectRequests.single.protocols, ['game.v1']);
|
||||
expect(network.wsMessages, {'chat': 'hello'});
|
||||
expect(network.closedWebSockets, ['chat']);
|
||||
});
|
||||
|
||||
test('rejects undeclared module imports', () async {
|
||||
final package = await _createPackage(
|
||||
mainScript: '''
|
||||
@@ -972,6 +1052,43 @@ function on_event(event) return {} end
|
||||
});
|
||||
}
|
||||
|
||||
class _RecordingNetworkManager extends RuntimeNetworkManager {
|
||||
_RecordingNetworkManager()
|
||||
: super(
|
||||
eventSink: (event) {
|
||||
events.add(event);
|
||||
},
|
||||
);
|
||||
|
||||
static final events = <RuntimeEvent>[];
|
||||
final httpRequests = <RuntimeHttpRequest>[];
|
||||
final wsConnectRequests = <RuntimeWebSocketConnectRequest>[];
|
||||
final wsMessages = <String, Object?>{};
|
||||
final closedWebSockets = <String>[];
|
||||
|
||||
@override
|
||||
Future<void> httpRequest(RuntimeHttpRequest request) async {
|
||||
httpRequests.add(request);
|
||||
}
|
||||
|
||||
@override
|
||||
void wsConnect(RuntimeWebSocketConnectRequest request) {
|
||||
wsConnectRequests.add(request);
|
||||
}
|
||||
|
||||
@override
|
||||
bool wsSend(String id, Object? message) {
|
||||
wsMessages[id] = message;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool closeWebSocket(String id, {bool emitClose = true}) {
|
||||
closedWebSockets.add(id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<GamePackage> _createPackage({
|
||||
required String mainScript,
|
||||
Map<String, String> modules = const {},
|
||||
|
||||
Reference in New Issue
Block a user