docs: adds tests and updates docs

Adds several tests.
Updates README.md with implementation instructions and usage examples.
This commit is contained in:
coreyphillips
2024-10-27 09:45:05 -04:00
parent 887a1d4ede
commit 0051371ce1
6 changed files with 544 additions and 24 deletions

36
Cargo.lock generated
View File

@@ -1332,6 +1332,24 @@ dependencies = [
"serde",
]
[[package]]
name = "pubkymobile"
version = "0.1.0"
dependencies = [
"base64",
"hex",
"once_cell",
"pkarr",
"pubky",
"pubky-common",
"serde",
"serde_json",
"sha2",
"tokio",
"uniffi",
"url",
]
[[package]]
name = "publicsuffix"
version = "2.2.3"
@@ -1429,24 +1447,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "react_native_pubky"
version = "0.1.0"
dependencies = [
"base64",
"hex",
"once_cell",
"pkarr",
"pubky",
"pubky-common",
"serde",
"serde_json",
"sha2",
"tokio",
"uniffi",
"url",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"

View File

@@ -1,11 +1,11 @@
[package]
name = "react_native_pubky"
name = "pubkymobile"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate_type = ["cdylib"]
crate_type = ["cdylib", "rlib"]
name = "pubkymobile"
[[bin]]
@@ -27,4 +27,9 @@ base64 = "0.22.1"
once_cell = "1.19.0"
pubky = "0.3.0"
pkarr = "2.2.1-alpha.2"
pubky-common = "0.1.0"
pubky-common = "0.1.0"
[dev-dependencies]
tokio = { version = "1.40.0", features = ["full"] }
serde_json = "1.0.114"
hex = "0.4.3"

265
README.md
View File

@@ -1,5 +1,8 @@
# pubky-core-mobile-sdk
Pubky Core Mobile SDK
# Pubky Core Mobile SDK
The Pubky Core Mobile SDK provides native bindings for iOS and Android platforms to interact with Pubky. This SDK allows you to perform operations like publishing content, retrieving data and managing authentication.
## Building the SDK
### To build both iOS and Android bindings:
```
@@ -14,4 +17,262 @@ Pubky Core Mobile SDK
### To build only Android bindings:
```
./build.sh android
```
## Run Tests:
```
cargo test -- --test-threads=1
```
## iOS Integration
### Installation
1. Add the XCFramework to your Xcode project:
- Drag bindings/ios/PubkyMobile.xcframework into your Xcode project
Ensure "Copy items if needed" is checked
Add the framework to your target
2. Copy the Swift bindings:
- Add bindings/ios/pubkymobile.swift to your project
### Basic Usage
```swift
import PubkyMobile
import PubkyMobile
class PubkyManager {
// Generate a new secret key
func generateNewAccount() throws -> String {
let result = try generateSecretKey()
guard let jsonData = result[1].data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let secretKey = json["secret_key"] as? String else {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to parse response"])
}
return secretKey
}
// Sign up with a homeserver
func signUp(secretKey: String, homeserver: String) async throws -> String {
let result = try signUp(secretKey: secretKey, homeserver: homeserver)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
// Publish content
func publishContent(recordName: String, content: String, secretKey: String) async throws -> String {
let result = try publish(recordName: recordName, recordContent: content, secretKey: secretKey)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
// Retrieve content
func getContent(url: String) async throws -> String {
let result = try get(url: url)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
}
```
### Example Implementation
```swift
class ViewController: UIViewController {
let pubkyManager = PubkyManager()
func setupAccount() async {
do {
// Generate new account
let secretKey = try pubkyManager.generateNewAccount()
// Sign up with homeserver
let homeserver = "pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"
let publicKey = try await pubkyManager.signUp(secretKey: secretKey, homeserver: homeserver)
// Publish content
let content = "Hello, Pubky!"
let recordName = "example.com"
let publishResult = try await pubkyManager.publishContent(
recordName: recordName,
content: content,
secretKey: secretKey
)
print("Published with public key: \(publishResult)")
} catch {
print("Error: \(error.localizedDescription)")
}
}
}
```
## Android Integration
### Installation
1. Add the JNI libraries to your project:
- Copy the contents of bindings/android/jniLibs to your project's app/src/main/jniLibs directory
2. Add the Kotlin bindings:
- Copy bindings/android/pubkymobile.kt to your project's source directory
### Basic Usage
```kotlin
class PubkyManager {
init {
// Initialize the library
System.loadLibrary("pubkymobile")
}
fun generateNewAccount(): String {
val result = generateSecretKey()
if (result[0] == "error") {
throw Exception(result[1])
}
val json = JSONObject(result[1])
return json.getString("secret_key")
}
suspend fun signUp(secretKey: String, homeserver: String): String {
val result = signUp(secretKey, homeserver)
if (result[0] == "error") {
throw Exception(result[1])
}
return result[1]
}
suspend fun publishContent(recordName: String, content: String, secretKey: String): String {
val result = publish(recordName, content, secretKey)
if (result[0] == "error") {
throw Exception(result[1])
}
return result[1]
}
suspend fun getContent(url: String): String {
val result = get(url)
if (result[0] == "error") {
throw Exception(result[1])
}
return result[1]
}
}
```
### Example Implementation
```kotlin
class MainActivity : AppCompatActivity() {
private val pubkyManager = PubkyManager()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
try {
// Generate new account
val secretKey = pubkyManager.generateNewAccount()
// Sign up with homeserver
val homeserver = "pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"
val publicKey = pubkyManager.signUp(secretKey, homeserver)
// Publish content
val content = "Hello, Pubky!"
val recordName = "example.com"
val publishResult = pubkyManager.publishContent(
recordName = recordName,
content = content,
secretKey = secretKey
)
Log.d("Pubky", "Published with public key: $publishResult")
} catch (e: Exception) {
Log.e("Pubky", "Error: ${e.message}")
}
}
}
}
```
## Advanced Features
### Working with HTTPS Records
```swift
// iOS
func publishHttps(recordName: String, target: String, secretKey: String) async throws -> String {
let result = try publishHttps(recordName: recordName, target: target, secretKey: secretKey)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
```
```kotlin
// Android
suspend fun publishHttps(recordName: String, target: String, secretKey: String): String {
val result = publishHttps(recordName, target, secretKey)
if (result[0] == "error") {
throw Exception(result[1])
}
return result[1]
}
```
### Recovery File Management
```swift
// iOS
func createRecoveryFile(secretKey: String, passphrase: String) throws -> String {
let result = try createRecoveryFile(secretKey: secretKey, passphrase: passphrase)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
```
```kotlin
// iOS
func createRecoveryFile(secretKey: String, passphrase: String) throws -> String {
let result = try createRecoveryFile(secretKey: secretKey, passphrase: passphrase)
if result[0] == "error" {
throw NSError(domain: "PubkyError", code: -1, userInfo: [NSLocalizedDescriptionKey: result[1]])
}
return result[1]
}
```
## Error Handling
All methods return a `Vec<String>` where:
- The first element ([0]) is either "success" or "error"
- The second element ([1]) contains either the result data or error message
It's recommended to wrap all calls in try-catch blocks and handle errors appropriately in your application.
## Network Configuration
You can switch between default and testnet:
```swift
// iOS
try switchNetwork(useTestnet: true) // For testnet
try switchNetwork(useTestnet: false) // For default
```
```kotlin
// Android
switchNetwork(true) // For testnet
switchNetwork(false) // For default
```

View File

@@ -67,7 +67,7 @@ pub fn get_pubky_client() -> Arc<PubkyClient> {
#[uniffi::export]
pub fn switch_network(use_testnet: bool) -> Vec<String> {
NETWORK_CLIENT.switch_network(use_testnet);
create_response_vector(false, format!("Switched to {} network", if use_testnet { "testnet" } else { "mainnet" }))
create_response_vector(false, format!("Switched to {} network", if use_testnet { "testnet" } else { "default" }))
}
static TOKIO_RUNTIME: Lazy<Arc<Runtime>> = Lazy::new(|| {

27
tests/common/mod.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::string::ToString;
use std::sync::Arc;
use once_cell::sync::Lazy;
use pubky::PubkyClient;
use pkarr::Keypair;
pub static TEST_CLIENT: Lazy<Arc<PubkyClient>> = Lazy::new(|| {
Arc::new(PubkyClient::default())
});
//pub const HOMESERVER: &str = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo";
pub const HOMESERVER: &str = "ufibwbmed6jeq9k4p583go95wofakh9fwpp4k734trq79pd9u1uy";
// For tests that need a consistent keypair
pub static SHARED_KEYPAIR: Lazy<Keypair> = Lazy::new(Keypair::random);
// For tests that need fresh keypairs
pub fn generate_test_keypair() -> Keypair {
Keypair::random()
}
pub fn get_test_setup() -> (Keypair, String, String) {
let keypair = SHARED_KEYPAIR.clone();
let secret_key = hex::encode(keypair.secret_key());
let homeserver = HOMESERVER.to_string();
(keypair, secret_key, homeserver)
}

227
tests/lib_tests.rs Normal file
View File

@@ -0,0 +1,227 @@
use pubkymobile::*;
use tokio;
use base64;
mod common;
use crate::common::{get_test_setup};
// Test keypair generation
#[test]
fn test_put_and_get_and_list() {
let (keypair, secret_key, homeserver) = get_test_setup();
let public_key = keypair.public_key().to_string();
let url = format!("pubky://{}/pub/test.com/testfile", public_key);
let content = "test content".to_string();
let sign_up_result = sign_up(secret_key, homeserver);
assert_eq!(sign_up_result[0], "success");
let inner_url = url.clone();
let put_result = put(url.clone(), content.clone());
assert_eq!(put_result[0], "success");
// Add a small delay to ensure the put operation completes
std::thread::sleep(std::time::Duration::from_secs(1));
let get_result = get(url);
assert_eq!(get_result[0], "success");
assert_eq!(get_result[1], content);
let list_result = list(inner_url);
println!("List result: {:?}", list_result);
assert_eq!(list_result[0], "success");
let json: serde_json::Value = serde_json::from_str(&list_result[1]).unwrap();
assert!(json.is_array());
if let Some(url_str) = json.as_array().and_then(|arr| arr.get(0)).and_then(|v| v.as_str()) {
assert!(url_str.contains(&public_key));
} else {
panic!("Expected array with URL string");
}
}
// Test generate secret key
#[tokio::test]
async fn test_generate_secret_key() {
let result = generate_secret_key();
assert_eq!(result[0], "success");
let json: serde_json::Value = serde_json::from_str(&result[1]).unwrap();
assert!(json["secret_key"].is_string());
assert!(json["public_key"].is_string());
assert!(json["uri"].is_string());
}
// Test get public key from secret key
#[tokio::test]
async fn test_get_public_key_from_secret_key() {
let (_, secret_key, _) = get_test_setup();
let result = get_public_key_from_secret_key(secret_key);
assert_eq!(result[0], "success");
let json: serde_json::Value = serde_json::from_str(&result[1]).unwrap();
assert!(json["public_key"].is_string());
assert!(json["uri"].is_string());
// Test with invalid secret key
let result = get_public_key_from_secret_key("invalid_key".to_string());
assert_eq!(result[0], "error");
}
// Test sign up functionality
#[test]
fn test_publish_and_resolve() {
let (keypair, secret_key, _) = get_test_setup();
let record_name = "test.record".to_string();
let record_content = "test content".to_string();
// Test publish
let publish_result = publish(record_name.clone(), record_content.clone(), secret_key.clone());
assert_eq!(publish_result[0], "success");
// Test resolve
let public_key = keypair.public_key().to_string();
let resolve_result = resolve(public_key);
assert_eq!(resolve_result[0], "success");
let json: serde_json::Value = serde_json::from_str(&resolve_result[1]).unwrap();
assert!(json["records"].is_array());
}
// Test recovery file creation and decryption
#[tokio::test]
async fn test_create_and_decrypt_recovery_file() {
let (_, secret_key, _) = get_test_setup();
let passphrase = "test_passphrase".to_string();
// Create recovery file
let create_result = create_recovery_file(secret_key.clone(), passphrase.clone());
assert_eq!(create_result[0], "success");
// Test recovery file decryption
let recovery_file = create_result[1].clone();
let decrypt_result = decrypt_recovery_file(recovery_file, passphrase);
assert_eq!(decrypt_result[0], "success");
assert_eq!(decrypt_result[1], secret_key);
}
// Test HTTPS publishing functionality
#[test]
fn test_publish_https() {
let (_, secret_key, _) = get_test_setup();
let record_name = "test.domain".to_string();
let target = "target.domain".to_string();
let result = publish_https(record_name, target, secret_key);
assert_eq!(result[0], "success");
}
// Test resolve HTTPS functionality
#[test]
fn test_resolve_https() {
let (keypair, _, _) = get_test_setup();
let public_key = keypair.public_key().to_string();
let result = resolve_https(public_key);
// Note: This might be "error" if no HTTPS records exist
assert!(result[0] == "success" || result[0] == "error");
}
// Test sign in functionality
#[test]
fn test_sign_in_and_out() {
let (_, secret_key, _) = get_test_setup();
// Test sign in
let sign_in_result = sign_in(secret_key.clone());
assert_eq!(sign_in_result[0], "success");
assert_eq!(sign_in_result[1], "Sign in success");
// Test sign out
let sign_out_result = sign_out(secret_key);
assert_eq!(sign_out_result[0], "success");
assert_eq!(sign_out_result[1], "Sign out success");
}
// Test delete functionality
#[test]
fn test_delete() {
let (keypair, secret_key, homeserver) = get_test_setup();
// First sign up
let sign_up_result = sign_up(secret_key.clone(), homeserver);
assert_eq!(sign_up_result[0], "success");
let public_key = keypair.public_key().to_string();
let url = format!("pubky://{}/pub/test.com/testfile", public_key);
let content = "test content".to_string();
// Put some content first
let put_result = put(url.clone(), content);
assert_eq!(put_result[0], "success");
// Test delete
let delete_result = delete_file(url.clone());
assert_eq!(delete_result[0], "success");
assert_eq!(delete_result[1], "Deleted successfully");
// Verify deletion by trying to get the file
let get_result = get(url);
assert_eq!(get_result[0], "error");
}
// Test network switching
#[test]
fn test_switch_network() {
// Test switching to testnet
let testnet_result = switch_network(true);
println!("Testnet result: {:?}", testnet_result);
assert_eq!(testnet_result[0], "success");
assert_eq!(testnet_result[1], "Switched to testnet network");
// Add a small delay to ensure the put operation completes
std::thread::sleep(std::time::Duration::from_secs(1));
// Test switching back to default
let default_result = switch_network(false);
println!("Default network result: {:?}", default_result);
assert_eq!(default_result[0], "success");
assert_eq!(default_result[1], "Switched to default network");
}
// Test auth URL parsing
#[test]
fn test_parse_auth_url() {
let test_url = "pubkyauth:///?caps=/pub/pubky.app/:rw,/pub/foo.bar/file:r&secret=U55XnoH6vsMCpx1pxHtt8fReVg4Brvu9C0gUBuw-Jkw&relay=http://167.86.102.121:4173/";
let result = parse_auth_url(test_url.to_string());
println!("test_parse_auth_url Result: {:?}", result);
assert_eq!(result[0], "success");
let json: serde_json::Value = serde_json::from_str(&result[1]).unwrap();
assert!(json.is_object());
}
// Test error cases
#[test]
fn test_error_cases() {
// Test invalid secret key
let sign_in_result = sign_in("invalid_key".to_string());
assert_eq!(sign_in_result[0], "error");
// Test invalid URL
let get_result = get("invalid_url".to_string());
assert_eq!(get_result[0], "error");
// Test invalid public key for resolve
let resolve_result = resolve("invalid_public_key".to_string());
assert_eq!(resolve_result[0], "error");
// Test empty recovery file creation
let recovery_result = create_recovery_file("".to_string(), "passphrase".to_string());
assert_eq!(recovery_result[0], "error");
}