diff --git a/src/dynamic/lang/js_shared.rs b/src/dynamic/lang/js_shared.rs index f7dab2f5..e12a070b 100644 --- a/src/dynamic/lang/js_shared.rs +++ b/src/dynamic/lang/js_shared.rs @@ -1156,15 +1156,135 @@ fn emit_graphql_resolver( ) -> HarnessSource { let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript); let handler = &spec.entry_name; + let framework = spec + .framework + .as_ref() + .map(|binding| binding.adapter.as_str()) + .unwrap_or(""); let body = format!( r#"{preamble} // Phase 21 (Track M.3) — GraphQL resolver. process.stdout.write('__NYX_GRAPHQL_RESOLVER__: ' + {type_name:?} + '.' + {field:?} + '\n'); +const _nyxFramework = {framework:?}; const _h = _nyxResolve({handler:?}); if (_h == null) {{ process.stderr.write('NYX_RESOLVER_NOT_FOUND: ' + {handler:?} + '\n'); process.exit(78); }} +async function _nyxTryGraphqlRelay(typeName, fieldName, resolver) {{ + let gql; + let relay; + try {{ + gql = require('graphql'); + relay = require('graphql-relay'); + }} catch (_) {{ + return false; + }} + const graphql = gql.graphql; + const GraphQLSchema = gql.GraphQLSchema; + const GraphQLObjectType = gql.GraphQLObjectType; + const GraphQLString = gql.GraphQLString; + const nodeDefinitions = relay.nodeDefinitions; + const globalIdField = relay.globalIdField; + const fromGlobalId = relay.fromGlobalId; + const toGlobalId = relay.toGlobalId; + if (typeof graphql !== 'function' || typeof nodeDefinitions !== 'function') return false; + const safeField = /^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName) ? fieldName : 'nyxField'; + const safeType = /^[A-Za-z_][A-Za-z0-9_]*$/.test(typeName) ? typeName : 'NyxNode'; + const nodeTypeName = (safeType === 'Query' || safeType === 'Node') ? 'NyxRelayNode' : safeType; + try {{ + let NodeType; + const defs = nodeDefinitions( + async function (globalId, context, info) {{ + let decoded = {{}}; + try {{ decoded = fromGlobalId(globalId) || {{}}; }} catch (_) {{}} + const relayId = decoded.id || globalId || payload; + const value = await Promise.resolve(resolver( + null, + {{ id: relayId, input: payload, value: payload, globalId }}, + context || {{}}, + info || {{ fieldName: safeField, parentType: nodeTypeName }} + )); + return value == null ? {{ id: relayId }} : value; + }}, + function () {{ return NodeType; }} + ); + NodeType = new GraphQLObjectType({{ + name: nodeTypeName, + interfaces: [defs.nodeInterface], + fields: function () {{ + const fields = {{ + id: globalIdField(nodeTypeName, function (obj) {{ + return obj && obj.id != null ? String(obj.id) : String(payload); + }}), + }}; + fields[safeField] = {{ + type: GraphQLString, + resolve: function (obj) {{ + if (obj == null) return null; + const value = obj[safeField] != null ? obj[safeField] + : (obj._sql != null ? obj._sql + : (obj._query != null ? obj._query + : (obj.name != null ? obj.name : obj.id))); + return value == null ? null : String(value); + }}, + }}; + return fields; + }}, + }}); + const QueryType = new GraphQLObjectType({{ + name: 'Query', + fields: function () {{ + const fields = {{ node: defs.nodeField }}; + fields[safeField] = {{ + type: GraphQLString, + args: {{ id: {{ type: GraphQLString }}, input: {{ type: GraphQLString }} }}, + resolve: async function (_parent, args, context, info) {{ + const value = await Promise.resolve(resolver( + null, + Object.assign({{ id: payload, input: payload, value: payload }}, args || {{}}), + context || {{}}, + info || {{ fieldName: safeField, parentType: safeType }} + )); + if (value == null) return null; + if (typeof value === 'object') {{ + const out = value[safeField] != null ? value[safeField] + : (value._sql != null ? value._sql + : (value._query != null ? value._query + : (value.name != null ? value.name : value.id))); + return out == null ? null : String(out); + }} + return String(value); + }}, + }}; + return fields; + }}, + }}); + const schema = new GraphQLSchema({{ query: QueryType, types: [NodeType] }}); + const globalId = toGlobalId(nodeTypeName, payload); + const nodeSelection = safeField === 'id' + ? 'id' + : 'id ... on ' + nodeTypeName + ' {{ ' + safeField + ' }}'; + const source = 'query($id: ID!, $value: String) {{ node(id: $id) {{ ' + + nodeSelection + ' }} ' + safeField + '(id: $value, input: $value) }}'; + const result = await graphql({{ + schema, + source, + variableValues: {{ id: globalId, value: payload }}, + }}); + if (result.errors && result.errors.length) return false; + if (result.data && result.data[safeField] != null) {{ + process.stdout.write(String(result.data[safeField]) + '\n'); + }} + if (result.data && result.data.node && result.data.node[safeField] != null) {{ + process.stdout.write(String(result.data.node[safeField]) + '\n'); + }} + return true; + }} catch (e) {{ + process.stderr.write('NYX_GRAPHQL_RELAY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return false; + }} +}} async function _nyxTryApolloServer(typeName, fieldName, resolver) {{ let ApolloServer; let needsStart = true; @@ -1257,6 +1377,7 @@ async function _nyxTryGraphqlJs(typeName, fieldName, resolver) {{ }} (async () => {{ try {{ + if (_nyxFramework === 'graphql-relay' && await _nyxTryGraphqlRelay({type_name:?}, {field:?}, _h)) return; if (await _nyxTryApolloServer({type_name:?}, {field:?}, _h)) return; if (await _nyxTryGraphqlJs({type_name:?}, {field:?}, _h)) return; // Apollo resolver shape: (parent, args, context, info). @@ -1272,6 +1393,7 @@ async function _nyxTryGraphqlJs(typeName, fieldName, resolver) {{ handler = handler, type_name = type_name, field = field, + framework = framework, ); HarnessSource { source: body, @@ -1522,15 +1644,100 @@ const _qi = _realSequelize ? _realSequelize.queryInterface : {{ sequelize: {{ query: async function(sql){{ _nyxMigrationSqlRecord(sql, 'sequelize'); return sql; }} }}, }}; const _sequelizeNamespace = _realSequelize ? _realSequelize.Sequelize : {{}}; -const _prisma = {{ +const _nyxPrismaShim = {{ $executeRaw: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }}, $executeRawUnsafe: async function(s){{ if (s) {{ _nyxMigrationSqlRecord(s, 'prisma'); process.stdout.write('NYX_PRISMA_SQL: ' + s + '\n'); }} return s; }}, $queryRaw: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }}, $queryRawUnsafe: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }}, }}; +let _realPrisma = null; +let _prisma = _nyxPrismaShim; global.__nyx_prisma = _prisma; +function _nyxPrismaSqlText(statement, values) {{ + if (Array.isArray(statement)) {{ + let out = ''; + for (let i = 0; i < statement.length; i++) {{ + out += String(statement[i]); + if (i < values.length) out += String(values[i]); + }} + return out; + }} + if (statement && Array.isArray(statement.raw)) {{ + return _nyxPrismaSqlText(statement.raw, values); + }} + return String(statement || ''); +}} +async function _nyxTryRealPrismaClient() {{ + try {{ + const PrismaPkg = require('@prisma/client'); + const PrismaClient = PrismaPkg.PrismaClient; + if (typeof PrismaClient !== 'function') return null; + const endpoint = process.env.NYX_SQL_ENDPOINT || ''; + if (endpoint && !process.env.DATABASE_URL) {{ + process.env.DATABASE_URL = 'file:' + endpoint; + }} + const client = new PrismaClient(); + const wrapped = Object.create(client); + wrapped.$executeRaw = async function(statement, ...values) {{ + const sql = _nyxPrismaSqlText(statement, values); + if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client'); + try {{ + if (typeof client.$executeRawUnsafe === 'function') {{ + return await client.$executeRawUnsafe(sql); + }} + return await client.$executeRaw(statement, ...values); + }} catch (e) {{ + process.stderr.write('NYX_PRISMA_CLIENT_EXEC_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return sql; + }} + }}; + wrapped.$executeRawUnsafe = async function(statement, ...values) {{ + const sql = _nyxPrismaSqlText(statement, values); + if (sql) {{ + _nyxMigrationSqlRecord(sql, 'prisma-client'); + process.stdout.write('NYX_PRISMA_CLIENT_SQL: ' + sql + '\n'); + }} + try {{ + return await client.$executeRawUnsafe(statement, ...values); + }} catch (e) {{ + process.stderr.write('NYX_PRISMA_CLIENT_EXEC_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return sql; + }} + }}; + wrapped.$queryRaw = async function(statement, ...values) {{ + const sql = _nyxPrismaSqlText(statement, values); + if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client'); + try {{ + return await client.$queryRaw(statement, ...values); + }} catch (e) {{ + process.stderr.write('NYX_PRISMA_CLIENT_QUERY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return sql; + }} + }}; + wrapped.$queryRawUnsafe = async function(statement, ...values) {{ + const sql = _nyxPrismaSqlText(statement, values); + if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client'); + try {{ + return await client.$queryRawUnsafe(statement, ...values); + }} catch (e) {{ + process.stderr.write('NYX_PRISMA_CLIENT_QUERY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return sql; + }} + }}; + return {{ client, prisma: wrapped }}; + }} catch (e) {{ + process.stderr.write('NYX_PRISMA_CLIENT_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n'); + return null; + }} +}} (async () => {{ try {{ + _realPrisma = await _nyxTryRealPrismaClient(); + if (_realPrisma && _realPrisma.prisma) {{ + _prisma = _realPrisma.prisma; + global.__nyx_prisma = _prisma; + process.stdout.write('NYX_PRISMA_CLIENT=1\n'); + }} let _result; // Sequelize migrations conventionally take (queryInterface, Sequelize). // Single-arg migrations are Prisma/raw shapes and should receive payload. @@ -1553,6 +1760,9 @@ global.__nyx_prisma = _prisma; process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n'); }} finally {{ if (_realSequelize && _realSequelize.close) await _realSequelize.close(); + if (_realPrisma && _realPrisma.client && typeof _realPrisma.client.$disconnect === 'function') {{ + try {{ await _realPrisma.client.$disconnect(); }} catch (e) {{}} + }} }} }})(); "#, diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index 81898b74..fd7582cc 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -1679,6 +1679,49 @@ if _h is None: print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True) sys.exit(78) try: + def _nyx_try_channels(handler_name, body, ws_path): + try: + import asyncio + from channels.testing import WebsocketCommunicator + except Exception: + return False + + def _nyx_find_consumer(): + for value in vars(_entry_mod).values(): + if isinstance(value, type) and ( + hasattr(value, "as_asgi") or hasattr(value, "receive") + ): + if value.__name__.lower().endswith(("consumer", "websocket")): + return value + return None + + async def _nyx_drive_consumer(): + consumer_cls = _nyx_find_consumer() + if consumer_cls is None or not hasattr(consumer_cls, "as_asgi"): + return False + communicator = WebsocketCommunicator(consumer_cls.as_asgi(), ws_path or "/ws/") + connected = False + try: + connected, _subprotocol = await communicator.connect() + if not connected: + return False + await communicator.send_to(text_data=body) + return True + finally: + if connected: + try: + await communicator.disconnect() + except Exception: + pass + + try: + return bool(asyncio.run(_nyx_drive_consumer())) + except SystemExit: + raise + except Exception as _e: + print(f"NYX_CHANNELS_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) + return False + def _nyx_try_socketio(handler_name, handler, body): try: import socketio @@ -1697,7 +1740,7 @@ try: print(f"NYX_SOCKETIO_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) return False - if not _nyx_try_socketio({handler:?}, _h, payload): + if not _nyx_try_channels({handler:?}, payload, {path:?}) and not _nyx_try_socketio({handler:?}, _h, payload): # python-socketio handlers are `def message(sid, data)`; Channels # consumers are `def receive(self, text_data=None, bytes_data=None)`. # Try (sid, payload) first, then fall back to (payload). @@ -1938,6 +1981,94 @@ def _nyx_install_migration_sql_hooks(): except Exception: pass +def _nyx_try_alembic_command_upgrade(): + try: + import tempfile + import textwrap + import types + from pathlib import Path + import alembic.command + from alembic.config import Config + except Exception: + return False + + handler_name = {handler:?} + if handler_name not in ("upgrade", "up"): + return False + + endpoint = os.environ.get("NYX_SQL_ENDPOINT", "") + url = "sqlite:///" + endpoint if endpoint else "sqlite:///:memory:" + root = Path(tempfile.mkdtemp(prefix="nyx-alembic-")) + versions = root / "versions" + versions.mkdir(parents=True, exist_ok=True) + (root / "script.py.mako").write_text( + "${{up_revision}} = ${{repr(up_revision)}}\n" + "${{down_revision}} = ${{repr(down_revision)}}\n" + "def upgrade():\n pass\n" + "def downgrade():\n pass\n", + encoding="utf-8", + ) + (root / "env.py").write_text(textwrap.dedent(""" + from alembic import context + from sqlalchemy import engine_from_config, pool + + target_metadata = None + + def run_migrations_online(): + cfg = context.config.get_section(context.config.config_ini_section) + connectable = engine_from_config(cfg, prefix="sqlalchemy.", poolclass=pool.NullPool) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + run_migrations_online() + """), encoding="utf-8") + + hooks = types.ModuleType("nyx_alembic_hooks") + hooks.payload = payload + hooks.load_entry = lambda: _entry_mod + hooks.op_proxy = lambda inner=None: _NyxMigrationOpProxy(inner) + hooks.record_result = _nyx_record_migration_result + sys.modules["nyx_alembic_hooks"] = hooks + + (versions / "0001_nyx_entry.py").write_text(textwrap.dedent(""" + revision = "0001_nyx_entry" + down_revision = None + branch_labels = None + depends_on = None + + def upgrade(): + from alembic import op as real_op + from nyx_alembic_hooks import load_entry, op_proxy, payload, record_result + mod = load_entry() + if hasattr(mod, "op"): + mod.op = op_proxy(real_op) + fn = getattr(mod, "upgrade", None) or getattr(mod, "up", None) + if fn is None: + return + try: + result = fn(payload) + except TypeError: + result = fn() + record_result(result) + + def downgrade(): + pass + """), encoding="utf-8") + + cfg = Config() + cfg.set_main_option("script_location", str(root)) + cfg.set_main_option("sqlalchemy.url", url) + try: + alembic.command.upgrade(cfg, "head") + return True + except SystemExit: + raise + except Exception as _e: + print(f"NYX_ALEMBIC_COMMAND_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True) + return False + def _nyx_record_migration_result(result): if result is None: return @@ -2000,8 +2131,11 @@ def _nyx_run_django_migration_operations(cls): try: _nyx_install_migration_sql_hooks() + _ran_alembic_command = _nyx_try_alembic_command_upgrade() _result = None - if _h is _migration_cls or ({handler:?} == "Migration" and _migration_cls is not None): + if _ran_alembic_command: + _nyx_run_django_migration_operations(_migration_cls) + elif _h is _migration_cls or ({handler:?} == "Migration" and _migration_cls is not None): _nyx_run_django_migration_operations(_migration_cls) else: # Migrations conventionally take no arguments; pass payload if the diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index f0a558d1..efca6472 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -818,10 +818,50 @@ fn emit_websocket_handler_harness(spec: &HarnessSpec, path: &str) -> HarnessSour r#"{preamble} puts "__NYX_WEBSOCKET__: " + {path:?} +def nyx_try_action_cable_channel(cls) + begin + require 'action_cable/channel/base' + require 'logger' + rescue LoadError + return false + end + return false unless defined?(ActionCable::Channel::Base) + return false unless cls.is_a?(Class) && cls < ActionCable::Channel::Base + + begin + connection = Object.new + def connection.transmit(data) + print(data.to_s) if data + end + def connection.logger + @logger ||= Logger.new(IO::NULL) + end + def connection.identifiers + [] + end + def connection.connection_identifier + 'nyx-action-cable' + end + channel = cls.new(connection, {{ 'channel' => cls.name }}, {{ 'nyx_payload' => $nyx_payload }}) + if channel.respond_to?(:perform_action) + channel.perform_action({{ 'action' => 'receive', 'data' => $nyx_payload }}) + elsif channel.respond_to?(:receive) + channel.receive($nyx_payload) + else + return false + end + true + rescue StandardError => e + STDERR.puts("NYX_ACTION_CABLE_FALLBACK: #{{e.class.name}}: #{{e.message}}") if ENV['NYX_DEBUG'] + false + end +end + # ActionCable channels expose `receive(data)` on a subclass. Find the # enclosing class via const lookup; fall back to top-level send. if Object.const_defined?({handler:?}) cls = Object.const_get({handler:?}) + exit 0 if nyx_try_action_cable_channel(cls) begin inst = cls.new rescue (cls.allocate rescue nil) if inst && inst.respond_to?(:receive) diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index 68d55f62..e817e7f9 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -2365,16 +2365,75 @@ fn emit_graphql_resolver_harness( let cargo_toml = generate_cargo_toml_for_spec(spec.expected_cap, RustShape::Generic, spec); let handler = &spec.entry_name; let label = format!("{type_name}.{field}"); + let use_juniper = spec + .framework + .as_ref() + .map(|binding| binding.adapter.as_str() == "graphql-juniper") + .unwrap_or(false); + let field_ident = safe_rust_graphql_field_ident(field); + let (juniper_driver, resolver_call) = if use_juniper { + ( + format!( + r#" +struct NyxQueryRoot; + +#[juniper::graphql_object] +impl NyxQueryRoot {{ + fn {field_ident}(id: String) -> String {{ + entry::{handler}(&id) + }} +}} + +fn nyx_try_juniper(payload: &str) -> bool {{ + let ctx = (); + let schema = juniper::RootNode::new( + NyxQueryRoot, + juniper::EmptyMutation::<()>::new(), + juniper::EmptySubscription::<()>::new(), + ); + let mut vars = juniper::Variables::new(); + vars.insert("id".to_string(), juniper::InputValue::scalar(payload.to_string())); + let query = "query Nyx($id: String!) {{ {field_ident}(id: $id) }}"; + match juniper::execute_sync(query, None, &schema, &vars, &ctx) {{ + Ok((value, errors)) if errors.is_empty() => {{ + println!("{{:?}}", value); + true + }} + Ok((_value, errors)) => {{ + eprintln!("NYX_JUNIPER_ERRORS: {{:?}}", errors); + false + }} + Err(err) => {{ + eprintln!("NYX_JUNIPER_FALLBACK: {{err:?}}"); + false + }} + }} +}} +"#, + field_ident = field_ident, + handler = handler, + ), + " if !nyx_try_juniper(&payload) {\n let _ = entry::".to_owned() + + handler + + "(&payload);\n }\n", + ) + } else { + ( + String::new(), + " let _ = entry::".to_owned() + handler + "(&payload);\n", + ) + }; let body = format!( r#"//! Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3). mod entry; {shim} +{juniper_driver} fn main() {{ let payload = nyx_payload(); __nyx_install_crash_guard("{label}"); println!("__NYX_GRAPHQL_RESOLVER__: {type_name}.{field}"); println!("__NYX_SINK_HIT__"); - let _ = entry::{handler}(&payload); +{resolver_call} }} fn nyx_payload() -> String {{ @@ -2419,10 +2478,11 @@ fn b64_decode(input: &[u8]) -> Option> {{ Some(out) }} "#, - handler = handler, type_name = type_name, field = field, label = label, + juniper_driver = juniper_driver, + resolver_call = resolver_call, ); HarnessSource { source: body, @@ -2433,6 +2493,60 @@ fn b64_decode(input: &[u8]) -> Option> {{ } } +fn safe_rust_graphql_field_ident(field: &str) -> String { + let mut out = String::with_capacity(field.len().max(8)); + for ch in field.chars() { + if ch.is_ascii_alphanumeric() || ch == '_' { + out.push(ch); + } else { + out.push('_'); + } + } + if out.is_empty() + || out.as_bytes().first().is_some_and(|b| b.is_ascii_digit()) + || matches!( + out.as_str(), + "as" | "break" + | "const" + | "continue" + | "crate" + | "else" + | "enum" + | "extern" + | "false" + | "fn" + | "for" + | "if" + | "impl" + | "in" + | "let" + | "loop" + | "match" + | "mod" + | "move" + | "mut" + | "pub" + | "ref" + | "return" + | "self" + | "Self" + | "static" + | "struct" + | "super" + | "trait" + | "true" + | "type" + | "unsafe" + | "use" + | "where" + | "while" + ) + { + out.insert_str(0, "nyx_"); + } + out +} + /// True when the entry source declares `class` as a type that derives /// or implements `Default`. Two byte-level patterns are recognised: /// diff --git a/tests/phase21_corpus.rs b/tests/phase21_corpus.rs index 97fcd8ac..c7e0c5ff 100644 --- a/tests/phase21_corpus.rs +++ b/tests/phase21_corpus.rs @@ -781,6 +781,34 @@ fn graphql_resolver_js_apollo_stages_runtime_deps() { assert!(package.contains("\"graphql\"")); } +#[test] +fn graphql_resolver_js_relay_harness_uses_relay_runtime() { + let spec = framework_bound_spec( + Lang::JavaScript, + EvEntryKind::GraphQLResolver { + type_name: "Node".into(), + field: "resolveNode".into(), + }, + "resolveNode", + "tests/dynamic_fixtures/graphql_resolver/relay/vuln.js", + "graphql-relay", + ); + let h = lang::emit(&spec).expect("emit ok"); + assert!(h.source.contains("_nyxTryGraphqlRelay")); + assert!(h.source.contains("require('graphql-relay')")); + assert!(h.source.contains("nodeDefinitions")); + assert!(h.source.contains("toGlobalId")); + assert!(h.source.contains("_nyxFramework === 'graphql-relay'")); + assert!( + h.source.find("_nyxTryGraphqlRelay").unwrap() + < h.source.find("_nyxTryApolloServer").unwrap(), + "Relay runtime should be attempted before the generic Apollo path for graphql-relay specs", + ); + let package = extra_file_content(&h.extra_files, "package.json"); + assert!(package.contains("\"graphql-relay\"")); + assert!(package.contains("\"graphql\"")); +} + #[test] fn graphql_resolver_rust_harness_carries_sentinel_and_field() { let spec = make_spec( @@ -797,6 +825,27 @@ fn graphql_resolver_rust_harness_carries_sentinel_and_field() { assert!(h.source.contains("entry::resolve_user")); } +#[test] +fn graphql_resolver_rust_juniper_harness_uses_execute_sync() { + let spec = framework_bound_spec( + Lang::Rust, + EvEntryKind::GraphQLResolver { + type_name: "Query".into(), + field: "user".into(), + }, + "resolve_user", + "tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs", + "graphql-juniper", + ); + let h = lang::emit(&spec).expect("emit ok"); + assert!(h.source.contains("juniper::RootNode::new")); + assert!(h.source.contains("juniper::execute_sync")); + assert!(h.source.contains("fn user(id: String) -> String")); + assert!(h.source.contains("if !nyx_try_juniper(&payload)")); + let cargo = extra_file_content(&h.extra_files, "Cargo.toml"); + assert!(cargo.contains("juniper = \"0.16\"")); +} + #[test] fn graphql_resolver_go_harness_carries_sentinel_and_field() { let spec = make_spec( @@ -828,6 +877,9 @@ fn websocket_python_harness_carries_sentinel_and_handler() { assert!(h.source.contains("__NYX_WEBSOCKET__")); assert!(h.source.contains("\"message\"")); assert!(h.source.contains("/ws/chat")); + assert!(h.source.contains("_nyx_try_channels")); + assert!(h.source.contains("WebsocketCommunicator")); + assert!(h.source.contains("as_asgi")); assert!(h.source.contains("_nyx_try_socketio")); assert!(h.source.contains("socketio.Server")); } @@ -862,6 +914,9 @@ fn websocket_ruby_harness_carries_sentinel_and_handler() { let h = lang::emit(&spec).expect("emit ok"); assert!(h.source.contains("__NYX_WEBSOCKET__")); assert!(h.source.contains("ChatChannel")); + assert!(h.source.contains("nyx_try_action_cable_channel")); + assert!(h.source.contains("ActionCable::Channel::Base")); + assert!(h.source.contains("perform_action")); } #[test] @@ -962,6 +1017,10 @@ fn migration_python_harness_carries_sentinel_and_handler() { assert!(h.source.contains("\"upgrade\"")); assert!(h.source.contains("__nyx_stub_sql_record")); assert!(h.source.contains("MigrationContext.configure")); + assert!(h.source.contains("_nyx_try_alembic_command_upgrade")); + assert!(h.source.contains("alembic.command.upgrade")); + assert!(h.source.contains("script_location")); + assert!(h.source.contains("nyx_alembic_hooks")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); assert!(h.source.contains("def create_table")); assert!(h.source.contains("def add_column")); @@ -983,6 +1042,10 @@ fn migration_js_harness_carries_sentinel_and_handler() { assert!(h.source.contains("require('sequelize')")); assert!(h.source.contains("getQueryInterface")); assert!(h.source.contains("global.__nyx_prisma")); + assert!(h.source.contains("require('@prisma/client')")); + assert!(h.source.contains("_nyxTryRealPrismaClient")); + assert!(h.source.contains("NYX_PRISMA_CLIENT_SQL")); + assert!(h.source.contains("$disconnect")); assert!(h.source.contains("node:sqlite")); assert!(h.source.contains("NYX_SQL_ENDPOINT")); }