fix: type runtime config and test passkey lifecycle

This commit is contained in:
2026-09-23 20:15:23 +09:00
parent 237ab0e592
commit ed99be3e24
12 changed files with 1210 additions and 25 deletions
+260 -4
View File
@@ -3157,10 +3157,171 @@ pub struct WorkspaceRuntimeConfigQuery {
pub profile: String,
}
/// Transparent JSON body for the Runtime-owned config bundle wire format.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(transparent)]
pub struct WorkspaceRuntimeConfigResponse(pub serde_json::Value);
/// Canonical Runtime config-bundle response projected by Workspace authority.
///
/// This is intentionally a concrete transport DTO rather than an opaque JSON value so generated
/// clients and OpenAPI consumers receive the same complete wire contract as the Runtime.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigResponse {
pub metadata: WorkspaceRuntimeConfigMetadata,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub profiles: Vec<WorkspaceRuntimeConfigProfile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub declarations: Vec<WorkspaceRuntimeConfigDeclaration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_catalog: Option<WorkspaceRuntimePromptCatalog>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive: Option<WorkspaceRuntimeProfileSourceArchive>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_archive_handle: Option<WorkspaceRuntimeResourceHandle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigMetadata {
pub id: String,
pub digest: String,
pub revision: String,
pub workspace_id: String,
pub created_at: String,
pub provenance: WorkspaceRuntimeConfigProvenance,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigProvenance {
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigProfile {
pub selector: WorkspaceRuntimeProfileSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum WorkspaceRuntimeProfileSelector {
Builtin(String),
Named(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeConfigDeclaration {
pub kind: WorkspaceRuntimeConfigDeclarationKind,
pub name: String,
pub reference: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeConfigDeclarationKind {
SecretRef,
MountGrant,
NetworkPolicy,
ShellPolicy,
GitPolicy,
CapabilityGrant,
Unsupported,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimePromptCatalog {
pub templates: BTreeMap<String, String>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub config_revision: u64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_digest: String,
pub schema_fingerprint: String,
pub toolchain_fingerprint: String,
pub catalog_digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceArchive {
pub reference: WorkspaceRuntimeProfileSourceArchiveRef,
pub content: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceArchiveRef {
pub id: String,
pub digest: String,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub size_bytes: u64,
pub source_graph: WorkspaceRuntimeProfileSourceGraph,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeProfileSourceGraph {
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub source_count: usize,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub total_source_bytes: u64,
pub entrypoints: BTreeMap<String, String>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_usize))]
pub import_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeResourceHandle {
pub kind: WorkspaceRuntimeResourceKind,
pub workspace_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
pub resource_id: String,
pub digest: String,
pub operation: WorkspaceRuntimeResourceOperation,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_i64))]
pub expires_at_unix_seconds: i64,
pub nonce: String,
pub revision: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub generation: Option<u64>,
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub max_bytes: u64,
pub content_type: String,
pub redaction: WorkspaceRuntimeResourceRedactionPolicy,
pub audit_correlation_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_source_graph: Option<WorkspaceRuntimeProfileSourceGraph>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceKind {
ProfileSourceArchive,
RepositorySshAccess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceOperation {
FetchArchive,
FetchOnce,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRuntimeResourceRedactionPolicy {
RuntimeInternalOnly,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
@@ -8962,6 +9123,89 @@ mod skill_typescript_tests {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_config_response_round_trips_the_complete_config_bundle_shape() {
let fixture = serde_json::json!({
"metadata": {
"id": "bundle-a",
"digest": "0123456789abcdef",
"revision": "revision-a",
"workspace_id": "workspace-a",
"created_at": "2026-01-01T00:00:00Z",
"provenance": {
"source": "workspace_config",
"detail": "projected"
}
},
"profiles": [{
"selector": { "kind": "builtin", "value": "builtin:companion" },
"label": "Companion"
}],
"declarations": [{
"kind": "secret_ref",
"name": "github-token",
"reference": "secret:github-token"
}],
"prompt_catalog": {
"templates": { "default": "Prompt" },
"config_revision": 7,
"source_digest": "source-digest",
"schema_fingerprint": "schema-a",
"toolchain_fingerprint": "toolchain-a",
"catalog_digest": "catalog-a"
},
"profile_source_archive": {
"reference": {
"id": "archive-a",
"digest": "archive-digest",
"size_bytes": 3,
"source_graph": {
"source_count": 1,
"total_source_bytes": 3,
"entrypoints": { "builtin:companion": "companion.dcdl" },
"import_count": 0
}
},
"content": [1, 2, 3]
},
"profile_source_archive_handle": {
"kind": "profile_source_archive",
"workspace_id": "workspace-a",
"scope_id": "scope-a",
"runtime_id": "runtime-a",
"worker_id": "worker-a",
"resource_id": "resource-a",
"digest": "resource-digest",
"operation": "fetch_archive",
"expires_at_unix_seconds": 1_800_000_000,
"nonce": "nonce-a",
"revision": "revision-a",
"generation": 2,
"max_bytes": 1_048_576,
"content_type": "application/vnd.yoi.profile-source-archive+tar",
"redaction": "runtime_internal_only",
"audit_correlation_id": "audit-a",
"profile_source_graph": {
"source_count": 1,
"total_source_bytes": 3,
"entrypoints": { "builtin:companion": "companion.dcdl" },
"import_count": 0
}
}
});
let response: WorkspaceRuntimeConfigResponse =
serde_json::from_value(fixture.clone()).unwrap();
assert_eq!(serde_json::to_value(response).unwrap(), fixture);
let mut unknown_nested_field = fixture;
unknown_nested_field["metadata"]["runtime_local_path"] =
serde_json::Value::String("/private/path".to_string());
assert!(
serde_json::from_value::<WorkspaceRuntimeConfigResponse>(unknown_nested_field).is_err()
);
}
#[test]
fn worker_session_contract_and_flattened_availability_are_stable() {
let operations = ServerApiMetadata::OPERATIONS;
@@ -10896,6 +11140,18 @@ mod openapi_artifact_tests {
runtime_config["responses"]["200"]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/WorkspaceRuntimeConfigResponse"
);
let runtime_config_schema =
&value["components"]["schemas"]["WorkspaceRuntimeConfigResponse"];
assert_eq!(runtime_config_schema["type"], "object");
assert_eq!(runtime_config_schema["additionalProperties"], false);
assert!(runtime_config_schema["properties"]["metadata"].is_object());
assert!(runtime_config_schema["properties"]["profiles"].is_object());
assert!(runtime_config_schema["properties"]["declarations"].is_object());
assert!(
value["components"]["schemas"]["WorkspaceRuntimeConfigMetadata"]["properties"]
["digest"]
.is_object()
);
assert!(runtime_config["responses"]["304"].get("content").is_none());
for status in ["200", "304"] {
assert!(runtime_config["responses"][status]["headers"]["etag"].is_object());
+180 -2
View File
@@ -1,10 +1,13 @@
use crate::catalog::{ConfigBundleRef, ProfileSelector};
use crate::error::RuntimeError;
use crate::profile_archive::{
ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef,
ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef, ProfileSourceGraphSummary,
VerifiedProfileSourceArchive,
};
use crate::resource::{BackendResourceHandle, validate_resource_handle_text};
use crate::resource::{
BackendResourceHandle, BackendResourceKind, BackendResourceOperation, ResourceRedactionPolicy,
validate_resource_handle_text,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -162,6 +165,181 @@ impl ConfigBundle {
}
}
impl From<ConfigBundle> for server_api::WorkspaceRuntimeConfigResponse {
fn from(bundle: ConfigBundle) -> Self {
let ConfigBundle {
metadata,
profiles,
declarations,
prompt_catalog,
profile_source_archive,
profile_source_archive_handle,
} = bundle;
Self {
metadata: metadata.into(),
profiles: profiles.into_iter().map(Into::into).collect(),
declarations: declarations.into_iter().map(Into::into).collect(),
prompt_catalog: prompt_catalog.map(|catalog| {
server_api::WorkspaceRuntimePromptCatalog {
templates: catalog.templates,
config_revision: catalog.config_revision,
source_digest: catalog.source_digest,
schema_fingerprint: catalog.schema_fingerprint,
toolchain_fingerprint: catalog.toolchain_fingerprint,
catalog_digest: catalog.catalog_digest,
}
}),
profile_source_archive: profile_source_archive.map(Into::into),
profile_source_archive_handle: profile_source_archive_handle.map(Into::into),
}
}
}
impl From<ConfigBundleMetadata> for server_api::WorkspaceRuntimeConfigMetadata {
fn from(metadata: ConfigBundleMetadata) -> Self {
Self {
id: metadata.id,
digest: metadata.digest,
revision: metadata.revision,
workspace_id: metadata.workspace_id,
created_at: metadata.created_at,
provenance: metadata.provenance.into(),
}
}
}
impl From<ConfigBundleProvenance> for server_api::WorkspaceRuntimeConfigProvenance {
fn from(provenance: ConfigBundleProvenance) -> Self {
Self {
source: provenance.source,
detail: provenance.detail,
}
}
}
impl From<ConfigProfileDescriptor> for server_api::WorkspaceRuntimeConfigProfile {
fn from(profile: ConfigProfileDescriptor) -> Self {
Self {
selector: profile.selector.into(),
label: profile.label,
}
}
}
impl From<ProfileSelector> for server_api::WorkspaceRuntimeProfileSelector {
fn from(selector: ProfileSelector) -> Self {
match selector {
ProfileSelector::Builtin(value) => Self::Builtin(value),
ProfileSelector::Named(value) => Self::Named(value),
}
}
}
impl From<ConfigDeclaration> for server_api::WorkspaceRuntimeConfigDeclaration {
fn from(declaration: ConfigDeclaration) -> Self {
Self {
kind: declaration.kind.into(),
name: declaration.name,
reference: declaration.reference,
}
}
}
impl From<ConfigDeclarationKind> for server_api::WorkspaceRuntimeConfigDeclarationKind {
fn from(kind: ConfigDeclarationKind) -> Self {
match kind {
ConfigDeclarationKind::SecretRef => Self::SecretRef,
ConfigDeclarationKind::MountGrant => Self::MountGrant,
ConfigDeclarationKind::NetworkPolicy => Self::NetworkPolicy,
ConfigDeclarationKind::ShellPolicy => Self::ShellPolicy,
ConfigDeclarationKind::GitPolicy => Self::GitPolicy,
ConfigDeclarationKind::CapabilityGrant => Self::CapabilityGrant,
ConfigDeclarationKind::Unsupported => Self::Unsupported,
}
}
}
impl From<ProfileSourceArchive> for server_api::WorkspaceRuntimeProfileSourceArchive {
fn from(archive: ProfileSourceArchive) -> Self {
Self {
reference: archive.reference.into(),
content: archive.content,
}
}
}
impl From<ProfileSourceArchiveRef> for server_api::WorkspaceRuntimeProfileSourceArchiveRef {
fn from(reference: ProfileSourceArchiveRef) -> Self {
Self {
id: reference.id,
digest: reference.digest,
size_bytes: reference.size_bytes,
source_graph: reference.source_graph.into(),
}
}
}
impl From<ProfileSourceGraphSummary> for server_api::WorkspaceRuntimeProfileSourceGraph {
fn from(summary: ProfileSourceGraphSummary) -> Self {
Self {
source_count: summary.source_count,
total_source_bytes: summary.total_source_bytes,
entrypoints: summary.entrypoints,
import_count: summary.import_count,
}
}
}
impl From<BackendResourceHandle> for server_api::WorkspaceRuntimeResourceHandle {
fn from(handle: BackendResourceHandle) -> Self {
Self {
kind: handle.kind.into(),
workspace_id: handle.workspace_id,
scope_id: handle.scope_id,
runtime_id: handle.runtime_id,
worker_id: handle.worker_id,
resource_id: handle.resource_id,
digest: handle.digest,
operation: handle.operation.into(),
expires_at_unix_seconds: handle.expires_at_unix_seconds,
nonce: handle.nonce,
revision: handle.revision,
generation: handle.generation,
max_bytes: handle.max_bytes,
content_type: handle.content_type,
redaction: handle.redaction.into(),
audit_correlation_id: handle.audit_correlation_id,
profile_source_graph: handle.profile_source_graph.map(Into::into),
}
}
}
impl From<BackendResourceKind> for server_api::WorkspaceRuntimeResourceKind {
fn from(kind: BackendResourceKind) -> Self {
match kind {
BackendResourceKind::ProfileSourceArchive => Self::ProfileSourceArchive,
BackendResourceKind::RepositorySshAccess => Self::RepositorySshAccess,
}
}
}
impl From<BackendResourceOperation> for server_api::WorkspaceRuntimeResourceOperation {
fn from(operation: BackendResourceOperation) -> Self {
match operation {
BackendResourceOperation::FetchArchive => Self::FetchArchive,
BackendResourceOperation::FetchOnce => Self::FetchOnce,
}
}
}
impl From<ResourceRedactionPolicy> for server_api::WorkspaceRuntimeResourceRedactionPolicy {
fn from(policy: ResourceRedactionPolicy) -> Self {
match policy {
ResourceRedactionPolicy::RuntimeInternalOnly => Self::RuntimeInternalOnly,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigBundleMetadata {
pub id: String,
+1
View File
@@ -53,3 +53,4 @@ webauthn-rs = { workspace = true }
tempfile.workspace = true
tower = { workspace = true, features = ["util"] }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
webauthn-authenticator-rs = { version = "0.5.5", default-features = false, features = ["softpasskey"] }
+254 -11
View File
@@ -14855,15 +14855,7 @@ fn workspace_runtime_config_result(
},
);
}
let body = serde_json::to_value(bundle)
.map(server_api::WorkspaceRuntimeConfigResponse)
.map_err(|error| {
ApiError::from(invalid_contract_value(
"Workspace Runtime Config response",
error,
))
.into_repository_api_error()
})?;
let body = server_api::WorkspaceRuntimeConfigResponse::from(bundle);
Ok(
server_api::server_api_responses::WorkspaceRuntimeConfig::Status200 {
body,
@@ -31612,6 +31604,255 @@ mod tests {
assert_ne!(same_origin.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn generated_passkey_routes_complete_registration_login_and_logout_cookie_lifecycle() {
use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey};
let workspace = tempfile::tempdir().unwrap();
let mut api = test_api(workspace.path()).await;
let AuthConfig::Passkey {
rp_id,
origin,
public_base_url,
cookie_name,
} = &mut api.config.auth;
*rp_id = "workspace.test".to_string();
*origin = "https://workspace.test".to_string();
*public_base_url = origin.clone();
let origin = origin.clone();
let cookie_name = cookie_name.clone();
let app = generated_auth_contract_router(ServerApiContractService::Workspace(api));
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let authenticator_origin = url::Url::parse(&origin).unwrap();
let registration_options_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/registration/options")
.header(ORIGIN, &origin)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyRegistrationOptionsRequest {
handle: "generated-passkey-user".to_string(),
display_name: Some("Generated Passkey User".to_string()),
browser_origin: None,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(registration_options_response.status(), StatusCode::OK);
let registration_options: PasskeyRegistrationOptionsResponse = serde_json::from_slice(
&to_bytes(registration_options_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
let registration_credential = authenticator
.do_registration(
authenticator_origin.clone(),
registration_options.public_key,
)
.unwrap();
let registration_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/registration/complete")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyRegistrationCompleteRequest {
challenge_id: registration_options.challenge_id,
credential: registration_credential,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(registration_response.status(), StatusCode::OK);
let registration_cookie = registration_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap()
.to_string();
assert!(registration_cookie.starts_with(&format!("{cookie_name}=")));
assert!(registration_cookie.contains("; Secure"));
assert!(registration_cookie.contains("; HttpOnly; SameSite=Lax"));
let registration_body: AuthUserResponse = serde_json::from_slice(
&to_bytes(registration_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(registration_body.user.handle, "generated-passkey-user");
let registration_cookie_pair = registration_cookie.split(';').next().unwrap().to_string();
let registered_whoami = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, &registration_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(registered_whoami.status(), StatusCode::OK);
let registered_whoami: Value = serde_json::from_slice(
&to_bytes(registered_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(
registered_whoami["actor"]["handle"],
"generated-passkey-user"
);
let login_options_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/login/options")
.header(ORIGIN, &origin)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyLoginOptionsRequest {
handle: Some("generated-passkey-user".to_string()),
browser_origin: None,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(login_options_response.status(), StatusCode::OK);
let login_options: PasskeyLoginOptionsResponse = serde_json::from_slice(
&to_bytes(login_options_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
let login_credential = authenticator
.do_authentication(authenticator_origin, login_options.public_key)
.unwrap();
let login_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/passkeys/login/complete")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&PasskeyLoginCompleteRequest {
challenge_id: login_options.challenge_id,
credential: login_credential,
})
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(login_response.status(), StatusCode::OK);
let login_cookie = login_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap()
.to_string();
assert!(login_cookie.starts_with(&format!("{cookie_name}=")));
assert!(login_cookie.contains("; Secure"));
assert!(login_cookie.contains("; HttpOnly; SameSite=Lax"));
let login_body: AuthUserResponse = serde_json::from_slice(
&to_bytes(login_response.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(login_body.user, registration_body.user);
let login_cookie_pair = login_cookie.split(';').next().unwrap().to_string();
let logged_in_whoami = app
.clone()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, &login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logged_in_whoami.status(), StatusCode::OK);
let logged_in_whoami: Value = serde_json::from_slice(
&to_bytes(logged_in_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert_eq!(
logged_in_whoami["actor"]["handle"],
"generated-passkey-user"
);
let logout_response = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/api/auth/logout")
.header(axum::http::header::COOKIE, &login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logout_response.status(), StatusCode::OK);
let logout_cookie = logout_response
.headers()
.get(axum::http::header::SET_COOKIE)
.unwrap()
.to_str()
.unwrap();
assert!(logout_cookie.starts_with(&format!("{cookie_name}=")));
assert!(logout_cookie.contains("Max-Age=0"));
let logged_out_whoami = app
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header(axum::http::header::COOKIE, login_cookie_pair)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(logged_out_whoami.status(), StatusCode::OK);
let logged_out_whoami: Value = serde_json::from_slice(
&to_bytes(logged_out_whoami.into_body(), usize::MAX)
.await
.unwrap(),
)
.unwrap();
assert!(logged_out_whoami["actor"].is_null());
}
#[tokio::test]
async fn browser_session_set_and_clear_cookies_follow_public_https_scheme() {
for (scheme, secure) in [("http", false), ("https", true)] {
@@ -37723,9 +37964,11 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
let etag = response.headers().get(ETAG).unwrap().clone();
assert_eq!(response.headers().get(CACHE_CONTROL).unwrap(), "no-cache");
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let contract_bundle: server_api::WorkspaceRuntimeConfigResponse =
serde_json::from_slice(&body).unwrap();
let bundle: worker_runtime::config_bundle::ConfigBundle =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
serde_json::from_value(serde_json::to_value(contract_bundle).unwrap()).unwrap();
assert!(bundle.profile_source_archive.is_some());
assert_eq!(
etag.to_str().unwrap(),