feat(js): pass all unit tests

This commit is contained in:
nazeh
2024-12-18 00:16:49 +03:00
parent 992607a7a7
commit f42797a234
8 changed files with 170 additions and 148 deletions

View File

@@ -42,20 +42,22 @@ let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`;
// Verify that you are signed in.
const session = await client.session(publicKey)
const body = Buffer.from(JSON.stringify({ foo: 'bar' }))
// PUT public data, by authorized client
await client.put(url, body);
await client.fetch(url, {
method: "PUT",
body: JSON.stringify({foo: "bar"}),
credentials: "include"
});
// GET public data without signup or signin
{
const client = new Client();
let response = await client.get(url);
let response = await client.fetch(url);
}
// Delete public data, by authorized client
await client.delete(url);
await client.fetch(url, { method: "DELETE", credentials: "include "});
```
## API
@@ -67,6 +69,13 @@ await client.delete(url);
let client = new Client()
```
#### fetch
```js
let response = await client.fetch(url, opts);
```
Just like normal Fetch API, but it can handle `pubky://` urls and `http(s)://` urls with Pkarr domains.
#### signup
```js
await client.signup(keypair, homeserver)
@@ -127,27 +136,6 @@ let session = await client.session(publicKey)
- publicKey: An instance of [PublicKey](#publickey).
- Returns: A [Session](#session) object if signed in, or undefined if not.
#### put
```js
let response = await client.put(url, body);
```
- url: A string representing the Pubky URL.
- body: A Buffer containing the data to be stored.
### get
```js
let response = await client.get(url)
```
- url: A string representing the Pubky URL.
- Returns: A Uint8Array object containing the requested data, or `undefined` if `NOT_FOUND`.
### delete
```js
let response = await client.delete(url);
```
- url: A string representing the Pubky URL.
### list
```js
let response = await client.list(url, cursor, reverse, limit)

View File

@@ -1,10 +1,10 @@
import test from 'tape'
import { Client, Keypair, PublicKey } from '../index.cjs'
import { Client, Keypair, PublicKey ,setLogLevel} from '../index.cjs'
const HOMESERVER_PUBLICKEY = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo')
test.skip('public: put/get', async (t) => {
test('public: put/get', async (t) => {
const client = Client.testnet();
const keypair = Keypair.random();
@@ -15,33 +15,43 @@ test.skip('public: put/get', async (t) => {
let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`;
const body = Buffer.from(JSON.stringify({ foo: 'bar' }))
const json = { foo: 'bar' }
// PUT public data, by authorized client
await client.put(url, body);
await client.fetch(url, {
method:"PUT",
body: JSON.stringify(json),
contentType: "json",
credentials: "include"
});
const otherClient = Client.testnet();
// GET public data without signup or signin
{
let response = await otherClient.get(url);
let response = await otherClient.fetch(url)
t.ok(Buffer.from(response).equals(body))
t.is(response.status, 200);
t.deepEquals(await response.json(), {foo: "bar"})
}
// DELETE public data, by authorized client
await client.delete(url);
await client.fetch(url, {
method:"DELETE",
credentials: "include"
});
// GET public data without signup or signin
{
let response = await otherClient.get(url);
let response = await otherClient.fetch(url);
t.notOk(response)
t.is(response.status, 404)
}
})
test.skip("not found", async (t) => {
test("not found", async (t) => {
const client = Client.testnet();
@@ -53,12 +63,12 @@ test.skip("not found", async (t) => {
let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`;
let result = await client.get(url).catch(e => e);
let result = await client.fetch(url);
t.notOk(result);
t.is(result.status, 404);
})
test.skip("unauthorized", async (t) => {
test("unauthorized", async (t) => {
const client = Client.testnet();
const keypair = Keypair.random()
@@ -71,21 +81,20 @@ test.skip("unauthorized", async (t) => {
await client.signout(publicKey)
const body = Buffer.from(JSON.stringify({ foo: 'bar' }))
let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`;
// PUT public data, by authorized client
let result = await client.put(url, body).catch(e => e);
let response = await client.fetch(url, {
method: "PUT",
body: JSON.stringify({ foo: 'bar' }),
contentType: "json",
credentials: "include"
});
t.ok(result instanceof Error);
t.is(
result.message,
`HTTP status client error (401 Unauthorized) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)`
)
t.equals(response.status,401);
})
test.skip("forbidden", async (t) => {
test("forbidden", async (t) => {
const client = Client.testnet();
const keypair = Keypair.random()
@@ -96,21 +105,22 @@ test.skip("forbidden", async (t) => {
const session = await client.session(publicKey)
t.ok(session, "signup")
const body = Buffer.from(JSON.stringify({ foo: 'bar' }))
const body = (JSON.stringify({ foo: 'bar' }))
let url = `pubky://${publicKey.z32()}/priv/example.com/arbitrary`;
// PUT public data, by authorized client
let result = await client.put(url, body).catch(e => e);
let response = await client.fetch(url, {
method: "PUT",
body: JSON.stringify({ foo: 'bar' }),
credentials: "include"
});
t.ok(result instanceof Error);
t.is(
result.message,
`HTTP status client error (403 Forbidden) for url (http://localhost:15411/${publicKey.z32()}/priv/example.com/arbitrary)`
)
t.is(response.status, 403)
t.is(await response.text(), 'Writing to directories other than \'/pub/\' is forbidden')
})
test.skip("list", async (t) => {
test("list", async (t) => {
const client = Client.testnet();
const keypair = Keypair.random()
@@ -132,7 +142,11 @@ test.skip("list", async (t) => {
]
for (let url of urls) {
await client.put(url, Buffer.from(""));
await client.fetch(url, {
method: "PUT",
body:Buffer.from(""),
credentials: "include"
});
}
let url = `pubky://${pubky}/pub/example.com/`;
@@ -241,7 +255,7 @@ test.skip("list", async (t) => {
}
})
test.skip('list shallow', async (t) => {
test('list shallow', async (t) => {
const client = Client.testnet();
const keypair = Keypair.random()
@@ -264,7 +278,11 @@ test.skip('list shallow', async (t) => {
]
for (let url of urls) {
await client.put(url, Buffer.from(""));
await client.fetch(url, {
method: "PUT",
body: Buffer.from(""),
credentials: "include"
});
}
let url = `pubky://${pubky}/pub/`;

View File

@@ -7,7 +7,6 @@ use crate::Client;
mod api;
mod cookies;
mod http;
mod internal;
pub(crate) use cookies::CookieJar;

View File

@@ -123,6 +123,12 @@ impl Client {
pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::HEAD, url)
}
// === Private Methods ===
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
self.request(method, url)
}
}
#[cfg(test)]

View File

@@ -1,12 +0,0 @@
//! Native specific implementation of methods used in the shared module
//!
use reqwest::{IntoUrl, Method, RequestBuilder};
use crate::Client;
impl Client {
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
self.request(method, url)
}
}

View File

@@ -4,7 +4,6 @@ use crate::Client;
mod api;
mod http;
mod internals;
mod wrappers;
impl Default for Client {

View File

@@ -2,8 +2,9 @@
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use web_sys::{Headers, RequestInit};
use reqwest::Url;
use reqwest::{IntoUrl, Method, RequestBuilder, Url};
use futures_lite::StreamExt;
@@ -18,70 +19,40 @@ impl Client {
pub async fn fetch(
&self,
url: &str,
init: &web_sys::RequestInit,
request_init: Option<RequestInit>,
) -> Result<js_sys::Promise, JsValue> {
let mut url: Url = url.try_into().map_err(|err| {
JsValue::from_str(&format!("pubky::Client::fetch(): Invalid `url`; {:?}", err))
})?;
self.transform_url(&mut url).await;
let request_init = request_init.unwrap_or_default();
let js_req =
web_sys::Request::new_with_str_and_init(url.as_str(), init).map_err(|err| {
if let Some(pkarr_host) = self.prepare_request(&mut url).await {
let headers = request_init.get_headers();
let headers = if headers.is_null() || headers.is_undefined() {
Headers::new()?
} else {
Headers::from(headers)
};
headers.append("pkarr-host", &pkarr_host)?;
request_init.set_headers(&headers.into());
}
let js_req = web_sys::Request::new_with_str_and_init(url.as_str(), &request_init).map_err(
|err| {
JsValue::from_str(&format!(
"pubky::Client::fetch(): Invalid `init`; {:?}",
err
))
})?;
},
)?;
Ok(js_fetch(&js_req))
}
pub(super) async fn transform_url(&self, url: &mut Url) {
if url.scheme() == "pubky" {
*url = Url::parse(&format!("https{}", &url.as_str()[5..]))
.expect("couldn't replace pubky:// with https://");
url.set_host(Some(&format!("_pubky.{}", url.host_str().unwrap_or(""))))
.expect("couldn't map pubk://<pubky> to https://_pubky.<pubky>");
}
let qname = url.host_str().unwrap_or("").to_string();
if PublicKey::try_from(qname.to_string()).is_ok() {
let mut stream = self.pkarr.resolve_https_endpoints(&qname);
let mut so_far: Option<Endpoint> = None;
// TODO: currently we return the first thing we can see,
// in the future we might want to failover to other endpoints
while so_far.is_none() {
while let Some(endpoint) = stream.next().await {
if endpoint.domain() != "." {
so_far = Some(endpoint);
}
}
}
if let Some(e) = so_far {
// TODO: detect loopback IPs and other equivilants to localhost
if self.testnet && e.domain() == "localhost" {
url.set_scheme("http")
.expect("couldn't replace pubky:// with http://");
}
url.set_host(Some(e.domain()))
.expect("coultdn't use the resolved endpoint's domain");
url.set_port(Some(e.port()))
.expect("coultdn't use the resolved endpoint's port");
} else {
// TODO: didn't find any domain, what to do?
}
}
log::debug!("Transformed URL to: {}", url.as_str());
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = fetch)]
@@ -102,3 +73,80 @@ fn js_fetch(req: &web_sys::Request) -> Promise {
fetch_with_request(req)
}
}
impl Client {
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
let original_url = url.as_str();
let mut url = Url::parse(original_url).expect("Invalid url in inner_request");
if let Some(pkarr_host) = self.prepare_request(&mut url).await {
self.http
.request(method, url.clone())
.header::<&str, &str>("pkarr-host", &pkarr_host)
.fetch_credentials_include()
} else {
self.http
.request(method, url.clone())
.fetch_credentials_include()
}
}
/// - Transforms pubky:// url to http(s):// urls
/// - Resolves a clearnet host to call with fetch
/// - Returns the `pkarr-host` value if available
pub(super) async fn prepare_request(&self, url: &mut Url) -> Option<String> {
if url.scheme() == "pubky" {
*url = Url::parse(&format!("https{}", &url.as_str()[5..]))
.expect("couldn't replace pubky:// with https://");
url.set_host(Some(&format!("_pubky.{}", url.host_str().unwrap_or(""))))
.expect("couldn't map pubk://<pubky> to https://_pubky.<pubky>");
}
// TODO: move at the begining of the method.
let host = url.host_str().unwrap_or("").to_string();
let mut pkarr_host = None;
if PublicKey::try_from(host.clone()).is_ok() {
self.transform_url(url, &host).await;
pkarr_host = Some(host);
log::debug!("Transformed URL to: {}", url.as_str());
};
pkarr_host
}
pub async fn transform_url(&self, url: &mut Url, qname: &str) {
let mut stream = self.pkarr.resolve_https_endpoints(qname);
let mut so_far: Option<Endpoint> = None;
// TODO: currently we return the first thing we can see,
// in the future we might want to failover to other endpoints
while so_far.is_none() {
while let Some(endpoint) = stream.next().await {
if endpoint.domain() != "." {
so_far = Some(endpoint);
}
}
}
if let Some(e) = so_far {
// TODO: detect loopback IPs and other equivilants to localhost
if self.testnet && e.domain() == "localhost" {
url.set_scheme("http")
.expect("couldn't replace pubky:// with http://");
}
url.set_host(Some(e.domain()))
.expect("coultdn't use the resolved endpoint's domain");
url.set_port(Some(e.port()))
.expect("coultdn't use the resolved endpoint's port");
} else {
// TODO: didn't find any domain, what to do?
}
}
}

View File

@@ -1,24 +0,0 @@
//! Wasm specific implementation of methods used in the shared module
//!
use reqwest::{IntoUrl, Method, RequestBuilder};
use url::Url;
use crate::Client;
impl Client {
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
let original_url = url.as_str();
let mut url = Url::parse(original_url).expect("Invalid url in inner_request");
let original_host = url.host_str().unwrap_or("").to_string();
self.transform_url(&mut url).await;
self.http
.request(method, url)
.header::<&str, &str>("pkarr-host", &original_host)
.fetch_credentials_include()
}
}