#![allow(
clippy::expect_used,
reason = "Fixture setup and response assertions require successful fallible construction."
)]
use std::collections::HashMap;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use chrono::{Duration, Utc};
use serde_json::Value;
use better_auth_core::{AuthResponse, CreateDeviceCode, CreateUser, HttpMethod};
use crate::plugins::test_helpers;
use super::*;
type TestSchema = better_auth_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
pub(super) fn json_body(response: &AuthResponse) -> Value {
let value = if response.is_api_error().unwrap() {
better_auth_core::ResponseBody::Native(response.api_error_body().unwrap())
} else {
response.body.clone()
};
serde_json::from_slice(&value.bytes().unwrap()).unwrap()
}
pub(super) fn assert_device_error(
error: &AuthError,
status: &str,
http_status: u16,
code: &str,
description: &str,
) {
assert!(error.is_api_error_instance(better_auth_core::ApiErrorClass::BetterAuth));
assert_eq!(error.status_value().unwrap(), status.into());
assert_eq!(error.status_code().unwrap(), http_status);
let fields = error
.body_value()
.unwrap()
.as_object()
.unwrap()
.snapshot_fields()
.unwrap();
assert_eq!(
fields.keys().map(String::as_str).collect::<Vec<_>>(),
["error", "error_description"]
);
assert_eq!(
fields,
FieldMap::from([
("error".into(), code.into()),
("error_description".into(), description.into()),
])
);
}
#[tokio::test]
async fn generated_code_configuration_checks_storage_limits_at_initialization() {
let ctx = test_helpers::create_test_context().await;
for (device, user, valid) in [
(0, 8, false),
(40, 0, false),
(192, 8, false),
(40, 192, false),
(191, 191, true),
] {
let plugin = DeviceAuthorizationPlugin::new()
.device_code_length(device)
.user_code_length(user);
let mut init =
better_auth_core::AuthInitContext::new(ctx.config.clone(), ctx.database.clone())
.expect("fixture context initializes");
assert_eq!(
better_auth_core::AuthPlugin::on_init(&plugin, &mut init)
.await
.is_ok(),
valid
);
}
}
pub(super) fn device_token_request(
device_code: &str,
client_id: &str,
) -> better_auth_core::AuthRequest {
test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/token",
None,
Some(serde_json::json!({
"grant_type": DEVICE_GRANT_TYPE,
"device_code": device_code,
"client_id": client_id,
})),
)
}
fn device_verify_request(user_code: &str) -> better_auth_core::AuthRequest {
let mut query = HashMap::new();
let _ = query.insert("user_code".to_string(), user_code.to_string());
test_helpers::create_auth_request(HttpMethod::Get, "/device", None, None, query)
}
fn device_claim_request(user_code: &str, token: &str) -> better_auth_core::AuthRequest {
let mut query = HashMap::new();
let _ = query.insert("user_code".to_string(), user_code.to_string());
test_helpers::create_auth_request(HttpMethod::Get, "/device", Some(token), None, query)
}
async fn create_context_with_user(
email: &str,
) -> (
better_auth_core::AuthContext<TestSchema>,
better_auth_core::wire::UserView,
better_auth_core::wire::SessionView,
) {
test_helpers::create_test_context_with_user(
CreateUser::new()
.with_email(email.to_string())
.with_name("Device Auth User"),
Duration::hours(1),
)
.await
}
#[tokio::test]
async fn test_device_code_response_shape_and_storage() {
let plugin = DeviceAuthorizationPlugin::new()
.expires_in(Duration::minutes(5))
.interval(Duration::seconds(2))
.verification_uri("/auth/device?lang=en");
let ctx = test_helpers::create_test_context().await;
let request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({
"client_id": "test-client",
"scope": "openid profile",
})),
);
let response = plugin.handle_device_code(&request, &ctx).await.unwrap();
let body = json_body(&response);
assert_eq!(response.status, 200);
assert_eq!(
response.headers.get("Cache-Control"),
Some(&"no-store".to_string())
);
assert_eq!(body["expires_in"], 300);
assert_eq!(body["interval"], 2);
assert!(body["device_code"].as_str().unwrap().len() >= 40);
assert!(body["user_code"].as_str().unwrap().len() >= 8);
assert!(
body["user_code"]
.as_str()
.unwrap()
.chars()
.all(|char| { DEFAULT_USER_CODE_CHARSET.contains(&(char as u8)) })
);
assert!(
body["verification_uri"]
.as_str()
.unwrap()
.contains("/auth/device?lang=en")
);
assert!(
body["verification_uri_complete"]
.as_str()
.unwrap()
.contains("lang=en")
);
assert!(
body["verification_uri_complete"]
.as_str()
.unwrap()
.contains("user_code=")
);
let stored = ctx
.database
.get_device_code_by_device_code(body["device_code"].as_str().unwrap())
.await
.unwrap()
.unwrap();
assert_eq!(stored.polling_interval, Some(2000.0));
assert_eq!(
stored.client_id.typed().unwrap().as_deref(),
Some("test-client")
);
assert_eq!(
stored.scope.typed().unwrap().as_deref(),
Some("openid profile")
);
}
#[tokio::test]
async fn test_device_code_rejects_invalid_client() {
let plugin = DeviceAuthorizationPlugin::new()
.validate_client(|client_id| async move { Ok(client_id == "valid-client") });
let ctx = test_helpers::create_test_context().await;
let request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({
"client_id": "invalid-client",
})),
);
let error = plugin.handle_device_code(&request, &ctx).await.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_client",
INVALID_CLIENT_ID,
);
}
#[tokio::test]
async fn test_device_code_uses_custom_generators_and_hook() {
let hook_calls = Arc::new(AtomicUsize::new(0));
let hook_calls_clone = hook_calls.clone();
let plugin = DeviceAuthorizationPlugin::new()
.generate_device_code_with(|| async { Ok("custom-device-code".to_string()) })
.generate_user_code_with(|| async { Ok("CUSTOM12".to_string()) })
.on_device_auth_request(move |client_id, scope| {
let hook_calls = hook_calls_clone.clone();
async move {
assert_eq!(client_id, "hook-client");
assert_eq!(scope.as_deref(), Some("openid"));
hook_calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
})
.verification_uri("https://example.com/device");
let ctx = test_helpers::create_test_context().await;
let request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({
"client_id": "hook-client",
"scope": "openid",
})),
);
let response = plugin.handle_device_code(&request, &ctx).await.unwrap();
let body = json_body(&response);
assert_eq!(body["device_code"], "custom-device-code");
assert_eq!(body["user_code"], "CUSTOM12");
assert_eq!(body["verification_uri"], "https://example.com/device");
assert_eq!(
body["verification_uri_complete"],
"https://example.com/device?user_code=CUSTOM12"
);
assert_eq!(hook_calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_device_token_pending_returns_authorization_pending() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let token_request =
device_token_request(create_body["device_code"].as_str().unwrap(), "test-client");
let error = plugin
.handle_device_token(&token_request, &ctx)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"authorization_pending",
AUTHORIZATION_PENDING,
);
}
#[tokio::test]
async fn test_device_token_expired_returns_error_and_deletes_record() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
let stored = ctx
.database
.create_device_code(CreateDeviceCode {
additional_fields: Default::default(),
device_code: "expired-device-code".to_string(),
user_code: "EXPIRED12".to_string(),
user_id: None,
expires_at: (Utc::now() - Duration::seconds(1)).into(),
status: DEVICE_STATUS_PENDING.to_string(),
last_polled_at: None,
polling_interval: Some(5000.0),
client_id: Some("test-client".to_string()),
scope: Default::default(),
})
.await
.unwrap();
let error = plugin
.handle_device_token(
&device_token_request(stored.device_code.typed().unwrap(), "test-client"),
&ctx,
)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"expired_token",
EXPIRED_DEVICE_CODE,
);
assert!(
ctx.database
.get_device_code_by_device_code(stored.device_code.typed().unwrap())
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn test_device_token_invalid_device_code_returns_invalid_grant() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
let error = plugin
.handle_device_token(
&device_token_request("invalid-device-code", "test-client"),
&ctx,
)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_grant",
INVALID_DEVICE_CODE,
);
}
#[tokio::test]
async fn test_device_token_rate_limits_with_slow_down() {
let plugin = DeviceAuthorizationPlugin::new().interval(Duration::seconds(5));
let ctx = test_helpers::create_test_context().await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let device_code = create_body["device_code"].as_str().unwrap();
let first = plugin
.handle_device_token(&device_token_request(device_code, "test-client"), &ctx)
.await
.unwrap_err();
assert_device_error(
&first,
"BAD_REQUEST",
400,
"authorization_pending",
AUTHORIZATION_PENDING,
);
let second = plugin
.handle_device_token(&device_token_request(device_code, "test-client"), &ctx)
.await
.unwrap_err();
assert_device_error(
&second,
"BAD_REQUEST",
400,
"slow_down",
POLLING_TOO_FREQUENTLY,
);
}
#[tokio::test]
async fn test_device_token_zero_interval_skips_future_poll_timestamp() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
for (name, interval, expected_error, expected_description) in [
("zero", 0.0, "authorization_pending", AUTHORIZATION_PENDING),
(
"negative-zero",
-0.0,
"authorization_pending",
AUTHORIZATION_PENDING,
),
("positive", 5000.0, "slow_down", POLLING_TOO_FREQUENTLY),
] {
let started = Utc::now();
let row = ctx
.database
.create_device_code(CreateDeviceCode {
additional_fields: Default::default(),
device_code: format!("polling-{name}"),
user_code: format!("polling-user-{name}"),
user_id: None,
expires_at: (started + Duration::hours(2)).into(),
status: DEVICE_STATUS_PENDING.into(),
last_polled_at: Some((started + Duration::hours(1)).into()),
polling_interval: Some(interval),
client_id: Some("test-client".into()),
scope: Default::default(),
})
.await
.unwrap();
let error = plugin
.handle_device_token(
&device_token_request(row.device_code.typed().unwrap(), "test-client"),
&ctx,
)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
expected_error,
expected_description,
);
let remaining = ctx
.database
.get_device_code_by_device_code(row.device_code.typed().unwrap())
.await
.unwrap()
.unwrap();
if expected_error == "slow_down" {
assert_eq!(remaining, row);
} else {
let polled = remaining
.last_polled_at
.typed()
.unwrap()
.as_ref()
.unwrap()
.milliseconds();
assert!(polled >= started.timestamp_millis() as f64);
assert!(polled <= Utc::now().timestamp_millis() as f64);
let mut expected = row;
expected.last_polled_at = remaining.last_polled_at.clone();
assert_eq!(remaining, expected);
}
}
}
#[tokio::test]
async fn test_device_verify_strips_hyphens_and_preserves_input_shape() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
ctx.database
.create_device_code(CreateDeviceCode {
additional_fields: Default::default(),
device_code: "verify-device-code".to_string(),
user_code: "ABCD2345".to_string(),
user_id: None,
expires_at: (Utc::now() + Duration::minutes(5)).into(),
status: DEVICE_STATUS_PENDING.to_string(),
last_polled_at: None,
polling_interval: Some(5000.0),
client_id: Some("test-client".to_string()),
scope: Default::default(),
})
.await
.unwrap();
let response = plugin
.handle_device_verify(&device_verify_request("ABCD-2345"), &ctx)
.await
.unwrap();
let body = json_body(&response);
assert_eq!(response.status, 200);
assert_eq!(body["user_code"], "ABCD-2345");
assert_eq!(body["status"], DEVICE_STATUS_PENDING);
}
#[tokio::test]
async fn test_device_verify_invalid_user_code_returns_error() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
let error = plugin
.handle_device_verify(&device_verify_request("INVALID"), &ctx)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_request",
INVALID_USER_CODE,
);
}
#[tokio::test]
async fn test_device_approve_flow_creates_session_and_returns_oauth_token_response() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("approve@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({
"client_id": "test-client",
"scope": "read write profile",
})),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let device_code = create_body["device_code"].as_str().unwrap().to_string();
let user_code = create_body["user_code"].as_str().unwrap().to_string();
plugin
.handle_device_verify(
&device_claim_request(&user_code, session.token.typed().unwrap()),
&ctx,
)
.await
.unwrap();
let approve_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code })),
);
let approve_response = plugin
.handle_device_approve(&approve_request, &ctx)
.await
.unwrap();
assert_eq!(json_body(&approve_response)["success"], true);
let token_response = plugin
.handle_device_token(&device_token_request(&device_code, "test-client"), &ctx)
.await
.unwrap();
let token_body = json_body(&token_response);
assert_eq!(token_response.status, 200);
assert_eq!(
token_response.headers.get("Cache-Control"),
Some(&"no-store".to_string())
);
assert_eq!(
token_response.headers.get("Pragma"),
Some(&"no-cache".to_string())
);
assert_eq!(token_body["token_type"], "Bearer");
assert_eq!(token_body["scope"], "read write profile");
assert!(token_body["expires_in"].as_i64().unwrap() > 0);
let access_token = token_body["access_token"].as_str().unwrap();
assert!(
ctx.database
.get_session(access_token)
.await
.unwrap()
.is_some()
);
assert!(
ctx.database
.get_device_code_by_device_code(&device_code)
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn test_device_approve_rejects_unclaimed_code() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("unclaimed@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let device_code = create_body["device_code"].as_str().unwrap().to_string();
let user_code = create_body["user_code"].as_str().unwrap().to_string();
let approve_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code })),
);
let error = plugin
.handle_device_approve(&approve_request, &ctx)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_request",
DEVICE_CODE_NOT_CLAIMED,
);
let stored = ctx
.database
.get_device_code_by_device_code(&device_code)
.await
.unwrap()
.unwrap();
assert_eq!(stored.status, DEVICE_STATUS_PENDING);
assert_eq!(stored.user_id, None);
}
#[tokio::test]
async fn test_device_deny_flow_returns_access_denied_and_deletes_record() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("deny@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let device_code = create_body["device_code"].as_str().unwrap().to_string();
let user_code = create_body["user_code"].as_str().unwrap().to_string();
plugin
.handle_device_verify(
&device_claim_request(&user_code, session.token.typed().unwrap()),
&ctx,
)
.await
.unwrap();
let deny_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/deny",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code })),
);
let deny_response = plugin
.handle_device_deny(&deny_request, &ctx)
.await
.unwrap();
assert_eq!(json_body(&deny_response)["success"], true);
let error = plugin
.handle_device_token(&device_token_request(&device_code, "test-client"), &ctx)
.await
.unwrap_err();
assert_device_error(&error, "BAD_REQUEST", 400, "access_denied", ACCESS_DENIED);
assert!(
ctx.database
.get_device_code_by_device_code(&device_code)
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn test_device_approve_requires_authentication_and_blocks_double_processing() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("double@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let user_code = create_body["user_code"].as_str().unwrap().to_string();
plugin
.handle_device_verify(
&device_claim_request(&user_code, session.token.typed().unwrap()),
&ctx,
)
.await
.unwrap();
let unauthenticated_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
None,
Some(serde_json::json!({ "userCode": user_code.clone() })),
);
let error = plugin
.handle_device_approve(&unauthenticated_request, &ctx)
.await
.unwrap_err();
assert_device_error(
&error,
"UNAUTHORIZED",
401,
"unauthorized",
AUTHENTICATION_REQUIRED,
);
let approve_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code.clone() })),
);
let first_response = plugin
.handle_device_approve(&approve_request, &ctx)
.await
.unwrap();
assert_eq!(json_body(&first_response)["success"], true);
let error = plugin
.handle_device_approve(&approve_request, &ctx)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_request",
DEVICE_CODE_ALREADY_PROCESSED,
);
}
#[tokio::test]
async fn test_device_approve_allows_only_one_concurrent_decision() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("decision-race@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let user_code = create_body["user_code"].as_str().unwrap().to_string();
plugin
.handle_device_verify(
&device_claim_request(&user_code, session.token.typed().unwrap()),
&ctx,
)
.await
.unwrap();
let first_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code.clone() })),
);
let second_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code })),
);
let (first_response, second_response) = tokio::join!(
plugin.handle_device_approve(&first_request, &ctx),
plugin.handle_device_approve(&second_request, &ctx),
);
let mut success_count = 0;
let mut already_processed_count = 0;
for result in [first_response, second_response] {
match result {
Ok(response) => {
assert_eq!(response.status, 200);
assert_eq!(json_body(&response), serde_json::json!({"success": true}));
success_count += 1;
}
Err(error) => {
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_request",
DEVICE_CODE_ALREADY_PROCESSED,
);
already_processed_count += 1;
}
}
}
assert_eq!(success_count, 1);
assert_eq!(already_processed_count, 1);
}
#[tokio::test]
async fn test_device_token_rejects_client_id_mismatch() {
let plugin = DeviceAuthorizationPlugin::new();
let ctx = test_helpers::create_test_context().await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "client-a" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let error = plugin
.handle_device_token(
&device_token_request(create_body["device_code"].as_str().unwrap(), "client-b"),
&ctx,
)
.await
.unwrap_err();
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_grant",
CLIENT_ID_MISMATCH,
);
}
#[tokio::test]
async fn test_device_token_allows_only_one_concurrent_redemption() {
let plugin = DeviceAuthorizationPlugin::new();
let (ctx, _user, session) = create_context_with_user("concurrent@example.com").await;
let create_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/code",
None,
Some(serde_json::json!({ "client_id": "test-client" })),
);
let create_response = plugin
.handle_device_code(&create_request, &ctx)
.await
.unwrap();
let create_body = json_body(&create_response);
let device_code = create_body["device_code"].as_str().unwrap().to_string();
let user_code = create_body["user_code"].as_str().unwrap().to_string();
plugin
.handle_device_verify(
&device_claim_request(&user_code, session.token.typed().unwrap()),
&ctx,
)
.await
.unwrap();
let approve_request = test_helpers::create_auth_json_request_no_query(
HttpMethod::Post,
"/device/approve",
Some(session.token.typed().unwrap()),
Some(serde_json::json!({ "userCode": user_code })),
);
let approve_response = plugin
.handle_device_approve(&approve_request, &ctx)
.await
.unwrap();
assert_eq!(json_body(&approve_response)["success"], true);
let first_request = device_token_request(&device_code, "test-client");
let second_request = device_token_request(&device_code, "test-client");
let (first_response, second_response) = tokio::join!(
plugin.handle_device_token(&first_request, &ctx),
plugin.handle_device_token(&second_request, &ctx),
);
let mut success_count = 0;
let mut invalid_grant_count = 0;
for result in [first_response, second_response] {
match result {
Ok(response) => {
assert_eq!(response.status, 200);
success_count += 1;
}
Err(error) => {
assert_device_error(
&error,
"BAD_REQUEST",
400,
"invalid_grant",
INVALID_DEVICE_CODE,
);
invalid_grant_count += 1;
}
}
}
assert_eq!(success_count, 1);
assert_eq!(invalid_grant_count, 1);
assert!(
ctx.database
.get_device_code_by_device_code(&device_code)
.await
.unwrap()
.is_none()
);
}