Skip to content

Commit b430414

Browse files
committed
fix: create fresh Client per test to avoid cross-runtime transport errors
Each #[tokio::test] creates its own tokio runtime. The tonic Channel created during container initialization is bound to the initializing runtime's executor. When other tests (on different runtimes) tried to use the shared Channel, they got 'transport error' / 'Service was not ready' failures. Fix: share only the testcontainer (via OnceCell), and create a new Client on the caller's runtime for each test invocation.
1 parent 484eab1 commit b430414

1 file changed

Lines changed: 38 additions & 25 deletions

File tree

tests/integration.rs

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,27 @@ impl testcontainers::Image for SpiceDbImage {
5959

6060
// ── Shared container ──────────────────────────────────────────
6161

62+
/// Holds the running container and its mapped port.
63+
/// The `Client` is NOT shared because each `#[tokio::test]` creates its
64+
/// own tokio runtime, and tonic `Channel` is tied to the runtime that
65+
/// created it. Sharing a single Channel across runtimes causes transport
66+
/// errors. Instead we share only the container and create a fresh Client
67+
/// per test invocation.
6268
struct SharedSpiceDb {
6369
_container: ContainerAsync<SpiceDbImage>,
64-
client: Client,
70+
port: u16,
71+
schema_written: bool,
6572
}
6673

6774
static SPICEDB: OnceCell<Arc<SharedSpiceDb>> = OnceCell::const_new();
6875

69-
async fn spicedb() -> Arc<SharedSpiceDb> {
70-
SPICEDB
76+
/// Returns a fresh `Client` connected to the shared SpiceDB container.
77+
/// The container is started once (lazily) and the schema is written on
78+
/// first access. Each call creates a new tonic Channel on the caller's
79+
/// runtime, avoiding cross-runtime transport errors.
80+
async fn spicedb() -> Client {
81+
// Ensure container is started and schema is written (once)
82+
let shared = SPICEDB
7183
.get_or_init(|| async {
7284
let container = SpiceDbImage
7385
.start()
@@ -123,11 +135,19 @@ async fn spicedb() -> Arc<SharedSpiceDb> {
123135

124136
Arc::new(SharedSpiceDb {
125137
_container: container,
126-
client,
138+
port,
139+
schema_written: true,
127140
})
128141
})
142+
.await;
143+
144+
assert!(shared.schema_written, "schema should have been written");
145+
146+
// Create a fresh client on the CURRENT runtime
147+
let endpoint = format!("http://localhost:{}", shared.port);
148+
Client::new(&endpoint, SPICEDB_TOKEN)
129149
.await
130-
.clone()
150+
.expect("failed to create client for test")
131151
}
132152

133153
const TEST_SCHEMA: &str = r#"
@@ -146,33 +166,32 @@ definition document {
146166

147167
#[tokio::test]
148168
async fn write_and_read_schema() {
149-
let db = spicedb().await;
169+
let c = spicedb().await;
150170

151-
let (schema_text, read_at) = db.client.read_schema().await.expect("read_schema failed");
171+
let (schema_text, read_at) = c.read_schema().await.expect("read_schema failed");
152172
assert!(schema_text.contains("definition document"));
153173
assert!(!read_at.token().is_empty());
154174
}
155175

156176
#[tokio::test]
157177
async fn write_schema_empty_rejected() {
158-
let db = spicedb().await;
159-
let err = db.client.write_schema("").await.unwrap_err();
178+
let c = spicedb().await;
179+
let err = c.write_schema("").await.unwrap_err();
160180
assert!(matches!(err, prescience::Error::InvalidArgument(_)));
161181
}
162182

163183
// ── Relationships ─────────────────────────────────────────────
164184

165185
#[tokio::test]
166186
async fn write_relationships_empty_rejected() {
167-
let db = spicedb().await;
168-
let err = db.client.write_relationships(vec![]).await.unwrap_err();
187+
let c = spicedb().await;
188+
let err = c.write_relationships(vec![]).await.unwrap_err();
169189
assert!(matches!(err, prescience::Error::InvalidArgument(_)));
170190
}
171191

172192
#[tokio::test]
173193
async fn write_and_check_permission() {
174-
let db = spicedb().await;
175-
let c = &db.client;
194+
let c = spicedb().await;
176195

177196
let token = c
178197
.write_relationships(vec![RelationshipUpdate::create(Relationship::new(
@@ -224,8 +243,7 @@ async fn write_and_check_permission() {
224243

225244
#[tokio::test]
226245
async fn read_relationships() {
227-
let db = spicedb().await;
228-
let c = &db.client;
246+
let c = spicedb().await;
229247

230248
let token = c
231249
.write_relationships(vec![
@@ -268,8 +286,7 @@ async fn read_relationships() {
268286

269287
#[tokio::test]
270288
async fn lookup_resources() {
271-
let db = spicedb().await;
272-
let c = &db.client;
289+
let c = spicedb().await;
273290

274291
let token = c
275292
.write_relationships(vec![
@@ -320,8 +337,7 @@ async fn lookup_resources() {
320337

321338
#[tokio::test]
322339
async fn lookup_subjects() {
323-
let db = spicedb().await;
324-
let c = &db.client;
340+
let c = spicedb().await;
325341

326342
let token = c
327343
.write_relationships(vec![
@@ -364,8 +380,7 @@ async fn lookup_subjects() {
364380

365381
#[tokio::test]
366382
async fn delete_relationships() {
367-
let db = spicedb().await;
368-
let c = &db.client;
383+
let c = spicedb().await;
369384

370385
let token = c
371386
.write_relationships(vec![RelationshipUpdate::create(Relationship::new(
@@ -425,8 +440,7 @@ async fn delete_relationships() {
425440
#[cfg(feature = "watch")]
426441
#[tokio::test]
427442
async fn watch_receives_updates() {
428-
let db = spicedb().await;
429-
let c = &db.client;
443+
let c = spicedb().await;
430444

431445
let mut stream = c
432446
.watch(vec!["document"])
@@ -464,8 +478,7 @@ async fn watch_receives_updates() {
464478
async fn bulk_check_permissions() {
465479
use prescience::BulkCheckItem;
466480

467-
let db = spicedb().await;
468-
let c = &db.client;
481+
let c = spicedb().await;
469482

470483
let token = c
471484
.write_relationships(vec![RelationshipUpdate::create(Relationship::new(

0 commit comments

Comments
 (0)