Skip to content

Commit

Permalink
refactor: Moving from anyhow to thiserror part 1 (#6250)
Browse files Browse the repository at this point in the history
### Description

Part 1 of moving us away from anyhow and to thiserror. We're doing this
migration so we can explicitly categorize our errors and eventually
provide nicer messages via miette.

This converts a lot of the auxiliary commands like login, link, logout,
etc.

### Testing Instructions

<!--
  Give a quick description of steps to test your changes.
-->


Closes TURBO-1518

---------

Co-authored-by: nicholaslyang <Nicholas Yang>
  • Loading branch information
NicholasLYang committed Oct 24, 2023
1 parent d30264d commit cde6e21
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 162 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/turborepo-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ turborepo-api-client = { workspace = true }
turborepo-ui.workspace = true
turborepo-vercel-api = { workspace = true }
turborepo-vercel-api-mock = { workspace = true }
url = { workspace = true }
webbrowser = { workspace = true }

[dev-dependencies]
Expand Down
52 changes: 32 additions & 20 deletions crates/turborepo-auth/src/auth/login.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::{borrow::Cow, sync::Arc};

use anyhow::{anyhow, Result};
pub use error::Error;
use reqwest::Url;
use tokio::sync::OnceCell;
Expand All @@ -21,7 +20,7 @@ pub async fn login<'a>(
existing_token: Option<&'a str>,
login_url_configuration: &str,
login_server: &impl LoginServer,
) -> Result<Cow<'a, str>> {
) -> Result<Cow<'a, str>, Error> {
// Check if token exists first.
if let Some(token) = existing_token {
if let Ok(response) = api_client.get_user(token).await {
Expand Down Expand Up @@ -65,12 +64,13 @@ pub async fn login<'a>(

spinner.finish_and_clear();

let token = token_cell
.get()
.ok_or_else(|| anyhow!("Failed to get token"))?;
let token = token_cell.get().ok_or(Error::FailedToGetToken)?;

// TODO: make this a request to /teams endpoint instead?
let user_response = api_client.get_user(token.as_str()).await?;
let user_response = api_client
.get_user(token.as_str())
.await
.map_err(Error::FailedToFetchUser)?;

ui::print_cli_authorized(&user_response.user.email, ui);

Expand All @@ -83,7 +83,7 @@ mod tests {

use async_trait::async_trait;
use reqwest::{Method, RequestBuilder, Response};
use turborepo_api_client::{Client, Error, Result};
use turborepo_api_client::Client;
use turborepo_vercel_api::{
CachingStatusResponse, Membership, PreflightResponse, Role, SpacesResponse, Team,
TeamsResponse, User, UserResponse, VerifiedSsoUser,
Expand All @@ -103,7 +103,7 @@ mod tests {
_: u16,
_: String,
login_token: Arc<OnceCell<String>>,
) -> anyhow::Result<()> {
) -> Result<(), Error> {
self.hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
login_token
.set(turborepo_vercel_api_mock::EXPECTED_TOKEN.to_string())
Expand Down Expand Up @@ -144,7 +144,7 @@ mod tests {

#[async_trait]
impl Client for MockApiClient {
async fn get_user(&self, token: &str) -> Result<UserResponse> {
async fn get_user(&self, token: &str) -> turborepo_api_client::Result<UserResponse> {
if token.is_empty() {
return Err(MockApiError::EmptyToken.into());
}
Expand All @@ -159,7 +159,7 @@ mod tests {
},
})
}
async fn get_teams(&self, token: &str) -> Result<TeamsResponse> {
async fn get_teams(&self, token: &str) -> turborepo_api_client::Result<TeamsResponse> {
if token.is_empty() {
return Err(MockApiError::EmptyToken.into());
}
Expand All @@ -175,7 +175,11 @@ mod tests {
}],
})
}
async fn get_team(&self, _token: &str, _team_id: &str) -> Result<Option<Team>> {
async fn get_team(
&self,
_token: &str,
_team_id: &str,
) -> turborepo_api_client::Result<Option<Team>> {
unimplemented!("get_team")
}
fn add_ci_header(_request_builder: RequestBuilder) -> RequestBuilder {
Expand All @@ -193,13 +197,21 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<CachingStatusResponse> {
) -> turborepo_api_client::Result<CachingStatusResponse> {
unimplemented!("get_caching_status")
}
async fn get_spaces(&self, _token: &str, _team_id: Option<&str>) -> Result<SpacesResponse> {
async fn get_spaces(
&self,
_token: &str,
_team_id: Option<&str>,
) -> turborepo_api_client::Result<SpacesResponse> {
unimplemented!("get_spaces")
}
async fn verify_sso_token(&self, token: &str, _: &str) -> Result<VerifiedSsoUser> {
async fn verify_sso_token(
&self,
token: &str,
_: &str,
) -> turborepo_api_client::Result<VerifiedSsoUser> {
Ok(VerifiedSsoUser {
token: token.to_string(),
team_id: Some("team_id".to_string()),
Expand All @@ -212,10 +224,10 @@ mod tests {
_duration: u64,
_tag: Option<&str>,
_token: &str,
) -> Result<()> {
) -> turborepo_api_client::Result<()> {
unimplemented!("put_artifact")
}
async fn handle_403(_response: Response) -> Error {
async fn handle_403(_response: Response) -> turborepo_api_client::Error {
unimplemented!("handle_403")
}
async fn fetch_artifact(
Expand All @@ -224,7 +236,7 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("fetch_artifact")
}
async fn artifact_exists(
Expand All @@ -233,7 +245,7 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("artifact_exists")
}
async fn get_artifact(
Expand All @@ -243,7 +255,7 @@ mod tests {
_team_id: &str,
_team_slug: Option<&str>,
_method: Method,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("get_artifact")
}
async fn do_preflight(
Expand All @@ -252,7 +264,7 @@ mod tests {
_request_url: &str,
_request_method: &str,
_request_headers: &str,
) -> Result<PreflightResponse> {
) -> turborepo_api_client::Result<PreflightResponse> {
unimplemented!("do_preflight")
}
fn make_url(&self, endpoint: &str) -> String {
Expand Down
67 changes: 42 additions & 25 deletions crates/turborepo-auth/src/auth/sso.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
use std::{borrow::Cow, sync::Arc};

use anyhow::{anyhow, Context, Result};
use reqwest::Url;
use tokio::sync::OnceCell;
use tracing::warn;
use turborepo_api_client::Client;
use turborepo_ui::{start_spinner, BOLD, UI};

use crate::{error, server, ui};
use crate::{error, server, ui, Error};

const DEFAULT_HOST_NAME: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 9789;
const DEFAULT_SSO_PROVIDER: &str = "SAML/OIDC Single Sign-On";

fn make_token_name() -> Result<String> {
let host = hostname::get()?;
fn make_token_name() -> Result<String, Error> {
let host = hostname::get().map_err(Error::FailedToMakeSSOTokenName)?;

Ok(format!(
"Turbo CLI on {} via {DEFAULT_SSO_PROVIDER}",
Expand All @@ -31,7 +30,7 @@ pub async fn sso_login<'a>(
login_url_configuration: &str,
sso_team: &str,
login_server: &impl server::SSOLoginServer,
) -> Result<Cow<'a, str>> {
) -> Result<Cow<'a, str>, Error> {
// Check if token exists first. Must be there for the user and contain the
// sso_team passed into this function.
if let Some(token) = existing_token {
Expand Down Expand Up @@ -80,14 +79,19 @@ pub async fn sso_login<'a>(
login_server.run(DEFAULT_PORT, token_cell.clone()).await?;
spinner.finish_and_clear();

let token = token_cell
.get()
.ok_or_else(|| anyhow!("no token auth token found"))?;
let token = token_cell.get().ok_or(Error::FailedToGetToken)?;

let token_name = make_token_name().context("failed to make sso token name")?;
let token_name = make_token_name()?;

let verified_user = api_client.verify_sso_token(token, &token_name).await?;
let user_response = api_client.get_user(&verified_user.token).await?;
let verified_user = api_client
.verify_sso_token(token, &token_name)
.await
.map_err(Error::FailedToValidateSSOToken)?;

let user_response = api_client
.get_user(&verified_user.token)
.await
.map_err(Error::FailedToFetchUser)?;

ui::print_cli_authorized(&user_response.user.email, ui);

Expand All @@ -100,7 +104,7 @@ mod tests {

use async_trait::async_trait;
use reqwest::{Method, RequestBuilder, Response};
use turborepo_api_client::{Client, Error, Result};
use turborepo_api_client::Client;
use turborepo_vercel_api::{
CachingStatusResponse, Membership, PreflightResponse, Role, SpacesResponse, Team,
TeamsResponse, User, UserResponse, VerifiedSsoUser,
Expand Down Expand Up @@ -151,7 +155,7 @@ mod tests {

#[async_trait]
impl Client for MockApiClient {
async fn get_user(&self, token: &str) -> Result<UserResponse> {
async fn get_user(&self, token: &str) -> turborepo_api_client::Result<UserResponse> {
if token.is_empty() {
return Err(MockApiError::EmptyToken.into());
}
Expand All @@ -166,7 +170,7 @@ mod tests {
},
})
}
async fn get_teams(&self, token: &str) -> Result<TeamsResponse> {
async fn get_teams(&self, token: &str) -> turborepo_api_client::Result<TeamsResponse> {
if token.is_empty() {
return Err(MockApiError::EmptyToken.into());
}
Expand All @@ -182,7 +186,11 @@ mod tests {
}],
})
}
async fn get_team(&self, _token: &str, _team_id: &str) -> Result<Option<Team>> {
async fn get_team(
&self,
_token: &str,
_team_id: &str,
) -> turborepo_api_client::Result<Option<Team>> {
unimplemented!("get_team")
}
fn add_ci_header(_request_builder: RequestBuilder) -> RequestBuilder {
Expand All @@ -200,13 +208,21 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<CachingStatusResponse> {
) -> turborepo_api_client::Result<CachingStatusResponse> {
unimplemented!("get_caching_status")
}
async fn get_spaces(&self, _token: &str, _team_id: Option<&str>) -> Result<SpacesResponse> {
async fn get_spaces(
&self,
_token: &str,
_team_id: Option<&str>,
) -> turborepo_api_client::Result<SpacesResponse> {
unimplemented!("get_spaces")
}
async fn verify_sso_token(&self, token: &str, _: &str) -> Result<VerifiedSsoUser> {
async fn verify_sso_token(
&self,
token: &str,
_: &str,
) -> turborepo_api_client::Result<VerifiedSsoUser> {
Ok(VerifiedSsoUser {
token: token.to_string(),
team_id: Some("team_id".to_string()),
Expand All @@ -219,10 +235,10 @@ mod tests {
_duration: u64,
_tag: Option<&str>,
_token: &str,
) -> Result<()> {
) -> turborepo_api_client::Result<()> {
unimplemented!("put_artifact")
}
async fn handle_403(_response: Response) -> Error {
async fn handle_403(_response: Response) -> turborepo_api_client::Error {
unimplemented!("handle_403")
}
async fn fetch_artifact(
Expand All @@ -231,7 +247,7 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("fetch_artifact")
}
async fn artifact_exists(
Expand All @@ -240,7 +256,7 @@ mod tests {
_token: &str,
_team_id: &str,
_team_slug: Option<&str>,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("artifact_exists")
}
async fn get_artifact(
Expand All @@ -250,7 +266,7 @@ mod tests {
_team_id: &str,
_team_slug: Option<&str>,
_method: Method,
) -> Result<Response> {
) -> turborepo_api_client::Result<Response> {
unimplemented!("get_artifact")
}
async fn do_preflight(
Expand All @@ -259,14 +275,15 @@ mod tests {
_request_url: &str,
_request_method: &str,
_request_headers: &str,
) -> Result<PreflightResponse> {
) -> turborepo_api_client::Result<PreflightResponse> {
unimplemented!("do_preflight")
}
fn make_url(&self, endpoint: &str) -> String {
format!("{}{}", self.base_url, endpoint)
}
}

#[derive(Clone)]
struct MockSSOLoginServer {
hits: Arc<AtomicUsize>,
}
Expand All @@ -277,7 +294,7 @@ mod tests {
&self,
_port: u16,
verification_token: Arc<OnceCell<String>>,
) -> anyhow::Result<()> {
) -> Result<(), Error> {
self.hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
verification_token
.set(EXPECTED_VERIFICATION_TOKEN.to_string())
Expand Down
12 changes: 12 additions & 0 deletions crates/turborepo-auth/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::io;

use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -7,4 +9,14 @@ pub enum Error {
situations like using a `data:` URL."
)]
LoginUrlCannotBeABase { value: String },
#[error("failed to get token")]
FailedToGetToken,
#[error("failed to fetch user: {0}")]
FailedToFetchUser(turborepo_api_client::Error),
#[error("url is invalid: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("failed to validate sso token")]
FailedToValidateSSOToken(turborepo_api_client::Error),
#[error("failed to make sso token name")]
FailedToMakeSSOTokenName(io::Error),
}
Loading

0 comments on commit cde6e21

Please sign in to comment.