196 lines
6.5 KiB
Rust
196 lines
6.5 KiB
Rust
use axum::{
|
|
Json,
|
|
extract::{Extension, Path, State},
|
|
http::StatusCode,
|
|
response::Response,
|
|
};
|
|
use serde_json::{Value, json};
|
|
use shared_contracts::runtime::{RuntimeInventorySlotResponse, RuntimeInventoryStateResponse};
|
|
use spacetime_client::SpacetimeClientError;
|
|
|
|
use crate::{
|
|
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
|
|
request_context::RequestContext, state::AppState,
|
|
};
|
|
|
|
pub async fn get_runtime_inventory_state(
|
|
State(state): State<AppState>,
|
|
Path(runtime_session_id): Path<String>,
|
|
Extension(request_context): Extension<RequestContext>,
|
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
|
) -> Result<Json<Value>, Response> {
|
|
let actor_user_id = authenticated.claims().user_id().to_string();
|
|
let record = state
|
|
.spacetime_client()
|
|
.get_runtime_inventory_state(runtime_session_id, actor_user_id)
|
|
.await
|
|
.map_err(|error| {
|
|
runtime_inventory_error_response(
|
|
&request_context,
|
|
map_runtime_inventory_client_error(error),
|
|
)
|
|
})?;
|
|
|
|
Ok(json_success_body(
|
|
Some(&request_context),
|
|
RuntimeInventoryStateResponse {
|
|
runtime_session_id: record.runtime_session_id,
|
|
actor_user_id: record.actor_user_id,
|
|
backpack_items: record
|
|
.backpack_items
|
|
.into_iter()
|
|
.map(map_runtime_inventory_slot_response)
|
|
.collect(),
|
|
equipment_items: record
|
|
.equipment_items
|
|
.into_iter()
|
|
.map(map_runtime_inventory_slot_response)
|
|
.collect(),
|
|
},
|
|
))
|
|
}
|
|
|
|
fn map_runtime_inventory_slot_response(
|
|
record: module_inventory::RuntimeInventorySlotRecord,
|
|
) -> RuntimeInventorySlotResponse {
|
|
RuntimeInventorySlotResponse {
|
|
slot_id: record.slot_id,
|
|
container_kind: record.container_kind,
|
|
slot_key: record.slot_key,
|
|
item_id: record.item_id,
|
|
category: record.category,
|
|
name: record.name,
|
|
description: record.description,
|
|
quantity: record.quantity,
|
|
rarity: record.rarity,
|
|
tags: record.tags,
|
|
stackable: record.stackable,
|
|
stack_key: record.stack_key,
|
|
equipment_slot_id: record.equipment_slot_id,
|
|
source_kind: record.source_kind,
|
|
source_reference_id: record.source_reference_id,
|
|
created_at: record.created_at,
|
|
updated_at: record.updated_at,
|
|
}
|
|
}
|
|
|
|
fn map_runtime_inventory_client_error(error: SpacetimeClientError) -> AppError {
|
|
let (status, provider) = match error {
|
|
SpacetimeClientError::Runtime(_) => (StatusCode::BAD_REQUEST, "runtime-inventory"),
|
|
_ => (StatusCode::BAD_GATEWAY, "spacetimedb"),
|
|
};
|
|
|
|
AppError::from_status(status).with_details(json!({
|
|
"provider": provider,
|
|
"message": error.to_string(),
|
|
}))
|
|
}
|
|
|
|
fn runtime_inventory_error_response(request_context: &RequestContext, error: AppError) -> Response {
|
|
error.into_response_with_context(Some(request_context))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use http_body_util::BodyExt;
|
|
use platform_auth::{
|
|
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, sign_access_token,
|
|
};
|
|
use serde_json::Value;
|
|
use time::OffsetDateTime;
|
|
use tower::ServiceExt;
|
|
|
|
use crate::{app::build_router, config::AppConfig, state::AppState};
|
|
|
|
#[tokio::test]
|
|
async fn runtime_inventory_requires_authentication() {
|
|
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
|
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/runtime/sessions/runtime_001/inventory")
|
|
.body(Body::empty())
|
|
.expect("request should build"),
|
|
)
|
|
.await
|
|
.expect("request should succeed");
|
|
|
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_inventory_returns_bad_gateway_when_spacetime_not_published() {
|
|
let state = seed_authenticated_state().await;
|
|
let token = issue_access_token(&state);
|
|
let app = build_router(state);
|
|
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/runtime/sessions/runtime_001/inventory")
|
|
.header("authorization", format!("Bearer {token}"))
|
|
.header("x-genarrative-response-envelope", "v1")
|
|
.body(Body::empty())
|
|
.expect("request should build"),
|
|
)
|
|
.await
|
|
.expect("request should succeed");
|
|
|
|
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
|
|
|
let body = response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.expect("body should collect")
|
|
.to_bytes();
|
|
let payload: Value =
|
|
serde_json::from_slice(&body).expect("response body should be valid json");
|
|
|
|
assert_eq!(payload["ok"], Value::Bool(false));
|
|
assert_eq!(
|
|
payload["error"]["details"]["provider"],
|
|
Value::String("spacetimedb".to_string())
|
|
);
|
|
}
|
|
|
|
async fn seed_authenticated_state() -> AppState {
|
|
let state = AppState::new(AppConfig::default()).expect("state should build");
|
|
state
|
|
.seed_test_phone_user_with_password("13800138103", "secret123")
|
|
.await
|
|
.id;
|
|
state
|
|
}
|
|
|
|
fn issue_access_token(state: &AppState) -> String {
|
|
let claims = AccessTokenClaims::from_input(
|
|
AccessTokenClaimsInput {
|
|
user_id: "user_00000001".to_string(),
|
|
session_id: state.seed_test_refresh_session_for_user_id(
|
|
"user_00000001",
|
|
"sess_runtime_inventory",
|
|
),
|
|
provider: AuthProvider::Password,
|
|
roles: vec!["user".to_string()],
|
|
token_version: 2,
|
|
phone_verified: true,
|
|
binding_status: BindingStatus::Active,
|
|
display_name: Some("背包查询用户".to_string()),
|
|
},
|
|
state.auth_jwt_config(),
|
|
OffsetDateTime::now_utc(),
|
|
)
|
|
.expect("claims should build");
|
|
|
|
sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign")
|
|
}
|
|
}
|