//! Lease-bound command service over the transport-neutral configuration core. use core::num::NonZeroU32; use kaptan_config::{ BeginError, CommitError, ConfigManager, ConfigModel, ConfigState, FieldWriteError, Generation, ProfileStore, SetError, }; use super::{ import::{CAPABILITY_CBOR_IMPORT, ImportBuffer, NoSnapshotImporter, SnapshotImporter}, query::ServiceResponse, replay::{REQUEST_CACHE_CAPACITY, RememberedResult}, }; #[cfg(test)] use super::{ import::{IMPORT_CHUNK_BYTES, crc32c}, query::{ConfigInfo, DescriptorPage, ValueResponse}, }; use crate::{ command::{Authorized, ConfigCommand, Target}, protocol::{ConfigReply, Operation, ProtocolError, ResultKind}, }; #[derive(Clone, Copy)] pub(super) struct Session { pub(super) id: u32, pub(super) owner: u64, pub(super) revision: u32, } /// Owns only the one GCS transaction lease and bounded protocol state. /// /// A future signing/replay dispatcher creates [`Authorized`] commands. This /// protocol never accepts raw frames and can be expired by the composition /// owner on lease or link loss. The same composition owner retains the one /// [`ConfigManager`] and one runtime log hub. pub struct ConfigProtocol { pub(super) session: Option, pub(super) next_session_id: Option, pub(super) next_transfer_id: Option, pub(super) target: Target, pub(super) remembered: [Option; REQUEST_CACHE_CAPACITY], pub(super) next_remembered: usize, pub(super) importer: I, pub(super) import: Option, pub(super) generation: u64, pub(super) state: ConfigState, } impl ConfigProtocol { /// Creates a custom-protocol state machine without CBOR import support. pub const fn new(target: Target) -> Self { Self::with_importer(target, NoSnapshotImporter) } } impl ConfigProtocol { /// Creates a custom-protocol state machine with a vehicle-specific CBOR /// importer. Profile/store ownership stays outside this adapter. pub const fn with_importer(target: Target, importer: I) -> Self { Self { session: None, next_session_id: Some(NonZeroU32::MIN), next_transfer_id: Some(NonZeroU32::MIN), target, remembered: [None; REQUEST_CACHE_CAPACITY], next_remembered: 0, importer, import: None, generation: 0, state: ConfigState::Active, } } } impl ConfigProtocol { /// Feature bits for `KAPTAN_CONFIG_INFO` for a concrete candidate type. pub const fn capability_flags_for(&self) -> u8 where I: SnapshotImporter, { if I::IMPORT_SUPPORTED { CAPABILITY_CBOR_IMPORT } else { 0 } } /// Dispatches one authorized configuration command. The authoritative /// `KAPTAN_CONFIG_RESULT` response remains the complete configuration /// audit; generic human logging is intentionally independent. pub fn dispatch( &mut self, manager: &mut ConfigManager, authorized: Authorized, ) -> ServiceResponse where C: ConfigModel, S: ProfileStore, I: SnapshotImporter, { self.refresh(manager); let (authority, command) = authorized.into_parts(); self.dispatch_inner(manager, authority.principal(), command) } /// Expires the bounded session and drops only its RAM transaction. pub fn expire(&mut self, manager: &mut ConfigManager) -> bool where C: ConfigModel, S: ProfileStore, { let expired = self.session.take().is_some(); if expired { manager.abort(); self.import = None; self.remembered = [None; REQUEST_CACHE_CAPACITY]; self.next_remembered = 0; } expired } fn dispatch_inner( &mut self, manager: &mut ConfigManager, owner: u64, command: ConfigCommand, ) -> ServiceResponse where C: ConfigModel, S: ProfileStore, I: SnapshotImporter, { self.refresh(manager); if command.target() != self.target { return ServiceResponse::Result(self.rejected(command, ProtocolError::BadTarget)); } if let Some(cached) = self.cached(owner, command) { return ServiceResponse::Result(cached); } let response = match command { ConfigCommand::InfoRequest { request_id, .. } => { ServiceResponse::Info(self.info(manager, request_id)) } ConfigCommand::DescriptorRequest { request_id, first, count, .. } => ServiceResponse::Descriptors(self.descriptors(manager, request_id, first, count)), ConfigCommand::ValueRequest { request_id, session_id, field, source, .. } => self.value_response(manager, owner, request_id, session_id, field, source), ConfigCommand::ImportBegin { .. } | ConfigCommand::ImportChunk { .. } | ConfigCommand::ImportFinish { .. } => { ServiceResponse::Result(self.in_session(manager, owner, command)) } ConfigCommand::Begin { .. } => { ServiceResponse::Result(self.begin(manager, owner, command)) } ConfigCommand::Set { .. } | ConfigCommand::Validate { .. } | ConfigCommand::Commit { .. } | ConfigCommand::Abort { .. } => { ServiceResponse::Result(self.in_session(manager, owner, command)) } }; self.refresh(manager); match response { ServiceResponse::Result(reply) => { self.remember(RememberedResult { owner, command, reply, }); ServiceResponse::Result(reply) } response => response, } } fn begin( &mut self, manager: &mut ConfigManager, owner: u64, command: ConfigCommand, ) -> ConfigReply where C: ConfigModel, S: ProfileStore, { let ConfigCommand::Begin { request_id, expected_generation, .. } = command else { return self.rejected(command, ProtocolError::Internal); }; if self.session.is_some() { return self.rejected(command, ProtocolError::SessionBusy); } if expected_generation != self.generation() { return self.rejected(command, ProtocolError::GenerationConflict); } let Some(id) = self.allocate_session_id() else { return self.rejected(command, ProtocolError::Internal); }; match manager.begin() { Ok(()) => { self.session = Some(Session { id, owner, revision: 0, }); self.reply( request_id, id, 0, Operation::Begin, ResultKind::Accepted, None, ) } Err(BeginError::TransactionActive) => { self.rejected(command, ProtocolError::SessionBusy) } } } fn in_session( &mut self, manager: &mut ConfigManager, owner: u64, command: ConfigCommand, ) -> ConfigReply where C: ConfigModel, S: ProfileStore, I: SnapshotImporter, { let Some(session) = self.session else { return self.rejected(command, ProtocolError::InvalidSession); }; let session_id = command_session_id(command); if session.id != session_id || session.owner != owner { return self.rejected(command, ProtocolError::InvalidSession); } if let Some(expected_revision) = command_expected_revision(command) && expected_revision != session.revision { return self.rejected(command, ProtocolError::StaleRevision); } match command { ConfigCommand::ImportBegin { .. } => self.begin_import::(owner, command, session), ConfigCommand::ImportChunk { .. } => self.import_chunk(command, session), ConfigCommand::ImportFinish { request_id, transfer_id, .. } => self.finish_import(manager, command, session, request_id, transfer_id), ConfigCommand::Set { request_id, field, value, .. } => { let Some(next_revision) = session.revision.checked_add(1) else { return self.rejected(command, ProtocolError::Internal); }; match manager.set(field, value) { Ok(_) => { let Some(session) = self.session.as_mut() else { return self.rejected(command, ProtocolError::Internal); }; session.revision = next_revision; let (session_id, revision) = (session.id, session.revision); self.reply( request_id, session_id, revision, Operation::Set, ResultKind::Accepted, None, ) } Err(SetError::NoTransaction) => self.rejected(command, ProtocolError::BadState), Err(SetError::Field(error)) => self.rejected(command, field_error(error)), } } ConfigCommand::Validate { request_id, .. } => match manager.validate() { Ok(()) => self.reply( request_id, session.id, session.revision, Operation::Validate, ResultKind::Accepted, None, ), Err(CommitError::NoTransaction) => self.rejected(command, ProtocolError::BadState), Err(CommitError::InvalidCandidate(violation)) => { self.rejected(command, ProtocolError::Violation(violation)) } Err(CommitError::Store(_)) => self.rejected(command, ProtocolError::Internal), }, ConfigCommand::Commit { request_id, expected_generation, .. } => { if expected_generation != self.generation() { return self.rejected(command, ProtocolError::GenerationConflict); } match manager.commit() { Ok(_) => { self.session = None; self.import = None; self.refresh(manager); self.reply( request_id, 0, session.revision, Operation::Commit, ResultKind::CommittedRestartRequired, None, ) } Err(CommitError::NoTransaction) => { self.rejected(command, ProtocolError::BadState) } Err(CommitError::InvalidCandidate(violation)) => { self.rejected(command, ProtocolError::Violation(violation)) } Err(CommitError::Store(_)) => { self.rejected(command, ProtocolError::StorageFailure) } } } ConfigCommand::Abort { request_id, .. } => { manager.abort(); self.session = None; self.import = None; self.refresh(manager); self.reply( request_id, 0, session.revision, Operation::Abort, ResultKind::Accepted, None, ) } ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::ValueRequest { .. } | ConfigCommand::Begin { .. } => self.rejected(command, ProtocolError::Internal), } } fn allocate_session_id(&mut self) -> Option { let id = self.next_session_id?; self.next_session_id = id.get().checked_add(1).and_then(NonZeroU32::new); Some(id.get()) } pub(super) fn allocate_transfer_id(&mut self) -> Option { let id = self.next_transfer_id?; self.next_transfer_id = id.get().checked_add(1).and_then(NonZeroU32::new); Some(id.get()) } fn refresh(&mut self, manager: &ConfigManager) where C: ConfigModel, S: ProfileStore, { self.generation = manager.generation().map_or(0, Generation::get); self.state = manager.state(); } fn generation(&self) -> u64 { self.generation } pub(super) fn reply( &self, request_id: u32, session_id: u32, revision: u32, operation: Operation, result: ResultKind, error: Option, ) -> ConfigReply { ConfigReply { request_id, session_id, transfer_id: 0, revision, generation: self.generation, operation, result, error, state: self.state, } } pub(super) fn rejected(&self, command: ConfigCommand, error: ProtocolError) -> ConfigReply { self.reply( command.request_id(), command_session_id(command), self.session.map_or(0, |session| session.revision), operation(command), ResultKind::Rejected, Some(error), ) } pub(super) fn rejected_for( &self, request_id: u32, session_id: u32, error: ProtocolError, ) -> ConfigReply { self.reply( request_id, session_id, self.session.map_or(0, |session| session.revision), Operation::Query, ResultKind::Rejected, Some(error), ) } } fn command_session_id(command: ConfigCommand) -> u32 { match command { ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::Begin { .. } => 0, ConfigCommand::ValueRequest { session_id, .. } => session_id, ConfigCommand::Set { session_id, .. } | ConfigCommand::ImportBegin { session_id, .. } | ConfigCommand::ImportChunk { session_id, .. } | ConfigCommand::ImportFinish { session_id, .. } | ConfigCommand::Validate { session_id, .. } | ConfigCommand::Commit { session_id, .. } | ConfigCommand::Abort { session_id, .. } => session_id, } } fn command_expected_revision(command: ConfigCommand) -> Option { match command { ConfigCommand::Set { expected_revision, .. } | ConfigCommand::ImportBegin { expected_revision, .. } | ConfigCommand::ImportFinish { expected_revision, .. } | ConfigCommand::Validate { expected_revision, .. } | ConfigCommand::Commit { expected_revision, .. } => Some(expected_revision), ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::ValueRequest { .. } | ConfigCommand::ImportChunk { .. } | ConfigCommand::Begin { .. } | ConfigCommand::Abort { .. } => None, } } const fn operation(command: ConfigCommand) -> Operation { match command { ConfigCommand::InfoRequest { .. } | ConfigCommand::DescriptorRequest { .. } | ConfigCommand::ValueRequest { .. } => Operation::Query, ConfigCommand::ImportBegin { .. } | ConfigCommand::ImportChunk { .. } | ConfigCommand::ImportFinish { .. } => Operation::Import, ConfigCommand::Begin { .. } => Operation::Begin, ConfigCommand::Set { .. } => Operation::Set, ConfigCommand::Validate { .. } => Operation::Validate, ConfigCommand::Commit { .. } => Operation::Commit, ConfigCommand::Abort { .. } => Operation::Abort, } } fn field_error(error: FieldWriteError) -> ProtocolError { match error { FieldWriteError::UnknownField(field) => ProtocolError::UnknownField(field), FieldWriteError::WrongType { field, .. } => ProtocolError::TypeMismatch(field), FieldWriteError::Violation(violation) => ProtocolError::Violation(violation), } } #[cfg(test)] mod tests { use kaptan_config::{ ConfigManager, ConfigModel, ConfigValue, ConfigViolation, FieldDescriptor, FieldId, FieldWriteError, Generation, ProfileStore, ValidationRule, }; use super::*; use crate::{Authorized, ConfigCommand, ProtocolError, ResultKind, Target, ValueSource}; const VALUE: FieldId = FieldId::new(1); const DESCRIPTOR: FieldDescriptor = FieldDescriptor { id: VALUE, display_key: "test.value", kind: kaptan_config::ValueKind::U32, minimum: Some(ConfigValue::U32(0)), maximum: Some(ConfigValue::U32(10)), default: ConfigValue::U32(2), unit: "count", apply_policy: kaptan_config::ApplyPolicy::RestartRequired, }; #[derive(Clone)] struct Candidate(u32); impl ConfigModel for Candidate { type Active = u32; type Context = (); const SCHEMA_ID: [u8; 16] = *b"CONFIGSERVICE-01"; fn descriptor(index: u16) -> Option<&'static FieldDescriptor> { (index == 0).then_some(&DESCRIPTOR) } fn field_count() -> u16 { 1 } fn field_value(&self, id: FieldId) -> Option { (id == VALUE).then_some(ConfigValue::U32(self.0)) } fn active_field_value(active: &Self::Active, id: FieldId) -> Option { (id == VALUE).then_some(ConfigValue::U32(*active)) } fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> { if id != VALUE { return Err(FieldWriteError::UnknownField(id)); } let ConfigValue::U32(value) = value else { return Err(FieldWriteError::WrongType { field: id, expected: kaptan_config::ValueKind::U32, actual: value.kind(), }); }; if value > 10 { return Err(FieldWriteError::Violation(ConfigViolation::field( id, ValidationRule::AboveMaximum, ))); } self.0 = value; Ok(()) } fn validate(&self, _: &Self::Context) -> Result { Ok(self.0) } } struct Store; impl ProfileStore for Store { type Error = (); fn load(&mut self) -> Result, Self::Error> { Ok(None) } fn persist(&mut self, _: &Candidate) -> Result { Ok(Generation::new(1)) } } struct Importer; impl SnapshotImporter for Importer { const IMPORT_SUPPORTED: bool = true; const PROFILE_FORMAT: u16 = 1; fn decode(&self, bytes: &[u8]) -> Result { if bytes != [7] && !(bytes.len() == IMPORT_CHUNK_BYTES + 1 && bytes[0] == 7 && bytes[IMPORT_CHUNK_BYTES] == 9) { return Err(ProtocolError::CborMalformed); } Ok(Candidate(7)) } } /// Test-only composition root. Production code must retain these two /// objects separately; this wrapper keeps protocol examples compact while /// exercising the same externally-owned-manager dispatch API. struct TestService { manager: ConfigManager, protocol: ConfigProtocol, } impl TestService { fn new(manager: ConfigManager, target: Target) -> Self { Self { manager, protocol: ConfigProtocol::new(target), } } } impl TestService where I: SnapshotImporter, { fn with_importer( manager: ConfigManager, target: Target, importer: I, ) -> Self { Self { manager, protocol: ConfigProtocol::with_importer(target, importer), } } fn dispatch(&mut self, command: Authorized) -> ServiceResponse { self.protocol.dispatch(&mut self.manager, command) } fn manager(&self) -> &ConfigManager { &self.manager } fn capability_flags(&self) -> u8 { self.protocol.capability_flags_for::() } } fn result(response: ServiceResponse) -> ConfigReply { match response { ServiceResponse::Result(reply) => reply, ServiceResponse::Info(_) | ServiceResponse::Descriptors(_) | ServiceResponse::Value(_) => { panic!("test expected a result response") } } } #[test] fn retries_are_idempotent_and_wrong_owner_cannot_mutate() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }; let started = result(service.dispatch(Authorized::host(7, begin))); assert_eq!(started.result, ResultKind::Accepted); let set = ConfigCommand::Set { request_id: 2, session_id: started.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(3), target: Target::new(1, 1), }; let accepted = result(service.dispatch(Authorized::host(7, set))); assert_eq!(accepted.result, ResultKind::Accepted); assert_eq!(result(service.dispatch(Authorized::host(7, set))), accepted); let foreign = result(service.dispatch(Authorized::host( 8, ConfigCommand::Set { request_id: 3, session_id: started.session_id, expected_revision: 1, field: VALUE, value: ConfigValue::U32(3), target: Target::new(1, 1), }, ))); assert_eq!(foreign.error, Some(ProtocolError::InvalidSession)); } #[test] fn exact_validation_issue_reaches_result() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let rejected = result(service.dispatch(Authorized::host( 7, ConfigCommand::Set { request_id: 2, session_id: begin.session_id, expected_revision: 0, field: VALUE, value: ConfigValue::U32(11), target: Target::new(1, 1), }, ))); assert!( matches!(rejected.error, Some(ProtocolError::Violation(issue)) if issue.rule() == ValidationRule::AboveMaximum) ); } #[test] fn discovery_and_value_reads_are_service_responses() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let descriptors = service.dispatch(Authorized::host( 7, ConfigCommand::DescriptorRequest { request_id: 1, first: 0, count: 4, target: Target::new(1, 1), }, )); assert!(matches!( descriptors, ServiceResponse::Descriptors(DescriptorPage { len: 1, descriptors: [Some(&DESCRIPTOR), None, None, None], .. }) )); let value = service.dispatch(Authorized::host( 7, ConfigCommand::ValueRequest { request_id: 2, session_id: 0, field: VALUE, source: ValueSource::Active, target: Target::new(1, 1), }, )); assert!(matches!( value, ServiceResponse::Value(ValueResponse { value: ConfigValue::U32(2), .. }) )); let info = service.dispatch(Authorized::host( 7, ConfigCommand::InfoRequest { request_id: 3, target: Target::new(1, 1), }, )); assert!(matches!( info, ServiceResponse::Info(ConfigInfo { request_id: 3, field_count: 1, profile_format: 0, capability_flags: 0, .. }) )); } #[test] fn complete_cbor_import_replaces_only_the_open_transaction() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::with_importer(manager, Target::new(1, 1), Importer); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let bytes = [7]; let mut chunk = [0; IMPORT_CHUNK_BYTES]; chunk[0] = 7; let import_begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 2, session_id: begin.session_id, expected_revision: 0, length: 1, schema_id: Candidate::SCHEMA_ID, crc32c: crc32c(&bytes), target: Target::new(1, 1), }, ))); assert_eq!(import_begin.result, ResultKind::Accepted); assert_ne!(import_begin.transfer_id, 0); assert_eq!( result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportChunk { request_id: 3, session_id: begin.session_id, transfer_id: import_begin.transfer_id, offset: 0, length: 1, data: chunk, target: Target::new(1, 1), }, ))) .result, ResultKind::Accepted ); let complete = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportFinish { request_id: 4, session_id: begin.session_id, transfer_id: import_begin.transfer_id, expected_revision: 0, target: Target::new(1, 1), }, ))); assert_eq!(complete.result, ResultKind::Accepted); assert_eq!(complete.revision, 1); assert_eq!( service.manager().active_value(VALUE), Some(ConfigValue::U32(2)) ); assert_eq!( service.manager().transaction_value(VALUE), Some(ConfigValue::U32(7)) ); } #[test] fn stale_import_transfer_cannot_feed_a_new_import() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::with_importer(manager, Target::new(1, 1), Importer); let session = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let first = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 2, session_id: session.session_id, expected_revision: 0, length: 1, schema_id: Candidate::SCHEMA_ID, crc32c: crc32c(&[7]), target: Target::new(1, 1), }, ))); let second = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 3, session_id: session.session_id, expected_revision: 0, length: 1, schema_id: Candidate::SCHEMA_ID, crc32c: crc32c(&[7]), target: Target::new(1, 1), }, ))); assert_ne!(first.transfer_id, second.transfer_id); let mut chunk = [0; IMPORT_CHUNK_BYTES]; chunk[0] = 7; let stale = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportChunk { request_id: 4, session_id: session.session_id, transfer_id: first.transfer_id, offset: 0, length: 1, data: chunk, target: Target::new(1, 1), }, ))); assert_eq!(stale.error, Some(ProtocolError::InvalidSession)); } #[test] fn import_accepts_aligned_out_of_order_idempotent_chunks_and_rejects_conflicts() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::with_importer(manager, Target::new(1, 1), Importer); let session = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let mut bytes = [0_u8; IMPORT_CHUNK_BYTES + 1]; bytes[0] = 7; bytes[IMPORT_CHUNK_BYTES] = 9; let imported = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 2, session_id: session.session_id, expected_revision: 0, length: bytes.len() as u16, schema_id: Candidate::SCHEMA_ID, crc32c: crc32c(&bytes), target: Target::new(1, 1), }, ))); let mut tail = [0_u8; IMPORT_CHUNK_BYTES]; tail[0] = 9; let tail_command = ConfigCommand::ImportChunk { request_id: 3, session_id: session.session_id, transfer_id: imported.transfer_id, offset: IMPORT_CHUNK_BYTES as u16, length: 1, data: tail, target: Target::new(1, 1), }; assert_eq!( result(service.dispatch(Authorized::host(7, tail_command))).result, ResultKind::Accepted ); let mut head = [0_u8; IMPORT_CHUNK_BYTES]; head[0] = 7; let head_command = ConfigCommand::ImportChunk { request_id: 4, session_id: session.session_id, transfer_id: imported.transfer_id, offset: 0, length: IMPORT_CHUNK_BYTES as u8, data: head, target: Target::new(1, 1), }; assert_eq!( result(service.dispatch(Authorized::host(7, head_command))).result, ResultKind::Accepted ); let retry = ConfigCommand::ImportChunk { request_id: 5, session_id: session.session_id, transfer_id: imported.transfer_id, offset: 0, length: IMPORT_CHUNK_BYTES as u8, data: head, target: Target::new(1, 1), }; assert_eq!( result(service.dispatch(Authorized::host(7, retry))).result, ResultKind::Accepted ); let mut conflicting = head; conflicting[0] = 8; let conflict = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportChunk { request_id: 6, data: conflicting, session_id: session.session_id, transfer_id: imported.transfer_id, offset: 0, length: IMPORT_CHUNK_BYTES as u8, target: Target::new(1, 1), }, ))); assert_eq!(conflict.error, Some(ProtocolError::RequestConflict)); let complete = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportFinish { request_id: 7, session_id: session.session_id, transfer_id: imported.transfer_id, expected_revision: 0, target: Target::new(1, 1), }, ))); assert_eq!(complete.result, ResultKind::Accepted); assert_eq!( service.manager().transaction_value(VALUE), Some(ConfigValue::U32(7)) ); } #[test] fn configuration_result_remains_the_structured_failure_authority() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::new(manager, Target::new(1, 1)); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let rejected = result(service.dispatch(Authorized::host( 7, ConfigCommand::Set { request_id: 2, session_id: begin.session_id, expected_revision: 0, field: FieldId::new(2), value: ConfigValue::U32(3), target: Target::new(1, 1), }, ))); assert_eq!( rejected.error, Some(ProtocolError::UnknownField(FieldId::new(2))) ); assert_eq!(rejected.request_id, 2); assert_eq!(rejected.primary_field(), FieldId::new(2).get()); } #[test] fn import_capability_truthfully_reflects_the_installed_importer() { let default_manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut default_service = TestService::new(default_manager, Target::new(1, 1)); assert_eq!(default_service.capability_flags(), 0); let begin = result(default_service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let unavailable = result(default_service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 2, session_id: begin.session_id, expected_revision: 0, length: 1, schema_id: [0; 16], crc32c: 0, target: Target::new(1, 1), }, ))); assert_eq!(unavailable.error, Some(ProtocolError::ImportUnavailable)); let importing_manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let importing_service = TestService::with_importer(importing_manager, Target::new(1, 1), Importer); assert_eq!(importing_service.capability_flags(), CAPABILITY_CBOR_IMPORT); } #[test] fn import_requires_the_discovered_schema_id_before_receiving_bytes() { let manager = ConfigManager::new(Candidate(2), (), Store).unwrap(); let mut service = TestService::with_importer(manager, Target::new(1, 1), Importer); let begin = result(service.dispatch(Authorized::host( 7, ConfigCommand::Begin { request_id: 1, expected_generation: 0, target: Target::new(1, 1), }, ))); let rejected = result(service.dispatch(Authorized::host( 7, ConfigCommand::ImportBegin { request_id: 2, session_id: begin.session_id, expected_revision: 0, length: 1, schema_id: [0; 16], crc32c: crc32c(&[7]), target: Target::new(1, 1), }, ))); assert_eq!(rejected.error, Some(ProtocolError::SchemaMismatch)); } }