1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//! Platform E2E Integration Tests — Phase 8 Cycle 7
//!
//! End-to-end tests that verify the complete platform integration:
//! storage and functions (before/after mutation, cron).
//!
//! # Test tiers
//!
//! ## Tier 1 — Structural (no infrastructure required)
//!
//! These tests run in all CI environments and verify that subsystem types
//! compose correctly. They do NOT execute functions or query a database.
//!
//! ## Tier 2 — Database (requires PostgreSQL via testcontainers)
//!
//! These tests are marked `#[ignore]` and run only when explicitly requested.
//! They spin up a PostgreSQL container via testcontainers and verify the
//! full function execution pipeline against a real database.
//!
//! Run with:
//! ```bash
//! cargo test --test platform_e2e_test -- --include-ignored
//! ```
//!
//! ## Tier 3 — Full platform (requires PostgreSQL + `MinIO` + Deno runtime)
//!
//! These tests require a full platform stack and are gated behind the
//! `FRAISEQL_PLATFORM_E2E` environment variable to prevent accidental
//! execution in constrained CI environments.
//!
//! Run with:
//! ```bash
//! FRAISEQL_PLATFORM_E2E=1 cargo test --test platform_e2e_test -- --include-ignored
//! ```
//!
//! **Execution engine:** none (Tier 1), testcontainers (Tier 2), Docker (Tier 3)
//! **Infrastructure:** none (Tier 1), PostgreSQL (Tier 2), PostgreSQL + `MinIO` (Tier 3)
//! **Parallelism:** safe
#![allow(clippy::unwrap_used, clippy::print_stdout, clippy::print_stderr)] // Reason: test code, panics acceptable
#![allow(clippy::missing_panics_doc)] // Reason: test helpers
#![allow(clippy::missing_errors_doc)] // Reason: test helpers
#![allow(missing_docs)] // Reason: test code
#![allow(clippy::items_after_statements)] // Reason: test helpers near use site
#![allow(clippy::todo)] // Reason: documented #[ignore]d Tier 3 stubs awaiting full-platform infra
// ── Tier 1: Structural Tests ──────────────────────────────────────────────────
use std::{collections::HashMap, sync::Arc};
use fraiseql_functions::{
FunctionDefinition, FunctionModule, FunctionObserver, RuntimeType, TriggerRegistry,
};
use fraiseql_server::subsystems::{BeforeMutationHooks, FunctionsSubsystem};
/// Verify that `BeforeMutationHooks` can be constructed from a trigger registry.
#[test]
fn test_platform_e2e_before_mutation_hooks_construction() {
let defs = vec![FunctionDefinition::new(
"validate",
"before:mutation:createUser",
RuntimeType::Deno,
)];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
let observer = Arc::new(FunctionObserver::new());
let module_registry: HashMap<String, FunctionModule> = HashMap::new();
let hooks = BeforeMutationHooks::new(registry, module_registry, observer);
assert!(
hooks.trigger_registry.before_chain("createUser").is_some(),
"should find before:mutation chain for createUser"
);
assert!(
hooks.trigger_registry.before_chain("deleteUser").is_none(),
"should return None for unregistered mutation"
);
}
/// Verify that `FunctionsSubsystem` can be constructed with all fields.
#[test]
fn test_platform_e2e_functions_subsystem_full_construction() {
let defs = vec![
FunctionDefinition::new("validate", "before:mutation:createUser", RuntimeType::Deno),
FunctionDefinition::new("onCreated", "after:mutation:User:insert", RuntimeType::Deno),
FunctionDefinition::new("dailyJob", "cron:0 2 * * *", RuntimeType::Deno),
];
let trigger_registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
let observer = Arc::new(FunctionObserver::new());
let module_registry: HashMap<String, FunctionModule> = HashMap::new();
let config = fraiseql_server::schema::loader::FunctionsConfig {
definitions: defs,
module_dir: std::env::temp_dir().join("fraiseql_test_functions"),
dlq_store: None,
};
let subsystem = FunctionsSubsystem {
observer,
trigger_registry,
module_registry,
config,
};
assert_eq!(subsystem.trigger_registry.before_mutation_count(), 1);
assert_eq!(subsystem.trigger_registry.cron_trigger_count(), 1);
}
/// Verify that the registry → `CronScheduler` pipeline works end-to-end.
#[test]
fn test_platform_e2e_registry_to_cron_scheduler_pipeline() {
let defs = vec![
FunctionDefinition::new("dailyCleanup", "cron:0 2 * * *", RuntimeType::Deno),
FunctionDefinition::new("hourlySync", "cron:0 * * * *", RuntimeType::Deno),
// Non-cron trigger must coexist without interfering
FunctionDefinition::new("validate", "before:mutation:createUser", RuntimeType::Deno),
];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
// Fast path: 2 cron triggers → scheduler is Some
let scheduler = registry.cron_scheduler();
assert!(scheduler.is_some(), "should build a scheduler when cron triggers exist");
let scheduler = scheduler.unwrap();
assert_eq!(scheduler.trigger_count(), 2);
}
/// Verify that a registry with no cron triggers returns None from `cron_scheduler()`.
#[test]
fn test_platform_e2e_registry_no_cron_returns_none() {
let defs = vec![FunctionDefinition::new(
"validate",
"before:mutation:createUser",
RuntimeType::Deno,
)];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
assert!(
registry.cron_scheduler().is_none(),
"no cron triggers → cron_scheduler() returns None (zero-overhead fast path)"
);
}
/// Verify that `BeforeMutationHooks` correctly exposes the before:mutation chain
/// for finding triggers associated with specific mutations.
#[test]
fn test_platform_e2e_before_mutation_chain_finds_correct_triggers() {
let defs = vec![
FunctionDefinition::new("validateName", "before:mutation:createUser", RuntimeType::Deno),
FunctionDefinition::new("checkDups", "before:mutation:createUser", RuntimeType::Deno),
FunctionDefinition::new("auditDelete", "before:mutation:deleteUser", RuntimeType::Deno),
];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
let observer = Arc::new(FunctionObserver::new());
let hooks = BeforeMutationHooks::new(registry, HashMap::new(), observer);
// createUser has 2 triggers
let chain = hooks.trigger_registry.before_chain("createUser");
assert!(chain.is_some());
assert_eq!(chain.unwrap().triggers.len(), 2);
// deleteUser has 1 trigger
let chain = hooks.trigger_registry.before_chain("deleteUser");
assert!(chain.is_some());
assert_eq!(chain.unwrap().triggers.len(), 1);
// updateUser has no triggers — fast path, no allocation
assert!(hooks.trigger_registry.before_chain("updateUser").is_none());
}
/// Verify that multiple trigger types coexist in the registry without interference.
#[test]
fn test_platform_e2e_all_trigger_types_coexist() {
let defs = vec![
FunctionDefinition::new("onUserCreated", "after:mutation:User:insert", RuntimeType::Deno),
FunctionDefinition::new("validateUser", "before:mutation:createUser", RuntimeType::Deno),
FunctionDefinition::new("dailyReport", "cron:0 2 * * *", RuntimeType::Deno),
];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
assert_eq!(registry.function_count, 3);
assert_eq!(registry.before_mutation_count(), 1);
assert_eq!(registry.cron_trigger_count(), 1);
// Cron scheduler builds from cron triggers only
let scheduler = registry.cron_scheduler().unwrap();
assert_eq!(scheduler.trigger_count(), 1);
}
// ── Tier 2: CronScheduler lifecycle (tokio runtime, no DB) ───────────────────
/// Verify the full lifecycle: registry → scheduler → start → stop.
///
/// This test does not execute any functions; it only verifies the scheduler
/// lifecycle under a tokio runtime.
#[tokio::test]
async fn test_platform_e2e_cron_scheduler_starts_on_server_start() {
let defs = vec![
// Use a schedule that will never match in the test window (Feb 31)
FunctionDefinition::new("neverFires", "cron:0 0 31 2 *", RuntimeType::Deno),
];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
let observer = Arc::new(FunctionObserver::new());
let scheduler = registry.cron_scheduler().expect("should have scheduler");
// Simulates server startup: build and start the scheduler
let handle = scheduler.start(observer, HashMap::new());
// Simulates server shutdown: stop the scheduler
handle.stop();
// Yield to allow the background task to process the shutdown signal
tokio::task::yield_now().await;
}
// ── Tier 3: Full platform E2E (requires PostgreSQL + Deno) ───────────────────
//
// These tests are `#[ignore]` and require the full platform stack.
// Run with: FRAISEQL_PLATFORM_E2E=1 cargo test --test platform_e2e_test -- --include-ignored
/// Guard: returns false when the full platform E2E environment is not configured.
fn platform_e2e_available() -> bool {
std::env::var("FRAISEQL_PLATFORM_E2E").is_ok()
}
/// E2E: `before:mutation:createUser` function rejects empty name → mutation fails.
///
/// This test requires:
/// - A running FraiseQL server with the test compiled schema loaded
/// - The `validateInput` function registered as `before:mutation:createUser`
/// - The function returns `{ "abort": "name is required" }` for empty input
///
/// Run with: `FRAISEQL_PLATFORM_E2E=1 FRAISEQL_TEST_URL=http://localhost:8000 cargo test ...`
#[tokio::test]
#[ignore = "requires full platform stack (FRAISEQL_PLATFORM_E2E=1)"]
async fn test_e2e_before_mutation_validates_input() {
if !platform_e2e_available() {
eprintln!("skipped: FRAISEQL_PLATFORM_E2E not set");
return;
}
let base_url =
std::env::var("FRAISEQL_TEST_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let client = reqwest::Client::new();
// Mutation with empty name — the before:mutation hook should reject this
let mutation = serde_json::json!({
"query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } }",
"variables": { "input": { "name": "" } }
});
let response = client
.post(format!("{base_url}/graphql"))
.json(&mutation)
.send()
.await
.expect("request failed");
let body: serde_json::Value = response.json().await.expect("parse response");
// before:mutation should abort with a validation error
assert!(
body.get("errors").is_some(),
"before:mutation abort should produce a GraphQL error"
);
let errors = body["errors"].as_array().unwrap();
assert!(!errors.is_empty(), "should have at least one error");
}
/// E2E: Cron function fires and persists state to `_fraiseql_cron_state`.
///
/// Flow:
/// 1. Register a `cron:* * * * *` (every minute) function
/// 2. Wait for the scheduler to tick
/// 3. Assert `_fraiseql_cron_state` has an updated `last_fire` for the function
///
/// Run with: `FRAISEQL_PLATFORM_E2E=1 DATABASE_URL=... cargo test ...`
#[tokio::test]
#[ignore = "requires PostgreSQL and cron scheduler running"]
async fn test_e2e_cron_fires_and_persists_state() {
if !platform_e2e_available() {
eprintln!("skipped: FRAISEQL_PLATFORM_E2E not set");
return;
}
// Implementation: start scheduler with every-minute trigger, wait for tick,
// query _fraiseql_cron_state, assert last_fire was updated.
// Tracked for implementation when the migrations and full cron state persistence
// (Phase 8 Cycle 6 testcontainers pattern) are available.
todo!("requires running PostgreSQL with _fraiseql_cron_state table")
}
/// E2E: `after:mutation` function receives entity event after DB insert.
///
/// Flow:
/// 1. Register `onUserCreated` as `after:mutation:User:insert`
/// 2. Insert a User via GraphQL mutation
/// 3. Assert the `onUserCreated` function was invoked (check function log table)
///
/// Run with: `FRAISEQL_PLATFORM_E2E=1 DATABASE_URL=... cargo test ...`
#[tokio::test]
#[ignore = "requires PostgreSQL + Deno runtime (FRAISEQL_PLATFORM_E2E=1)"]
async fn test_e2e_after_mutation_function_receives_event() {
if !platform_e2e_available() {
eprintln!("skipped: FRAISEQL_PLATFORM_E2E not set");
return;
}
// Implementation: execute mutation, wait for async dispatch, check function
// invocation log in _fraiseql_function_log or similar audit table.
todo!("requires full platform stack with observer pipeline active")
}