mod nrs_map;
pub use crate::app::multimap::Multimap;
pub use crate::safeurl::{ContentType, DataType, VersionHash};
pub use nrs_map::NrsMap;
use crate::{app::Safe, register::EntryHash, Error, Result, SafeUrl};
use log::{debug, info};
use std::collections::{BTreeMap, BTreeSet};
use std::str;
pub const NRS_MAP_TYPE_TAG: u64 = 1_500;
impl Safe {
pub async fn nrs_create(&self, top_name: &str) -> Result<SafeUrl> {
info!("Creating an NRS map for: {}", top_name);
let mut nrs_url = validate_nrs_top_name(top_name)?;
nrs_url.set_content_type(ContentType::NrsMapContainer)?;
let nrs_xorname = SafeUrl::from_nrsurl(&nrs_url.to_string())?.xorname();
debug!("XorName for \"{:?}\" is \"{:?}\"", &nrs_url, &nrs_xorname);
if self.nrs_get_subnames_map(top_name, None).await.is_ok() {
return Err(Error::NrsNameAlreadyExists(top_name.to_owned()));
}
let _ = self
.multimap_create(Some(nrs_xorname), NRS_MAP_TYPE_TAG)
.await?;
Ok(nrs_url)
}
pub async fn nrs_associate(&self, public_name: &str, link: &SafeUrl) -> Result<SafeUrl> {
info!(
"Associating public name \"{}\" to \"{}\" in NRS map container",
public_name, link
);
let mut url = validate_nrs_public_name(public_name)?;
validate_nrs_url(link)?;
let current_versions = self
.fetch_multimap_values_by_key(&url, public_name.as_bytes())
.await?
.into_iter()
.map(|(hash, _)| hash)
.collect();
let entry = (
public_name.as_bytes().to_vec(),
link.to_string().as_bytes().to_vec(),
);
let entry_hash = self
.multimap_insert(&url.to_string(), entry, current_versions)
.await?;
set_nrs_url_props(&mut url, entry_hash)?;
Ok(url)
}
pub async fn nrs_add(&self, public_name: &str, link: &SafeUrl) -> Result<(SafeUrl, bool)> {
info!(
"Adding public name \"{}\" to \"{}\" in an NRS map container",
public_name, link
);
let url = validate_nrs_public_name(public_name)?;
let top_name = url.top_name();
let creation_result = self.nrs_create(top_name).await;
let did_register_topname = match creation_result {
Ok(_) => Ok(true),
Err(Error::NrsNameAlreadyExists(_)) => Ok(false),
Err(e) => Err(e),
}?;
let new_url = self.nrs_associate(public_name, link).await?;
Ok((new_url, did_register_topname))
}
pub async fn nrs_remove(&self, public_name: &str) -> Result<SafeUrl> {
info!(
"Removing public name \"{}\" from NRS map container",
public_name
);
let mut url = validate_nrs_public_name(public_name)?;
let current_versions = self
.fetch_multimap_values_by_key(&url, public_name.as_bytes())
.await?
.into_iter()
.map(|(hash, _)| hash)
.collect();
let entry_hash = self
.multimap_remove(&url.to_string(), current_versions)
.await?;
set_nrs_url_props(&mut url, entry_hash)?;
Ok(url)
}
pub async fn nrs_get(
&self,
public_name: &str,
version: Option<VersionHash>,
) -> Result<(Option<SafeUrl>, NrsMap)> {
info!(
"Getting link for public name: {} for version: {:?}",
public_name, version
);
let nrs_map = match self.nrs_get_subnames_map(public_name, version).await {
Ok(result) => Ok(result),
Err(Error::ConflictingNrsEntries(str, conflicting_entries, map)) => {
if conflicting_entries.iter().any(|(p, _)| p == public_name) {
Err(Error::ConflictingNrsEntries(str, conflicting_entries, map))
} else {
Ok(map)
}
}
Err(e) => Err(e),
}?;
let url = nrs_map.get(public_name)?;
Ok((url, nrs_map))
}
pub async fn nrs_get_subnames_map(
&self,
public_name: &str,
version: Option<VersionHash>,
) -> Result<NrsMap> {
let url = SafeUrl::from_url(&format!("safe://{public_name}"))?;
let mut multimap = match self.fetch_multimap(&url).await {
Ok(s) => Ok(s),
Err(Error::EmptyContent(_)) => Ok(BTreeSet::new()),
Err(Error::ContentNotFound(e)) => Err(Error::ContentNotFound(format!(
"No Nrs Map entry found at {url}: {e}",
))),
Err(e) => Err(Error::NetDataError(format!(
"Failed to get Nrs Map entries: {e}"
))),
}?;
if let Some(version) = version {
if !multimap
.iter()
.any(|(h, _)| VersionHash::from(h) == version)
{
let key_val = self
.fetch_multimap_value_by_hash(&url, version.entry_hash())
.await?;
multimap.insert((version.entry_hash(), key_val));
}
}
let subnames_set = convert_multimap_to_nrs_set(&multimap, public_name, version)?;
let nrs_map = get_nrs_map_from_set(&subnames_set)?;
if nrs_map.map.len() != subnames_set.len() {
let diff_set: BTreeSet<(String, SafeUrl)> = nrs_map.map.clone().into_iter().collect();
let conflicting_entries: Vec<(String, SafeUrl)> =
subnames_set.difference(&diff_set).cloned().collect();
return Err(Error::ConflictingNrsEntries(
"Found multiple entries for the same name. This happens when 2 clients write \
concurrently to the same NRS mapping. It can be fixed by associating a new link to \
the conflicting names."
.to_string(),
conflicting_entries,
nrs_map,
));
}
Ok(nrs_map)
}
}
fn convert_multimap_to_nrs_set(
multimap: &Multimap,
public_name: &str,
subname_version: Option<VersionHash>,
) -> Result<BTreeSet<(String, SafeUrl)>> {
if let Some(version) = subname_version {
let mut versioned_set: BTreeSet<(VersionHash, String, SafeUrl)> = multimap
.clone()
.into_iter()
.map(|x| {
let version = VersionHash::from(&x.0);
let kv = x.1;
let public_name = str::from_utf8(&kv.0)?;
let url = SafeUrl::from_url(str::from_utf8(&kv.1)?)?;
Ok((version, public_name.to_owned(), url))
})
.collect::<Result<BTreeSet<(VersionHash, String, SafeUrl)>>>()?;
let duplicate_entries = versioned_set
.clone()
.into_iter()
.filter(|x| x.1 == public_name)
.filter(|x| x.0 != version)
.collect::<BTreeSet<(VersionHash, String, SafeUrl)>>();
for entry in &duplicate_entries {
versioned_set.remove(entry);
}
let set: BTreeSet<(String, SafeUrl)> = versioned_set
.iter()
.map(|x| (x.1.clone(), x.2.clone()))
.collect::<BTreeSet<(String, SafeUrl)>>();
return Ok(set);
}
let set: BTreeSet<(String, SafeUrl)> = multimap
.clone()
.into_iter()
.map(|x| {
let kv = x.1;
let public_name = str::from_utf8(&kv.0)?;
let url = SafeUrl::from_url(str::from_utf8(&kv.1)?)?;
Ok((public_name.to_owned(), url))
})
.collect::<Result<BTreeSet<(String, SafeUrl)>>>()?;
Ok(set)
}
fn get_nrs_map_from_set(set: &BTreeSet<(String, SafeUrl)>) -> Result<NrsMap> {
let public_names_map: BTreeMap<String, SafeUrl> = set
.clone()
.into_iter()
.map(|x| (x.0, x.1))
.collect::<BTreeMap<String, SafeUrl>>();
let nrs_map = NrsMap {
map: public_names_map,
};
Ok(nrs_map)
}
fn set_nrs_url_props(url: &mut SafeUrl, entry_hash: EntryHash) -> Result<()> {
url.set_content_version(Some(VersionHash::from(&entry_hash)));
url.set_content_type(ContentType::NrsMapContainer)?;
Ok(())
}
fn validate_nrs_top_name(top_name: &str) -> Result<SafeUrl> {
let url = SafeUrl::from_url(&format!("safe://{top_name}"))?;
if url.top_name() != top_name {
return Err(Error::InvalidInput(format!(
"The NRS top name \"{top_name}\" is invalid because it contains url parts. Please \
remove any path, version or subnames.",
)));
}
Ok(url)
}
fn validate_nrs_public_name(public_name: &str) -> Result<SafeUrl> {
let url = SafeUrl::from_url(&format!("safe://{public_name}"))?;
if url.public_name() != public_name {
return Err(Error::InvalidInput(format!(
"The NRS public name \"{public_name}\" is invalid because it contains url parts. \
Please remove any path or version.",
)));
}
Ok(url)
}
fn validate_nrs_url(link: &SafeUrl) -> Result<()> {
if link.content_version().is_none() {
let content_type = link.content_type();
let data_type = link.data_type();
if content_type == ContentType::FilesContainer
|| content_type == ContentType::NrsMapContainer
{
return Err(Error::UnversionedContentError(format!(
"{content_type} content is versionable. NRS requires the supplied link to specify a version hash.",
)));
} else if data_type == DataType::Register {
return Err(Error::UnversionedContentError(format!(
"{data_type} content is versionable. NRS requires the supplied link to specify a version hash.",
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
app::test_helpers::{new_safe_instance, random_nrs_name, TestDataFilesContainer},
Error, SafeUrl,
};
use anyhow::{anyhow, bail, Context, Result};
use assert_matches::assert_matches;
use std::matches;
const TEST_DATA_FILE: &str = "./testdata/test.md";
#[tokio::test]
async fn test_nrs_create() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let url = safe.nrs_create(&site_name).await?;
assert_eq!(url.content_type(), ContentType::NrsMapContainer);
Ok(())
}
#[tokio::test]
async fn test_nrs_create_with_invalid_topname() -> Result<()> {
let safe = new_safe_instance().await?;
let invalid_top_name = "atffdgasd/d";
assert_matches!(
safe.nrs_create(invalid_top_name).await, Err(Error::InvalidInput(err))
if err == format!("The NRS top name \"{invalid_top_name}\" is invalid because \
it contains url parts. Please remove any path, version or subnames.")
);
Ok(())
}
#[tokio::test]
async fn test_nrs_create_with_duplicate_topname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
safe.nrs_create(&site_name).await?;
let _ = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_matches!(
safe.nrs_create(&site_name).await, Err(Error::NrsNameAlreadyExists(err))
if err == site_name
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_topname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([]).await?;
safe.nrs_create(&site_name).await?;
let url = safe.nrs_associate(&site_name, &files_container.url).await?;
assert_eq!(url.public_name(), site_name);
assert!(url.content_version().is_some());
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 1);
assert_eq!(
*nrs_map.map.get(&site_name).ok_or_else(|| anyhow!(
"'{}' subname should have been present in retrieved NRS map",
site_name
))?,
files_container.url
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_subname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container(["/testdata/test.md"])
.await
.context("File container init failed")?;
let public_name = &format!("test.{site_name}");
safe.nrs_create(&site_name).await?;
let url = match safe
.nrs_associate(public_name, &files_container["/testdata/test.md"])
.await
{
Ok(res) => res,
Err(error) => {
bail!("Error during nrs associate {error:?}")
}
};
assert_eq!(url.public_name(), public_name);
assert!(url.content_version().is_some());
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 1, "expected map len to be 1");
assert_eq!(
*nrs_map
.map
.get(&format!("test.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'test.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/test.md"],
"expected nrs map container to match local"
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_multiple_subnames() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container =
TestDataFilesContainer::get_container(["/testdata/test.md", "/testdata/another.md"])
.await?;
safe.nrs_create(&site_name).await?;
safe.nrs_associate(
&format!("test.{site_name}"),
&files_container["/testdata/test.md"],
)
.await?;
safe.nrs_associate(
&format!("another.{site_name}"),
&files_container["/testdata/another.md"],
)
.await?;
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 2);
assert_eq!(
*nrs_map
.map
.get(&format!("test.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'test.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/test.md"]
);
assert_eq!(
*nrs_map
.map
.get(&format!("another.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'another.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/another.md"]
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_non_versioned_files_container_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([]).await?;
let mut url = files_container.url.clone();
url.set_content_version(None);
let public_name = &format!("test.{site_name}");
safe.nrs_create(&site_name).await?;
assert_matches!(
safe.nrs_associate(public_name, &url).await, Err(Error::UnversionedContentError(err))
if err.as_str() == "FilesContainer content is versionable. NRS requires the supplied \
link to specify a version hash."
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_non_versioned_nrs_container_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let public_name = &format!("test.{site_name}");
let mut nrs_container_url = safe.nrs_create(&site_name).await?;
nrs_container_url.set_content_version(None);
assert_matches!(
safe.nrs_associate(public_name, &nrs_container_url).await, Err(Error::UnversionedContentError(err))
if err.as_str() == "NrsMapContainer content is versionable. NRS requires the supplied \
link to specify a version hash."
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_register_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let register_link = safe
.register_create(None, NRS_MAP_TYPE_TAG, ContentType::Raw)
.await?;
let mut register_url = SafeUrl::from_xorurl(®ister_link)?;
register_url.set_content_version(None);
let result = safe
.nrs_associate(&format!("test.{site_name}"), ®ister_url)
.await;
assert_matches!(
result, Err(Error::UnversionedContentError(err))
if err.as_str() == "Register content is versionable. NRS requires the supplied link to \
specify a version hash."
);
Ok(())
}
#[tokio::test]
async fn test_nrs_associate_with_invalid_url() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container(["/testdata/test.md"]).await?;
let public_name = &format!("test./{site_name}");
safe.nrs_create(&site_name).await?;
let result = safe
.nrs_associate(public_name, &files_container["/testdata/test.md"])
.await;
assert_matches!(
result, Err(Error::InvalidInput(err))
if err == format!("The NRS public name \"{public_name}\" is invalid because \
it contains url parts. Please remove any path or version.")
);
Ok(())
}
#[tokio::test]
async fn test_nrs_add_with_subname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container(["/testdata/test.md"]).await?;
let public_name = &format!("test.{site_name}");
let (_, topname_registered) = match safe
.nrs_add(public_name, &files_container["/testdata/test.md"])
.await
{
Ok(res) => res,
Err(error) => bail!("Error during nrs add {error:?}"),
};
assert!(topname_registered);
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 1, "nrs map has len 1");
assert_eq!(
*nrs_map.map.get(public_name).ok_or_else(|| anyhow!(format!(
"'{public_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/test.md"],
"added subname is correct"
);
Ok(())
}
#[tokio::test]
async fn test_nrs_remove_with_subname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container =
TestDataFilesContainer::get_container(["/testdata/test.md", "/testdata/another.md"])
.await?;
safe.nrs_create(&site_name).await?;
safe.nrs_associate(
&format!("test.{site_name}"),
&files_container["/testdata/test.md"],
)
.await?;
safe.nrs_associate(
&format!("another.{site_name}"),
&files_container["/testdata/another.md"],
)
.await?;
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 2);
let url = safe.nrs_remove(&format!("another.{site_name}")).await?;
assert_eq!(url.public_name(), &format!("another.{site_name}"));
assert!(url.content_version().is_some());
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 1);
assert_eq!(
*nrs_map
.map
.get(&format!("test.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'test.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/test.md"]
);
Ok(())
}
#[tokio::test]
async fn test_nrs_remove_with_topname() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([]).await?;
safe.nrs_create(&site_name).await?;
safe.nrs_associate(&site_name, &files_container.url).await?;
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 1);
let url = safe.nrs_remove(&site_name).await?;
assert_eq!(url.public_name(), site_name);
assert!(url.content_version().is_some());
let nrs_map = safe.nrs_get_subnames_map(&site_name, None).await?;
assert_eq!(nrs_map.map.len(), 0);
Ok(())
}
#[tokio::test]
async fn test_nrs_conflicting_names() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let (link, _, _) = safe
.files_container_create_from(TEST_DATA_FILE, None, false, false)
.await
.context("failed to create container")?;
let (version0, _) = safe
.files_container_get(&link)
.await?
.ok_or_else(|| anyhow!("files container was unexpectedly empty"))?;
let mut valid_link = SafeUrl::from_url(&link).context("failed to create link")?;
valid_link.set_content_version(Some(version0));
let (nrs_url, did_create) = safe
.nrs_add(&site_name, &valid_link)
.await
.context("failed to nrs add")?;
assert!(did_create);
let _ = safe
.fetch(&nrs_url.to_string(), None)
.await
.context("failed to fetch")?;
debug!("--------------------------------------->1111111");
let second_valid_link =
SafeUrl::from_url(&link).context("failed to create anotehr link")?;
valid_link.set_content_version(Some(version0));
let site_name2 = format!("sub.{}", &site_name);
let (nrs_url2, did_create) = safe
.nrs_add(&site_name2, &second_valid_link)
.await
.context("failed to add another nrs")?;
assert!(!did_create);
let _ = safe
.fetch(&nrs_url2.to_string(), None)
.await
.context("failed to fetch again")?;
debug!("--------------------------------------->2222222222");
let another_valid_url = nrs_url;
let url = validate_nrs_top_name(&site_name).context("could not validate name")?;
let entry = (
site_name.as_bytes().to_vec(),
another_valid_url.to_string().as_bytes().to_vec(),
);
let _ = safe
.multimap_insert(&url.to_string(), entry, BTreeSet::new())
.await
.context("multimap insert failed")?;
debug!("--------------------------------------->333333");
let (res_url, _) = safe
.nrs_get(&site_name2, None)
.await
.context("failed get")?;
assert_eq!(
res_url.ok_or_else(|| anyhow!("url should not be None"))?,
second_valid_link
);
let conflict_error = safe.nrs_get(&site_name, None).await;
assert!(
matches!(conflict_error, Err(Error::ConflictingNrsEntries { .. }),),
"error was not expected {conflict_error:?}"
);
debug!("--------------------------------------->44444");
if let Err(Error::ConflictingNrsEntries(_, dups, _)) = conflict_error {
let got_entries: Result<()> = dups.into_iter().try_for_each(|(public_name, url)| {
assert_eq!(public_name, site_name, "problematic names match");
assert!(
url == valid_link || url == another_valid_url,
"theres a url conflict"
);
Ok(())
});
assert!(got_entries.is_ok(), "we have some entries");
}
let _ = safe
.nrs_associate(&site_name, &valid_link)
.await
.context("resolving the error failed")?;
let (res_url, _) = safe
.nrs_get(&site_name, None)
.await
.context("last get failed")?;
assert_eq!(
res_url.ok_or_else(|| anyhow!("url should not be None"))?,
valid_link,
"final url was as expcted"
);
Ok(())
}
#[tokio::test]
async fn test_nrs_get_with_duplicate_subname_versions() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([
"/testdata/test.md",
"/testdata/another.md",
"/testdata/noextension",
])
.await?;
let public_name = &format!("test.{site_name}");
safe.nrs_create(&site_name).await?;
let nrs_url = safe
.nrs_associate(public_name, &files_container["/testdata/test.md"])
.await?;
let version = nrs_url
.content_version()
.ok_or_else(|| anyhow!("nrs_url should have a version"))?;
safe.nrs_associate(public_name, &files_container["/testdata/another.md"])
.await?;
safe.nrs_associate(public_name, &files_container["/testdata/noextension"])
.await?;
safe.nrs_associate(
&format!("another.{site_name}"),
&files_container["/testdata/another.md"],
)
.await?;
let (url, nrs_map) = safe.nrs_get(public_name, Some(version)).await?;
assert_eq!(
url.ok_or_else(|| anyhow!("url should not be None"))?,
files_container["/testdata/test.md"]
);
assert_eq!(nrs_map.map.len(), 2);
assert_eq!(
*nrs_map
.map
.get(&format!("test.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'test.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/test.md"]
);
assert_eq!(
*nrs_map
.map
.get(&format!("another.{site_name}"))
.ok_or_else(|| anyhow!(format!(
"'another.{site_name}' subname should have been present in retrieved NRS map"
)))?,
files_container["/testdata/another.md"]
);
Ok(())
}
#[tokio::test]
async fn test_nrs_get_with_topname_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([]).await?;
safe.nrs_create(&site_name).await?;
safe.nrs_associate(&site_name, &files_container.url).await?;
let (url, _) = safe.nrs_get(&site_name, None).await?;
assert_eq!(
url.ok_or_else(|| anyhow!("url should not be None"))?,
files_container.url
);
Ok(())
}
#[tokio::test]
async fn test_nrs_get_with_nrs_map_container_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container([]).await?;
let nrs_url = safe.nrs_create(&site_name).await?;
let nrs_map_container_url = SafeUrl::from_url(&nrs_url.to_xorurl_string())?;
safe.nrs_associate(&site_name, &files_container.url).await?;
let (url, _) = safe
.nrs_get(nrs_map_container_url.public_name(), None)
.await?;
assert!(url.is_none());
Ok(())
}
#[tokio::test]
async fn test_nrs_get_when_topname_has_no_link() -> Result<()> {
let site_name = random_nrs_name();
let safe = new_safe_instance().await?;
let files_container = TestDataFilesContainer::get_container(["/testdata/test.md"]).await?;
safe.nrs_create(&site_name).await?;
safe.nrs_associate(
&format!("test.{site_name}"),
&files_container["/testdata/test.md"],
)
.await?;
let (url, _) = safe.nrs_get(&site_name, None).await?;
assert!(url.is_none());
Ok(())
}
}