Files
pubky-core/pubky/src/shared/public.rs

788 lines
23 KiB
Rust

use reqwest::IntoUrl;
use anyhow::Result;
use crate::Client;
use super::list_builder::ListBuilder;
impl Client {
pub(crate) fn inner_list<T: IntoUrl>(&self, url: T) -> Result<ListBuilder> {
Ok(ListBuilder::new(self, url))
}
}
#[cfg(test)]
mod tests {
use crate::*;
use bytes::Bytes;
use mainline::Testnet;
use pkarr::Keypair;
use pubky_homeserver::Homeserver;
use reqwest::{Method, StatusCode};
#[tokio::test]
async fn put_get_delete() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
let url = url.as_str();
client
.put(url)
.body(vec![0, 1, 2, 3, 4])
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
client
.delete(url)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let response = client.get(url).send().await.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn unauthorized_put_delete() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let public_key = keypair.public_key();
let url = format!("pubky://{public_key}/pub/foo.txt");
let url = url.as_str();
let other_client = Client::test(&testnet);
{
let other = Keypair::random();
// TODO: remove extra client after switching to subdomains.
other_client
.signup(&other, &server.public_key())
.await
.unwrap();
assert_eq!(
other_client
.put(url)
.body(vec![0, 1, 2, 3, 4])
.send()
.await
.unwrap()
.status(),
StatusCode::UNAUTHORIZED
);
}
client
.put(url)
.body(vec![0, 1, 2, 3, 4])
.send()
.await
.unwrap();
{
let other = Keypair::random();
// TODO: remove extra client after switching to subdomains.
other_client
.signup(&other, &server.public_key())
.await
.unwrap();
assert_eq!(
other_client.delete(url).send().await.unwrap().status(),
StatusCode::UNAUTHORIZED
);
}
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
}
#[tokio::test]
async fn list() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let pubky = keypair.public_key();
let urls = vec![
format!("pubky://{pubky}/pub/a.wrong/a.txt"),
format!("pubky://{pubky}/pub/example.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
format!("pubky://{pubky}/pub/example.wrong/a.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
format!("pubky://{pubky}/pub/example.com/d.txt"),
format!("pubky://{pubky}/pub/z.wrong/a.txt"),
];
for url in urls {
client.put(url).body(vec![0]).send().await.unwrap();
}
let url = format!("pubky://{pubky}/pub/example.com/extra");
{
let list = client.list(&url).unwrap().send().await.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
format!("pubky://{pubky}/pub/example.com/d.txt"),
],
"normal list with no limit or cursor"
);
}
{
let list = client.list(&url).unwrap().limit(2).send().await.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
],
"normal list with limit but no cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.limit(2)
.cursor("a.txt")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
],
"normal list with limit and a file cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.limit(2)
.cursor("cc-nested/")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
format!("pubky://{pubky}/pub/example.com/d.txt"),
],
"normal list with limit and a directory cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.limit(2)
.cursor(&format!("pubky://{pubky}/pub/example.com/a.txt"))
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
],
"normal list with limit and a full url cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.limit(2)
.cursor("/a.txt")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
],
"normal list with limit and a leading / cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.reverse(true)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/d.txt"),
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/a.txt"),
],
"reverse list with no limit or cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.reverse(true)
.limit(2)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/d.txt"),
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
],
"reverse list with limit but no cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.reverse(true)
.limit(2)
.cursor("d.txt")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
],
"reverse list with limit and cursor"
);
}
}
#[tokio::test]
async fn list_shallow() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let pubky = keypair.public_key();
let urls = vec![
format!("pubky://{pubky}/pub/a.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
format!("pubky://{pubky}/pub/example.com/d.txt"),
format!("pubky://{pubky}/pub/example.con/d.txt"),
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/file"),
format!("pubky://{pubky}/pub/file2"),
format!("pubky://{pubky}/pub/z.com/a.txt"),
];
for url in urls {
client.put(url).body(vec![0]).send().await.unwrap();
}
let url = format!("pubky://{pubky}/pub/");
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/a.com/"),
format!("pubky://{pubky}/pub/example.com/"),
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/example.con/"),
format!("pubky://{pubky}/pub/file"),
format!("pubky://{pubky}/pub/file2"),
format!("pubky://{pubky}/pub/z.com/"),
],
"normal list shallow"
);
}
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.limit(2)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/a.com/"),
format!("pubky://{pubky}/pub/example.com/"),
],
"normal list shallow with limit but no cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.limit(2)
.cursor("example.com/a.txt")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.com/"),
format!("pubky://{pubky}/pub/example.con"),
],
"normal list shallow with limit and a file cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.limit(3)
.cursor("example.com/")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/example.con/"),
format!("pubky://{pubky}/pub/file"),
],
"normal list shallow with limit and a directory cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.reverse(true)
.shallow(true)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/z.com/"),
format!("pubky://{pubky}/pub/file2"),
format!("pubky://{pubky}/pub/file"),
format!("pubky://{pubky}/pub/example.con/"),
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/example.com/"),
format!("pubky://{pubky}/pub/a.com/"),
],
"reverse list shallow"
);
}
{
let list = client
.list(&url)
.unwrap()
.reverse(true)
.shallow(true)
.limit(2)
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/z.com/"),
format!("pubky://{pubky}/pub/file2"),
],
"reverse list shallow with limit but no cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.reverse(true)
.limit(2)
.cursor("file2")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/file"),
format!("pubky://{pubky}/pub/example.con/"),
],
"reverse list shallow with limit and a file cursor"
);
}
{
let list = client
.list(&url)
.unwrap()
.shallow(true)
.reverse(true)
.limit(2)
.cursor("example.con/")
.send()
.await
.unwrap();
assert_eq!(
list,
vec![
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/example.com/"),
],
"reverse list shallow with limit and a directory cursor"
);
}
}
#[tokio::test]
async fn list_events() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let pubky = keypair.public_key();
let urls = vec![
format!("pubky://{pubky}/pub/a.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/a.txt"),
format!("pubky://{pubky}/pub/example.com/b.txt"),
format!("pubky://{pubky}/pub/example.com/c.txt"),
format!("pubky://{pubky}/pub/example.com/d.txt"),
format!("pubky://{pubky}/pub/example.con/d.txt"),
format!("pubky://{pubky}/pub/example.con"),
format!("pubky://{pubky}/pub/file"),
format!("pubky://{pubky}/pub/file2"),
format!("pubky://{pubky}/pub/z.com/a.txt"),
];
for url in urls {
client.put(&url).body(vec![0]).send().await.unwrap();
client.delete(url).send().await.unwrap();
}
let feed_url = format!("https://{}/events/", server.public_key());
let client = Client::test(&testnet);
let cursor;
{
let response = client
.request(Method::GET, format!("{feed_url}?limit=10"))
.send()
.await
.unwrap();
let text = response.text().await.unwrap();
let lines = text.split('\n').collect::<Vec<_>>();
cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
assert_eq!(
lines,
vec![
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
format!("DEL pubky://{pubky}/pub/a.com/a.txt"),
format!("PUT pubky://{pubky}/pub/example.com/a.txt"),
format!("DEL pubky://{pubky}/pub/example.com/a.txt"),
format!("PUT pubky://{pubky}/pub/example.com/b.txt"),
format!("DEL pubky://{pubky}/pub/example.com/b.txt"),
format!("PUT pubky://{pubky}/pub/example.com/c.txt"),
format!("DEL pubky://{pubky}/pub/example.com/c.txt"),
format!("PUT pubky://{pubky}/pub/example.com/d.txt"),
format!("DEL pubky://{pubky}/pub/example.com/d.txt"),
format!("cursor: {cursor}",)
]
);
}
{
let response = client
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
.send()
.await
.unwrap();
let text = response.text().await.unwrap();
let lines = text.split('\n').collect::<Vec<_>>();
assert_eq!(
lines,
vec![
format!("PUT pubky://{pubky}/pub/example.con/d.txt"),
format!("DEL pubky://{pubky}/pub/example.con/d.txt"),
format!("PUT pubky://{pubky}/pub/example.con"),
format!("DEL pubky://{pubky}/pub/example.con"),
format!("PUT pubky://{pubky}/pub/file"),
format!("DEL pubky://{pubky}/pub/file"),
format!("PUT pubky://{pubky}/pub/file2"),
format!("DEL pubky://{pubky}/pub/file2"),
format!("PUT pubky://{pubky}/pub/z.com/a.txt"),
format!("DEL pubky://{pubky}/pub/z.com/a.txt"),
lines.last().unwrap().to_string()
]
)
}
}
#[tokio::test]
async fn read_after_event() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let pubky = keypair.public_key();
let url = format!("pubky://{pubky}/pub/a.com/a.txt");
client.put(&url).body(vec![0]).send().await.unwrap();
let feed_url = format!("https://{}/events/", server.public_key());
let client = Client::test(&testnet);
{
let response = client
.request(Method::GET, format!("{feed_url}?limit=10"))
.send()
.await
.unwrap();
let text = response.text().await.unwrap();
let lines = text.split('\n').collect::<Vec<_>>();
let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
assert_eq!(
lines,
vec![
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
format!("cursor: {cursor}",)
]
);
}
let response = client.get(url).send().await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.bytes().await.unwrap();
assert_eq!(body.as_ref(), &[0]);
}
#[tokio::test]
async fn dont_delete_shared_blobs() {
let testnet = Testnet::new(10).unwrap();
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let homeserver_pubky = homeserver.public_key();
let user_1 = Keypair::random();
let user_2 = Keypair::random();
client.signup(&user_1, &homeserver_pubky).await.unwrap();
client.signup(&user_2, &homeserver_pubky).await.unwrap();
let user_1_id = user_1.public_key();
let user_2_id = user_2.public_key();
let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1");
let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1");
let file = vec![1];
client.put(&url_1).body(file.clone()).send().await.unwrap();
client.put(&url_2).body(file.clone()).send().await.unwrap();
// Delete file 1
client
.delete(url_1)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let blob = client
.get(url_2)
.send()
.await
.unwrap()
.bytes()
.await
.unwrap();
assert_eq!(blob, file);
let feed_url = format!("https://{}/events/", homeserver.public_key());
let response = client
.request(Method::GET, feed_url)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let text = response.text().await.unwrap();
let lines = text.split('\n').collect::<Vec<_>>();
assert_eq!(
lines,
vec![
format!("PUT pubky://{user_1_id}/pub/pubky.app/file/file_1",),
format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",),
format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",),
lines.last().unwrap().to_string()
]
);
}
#[tokio::test]
async fn stream() {
// TODO: test better streaming API
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = Client::test(&testnet);
let keypair = Keypair::random();
client.signup(&keypair, &server.public_key()).await.unwrap();
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
let url = url.as_str();
let bytes = Bytes::from(vec![0; 1024 * 1024]);
client.put(url).body(bytes.clone()).send().await.unwrap();
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
assert_eq!(response, bytes);
client.delete(url).send().await.unwrap();
let response = client.get(url).send().await.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}