Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
/Cargo.lock
/out

# Rust Code Coverage
tarpaulin-report.html
cobertura.xml
*.profraw

# Ignore Gradle project-specific cache directory
.gradle

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ test-server:
docker-compose up -d
docker-compose run wpcli

codecov-rust:
$(rust_docker_run) /bin/bash -c 'cargo install cargo-tarpaulin && cargo tarpaulin --out html --out xml'

stop-server:
docker-compose down

Expand Down
7 changes: 2 additions & 5 deletions native/swift/Example/Example/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,9 @@ struct LoginView: View {
callbackURLScheme: "exampleauth"
)

guard let loginDetails = WordPressAPI.Helpers.extractLoginDetails(from: urlWithToken) else {
debugPrint("Unable to parse login details")
abort()
}

let loginDetails = try urlWithToken.asOAuthResponseUrl().getPasswordDetails()
try await loginManager.setLoginCredentials(to: loginDetails)

} catch let err {
self.isLoggingIn = false
self.loginError = err.localizedDescription
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation
import wordpress_api_wrapper

public extension URL {
func asOAuthResponseUrl() -> OAuthResponseUrl {
OAuthResponseUrl(stringValue: self.absoluteString)
}
}

extension OAuthResponseUrl {
static func new(stringValue: String) -> OAuthResponseUrl {
OAuthResponseUrl(stringValue: stringValue)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any caller to this function. Can it be deleted?

}
20 changes: 20 additions & 0 deletions native/swift/Sources/wordpress-api/Extensions/WpRestApiurl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import wordpress_api_wrapper

enum WPRestAPIError: Error {
case invalidUrl
}

public extension WpRestApiurl {
func asUrl() throws -> URL {
guard
let url = URL(string: stringValue),
url.scheme != nil,
url.host != nil
else {
throw WPRestAPIError.invalidUrl
}

return url
}
}
2 changes: 1 addition & 1 deletion native/swift/Sources/wordpress-api/Login/API+Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public extension WordPressAPI {
let ephemeralClient = WordPressAPI(urlSession: session, baseUrl: url, authenticationStategy: .none)
let response = try await ephemeralClient.perform(request: request)

return wordpress_api_wrapper.getLinkHeader(response: response, name: "https://api.w.org/")?.asUrl()
return try wordpress_api_wrapper.getLinkHeader(response: response, name: "https://api.w.org/")?.asUrl()
}

func getRestAPICapabilities(forApiRoot url: URL, using session: URLSession) async throws -> WpapiDetails {
Expand Down
14 changes: 0 additions & 14 deletions native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ public struct WordPressAPI {

throw ParseError.invalidUrl
}

public static func extractLoginDetails(from url: URL) -> WpapiApplicationPasswordDetails? {
return wordpress_api_wrapper.extractLoginDetailsFromUrl(url: url.asRestUrl())
}
}

enum ParseError: Error {
Expand Down Expand Up @@ -164,16 +160,6 @@ extension WpNetworkRequest {
}
}

extension WpRestApiurl {
func asUrl() -> URL {
guard let url = URL(string: stringValue) else {
preconditionFailure("Invalid URL: \(stringValue)")
}

return url
}
}

extension URL {
func asRestUrl() -> WpRestApiurl {
WpRestApiurl(stringValue: self.absoluteString)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import XCTest
import wordpress_api
import wordpress_api_wrapper // We need to construct internal types to test them properly

final class WPRestAPIUrlTests: XCTestCase {

func testThatValidUrlCanBeParsed() throws {
XCTAssertEqual(URL(string: "http://example.com"), try WpRestApiurl(stringValue: "http://example.com").asUrl())
}

func testThatInvalidUrlThrowsError() throws {
XCTAssertThrowsError(try WpRestApiurl(stringValue: "invalid").asUrl())
}
}
2 changes: 1 addition & 1 deletion wp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ pub fn parse_api_details_response(response: WPNetworkResponse) -> Result<WPAPIDe
#[uniffi::export]
pub fn get_link_header(response: &WPNetworkResponse, name: &str) -> Option<WPRestAPIURL> {
if let Some(url) = response.get_link_header(name) {
return Some(url.into())
return Some(url.into());
}

None
Expand Down
229 changes: 206 additions & 23 deletions wp_api/src/login.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str;
use crate::url::*;

// After a successful login, the system will receive an OAuth callback with the login details
// embedded as query params. This function parses that URL and extracts the login details as an object.
#[derive(Debug, PartialEq, uniffi::Object)]
pub struct OAuthResponseUrl {
string_value: String,
}

#[uniffi::export]
pub fn extract_login_details_from_url(
url: WPRestAPIURL,
) -> Option<WPAPIApplicationPasswordDetails> {
if let (Some(site_url), Some(user_login), Some(password)) =
url.as_url()
.query_pairs()
.fold((None, None, None), |accum, (k, v)| {
match k.to_string().as_str() {
"site_url" => (Some(v.to_string()), accum.1, accum.2),
"user_login" => (accum.0, Some(v.to_string()), accum.2),
"password" => (accum.0, accum.1, Some(v.to_string())),
_ => accum,
impl OAuthResponseUrl {
#[uniffi::constructor]
pub fn new(string_value: String) -> Self {
Self { string_value }
}

pub fn get_password_details(
&self,
) -> Result<WPAPIApplicationPasswordDetails, OAuthResponseUrlError> {
let mut builder = WPAPIApplicationPasswordDetails::builder();

let url =
url::Url::parse(&self.string_value).map_err(|err| OAuthResponseUrlError::InvalidUrl)?;

for pair in url.query_pairs() {
match pair.0.to_string().as_str() {
"site_url" => builder = builder.site_url(pair.1.to_string()),
"user_login" => builder = builder.user_login(pair.1.to_string()),
"password" => builder = builder.password(pair.1.to_string()),
"success" => {
if pair.1 == "false" {
return Err(OAuthResponseUrlError::UnsuccessfulLogin);
}
}
})
{
Some(WPAPIApplicationPasswordDetails {
site_url,
user_login,
password,
})
} else {
None
_ => (),
};
}

builder.build() //.map_err(|err| UrlParsingError::InvalidUrl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, the previous implementation using fold is cleaner. The build function body can be merged into this function here.

}
}

impl From<&str> for OAuthResponseUrl {
fn from(str: &str) -> Self {
OAuthResponseUrl {
string_value: str.to_string(),
}
}
}

Expand All @@ -54,9 +73,173 @@ pub struct WPRestApiAuthenticationEndpoint {
pub authorization: String,
}

#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)]
pub struct WPAPIApplicationPasswordDetails {
pub site_url: String,
pub user_login: String,
pub password: String,
}

impl WPAPIApplicationPasswordDetails {
fn builder() -> WPAPIApplicationPasswordDetailsBuilder {
WPAPIApplicationPasswordDetailsBuilder::default()
}
}

#[derive(Default)]
struct WPAPIApplicationPasswordDetailsBuilder {
site_url: Option<String>,
user_login: Option<String>,
password: Option<String>,
}

impl WPAPIApplicationPasswordDetailsBuilder {
fn site_url(mut self, site_url: String) -> Self {
self.site_url = Some(site_url);
self
}

fn user_login(mut self, user_login: String) -> Self {
self.user_login = Some(user_login);
self
}

fn password(mut self, password: String) -> Self {
self.password = Some(password);
self
}

fn build(self) -> Result<WPAPIApplicationPasswordDetails, OAuthResponseUrlError> {
let site_url = if let Some(site_url) = self.site_url {
site_url
} else {
return Err(OAuthResponseUrlError::MissingSiteUrl);
};

let user_login = if let Some(user_login) = self.user_login {
user_login
} else {
return Err(OAuthResponseUrlError::MissingUsername);
};

let password = if let Some(password) = self.password {
password
} else {
return Err(OAuthResponseUrlError::MissingPassword);
};

Ok(WPAPIApplicationPasswordDetails {
site_url,
user_login,
password,
})
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum OAuthResponseUrlError {
#[error("Invalid URL")]
InvalidUrl,

#[error("The given URL is missing the `site_url` query parameter")]
MissingSiteUrl,

#[error("The given URL is missing the `username` query parameter")]
MissingUsername,

#[error("The given URL is missing the `password` query parameter")]
MissingPassword,

#[error("Unsuccessful Login")]
UnsuccessfulLogin,
}

#[cfg(test)]
mod oauth_response_url_tests {
use super::*;

#[test]
fn can_be_initialized() {
assert_eq!(OAuthResponseUrl::new("foo".to_string()), OAuthResponseUrl::from("foo"))
}

#[test]
fn creates_password_details_for_valid_url() {
let url = OAuthResponseUrl::from(
"exampleauth://login?site_url=http://example.com&user_login=test&password=1234",
);

assert_eq!(
url.get_password_details().unwrap(),
default_password_details()
);
}

#[test]
fn ignores_extra_query_params_for_valid_url() {
let url = OAuthResponseUrl::from(
"exampleauth://login?site_url=http://example.com&user_login=test&password=1234&foo=bar",
);

assert_eq!(
url.get_password_details().unwrap(),
default_password_details()
);
}

#[test]
fn throws_error_for_missing_site_url() {
let result = OAuthResponseUrl::from("exampleauth://login?user_login=test&password=1234")
.get_password_details();
assert!(matches!(result, Err(OAuthResponseUrlError::MissingSiteUrl)));
}

#[test]
fn throws_error_for_missing_user_login() {
let result =
OAuthResponseUrl::from("exampleauth://login?site_url=http://example.com&password=1234")
.get_password_details();
assert!(matches!(
result,
Err(OAuthResponseUrlError::MissingUsername)
));
}

#[test]
fn throws_error_for_missing_password() {
let result = OAuthResponseUrl::from(
"exampleauth://login?site_url=http://example.com&user_login=test",
)
.get_password_details();
assert!(matches!(
result,
Err(OAuthResponseUrlError::MissingPassword)
));
}

#[test]
fn throws_error_for_unsuccessful_login() {
let result =
OAuthResponseUrl::from("exampleauth://login?success=false").get_password_details();
assert!(matches!(
result,
Err(OAuthResponseUrlError::UnsuccessfulLogin)
));
}

#[test]
fn throws_appropriate_error_for_malformed_response() {
let result =
OAuthResponseUrl::from("exampleauth://login?success=true").get_password_details();
assert!(matches!(result, Err(OAuthResponseUrlError::MissingSiteUrl)));
}

fn default_password_details() -> WPAPIApplicationPasswordDetails {
WPAPIApplicationPasswordDetails::builder()
.site_url("http://example.com".to_string())
.user_login("test".to_string())
.password("1234".to_string())
.build()
.unwrap()
}
}
Loading