unplugged-system/packages/modules/Bluetooth/system/rust/tests/gatt_server_test.rs

616 lines
20 KiB
Rust
Raw Normal View History

use std::rc::Rc;
use bluetooth_core::{
core::uuid::Uuid,
gatt::{
self,
ffi::AttributeBackingType,
ids::{AttHandle, ConnectionId, ServerId, TransportIndex},
mocks::{
mock_datastore::{MockDatastore, MockDatastoreEvents},
mock_transport::MockAttTransport,
},
server::{
gatt_database::{
AttPermissions, GattCharacteristicWithHandle, GattDescriptorWithHandle,
GattServiceWithHandle, CHARACTERISTIC_UUID, PRIMARY_SERVICE_DECLARATION_UUID,
},
services::{
gap::DEVICE_NAME_UUID,
gatt::{
CLIENT_CHARACTERISTIC_CONFIGURATION_UUID, GATT_SERVICE_UUID,
SERVICE_CHANGE_UUID,
},
},
GattModule, IndicationError,
},
},
packets::{
AttAttributeDataChild, AttBuilder, AttChild, AttErrorCode, AttErrorResponseBuilder,
AttFindByTypeValueRequestBuilder, AttFindInformationRequestBuilder,
AttFindInformationResponseChild, AttHandleValueConfirmationBuilder,
AttHandleValueIndicationBuilder, AttOpcode, AttReadByTypeRequestBuilder,
AttReadRequestBuilder, AttReadResponseBuilder, AttWriteRequestBuilder,
AttWriteResponseBuilder, GattClientCharacteristicConfigurationBuilder,
GattServiceChangedBuilder, GattServiceDeclarationValueBuilder, Serializable,
UuidAsAttDataBuilder,
},
utils::packet::{build_att_data, build_att_view_or_crash},
};
use tokio::{
sync::mpsc::{error::TryRecvError, UnboundedReceiver},
task::spawn_local,
};
use utils::start_test;
mod utils;
const TCB_IDX: TransportIndex = TransportIndex(1);
const SERVER_ID: ServerId = ServerId(2);
const CONN_ID: ConnectionId = ConnectionId::new(TCB_IDX, SERVER_ID);
const ANOTHER_TCB_IDX: TransportIndex = TransportIndex(2);
const ANOTHER_SERVER_ID: ServerId = ServerId(3);
const ANOTHER_CONN_ID: ConnectionId = ConnectionId::new(ANOTHER_TCB_IDX, ANOTHER_SERVER_ID);
const SERVICE_HANDLE: AttHandle = AttHandle(6);
const CHARACTERISTIC_HANDLE: AttHandle = AttHandle(8);
const DESCRIPTOR_HANDLE: AttHandle = AttHandle(9);
const SERVICE_TYPE: Uuid = Uuid::new(0x0102);
const CHARACTERISTIC_TYPE: Uuid = Uuid::new(0x0103);
const DESCRIPTOR_TYPE: Uuid = Uuid::new(0x0104);
const DATA: [u8; 4] = [1, 2, 3, 4];
const ANOTHER_DATA: [u8; 4] = [5, 6, 7, 8];
fn start_gatt_module() -> (gatt::server::GattModule, UnboundedReceiver<(TransportIndex, AttBuilder)>)
{
let (transport, transport_rx) = MockAttTransport::new();
let gatt = GattModule::new(Rc::new(transport));
(gatt, transport_rx)
}
fn create_server_and_open_connection(
gatt: &mut GattModule,
) -> UnboundedReceiver<MockDatastoreEvents> {
gatt.open_gatt_server(SERVER_ID).unwrap();
let (datastore, data_rx) = MockDatastore::new();
gatt.register_gatt_service(
SERVER_ID,
GattServiceWithHandle {
handle: SERVICE_HANDLE,
type_: SERVICE_TYPE,
characteristics: vec![GattCharacteristicWithHandle {
handle: CHARACTERISTIC_HANDLE,
type_: CHARACTERISTIC_TYPE,
permissions: AttPermissions::READABLE
| AttPermissions::WRITABLE_WITH_RESPONSE
| AttPermissions::INDICATE,
descriptors: vec![GattDescriptorWithHandle {
handle: DESCRIPTOR_HANDLE,
type_: DESCRIPTOR_TYPE,
permissions: AttPermissions::READABLE | AttPermissions::WRITABLE_WITH_RESPONSE,
}],
}],
},
datastore,
)
.unwrap();
gatt.on_le_connect(CONN_ID).unwrap();
data_rx
}
#[test]
fn test_service_read() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
create_server_and_open_connection(&mut gatt);
// act
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadRequestBuilder {
attribute_handle: SERVICE_HANDLE.into(),
})
.view(),
);
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
// assert
assert_eq!(tcb_idx, TCB_IDX);
assert_eq!(
resp,
AttBuilder {
opcode: AttOpcode::READ_RESPONSE,
_child_: AttReadResponseBuilder {
value: build_att_data(GattServiceDeclarationValueBuilder {
uuid: SERVICE_TYPE.into()
})
}
.into()
}
);
})
}
#[test]
fn test_server_closed_while_connected() {
start_test(async move {
// arrange: set up a connection to a closed server
let (mut gatt, mut transport_rx) = start_gatt_module();
// open a server and connect
create_server_and_open_connection(&mut gatt);
gatt.close_gatt_server(SERVER_ID).unwrap();
// act: read from the closed server
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadRequestBuilder {
attribute_handle: SERVICE_HANDLE.into(),
})
.view(),
);
let (_, resp) = transport_rx.recv().await.unwrap();
// assert that the read failed, but that a response was provided
assert_eq!(resp.opcode, AttOpcode::ERROR_RESPONSE);
assert_eq!(
resp._child_,
AttErrorResponseBuilder {
opcode_in_error: AttOpcode::READ_REQUEST,
handle_in_error: SERVICE_HANDLE.into(),
error_code: AttErrorCode::INVALID_HANDLE
}
.into()
)
});
}
#[test]
fn test_characteristic_read() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
let data = AttAttributeDataChild::RawData(DATA.into());
let mut data_rx = create_server_and_open_connection(&mut gatt);
// act
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadRequestBuilder {
attribute_handle: CHARACTERISTIC_HANDLE.into(),
})
.view(),
);
let tx = if let MockDatastoreEvents::Read(
TCB_IDX,
CHARACTERISTIC_HANDLE,
AttributeBackingType::Characteristic,
tx,
) = data_rx.recv().await.unwrap()
{
tx
} else {
unreachable!()
};
tx.send(Ok(data.clone())).unwrap();
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
// assert
assert_eq!(tcb_idx, TCB_IDX);
assert_eq!(
resp,
AttBuilder {
opcode: AttOpcode::READ_RESPONSE,
_child_: AttReadResponseBuilder { value: build_att_data(data) }.into()
}
);
})
}
#[test]
fn test_characteristic_write() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
let data = AttAttributeDataChild::RawData(DATA.into());
let mut data_rx = create_server_and_open_connection(&mut gatt);
// act
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttWriteRequestBuilder {
handle: CHARACTERISTIC_HANDLE.into(),
value: build_att_data(data.clone()),
})
.view(),
);
let (tx, written_data) = if let MockDatastoreEvents::Write(
TCB_IDX,
CHARACTERISTIC_HANDLE,
AttributeBackingType::Characteristic,
written_data,
tx,
) = data_rx.recv().await.unwrap()
{
(tx, written_data)
} else {
unreachable!()
};
tx.send(Ok(())).unwrap();
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
// assert
assert_eq!(tcb_idx, TCB_IDX);
assert_eq!(
resp,
AttBuilder {
opcode: AttOpcode::WRITE_RESPONSE,
_child_: AttWriteResponseBuilder {}.into()
}
);
assert_eq!(
data.to_vec().unwrap(),
written_data.view().get_raw_payload().collect::<Vec<_>>()
)
})
}
#[test]
fn test_send_indication() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
let data = AttAttributeDataChild::RawData(DATA.into());
create_server_and_open_connection(&mut gatt);
// act
let pending_indication = spawn_local(
gatt.get_bearer(TCB_IDX).unwrap().send_indication(CHARACTERISTIC_HANDLE, data.clone()),
);
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
gatt.get_bearer(TCB_IDX)
.unwrap()
.handle_packet(build_att_view_or_crash(AttHandleValueConfirmationBuilder {}).view());
// assert
assert!(matches!(pending_indication.await.unwrap(), Ok(())));
assert_eq!(tcb_idx, TCB_IDX);
assert_eq!(
resp,
AttBuilder {
opcode: AttOpcode::HANDLE_VALUE_INDICATION,
_child_: AttHandleValueIndicationBuilder {
handle: CHARACTERISTIC_HANDLE.into(),
value: build_att_data(data),
}
.into()
}
);
})
}
#[test]
fn test_send_indication_and_disconnect() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
create_server_and_open_connection(&mut gatt);
// act: send an indication, then disconnect
let pending_indication = spawn_local(gatt.get_bearer(TCB_IDX).unwrap().send_indication(
CHARACTERISTIC_HANDLE,
AttAttributeDataChild::RawData([1, 2, 3, 4].into()),
));
transport_rx.recv().await.unwrap();
gatt.on_le_disconnect(TCB_IDX).unwrap();
// assert: the pending indication resolves appropriately
assert!(matches!(
pending_indication.await.unwrap(),
Err(IndicationError::ConnectionDroppedWhileWaitingForConfirmation)
));
})
}
#[test]
fn test_write_to_descriptor() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
let data = AttAttributeDataChild::RawData(DATA.into());
let mut data_rx = create_server_and_open_connection(&mut gatt);
// act
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttWriteRequestBuilder {
handle: DESCRIPTOR_HANDLE.into(),
value: build_att_data(data.clone()),
})
.view(),
);
let (tx, written_data) = if let MockDatastoreEvents::Write(
TCB_IDX,
DESCRIPTOR_HANDLE,
AttributeBackingType::Descriptor,
written_data,
tx,
) = data_rx.recv().await.unwrap()
{
(tx, written_data)
} else {
unreachable!()
};
tx.send(Ok(())).unwrap();
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
// assert
assert_eq!(tcb_idx, TCB_IDX);
assert_eq!(
resp,
AttBuilder {
opcode: AttOpcode::WRITE_RESPONSE,
_child_: AttWriteResponseBuilder {}.into()
}
);
assert_eq!(
data.to_vec().unwrap(),
written_data.view().get_raw_payload().collect::<Vec<_>>()
)
})
}
#[test]
fn test_multiple_servers() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
let data = AttAttributeDataChild::RawData(DATA.into());
let another_data = AttAttributeDataChild::RawData(ANOTHER_DATA.into());
// open the default server (SERVER_ID on CONN_ID)
let mut data_rx_1 = create_server_and_open_connection(&mut gatt);
// open a second server and connect to it (ANOTHER_SERVER_ID on ANOTHER_CONN_ID)
let (datastore, mut data_rx_2) = MockDatastore::new();
gatt.open_gatt_server(ANOTHER_SERVER_ID).unwrap();
gatt.register_gatt_service(
ANOTHER_SERVER_ID,
GattServiceWithHandle {
handle: SERVICE_HANDLE,
type_: SERVICE_TYPE,
characteristics: vec![GattCharacteristicWithHandle {
handle: CHARACTERISTIC_HANDLE,
type_: CHARACTERISTIC_TYPE,
permissions: AttPermissions::READABLE,
descriptors: vec![],
}],
},
datastore,
)
.unwrap();
gatt.on_le_connect(ANOTHER_CONN_ID).unwrap();
// act: read from both connections
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadRequestBuilder {
attribute_handle: CHARACTERISTIC_HANDLE.into(),
})
.view(),
);
gatt.get_bearer(ANOTHER_TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadRequestBuilder {
attribute_handle: CHARACTERISTIC_HANDLE.into(),
})
.view(),
);
// service the first read with `data`
let MockDatastoreEvents::Read(
TCB_IDX,
_, _,
tx,
) = data_rx_1.recv().await.unwrap() else {
unreachable!()
};
tx.send(Ok(data.clone())).unwrap();
// and then the second read with `another_data`
let MockDatastoreEvents::Read(
ANOTHER_TCB_IDX,
_, _,
tx,
) = data_rx_2.recv().await.unwrap() else {
unreachable!()
};
tx.send(Ok(another_data.clone())).unwrap();
// receive both response packets
let (tcb_idx_1, resp_1) = transport_rx.recv().await.unwrap();
let (tcb_idx_2, resp_2) = transport_rx.recv().await.unwrap();
// assert: the responses were routed to the correct connections
assert_eq!(tcb_idx_1, TCB_IDX);
assert_eq!(resp_1._child_.to_vec().unwrap(), DATA);
assert_eq!(tcb_idx_2, ANOTHER_TCB_IDX);
assert_eq!(resp_2._child_.to_vec().unwrap(), ANOTHER_DATA);
})
}
#[test]
fn test_read_device_name() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
create_server_and_open_connection(&mut gatt);
// act: try to read the device name
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadByTypeRequestBuilder {
starting_handle: AttHandle(1).into(),
ending_handle: AttHandle(0xFFFF).into(),
attribute_type: DEVICE_NAME_UUID.into(),
})
.view(),
);
let (tcb_idx, resp) = transport_rx.recv().await.unwrap();
// assert: the name should not be readable
assert_eq!(tcb_idx, TCB_IDX);
let AttChild::AttErrorResponse(resp) = resp._child_ else {
unreachable!("{resp:?}");
};
assert_eq!(resp.error_code, AttErrorCode::INSUFFICIENT_AUTHENTICATION);
});
}
#[test]
fn test_ignored_service_change_indication() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
create_server_and_open_connection(&mut gatt);
// act: add a new service
let (datastore, _) = MockDatastore::new();
gatt.register_gatt_service(
SERVER_ID,
GattServiceWithHandle {
handle: AttHandle(30),
type_: SERVICE_TYPE,
characteristics: vec![],
},
datastore,
)
.unwrap();
// assert: no packets should be sent
assert_eq!(transport_rx.try_recv().unwrap_err(), TryRecvError::Empty);
});
}
#[test]
fn test_service_change_indication() {
start_test(async move {
// arrange
let (mut gatt, mut transport_rx) = start_gatt_module();
create_server_and_open_connection(&mut gatt);
// act: discover the GATT server
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttFindByTypeValueRequestBuilder {
starting_handle: AttHandle::MIN.into(),
ending_handle: AttHandle::MAX.into(),
attribute_type: PRIMARY_SERVICE_DECLARATION_UUID.try_into().unwrap(),
attribute_value: build_att_data(UuidAsAttDataBuilder {
uuid: GATT_SERVICE_UUID.into(),
}),
})
.view(),
);
let AttChild::AttFindByTypeValueResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
unreachable!()
};
let (starting_handle, ending_handle) = (
resp.handles_info[0].clone().found_attribute_handle,
resp.handles_info[0].clone().group_end_handle,
);
// act: discover the service changed characteristic
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttReadByTypeRequestBuilder {
starting_handle,
ending_handle,
attribute_type: CHARACTERISTIC_UUID.into(),
})
.view(),
);
let AttChild::AttReadByTypeResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
unreachable!()
};
let service_change_char_handle = resp.data.into_vec().into_iter().find_map(|characteristic| {
let AttAttributeDataChild::GattCharacteristicDeclarationValue(decl) = characteristic.value._child_ else {
unreachable!();
};
if decl.uuid == SERVICE_CHANGE_UUID.into() {
Some(decl.handle)
} else {
None
}
}).unwrap();
// act: find the CCC descriptor for the service changed characteristic
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttFindInformationRequestBuilder {
starting_handle: service_change_char_handle.clone(),
ending_handle: AttHandle::MAX.into(),
})
.view(),
);
let AttChild::AttFindInformationResponse(resp) = transport_rx.recv().await.unwrap().1._child_ else {
unreachable!()
};
let AttFindInformationResponseChild::AttFindInformationShortResponse(resp) = resp._child_ else {
unreachable!()
};
let service_change_descriptor_handle = resp
.data
.into_vec()
.into_iter()
.find_map(|attr| {
if attr.uuid == CLIENT_CHARACTERISTIC_CONFIGURATION_UUID.try_into().unwrap() {
Some(attr.handle)
} else {
None
}
})
.unwrap();
// act: register for indications on this handle
gatt.get_bearer(TCB_IDX).unwrap().handle_packet(
build_att_view_or_crash(AttWriteRequestBuilder {
handle: service_change_descriptor_handle,
value: build_att_data(GattClientCharacteristicConfigurationBuilder {
notification: 0,
indication: 1,
}),
})
.view(),
);
let AttChild::AttWriteResponse(_) = transport_rx.recv().await.unwrap().1._child_ else {
unreachable!()
};
// act: add a new service
let (datastore, _) = MockDatastore::new();
gatt.register_gatt_service(
SERVER_ID,
GattServiceWithHandle {
handle: AttHandle(30),
type_: SERVICE_TYPE,
characteristics: vec![],
},
datastore,
)
.unwrap();
// assert: we got an indication
let AttChild::AttHandleValueIndication(indication) = transport_rx.recv().await.unwrap().1._child_ else {
unreachable!()
};
assert_eq!(indication.handle, service_change_char_handle);
assert_eq!(
indication.value,
build_att_data(GattServiceChangedBuilder {
start_handle: AttHandle(30).into(),
end_handle: AttHandle(30).into(),
})
);
});
}