tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

lib.rs (19990B)


      1 #![forbid(unsafe_code)]
      2 
      3 pub mod classification;
      4 pub mod errors;
      5 pub mod event_view;
      6 pub mod ids;
      7 pub mod kinds;
      8 pub mod metadata;
      9 pub mod outbox;
     10 pub mod policy;
     11 pub mod projection;
     12 pub mod read_gate;
     13 pub mod roles;
     14 pub mod signing;
     15 pub mod tags;
     16 pub mod write_gate;
     17 
     18 use core::fmt;
     19 use serde::Deserialize;
     20 use tangle_protocol::PublicKeyHex;
     21 
     22 pub use classification::{GroupEventClass, classify_group_event};
     23 pub use errors::{GroupError, GroupErrorKind, GroupReplyPrefix};
     24 pub use event_view::{GroupEventTag, GroupEventView};
     25 pub use ids::GroupId;
     26 pub use kinds::{
     27     KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, KIND_GROUP_DELETE_EVENT,
     28     KIND_GROUP_DELETE_GROUP, KIND_GROUP_EDIT_METADATA, KIND_GROUP_JOIN_REQUEST,
     29     KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, KIND_GROUP_PUT_USER,
     30     KIND_GROUP_REMOVE_USER, KIND_GROUP_ROLES, KIND_GROUP_STATE_39004, NIP29_GROUP_KIND_VALUES,
     31     NIP29_MODERATION_KIND_VALUES, NIP29_RELAY_GENERATED_KIND_VALUES,
     32     NIP29_USER_REQUEST_KIND_VALUES,
     33 };
     34 pub use metadata::{
     35     GroupMetadata, GroupMetadataFlags, GroupMetadataText, SupportedKinds, parse_group_metadata,
     36 };
     37 pub use outbox::{
     38     GroupCrashHooks, GroupCrashPoint, GroupOutbox, GroupOutboxEffect, GroupOutboxKey,
     39     GroupOutboxPayload, GroupOutboxRecord, GroupOutboxStatus, OutboxRecoveryReadiness,
     40     OutboxReplayPlan,
     41 };
     42 pub use policy::{
     43     GroupAuthority, GroupWriteDecision, GroupWritePolicy, non_enumerating_group_error,
     44 };
     45 pub use projection::{
     46     CanonicalGroupEvent, GROUP_POLICY_VERSION, GROUP_PROJECTION_SCHEMA_VERSION, GroupEventDeletion,
     47     GroupLifecycleState, GroupProjection, GroupRecoveryReadiness, GroupSnapshotIds, GroupState,
     48     GroupTombstone, MemberState, MemberStatus, ProjectedRoleDefinition, ProjectionApplyOutcome,
     49     ProjectionCheckpoint, ProjectionOrderTuple, ProjectionRebuildReport, StoreOffset,
     50     event_deletion_key, group_current_key, member_current_key, projection_checkpoint_key,
     51     rebuild_group_projection, role_current_key, tombstone_key,
     52 };
     53 pub use read_gate::{GroupReadDecision, GroupReadGate};
     54 pub use roles::{
     55     Capability, CapabilitySet, PERMANENT_RELAY_OVERRIDE_ROLE, RoleDefinition, RoleName,
     56     resolve_capabilities,
     57 };
     58 pub use signing::GroupGeneratedEventBuilder;
     59 pub use tags::{GroupTag, GroupTagName, extract_group_tag, has_group_identity_tag};
     60 pub use write_gate::{
     61     GroupAuthContext, require_group_auth_as_author, validate_client_group_event_structure,
     62 };
     63 
     64 #[derive(Clone, PartialEq, Eq)]
     65 pub struct RelaySecret(String);
     66 
     67 impl RelaySecret {
     68     pub const HEX_LENGTH: usize = 64;
     69 
     70     pub fn from_hex(value: &str) -> Result<Self, GroupConfigError> {
     71         require_lowercase_hex("groups.relay_secret", value, Self::HEX_LENGTH)?;
     72         Ok(Self(value.to_owned()))
     73     }
     74 
     75     pub fn expose_for_signing(&self) -> &str {
     76         &self.0
     77     }
     78 
     79     pub fn redacted(&self) -> &'static str {
     80         "<redacted>"
     81     }
     82 }
     83 
     84 impl fmt::Debug for RelaySecret {
     85     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     86         formatter.write_str("RelaySecret(<redacted>)")
     87     }
     88 }
     89 
     90 #[derive(Debug, Clone, PartialEq, Eq)]
     91 pub struct CanonicalRelayUrl(String);
     92 
     93 impl CanonicalRelayUrl {
     94     pub fn new(value: &str) -> Result<Self, GroupConfigError> {
     95         if value.is_empty() {
     96             return Err(GroupConfigError::invalid(
     97                 "groups.canonical_relay_url is required",
     98             ));
     99         }
    100         if value.trim() != value {
    101             return Err(GroupConfigError::invalid(
    102                 "groups.canonical_relay_url must not contain leading or trailing whitespace",
    103             ));
    104         }
    105         if !(value.starts_with("ws://") || value.starts_with("wss://")) {
    106             return Err(GroupConfigError::invalid(
    107                 "groups.canonical_relay_url must start with ws:// or wss://",
    108             ));
    109         }
    110         Ok(Self(value.to_owned()))
    111     }
    112 
    113     pub fn as_str(&self) -> &str {
    114         &self.0
    115     }
    116 }
    117 
    118 impl fmt::Display for CanonicalRelayUrl {
    119     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    120         formatter.write_str(self.as_str())
    121     }
    122 }
    123 
    124 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
    125 #[serde(deny_unknown_fields)]
    126 pub struct GroupPolicyConfig {
    127     #[serde(default)]
    128     public_join: bool,
    129     #[serde(default)]
    130     invites_enabled: bool,
    131 }
    132 
    133 impl GroupPolicyConfig {
    134     pub fn strict() -> Self {
    135         Self {
    136             public_join: false,
    137             invites_enabled: false,
    138         }
    139     }
    140 
    141     pub fn new(public_join: bool, invites_enabled: bool) -> Result<Self, GroupConfigError> {
    142         let value = Self {
    143             public_join,
    144             invites_enabled,
    145         };
    146         value.validate()?;
    147         Ok(value)
    148     }
    149 
    150     pub fn validate(&self) -> Result<(), GroupConfigError> {
    151         if self.invites_enabled {
    152             return Err(GroupConfigError::invalid(
    153                 "groups.policy.invites_enabled is not supported until invite flow is implemented",
    154             ));
    155         }
    156         Ok(())
    157     }
    158 
    159     pub fn public_join(&self) -> bool {
    160         self.public_join
    161     }
    162 
    163     pub fn invites_enabled(&self) -> bool {
    164         self.invites_enabled
    165     }
    166 }
    167 
    168 impl Default for GroupPolicyConfig {
    169     fn default() -> Self {
    170         Self::strict()
    171     }
    172 }
    173 
    174 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    175 pub struct GroupRuntimeSettingsConfig {
    176     policy: GroupPolicyConfig,
    177     limits: GroupLimitsConfig,
    178 }
    179 
    180 impl GroupRuntimeSettingsConfig {
    181     pub fn strict() -> Self {
    182         Self {
    183             policy: GroupPolicyConfig::strict(),
    184             limits: GroupLimitsConfig::default(),
    185         }
    186     }
    187 
    188     pub fn new(
    189         policy: GroupPolicyConfig,
    190         limits: GroupLimitsConfig,
    191     ) -> Result<Self, GroupConfigError> {
    192         let value = Self { policy, limits };
    193         value.validate()?;
    194         Ok(value)
    195     }
    196 
    197     pub fn validate(&self) -> Result<(), GroupConfigError> {
    198         self.policy.validate()?;
    199         self.limits.validate()
    200     }
    201 
    202     pub fn policy(&self) -> GroupPolicyConfig {
    203         self.policy
    204     }
    205 
    206     pub fn limits(&self) -> GroupLimitsConfig {
    207         self.limits
    208     }
    209 }
    210 
    211 impl Default for GroupRuntimeSettingsConfig {
    212     fn default() -> Self {
    213         Self::strict()
    214     }
    215 }
    216 
    217 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
    218 #[serde(deny_unknown_fields)]
    219 pub struct GroupLimitsConfig {
    220     #[serde(default = "default_max_group_id_bytes")]
    221     max_group_id_bytes: u16,
    222     #[serde(default = "default_max_group_tags_per_event")]
    223     max_group_tags_per_event: u16,
    224     #[serde(default = "default_max_supported_kinds")]
    225     max_supported_kinds: u16,
    226     #[serde(default = "default_max_member_list_pubkeys")]
    227     max_member_list_pubkeys: u32,
    228     #[serde(default = "default_max_outbox_replay_batch")]
    229     max_outbox_replay_batch: u32,
    230 }
    231 
    232 impl GroupLimitsConfig {
    233     pub fn new(
    234         max_group_id_bytes: u16,
    235         max_group_tags_per_event: u16,
    236         max_supported_kinds: u16,
    237         max_member_list_pubkeys: u32,
    238         max_outbox_replay_batch: u32,
    239     ) -> Result<Self, GroupConfigError> {
    240         let value = Self {
    241             max_group_id_bytes,
    242             max_group_tags_per_event,
    243             max_supported_kinds,
    244             max_member_list_pubkeys,
    245             max_outbox_replay_batch,
    246         };
    247         value.validate()?;
    248         Ok(value)
    249     }
    250 
    251     pub fn validate(&self) -> Result<(), GroupConfigError> {
    252         require_positive("groups.limits.max_group_id_bytes", self.max_group_id_bytes)?;
    253         require_positive(
    254             "groups.limits.max_group_tags_per_event",
    255             self.max_group_tags_per_event,
    256         )?;
    257         require_positive(
    258             "groups.limits.max_supported_kinds",
    259             self.max_supported_kinds,
    260         )?;
    261         require_positive(
    262             "groups.limits.max_member_list_pubkeys",
    263             self.max_member_list_pubkeys,
    264         )?;
    265         require_positive(
    266             "groups.limits.max_outbox_replay_batch",
    267             self.max_outbox_replay_batch,
    268         )?;
    269         Ok(())
    270     }
    271 
    272     pub fn max_group_id_bytes(&self) -> u16 {
    273         self.max_group_id_bytes
    274     }
    275 
    276     pub fn max_group_tags_per_event(&self) -> u16 {
    277         self.max_group_tags_per_event
    278     }
    279 
    280     pub fn max_supported_kinds(&self) -> u16 {
    281         self.max_supported_kinds
    282     }
    283 
    284     pub fn max_member_list_pubkeys(&self) -> u32 {
    285         self.max_member_list_pubkeys
    286     }
    287 
    288     pub fn max_outbox_replay_batch(&self) -> u32 {
    289         self.max_outbox_replay_batch
    290     }
    291 }
    292 
    293 impl Default for GroupLimitsConfig {
    294     fn default() -> Self {
    295         Self {
    296             max_group_id_bytes: default_max_group_id_bytes(),
    297             max_group_tags_per_event: default_max_group_tags_per_event(),
    298             max_supported_kinds: default_max_supported_kinds(),
    299             max_member_list_pubkeys: default_max_member_list_pubkeys(),
    300             max_outbox_replay_batch: default_max_outbox_replay_batch(),
    301         }
    302     }
    303 }
    304 
    305 #[derive(Debug, Clone, PartialEq, Eq)]
    306 pub struct GroupRuntimeConfig {
    307     enabled: bool,
    308     canonical_relay_url: Option<CanonicalRelayUrl>,
    309     relay_secret: Option<RelaySecret>,
    310     owner_pubkeys: Vec<PublicKeyHex>,
    311     admin_pubkeys: Vec<PublicKeyHex>,
    312     settings: GroupRuntimeSettingsConfig,
    313 }
    314 
    315 impl GroupRuntimeConfig {
    316     pub fn disabled() -> Self {
    317         Self {
    318             enabled: false,
    319             canonical_relay_url: None,
    320             relay_secret: None,
    321             owner_pubkeys: Vec::new(),
    322             admin_pubkeys: Vec::new(),
    323             settings: GroupRuntimeSettingsConfig::default(),
    324         }
    325     }
    326 
    327     pub fn new(
    328         enabled: bool,
    329         canonical_relay_url: Option<CanonicalRelayUrl>,
    330         relay_secret: Option<RelaySecret>,
    331         owner_pubkeys: Vec<PublicKeyHex>,
    332         admin_pubkeys: Vec<PublicKeyHex>,
    333         settings: GroupRuntimeSettingsConfig,
    334     ) -> Result<Self, GroupConfigError> {
    335         settings.validate()?;
    336         if enabled && canonical_relay_url.is_none() {
    337             return Err(GroupConfigError::invalid(
    338                 "groups.canonical_relay_url is required when groups are enabled",
    339             ));
    340         }
    341         if enabled && relay_secret.is_none() {
    342             return Err(GroupConfigError::invalid(
    343                 "groups.relay_secret is required when groups are enabled",
    344             ));
    345         }
    346         Ok(Self {
    347             enabled,
    348             canonical_relay_url,
    349             relay_secret,
    350             owner_pubkeys,
    351             admin_pubkeys,
    352             settings,
    353         })
    354     }
    355 
    356     pub fn enabled(&self) -> bool {
    357         self.enabled
    358     }
    359 
    360     pub fn canonical_relay_url(&self) -> Option<&CanonicalRelayUrl> {
    361         self.canonical_relay_url.as_ref()
    362     }
    363 
    364     pub fn relay_secret(&self) -> Option<&RelaySecret> {
    365         self.relay_secret.as_ref()
    366     }
    367 
    368     pub fn owner_pubkeys(&self) -> &[PublicKeyHex] {
    369         &self.owner_pubkeys
    370     }
    371 
    372     pub fn admin_pubkeys(&self) -> &[PublicKeyHex] {
    373         &self.admin_pubkeys
    374     }
    375 
    376     pub fn policy(&self) -> GroupPolicyConfig {
    377         self.settings.policy()
    378     }
    379 
    380     pub fn limits(&self) -> GroupLimitsConfig {
    381         self.settings.limits()
    382     }
    383 }
    384 
    385 #[derive(Debug, Clone, PartialEq, Eq)]
    386 pub struct GroupConfigError {
    387     message: String,
    388 }
    389 
    390 impl GroupConfigError {
    391     pub fn invalid(message: impl Into<String>) -> Self {
    392         Self {
    393             message: message.into(),
    394         }
    395     }
    396 
    397     pub fn message(&self) -> &str {
    398         &self.message
    399     }
    400 }
    401 
    402 impl fmt::Display for GroupConfigError {
    403     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    404         formatter.write_str(&self.message)
    405     }
    406 }
    407 
    408 impl std::error::Error for GroupConfigError {}
    409 
    410 #[derive(Debug, Deserialize)]
    411 #[serde(deny_unknown_fields)]
    412 struct GroupRuntimeConfigDocument {
    413     enabled: bool,
    414     canonical_relay_url: Option<String>,
    415     relay_secret: Option<String>,
    416     #[serde(default)]
    417     owner_pubkeys: Vec<String>,
    418     #[serde(default)]
    419     admin_pubkeys: Vec<String>,
    420     #[serde(default)]
    421     policy: GroupPolicyConfig,
    422     #[serde(default)]
    423     limits: GroupLimitsConfig,
    424 }
    425 
    426 pub fn parse_group_runtime_config_json(raw: &str) -> Result<GroupRuntimeConfig, GroupConfigError> {
    427     let document = serde_json::from_str::<GroupRuntimeConfigDocument>(raw).map_err(|error| {
    428         GroupConfigError::invalid(format!("groups config JSON is invalid: {error}"))
    429     })?;
    430     let canonical_relay_url = document
    431         .canonical_relay_url
    432         .as_deref()
    433         .map(CanonicalRelayUrl::new)
    434         .transpose()?;
    435     let relay_secret = document
    436         .relay_secret
    437         .as_deref()
    438         .map(RelaySecret::from_hex)
    439         .transpose()?;
    440     GroupRuntimeConfig::new(
    441         document.enabled,
    442         canonical_relay_url,
    443         relay_secret,
    444         parse_pubkeys("groups.owner_pubkeys", document.owner_pubkeys)?,
    445         parse_pubkeys("groups.admin_pubkeys", document.admin_pubkeys)?,
    446         GroupRuntimeSettingsConfig::new(document.policy, document.limits)?,
    447     )
    448 }
    449 
    450 fn parse_pubkeys(field: &str, values: Vec<String>) -> Result<Vec<PublicKeyHex>, GroupConfigError> {
    451     values
    452         .into_iter()
    453         .map(|value| {
    454             PublicKeyHex::new(&value).map_err(|error| {
    455                 GroupConfigError::invalid(format!("{field} contains invalid pubkey: {error}"))
    456             })
    457         })
    458         .collect()
    459 }
    460 
    461 fn require_lowercase_hex(field: &str, value: &str, length: usize) -> Result<(), GroupConfigError> {
    462     if value.len() != length {
    463         return Err(GroupConfigError::invalid(format!(
    464             "{field} must be {length} lowercase hex characters"
    465         )));
    466     }
    467     if !value
    468         .as_bytes()
    469         .iter()
    470         .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
    471     {
    472         return Err(GroupConfigError::invalid(format!(
    473             "{field} must be lowercase hex"
    474         )));
    475     }
    476     Ok(())
    477 }
    478 
    479 fn require_positive<T>(field: &str, value: T) -> Result<(), GroupConfigError>
    480 where
    481     T: Copy + PartialEq + From<u8> + fmt::Display,
    482 {
    483     if value == T::from(0) {
    484         return Err(GroupConfigError::invalid(format!(
    485             "{field} must be greater than zero"
    486         )));
    487     }
    488     Ok(())
    489 }
    490 
    491 fn default_max_group_id_bytes() -> u16 {
    492     128
    493 }
    494 
    495 fn default_max_group_tags_per_event() -> u16 {
    496     8
    497 }
    498 
    499 fn default_max_supported_kinds() -> u16 {
    500     512
    501 }
    502 
    503 fn default_max_member_list_pubkeys() -> u32 {
    504     100_000
    505 }
    506 
    507 fn default_max_outbox_replay_batch() -> u32 {
    508     1_000
    509 }
    510 
    511 #[cfg(test)]
    512 mod tests {
    513     use super::{
    514         CanonicalRelayUrl, GroupLimitsConfig, GroupPolicyConfig, RelaySecret,
    515         parse_group_runtime_config_json,
    516     };
    517 
    518     #[test]
    519     fn enabled_group_config_requires_relay_identity_material() {
    520         let error = parse_group_runtime_config_json(r#"{"enabled": true}"#).expect_err("error");
    521 
    522         assert_eq!(
    523             error.message(),
    524             "groups.canonical_relay_url is required when groups are enabled"
    525         );
    526     }
    527 
    528     #[test]
    529     fn enabled_group_config_parses_relay_identity_limits_and_flags() {
    530         let owner = "1".repeat(64);
    531         let admin = "2".repeat(64);
    532         let secret = "3".repeat(64);
    533         let raw = format!(
    534             r#"{{
    535                 "enabled": true,
    536                 "canonical_relay_url": "wss://relay.radroots.test",
    537                 "relay_secret": "{secret}",
    538                 "owner_pubkeys": ["{owner}"],
    539                 "admin_pubkeys": ["{admin}"],
    540                 "policy": {{"public_join": false, "invites_enabled": false}},
    541                 "limits": {{
    542                     "max_group_id_bytes": 64,
    543                     "max_group_tags_per_event": 4,
    544                     "max_supported_kinds": 32,
    545                     "max_member_list_pubkeys": 500,
    546                     "max_outbox_replay_batch": 25
    547                 }}
    548             }}"#
    549         );
    550 
    551         let config = parse_group_runtime_config_json(&raw).expect("config");
    552 
    553         assert!(config.enabled());
    554         assert_eq!(
    555             config.canonical_relay_url().expect("url").as_str(),
    556             "wss://relay.radroots.test"
    557         );
    558         assert_eq!(config.owner_pubkeys().len(), 1);
    559         assert_eq!(config.admin_pubkeys().len(), 1);
    560         assert_eq!(config.policy(), GroupPolicyConfig::strict());
    561         assert!(!config.policy().public_join());
    562         assert!(!config.policy().invites_enabled());
    563         assert_eq!(config.limits().max_group_id_bytes(), 64);
    564         assert_eq!(config.limits().max_group_tags_per_event(), 4);
    565         assert_eq!(config.limits().max_supported_kinds(), 32);
    566         assert_eq!(config.limits().max_member_list_pubkeys(), 500);
    567         assert_eq!(config.limits().max_outbox_replay_batch(), 25);
    568     }
    569 
    570     #[test]
    571     fn disabled_group_config_does_not_require_relay_secret() {
    572         let config = parse_group_runtime_config_json(r#"{"enabled": false}"#).expect("config");
    573 
    574         assert!(!config.enabled());
    575         assert!(config.canonical_relay_url().is_none());
    576         assert!(config.relay_secret().is_none());
    577         assert_eq!(config.policy(), GroupPolicyConfig::strict());
    578     }
    579 
    580     #[test]
    581     fn group_policy_rejects_enabled_invites_until_invite_flow_exists() {
    582         let error = parse_group_runtime_config_json(
    583             r#"{"enabled": false, "policy": {"invites_enabled": true}}"#,
    584         )
    585         .expect_err("invites");
    586 
    587         assert_eq!(
    588             error.message(),
    589             "groups.policy.invites_enabled is not supported until invite flow is implemented"
    590         );
    591     }
    592 
    593     #[test]
    594     fn group_policy_rejects_compatibility_fields() {
    595         let error = parse_group_runtime_config_json(
    596             r#"{"enabled": false, "policy": {"compat_closed_means_restricted": true}}"#,
    597         )
    598         .expect_err("compat");
    599 
    600         assert!(
    601             error
    602                 .message()
    603                 .contains("unknown field `compat_closed_means_restricted`")
    604         );
    605     }
    606 
    607     #[test]
    608     fn group_config_rejects_removed_and_unknown_fields() {
    609         let removed_redaction = parse_group_runtime_config_json(
    610             r#"{"enabled": false, "redaction": {"redact_private_tags": true}}"#,
    611         )
    612         .expect_err("redaction");
    613         assert!(
    614             removed_redaction
    615                 .message()
    616                 .contains("unknown field `redaction`")
    617         );
    618 
    619         let unknown_limit = parse_group_runtime_config_json(
    620             r#"{"enabled": false, "limits": {"max_unimplemented_outbox_batch": 10}}"#,
    621         )
    622         .expect_err("unknown limit");
    623         assert!(
    624             unknown_limit
    625                 .message()
    626                 .contains("unknown field `max_unimplemented_outbox_batch`")
    627         );
    628     }
    629 
    630     #[test]
    631     fn relay_secret_debug_output_is_redacted() {
    632         let secret = RelaySecret::from_hex(&"a".repeat(64)).expect("secret");
    633 
    634         assert_eq!(format!("{secret:?}"), "RelaySecret(<redacted>)");
    635         assert_eq!(secret.redacted(), "<redacted>");
    636         assert_eq!(secret.expose_for_signing(), "a".repeat(64));
    637     }
    638 
    639     #[test]
    640     fn relay_identity_validation_is_strict() {
    641         assert_eq!(
    642             RelaySecret::from_hex(&"A".repeat(64))
    643                 .expect_err("error")
    644                 .message(),
    645             "groups.relay_secret must be lowercase hex"
    646         );
    647         assert_eq!(
    648             CanonicalRelayUrl::new(" wss://relay.radroots.test")
    649                 .expect_err("error")
    650                 .message(),
    651             "groups.canonical_relay_url must not contain leading or trailing whitespace"
    652         );
    653         assert_eq!(
    654             CanonicalRelayUrl::new("https://relay.radroots.test")
    655                 .expect_err("error")
    656                 .message(),
    657             "groups.canonical_relay_url must start with ws:// or wss://"
    658         );
    659     }
    660 
    661     #[test]
    662     fn limits_reject_zero_values() {
    663         let error = GroupLimitsConfig::new(0, 1, 1, 1, 1).expect_err("error");
    664 
    665         assert_eq!(
    666             error.message(),
    667             "groups.limits.max_group_id_bytes must be greater than zero"
    668         );
    669     }
    670 }