radicle: Add Version to User Agent
The main motivation behind this change is to get just a little telemetry information from nodes on the network, namely the version of Radicle they are running. This is achieved by rewriting `impl Default for UserAgent` which now uses the version information provided by the build script also used in other crates. Also, a new configuration option `node.userAgent` is added, which allows users to override the user agent if they so please, or set the value `null`, which will in turn send the user agent `/radicle/`, which is not really helpful, and the default prior to this commit. Creations of `UserAgent` in the whole workspace is cleaned up. In order to do that `UserAgent::test` is introduced.
This commit is contained in:
parent
48551cde93
commit
22b2871f64
|
|
@ -15,6 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
using `--verbose`, and the full OID when using `--verbose`.
|
using `--verbose`, and the full OID when using `--verbose`.
|
||||||
These ranges make using `git range-diff` a lot easier, since you can copy
|
These ranges make using `git range-diff` a lot easier, since you can copy
|
||||||
the range from each revision you want to compare.
|
the range from each revision you want to compare.
|
||||||
|
- Nodes will now advertise the version of Radicle they are running in the node
|
||||||
|
announcement as part of the "user agent", which is shared among the network.
|
||||||
|
For example, the value that will be shared by Radicle 1.9.0 will be
|
||||||
|
`/radicle:1.9.0/`. This value is also customizable by node operators via the
|
||||||
|
configuration value `node.userAgent`. Refer to `rad config schema` for more
|
||||||
|
information on the possible values. Operators that choose to set a custom
|
||||||
|
value are asked to keep the substring `/radicle:{YOUR_VERSION}/` which allows
|
||||||
|
for better telemetry regarding version distribution on the network.
|
||||||
|
To opt-out of sending any meaningful user agent, set `node.userAgent = null`.
|
||||||
|
|
||||||
## 1.8.0
|
## 1.8.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,17 @@ $ rad config schema
|
||||||
"description": "Node alias.",
|
"description": "Node alias.",
|
||||||
"$ref": "#/$defs/Alias"
|
"$ref": "#/$defs/Alias"
|
||||||
},
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"description": "User agent string to advertise in the node announcement, which is sent out to other nodes.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/UserAgent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"listen": {
|
"listen": {
|
||||||
"description": "Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.",
|
"description": "Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -329,6 +340,17 @@ $ rad config schema
|
||||||
"description": "Node alias, i.e. a short and memorable name for it.",
|
"description": "Node alias, i.e. a short and memorable name for it.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"UserAgent": {
|
||||||
|
"description": "A user agent string that starts and ends with the symbol '/', and contains segments of the form 'client:version' separated by '/'. The client and version parts must be non-empty, and must consist of printable ASCII characters excluding '/' and ':'. The entire string must be at most 64 characters long.",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 64,
|
||||||
|
"examples": [
|
||||||
|
"/radicle:1.9.0/",
|
||||||
|
"/example:42.0.0/other-client:2.3.4/"
|
||||||
|
],
|
||||||
|
"pattern": "^/([^:///s]+((:[^:///s]+))?/)+$"
|
||||||
|
},
|
||||||
"PeerConfig": {
|
"PeerConfig": {
|
||||||
"description": "Peer configuration.",
|
"description": "Peer configuration.",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ use radicle::cob::cache::COBS_DB_FILE;
|
||||||
use radicle::crypto::ssh::{Keystore, keystore::MemorySigner};
|
use radicle::crypto::ssh::{Keystore, keystore::MemorySigner};
|
||||||
use radicle::crypto::{KeyPair, Seed};
|
use radicle::crypto::{KeyPair, Seed};
|
||||||
use radicle::git;
|
use radicle::git;
|
||||||
|
use radicle::node;
|
||||||
use radicle::node::policy::store as policy;
|
use radicle::node::policy::store as policy;
|
||||||
use radicle::node::{self, UserAgent};
|
|
||||||
use radicle::node::{Alias, Config, POLICIES_DB_FILE};
|
use radicle::node::{Alias, Config, POLICIES_DB_FILE};
|
||||||
|
use radicle::profile;
|
||||||
use radicle::profile::Home;
|
use radicle::profile::Home;
|
||||||
use radicle::profile::{self};
|
|
||||||
use radicle::storage::git::transport;
|
use radicle::storage::git::transport;
|
||||||
use radicle::{Profile, Storage};
|
use radicle::{Profile, Storage};
|
||||||
use radicle_localtime::LocalTime;
|
use radicle_localtime::LocalTime;
|
||||||
|
|
@ -161,7 +161,7 @@ impl Environment {
|
||||||
&keypair.pk.into(),
|
&keypair.pk.into(),
|
||||||
config.node.features(),
|
config.node.features(),
|
||||||
&alias,
|
&alias,
|
||||||
&UserAgent::default(),
|
&config.node.user_agent(),
|
||||||
LocalTime::now().into(),
|
LocalTime::now().into(),
|
||||||
config.node.external_addresses.iter(),
|
config.node.external_addresses.iter(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,11 @@ pub mod tests;
|
||||||
|
|
||||||
extern crate radicle_localtime as localtime;
|
extern crate radicle_localtime as localtime;
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use radicle::version::Version;
|
use radicle::version::Version;
|
||||||
|
|
||||||
pub use localtime::{LocalDuration, LocalTime};
|
pub use localtime::{LocalDuration, LocalTime};
|
||||||
pub use radicle::node::Link;
|
pub use radicle::node::Link;
|
||||||
pub use radicle::node::PROTOCOL_VERSION;
|
pub use radicle::node::PROTOCOL_VERSION;
|
||||||
pub use radicle::node::UserAgent;
|
|
||||||
pub use radicle::prelude::Timestamp;
|
pub use radicle::prelude::Timestamp;
|
||||||
pub use radicle::{collections, crypto, git, identity, node, profile, rad, storage};
|
pub use radicle::{collections, crypto, git, identity, node, profile, rad, storage};
|
||||||
pub use runtime::Runtime;
|
pub use runtime::Runtime;
|
||||||
|
|
@ -40,12 +36,6 @@ pub const VERSION: Version = Version {
|
||||||
timestamp: env!("SOURCE_DATE_EPOCH"),
|
timestamp: env!("SOURCE_DATE_EPOCH"),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This node's user agent string.
|
|
||||||
pub static USER_AGENT: LazyLock<UserAgent> = LazyLock::new(|| {
|
|
||||||
FromStr::from_str(format!("/radicle:{}/", VERSION.version).as_str())
|
|
||||||
.expect("user agent is valid")
|
|
||||||
});
|
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::crypto::{PublicKey, Signature};
|
pub use crate::crypto::{PublicKey, Signature};
|
||||||
pub use crate::identity::{Did, RepoId};
|
pub use crate::identity::{Did, RepoId};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod thread;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr as _;
|
||||||
use std::{fs, io, net};
|
use std::{fs, io, net};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
@ -197,7 +198,8 @@ impl Runtime {
|
||||||
radicle::node::Features::SEED,
|
radicle::node::Features::SEED,
|
||||||
&alias,
|
&alias,
|
||||||
0,
|
0,
|
||||||
&UserAgent::default(),
|
&UserAgent::from_str("/radicle/runtime/bootstrap/")
|
||||||
|
.expect("valid user agent"),
|
||||||
clock.into(),
|
clock.into(),
|
||||||
[node::KnownAddress::new(addr, address::Source::Bootstrap)],
|
[node::KnownAddress::new(addr, address::Source::Bootstrap)],
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::UserAgent;
|
use radicle::node::UserAgent;
|
||||||
use radicle::node::device::Device;
|
use radicle::node::device::Device;
|
||||||
|
|
@ -34,7 +32,7 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
|
||||||
alias: node::Alias::new(r#gen::string(5)),
|
alias: node::Alias::new(r#gen::string(5)),
|
||||||
addresses: None.into(),
|
addresses: None.into(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
agent: UserAgent::test(),
|
||||||
}
|
}
|
||||||
.solve(0)
|
.solve(0)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ where
|
||||||
alias: Alias::from_str(self.name).unwrap(),
|
alias: Alias::from_str(self.name).unwrap(),
|
||||||
addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
|
addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
agent: UserAgent::from_str("/radicle:test/").unwrap(),
|
agent: UserAgent::test(),
|
||||||
}
|
}
|
||||||
.solve(0)
|
.solve(0)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::bounded::BoundedVec;
|
use crate::bounded::BoundedVec;
|
||||||
use radicle::node::PROTOCOL_VERSION;
|
use radicle::node::PROTOCOL_VERSION;
|
||||||
use radicle::node::UserAgent;
|
|
||||||
|
|
||||||
pub use store::{AnnouncementId, Error, RelayStatus, Store};
|
pub use store::{AnnouncementId, Error, RelayStatus, Store};
|
||||||
|
|
||||||
/// This node's user agent string.
|
|
||||||
pub static PROTOCOL_VERSION_STRING: LazyLock<UserAgent> = LazyLock::new(|| {
|
|
||||||
FromStr::from_str(format!("/radicle:{PROTOCOL_VERSION}/").as_str())
|
|
||||||
.expect("user agent is valid")
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
||||||
let features = config.features();
|
let features = config.features();
|
||||||
let alias = config.alias.clone();
|
let alias = config.alias.clone();
|
||||||
|
|
@ -24,7 +14,9 @@ pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
|
||||||
.clone()
|
.clone()
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("external addresses are within the limit");
|
.expect("external addresses are within the limit");
|
||||||
let agent = PROTOCOL_VERSION_STRING.clone();
|
|
||||||
|
let agent = config.user_agent();
|
||||||
|
|
||||||
let version = PROTOCOL_VERSION;
|
let version = PROTOCOL_VERSION;
|
||||||
|
|
||||||
NodeAnnouncement {
|
NodeAnnouncement {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::str::FromStr;
|
||||||
use std::{fmt, mem};
|
use std::{fmt, mem};
|
||||||
|
|
||||||
use bytes::{Buf, BufMut};
|
use bytes::{Buf, BufMut};
|
||||||
|
|
@ -142,7 +143,9 @@ impl wire::Decode for NodeAnnouncement {
|
||||||
let nonce = u64::decode(buf)?;
|
let nonce = u64::decode(buf)?;
|
||||||
let agent = match UserAgent::decode(buf) {
|
let agent = match UserAgent::decode(buf) {
|
||||||
Ok(ua) => ua,
|
Ok(ua) => ua,
|
||||||
Err(wire::Error::UnexpectedEnd { .. }) => UserAgent::default(),
|
Err(wire::Error::UnexpectedEnd { .. }) => {
|
||||||
|
UserAgent::from_str("/radicle/message/truncated/").expect("valid user agent")
|
||||||
|
}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -683,7 +686,6 @@ impl qcheck::Arbitrary for ZeroBytes {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use fastrand;
|
use fastrand;
|
||||||
use localtime::LocalTime;
|
use localtime::LocalTime;
|
||||||
|
|
@ -784,12 +786,12 @@ mod tests {
|
||||||
alias: Alias::new("alice"),
|
alias: Alias::new("alice"),
|
||||||
addresses: BoundedVec::new(),
|
addresses: BoundedVec::new(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
agent: UserAgent::from_str("/heartwood:1.0.0/").unwrap(),
|
agent: UserAgent::test(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(ann.work(), 1);
|
assert_eq!(ann.work(), 2);
|
||||||
assert_eq!(ann.clone().solve(1).unwrap().work(), 1);
|
assert_eq!(ann.clone().solve(1).unwrap().work(), 1);
|
||||||
assert_eq!(ann.clone().solve(8).unwrap().work(), 10);
|
assert_eq!(ann.clone().solve(8).unwrap().work(), 8);
|
||||||
assert_eq!(ann.solve(14).unwrap().work(), 14);
|
assert_eq!(ann.solve(14).unwrap().work(), 14);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../../build.rs
|
||||||
|
|
@ -220,18 +220,56 @@ impl PartialOrd for SyncStatus {
|
||||||
|
|
||||||
/// Node user agent.
|
/// Node user agent.
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserAgent(String);
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "schemars",
|
||||||
|
schemars(description = "\
|
||||||
|
A user agent string that starts and ends with the symbol '/', and contains segments of the form 'client:version' separated by '/'. \
|
||||||
|
The client and version parts must be non-empty, and must consist of printable ASCII characters excluding '/' and ':'. \
|
||||||
|
The entire string must be at most 64 characters long.",
|
||||||
|
extend(
|
||||||
|
"examples" = [
|
||||||
|
"/radicle:1.9.0/",
|
||||||
|
"/example:42.0.0/other-client:2.3.4/",
|
||||||
|
],
|
||||||
|
"pattern" = r"^/([^:/\s]+((:[^:/\s]+))?/)+$",
|
||||||
|
),
|
||||||
|
))]
|
||||||
|
pub struct UserAgent(
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(
|
||||||
|
length(min = 3, max = UserAgent::LEN_MAX),
|
||||||
|
))]
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
impl UserAgent {
|
impl UserAgent {
|
||||||
|
const LEN_MAX: usize = 64;
|
||||||
|
|
||||||
/// Return a reference to the user agent string.
|
/// Return a reference to the user agent string.
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
self.0.as_str()
|
self.0.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a user agent that can be used for testing purposes.
|
||||||
|
#[cfg(any(test, feature = "test"))]
|
||||||
|
pub fn test() -> Self {
|
||||||
|
UserAgent("/radicle:test/".to_owned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserAgent {
|
impl Default for UserAgent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
UserAgent(String::from("/radicle/"))
|
const NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
|
const VERSION: &str = env!("RADICLE_VERSION");
|
||||||
|
|
||||||
|
// The length check can be performed at compile time.
|
||||||
|
#[allow(clippy::int_plus_one)]
|
||||||
|
const _: () = assert!(1 + NAME.len() + 1 + VERSION.len() + 1 <= UserAgent::LEN_MAX);
|
||||||
|
|
||||||
|
// All other checks are performed by the `FromStr` implementation
|
||||||
|
// at run time.
|
||||||
|
UserAgent::from_str(&format!("/{NAME}:{VERSION}/"))
|
||||||
|
.expect("default user agent should be valid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,7 +285,7 @@ impl FromStr for UserAgent {
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let reserved = ['/', ':'];
|
let reserved = ['/', ':'];
|
||||||
|
|
||||||
if input.len() > 64 {
|
if input.len() > UserAgent::LEN_MAX {
|
||||||
return Err(input.to_owned());
|
return Err(input.to_owned());
|
||||||
}
|
}
|
||||||
let Some(s) = input.strip_prefix('/') else {
|
let Some(s) = input.strip_prefix('/') else {
|
||||||
|
|
@ -1502,6 +1540,7 @@ mod test {
|
||||||
assert!(UserAgent::from_str("/radicle/").is_ok());
|
assert!(UserAgent::from_str("/radicle/").is_ok());
|
||||||
assert!(UserAgent::from_str("/rad/icle/").is_ok());
|
assert!(UserAgent::from_str("/rad/icle/").is_ok());
|
||||||
assert!(UserAgent::from_str("/rad:ic/le/").is_ok());
|
assert!(UserAgent::from_str("/rad:ic/le/").is_ok());
|
||||||
|
assert!(UserAgent::from_str("/heartwood:1.8.0-6-gf223afd9d-dirty/").is_ok());
|
||||||
|
|
||||||
assert!(UserAgent::from_str("/:/").is_err());
|
assert!(UserAgent::from_str("/:/").is_err());
|
||||||
assert!(UserAgent::from_str("//").is_err());
|
assert!(UserAgent::from_str("//").is_err());
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ use localtime::LocalDuration;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
|
|
||||||
use crate::node;
|
|
||||||
use crate::node::policy::SeedingPolicy;
|
use crate::node::policy::SeedingPolicy;
|
||||||
|
use crate::node::{self, UserAgent};
|
||||||
use crate::node::{Address, Alias, NodeId};
|
use crate::node::{Address, Alias, NodeId};
|
||||||
use crate::storage::refs::FeatureLevel;
|
use crate::storage::refs::FeatureLevel;
|
||||||
|
|
||||||
|
|
@ -530,6 +530,12 @@ impl Fetch {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Node alias.
|
/// Node alias.
|
||||||
pub alias: Alias,
|
pub alias: Alias,
|
||||||
|
/// User agent string to advertise in the node announcement, which is sent out to other nodes.
|
||||||
|
#[serde(
|
||||||
|
default = "crate::serde_ext::some_default::<UserAgent>",
|
||||||
|
skip_serializing_if = "crate::serde_ext::is_some_default"
|
||||||
|
)]
|
||||||
|
pub user_agent: Option<UserAgent>,
|
||||||
/// Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.
|
/// Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
|
#[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
|
||||||
|
|
@ -603,6 +609,7 @@ impl Config {
|
||||||
pub fn new(alias: Alias) -> Self {
|
pub fn new(alias: Alias) -> Self {
|
||||||
Self {
|
Self {
|
||||||
alias,
|
alias,
|
||||||
|
user_agent: Some(UserAgent::default()),
|
||||||
peers: PeerConfig::default(),
|
peers: PeerConfig::default(),
|
||||||
listen: vec![],
|
listen: vec![],
|
||||||
connect: HashSet::default(),
|
connect: HashSet::default(),
|
||||||
|
|
@ -652,6 +659,15 @@ impl Config {
|
||||||
pub fn features(&self) -> node::Features {
|
pub fn features(&self) -> node::Features {
|
||||||
node::Features::SEED
|
node::Features::SEED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the configured user agent, if set. Otherwise fall back to the
|
||||||
|
/// unintetesting value `"/radicle/"`.
|
||||||
|
pub fn user_agent(&self) -> UserAgent {
|
||||||
|
match self.user_agent.as_ref() {
|
||||||
|
Some(agent) => agent.clone(),
|
||||||
|
None => UserAgent::from_str("/radicle/").expect("valid user agent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, From)]
|
#[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, From)]
|
||||||
|
|
@ -781,7 +797,7 @@ wrapper!(
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{DefaultSeedingPolicy, Scope};
|
use super::{DefaultSeedingPolicy, Scope};
|
||||||
use crate::node::{Alias, policy};
|
use crate::node::{Alias, UserAgent, policy};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -974,4 +990,56 @@ mod test {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(super::AddressConfig::Drop, actual.onion);
|
assert_eq!(super::AddressConfig::Drop, actual.onion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_agent_opt_out() {
|
||||||
|
let actual: super::Config = serde_json::from_value(json!({
|
||||||
|
"alias": "radicle",
|
||||||
|
"userAgent": null,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(None, actual.user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_agent_default() {
|
||||||
|
let actual: super::Config = serde_json::from_value(json!({
|
||||||
|
"alias": "radicle",
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(Some(UserAgent::default()), actual.user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_agent_custom() {
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
|
let actual: super::Config = serde_json::from_value(json!({
|
||||||
|
"alias": "radicle",
|
||||||
|
"userAgent": "/example:0.1.0/",
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Some(UserAgent::from_str("/example:0.1.0/").unwrap()),
|
||||||
|
actual.user_agent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_agent_default_explicit() {
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
|
let default_as_string = UserAgent::default().to_string();
|
||||||
|
assert!(default_as_string.contains(":"));
|
||||||
|
|
||||||
|
let actual: super::Config = serde_json::from_value(json!({
|
||||||
|
"alias": "radicle",
|
||||||
|
"userAgent": default_as_string,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Some(UserAgent::from_str(&default_as_string).unwrap()),
|
||||||
|
actual.user_agent
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ use crate::crypto::ssh::agent::Agent;
|
||||||
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
|
use crate::crypto::ssh::{Keystore, Passphrase, keystore};
|
||||||
use crate::node::device::{BoxedDevice, Device};
|
use crate::node::device::{BoxedDevice, Device};
|
||||||
use crate::node::policy::config::store::Read;
|
use crate::node::policy::config::store::Read;
|
||||||
use crate::node::{
|
use crate::node::{Alias, AliasStore, Handle as _, Node, notifications, policy, policy::Scope};
|
||||||
Alias, AliasStore, Handle as _, Node, UserAgent, notifications, policy, policy::Scope,
|
|
||||||
};
|
|
||||||
use crate::prelude::{Did, NodeId, RepoId};
|
use crate::prelude::{Did, NodeId, RepoId};
|
||||||
use crate::storage::ReadRepository;
|
use crate::storage::ReadRepository;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
|
|
@ -257,7 +255,7 @@ impl Profile {
|
||||||
&public_key,
|
&public_key,
|
||||||
config.node.features(),
|
config.node.features(),
|
||||||
&config.node.alias,
|
&config.node.alias,
|
||||||
&UserAgent::default(),
|
&config.node.user_agent(),
|
||||||
LocalTime::now().into(),
|
LocalTime::now().into(),
|
||||||
config.node.external_addresses.iter(),
|
config.node.external_addresses.iter(),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,15 @@ where
|
||||||
use serde::Deserialize as _;
|
use serde::Deserialize as _;
|
||||||
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
|
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper that makes it easy to use `Option<T>` with the `serde(default)`
|
||||||
|
/// attribute, in case a default of `Some(T::default())` is desired instead
|
||||||
|
/// of `None`.
|
||||||
|
pub(crate) fn some_default<T: Default>() -> Option<T> {
|
||||||
|
Some(T::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`is_default`], but for use in combination with [`some_default`].
|
||||||
|
pub(crate) fn is_some_default<T: Default + PartialEq>(t: &Option<T>) -> bool {
|
||||||
|
t.as_ref() == Some(&T::default())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,12 @@ impl Arbitrary for Timestamp {
|
||||||
impl Arbitrary for UserAgent {
|
impl Arbitrary for UserAgent {
|
||||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
UserAgent::from_str(
|
UserAgent::from_str(
|
||||||
format!("/radicle:1.{}.{}/", u8::arbitrary(g), u8::arbitrary(g)).as_str(),
|
format!(
|
||||||
|
"/radicle:1.{}.{}/fake/arbitrary/",
|
||||||
|
u8::arbitrary(g),
|
||||||
|
u8::arbitrary(g)
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue