rust_supabase_sdk 0.3.0

An SDK kit for SupaBase so that Rust lovers can use SupaBase with the low level abstracted away. If you want new features tell me and I'll add them.
Documentation
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
//! Integration tests for the Auth namespace against a live Supabase project.
//!
//! Strategy: every test creates its own user (unique UUID-tagged email) via
//! the service-role admin API with `email_confirm: true`, runs whatever auth
//! flow it exercises with the anon-key client, then deletes the user in the
//! admin client to clean up. No project-wide email-confirmation flag changes
//! are required.
//!
//! Required env vars (from `.env`):
//!   SUPABASE_URL
//!   SUPABASE_API_KEY            anon key
//!   SUPABASE_SERVICE_WORKER     service_role key (for admin cleanup)

#![allow(clippy::unwrap_used)]

use std::env;

use dotenv::dotenv;
use rust_supabase_sdk::auth::{
    AdminUserAttributes, SignOutScope, UpdateUserAttributes,
};
use rust_supabase_sdk::{SupabaseClient, SupabaseError};
use uuid::Uuid;

const PASSWORD: &str = "Test12345!secret";

fn anon_client() -> SupabaseClient {
    SupabaseClient::new(
        env::var("SUPABASE_URL").expect("SUPABASE_URL not set"),
        env::var("SUPABASE_API_KEY").expect("SUPABASE_API_KEY not set"),
        None,
    )
}

fn admin_client() -> SupabaseClient {
    SupabaseClient::new(
        env::var("SUPABASE_URL").expect("SUPABASE_URL not set"),
        env::var("SUPABASE_SERVICE_WORKER").expect("SUPABASE_SERVICE_WORKER not set"),
        None,
    )
}

fn unique_email() -> String {
    format!("test-{}@example.test", Uuid::new_v4())
}

/// Create a confirmed user via admin and return (user_id, email).
async fn create_confirmed_user(admin: &SupabaseClient) -> (String, String) {
    let email = unique_email();
    let user = admin
        .auth()
        .admin()
        .create_user(AdminUserAttributes {
            email: Some(email.clone()),
            password: Some(PASSWORD.to_string()),
            email_confirm: Some(true),
            ..Default::default()
        })
        .await
        .expect("admin.create_user should succeed");
    (user.id, email)
}

async fn delete_user(admin: &SupabaseClient, user_id: &str) {
    let _ = admin.auth().admin().delete_user(user_id, false).await;
}

// ===========================================================================
// sign_in_with_password
// ===========================================================================

#[tokio::test]
async fn sign_in_with_password_returns_session() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    let session = client
        .auth()
        .sign_in_with_password(&email, PASSWORD)
        .await
        .expect("sign-in should succeed");

    assert_eq!(session.token_type, "bearer");
    assert!(!session.access_token.is_empty());
    assert!(!session.refresh_token.is_empty());
    assert_eq!(session.user.email.as_deref(), Some(email.as_str()));

    // Session is automatically persisted to the store.
    let stored = client.auth().get_session();
    assert!(stored.is_some(), "session should be persisted in store");
    assert_eq!(stored.unwrap().access_token, session.access_token);

    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn sign_in_with_wrong_password_returns_auth_error() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    let err = client
        .auth()
        .sign_in_with_password(&email, "wrong-password")
        .await
        .expect_err("wrong password should fail");
    match err {
        SupabaseError::Auth(_) => {}
        other => panic!("expected Auth error, got {other:?}"),
    }
    assert!(
        client.auth().get_session().is_none(),
        "no session should be stored after failed sign-in"
    );

    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn sign_in_unknown_email_returns_auth_error() {
    dotenv().ok();
    let client = anon_client();
    let err = client
        .auth()
        .sign_in_with_password(&unique_email(), PASSWORD)
        .await
        .expect_err("unknown email should fail");
    match err {
        SupabaseError::Auth(_) => {}
        other => panic!("expected Auth error, got {other:?}"),
    }
}

// ===========================================================================
// get_user
// ===========================================================================

#[tokio::test]
async fn get_user_returns_authenticated_user() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();

    let user = client.auth().get_user().await.expect("get_user should succeed");
    assert_eq!(user.id, user_id);
    assert_eq!(user.email.as_deref(), Some(email.as_str()));

    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn get_user_without_session_yields_auth_error() {
    dotenv().ok();
    let client = anon_client();
    // No sign-in, no session.
    let err = client.auth().get_user().await.expect_err("should fail without auth");
    match err {
        SupabaseError::Auth(_) => {}
        other => panic!("expected Auth error, got {other:?}"),
    }
}

// ===========================================================================
// update_user
// ===========================================================================

#[tokio::test]
async fn update_user_changes_metadata() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();

    let updated = client
        .auth()
        .update_user(UpdateUserAttributes {
            user_metadata: Some(serde_json::json!({ "nickname": "tester" })),
            ..Default::default()
        })
        .await
        .expect("update_user should succeed");
    assert_eq!(updated.user_metadata["nickname"], "tester");

    // Re-fetch to confirm persistence.
    let refetched = client.auth().get_user().await.unwrap();
    assert_eq!(refetched.user_metadata["nickname"], "tester");

    delete_user(&admin, &user_id).await;
}

// ===========================================================================
// refresh_session
// ===========================================================================

#[tokio::test]
async fn refresh_session_returns_new_access_token() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    let initial = client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();

    // Sleep briefly so the new token has a different iat — Supabase's refresh
    // sometimes issues identical access_tokens within the same second.
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    let refreshed = client
        .auth()
        .refresh_session(None)
        .await
        .expect("refresh_session should succeed");

    assert_eq!(refreshed.user.id, initial.user.id);
    assert!(!refreshed.access_token.is_empty());
    assert!(!refreshed.refresh_token.is_empty());
    // Session in the store reflects the refreshed token.
    assert_eq!(client.auth().get_session().unwrap().access_token, refreshed.access_token);

    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn refresh_with_invalid_token_errors() {
    dotenv().ok();
    let client = anon_client();
    let err = client
        .auth()
        .refresh_session(Some("totally-bogus-refresh-token"))
        .await
        .expect_err("invalid refresh token should error");
    match err {
        SupabaseError::Auth(_) => {}
        other => panic!("expected Auth error, got {other:?}"),
    }
}

// ===========================================================================
// sign_out
// ===========================================================================

#[tokio::test]
async fn sign_out_clears_local_session() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();
    assert!(client.auth().get_session().is_some());

    client
        .auth()
        .sign_out(SignOutScope::Local)
        .await
        .expect("sign_out should succeed");

    assert!(client.auth().get_session().is_none(), "session should be cleared");
    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn sign_out_global_invalidates_token_server_side() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    let session = client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();
    client.auth().sign_out(SignOutScope::Global).await.unwrap();

    // After global sign-out, the previous access_token should no longer work
    // for /user. Use a fresh client with the now-stale access_token.
    let stale = SupabaseClient::new(
        env::var("SUPABASE_URL").unwrap(),
        env::var("SUPABASE_API_KEY").unwrap(),
        Some(session.access_token.clone()),
    );
    let err = stale.auth().get_user().await.expect_err("stale token should be rejected");
    match err {
        SupabaseError::Auth(_) => {}
        other => panic!("expected Auth error, got {other:?}"),
    }
    delete_user(&admin, &user_id).await;
}

// ===========================================================================
// set_session / clear_session
// ===========================================================================

#[tokio::test]
async fn set_session_then_get_user_uses_provided_token() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    // First client signs in to grab a session.
    let c1 = anon_client();
    let session = c1.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();

    // Second client manually installs the session, then queries.
    let c2 = anon_client();
    c2.auth().set_session(session.clone());
    assert!(c2.auth().get_session().is_some());

    let user = c2.auth().get_user().await.unwrap();
    assert_eq!(user.id, user_id);

    delete_user(&admin, &user_id).await;
}

#[tokio::test]
async fn clear_session_drops_persisted_state() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    let client = anon_client();
    client.auth().sign_in_with_password(&email, PASSWORD).await.unwrap();
    assert!(client.auth().get_session().is_some());

    client.auth().clear_session();
    assert!(client.auth().get_session().is_none());

    delete_user(&admin, &user_id).await;
}

// ===========================================================================
// Anonymous sign-in
// ===========================================================================

#[tokio::test]
async fn sign_in_anonymously_returns_anon_session() {
    dotenv().ok();
    let client = anon_client();
    let session = match client.auth().sign_in_anonymously(None).await {
        Ok(s) => s,
        Err(SupabaseError::Auth(e))
            if e.message.contains("disabled") || e.message.to_lowercase().contains("anonymous") =>
        {
            // Project doesn't have anonymous sign-in enabled — that's fine.
            eprintln!("Anonymous sign-in not enabled on project; skipping.");
            return;
        }
        Err(other) => panic!("anonymous sign-in failed: {other:?}"),
    };
    assert!(session.user.is_anonymous);
    // Best-effort cleanup.
    let admin = admin_client();
    let _ = admin.auth().admin().delete_user(&session.user.id, false).await;
}

// ===========================================================================
// Admin API surface
// ===========================================================================

#[tokio::test]
async fn admin_create_get_update_delete_user_roundtrip() {
    dotenv().ok();
    let admin = admin_client();
    let email = unique_email();

    // CREATE
    let user = admin
        .auth()
        .admin()
        .create_user(AdminUserAttributes {
            email: Some(email.clone()),
            password: Some(PASSWORD.to_string()),
            email_confirm: Some(true),
            user_metadata: Some(serde_json::json!({"role": "tester"})),
            ..Default::default()
        })
        .await
        .unwrap();
    assert_eq!(user.email.as_deref(), Some(email.as_str()));
    assert_eq!(user.user_metadata["role"], "tester");

    // GET BY ID
    let fetched = admin.auth().admin().get_user_by_id(&user.id).await.unwrap();
    assert_eq!(fetched.id, user.id);

    // UPDATE
    let updated = admin
        .auth()
        .admin()
        .update_user_by_id(
            &user.id,
            AdminUserAttributes {
                user_metadata: Some(serde_json::json!({"role": "admin"})),
                ..Default::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(updated.user_metadata["role"], "admin");

    // DELETE
    admin.auth().admin().delete_user(&user.id, false).await.unwrap();

    // GET BY ID after delete → error (user not found)
    let err = admin.auth().admin().get_user_by_id(&user.id).await;
    assert!(err.is_err(), "deleted user should not be fetchable");
}

#[tokio::test]
async fn admin_list_users_pagination() {
    dotenv().ok();
    let admin = admin_client();
    // Create a few users we know exist for this run.
    let mut created_ids: Vec<String> = Vec::new();
    for _ in 0..3 {
        let (id, _email) = create_confirmed_user(&admin).await;
        created_ids.push(id);
    }

    let page = admin.auth().admin().list_users(1, 100).await.unwrap();
    // We can't assert exact counts (other users exist), but we can assert
    // we got users back and we can find at least one of ours.
    assert!(!page.users.is_empty());
    let found = page
        .users
        .iter()
        .any(|u| created_ids.contains(&u.id));
    assert!(found, "expected to find at least one created user in page");

    for id in created_ids {
        delete_user(&admin, &id).await;
    }
}

// ===========================================================================
// Auth API surface — duplicate sign-up via admin
// ===========================================================================

#[tokio::test]
async fn admin_create_user_duplicate_email_errors() {
    dotenv().ok();
    let admin = admin_client();
    let (user_id, email) = create_confirmed_user(&admin).await;

    // Second create with same email should error.
    let err = admin
        .auth()
        .admin()
        .create_user(AdminUserAttributes {
            email: Some(email.clone()),
            password: Some(PASSWORD.to_string()),
            email_confirm: Some(true),
            ..Default::default()
        })
        .await
        .expect_err("duplicate email should fail");
    match err {
        SupabaseError::Auth(_) => {}
        SupabaseError::Postgrest(_) => {} // some servers wrap it differently
        other => panic!("expected Auth or Postgrest error, got {other:?}"),
    }

    delete_user(&admin, &user_id).await;
}