Skip to content
Merged
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
26 changes: 26 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::config::{ServerConfig, parse_server_url};
use anyhow::{Context, Result};
use colored::Colorize;
use reqwest::{Client, Response, StatusCode};
use ricochet_core::content::ContentItem;
use serde::de::DeserializeOwned;
Expand Down Expand Up @@ -113,6 +114,31 @@ impl RicochetClient {
Ok(response.status() == StatusCode::OK)
}

/// Check if a key is expired and report if so
/// Use this as a pre-flight check for all API calls where appropriate
pub async fn preflight_key_check(&self) -> Result<()> {
let server_url = self.base_url.as_str().trim_end_matches('/');
let login_cmd = format!("ricochet login -S {server_url}").bright_cyan();
match self.validate_key().await {
Ok(v) => {
if !v {
anyhow::bail!(
"Credentials are invalid or expired for server {server_url}.\nRun {login_cmd} to authenticate."
);
} else {
Ok(())
}
}
Err(e) => {
anyhow::bail!(
"Failed to validate credentials for {server_url}:\n{} {}\nRun {login_cmd} to authenticate.",
"⚠".bright_yellow(),
e.to_string().dimmed()
);
}
}
}

pub async fn list_items(&self) -> Result<Vec<serde_json::Value>> {
let mut url = self.base_url.clone();
url.set_path("/api/v0/user/items");
Expand Down
8 changes: 8 additions & 0 deletions src/commands/auth/auth_unit_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod tests {
}

/// Test that logout clears the API key
#[serial]
#[test]
fn test_logout_clears_api_key() {
let _temp_dir = setup_test_env();
Expand All @@ -53,6 +54,7 @@ mod tests {
}

/// Test that logout handles already logged out state
#[serial]
#[test]
fn test_logout_when_not_logged_in() {
let _temp_dir = setup_test_env();
Expand Down Expand Up @@ -290,6 +292,7 @@ mod tests {
}

/// Test logout from a specific named server
#[serial]
#[test]
fn test_logout_from_named_server() {
let _temp_dir = setup_test_env();
Expand All @@ -313,6 +316,7 @@ mod tests {
}

/// Test logout from server specified by URL
#[serial]
#[test]
fn test_logout_from_server_by_url() {
let _temp_dir = setup_test_env();
Expand All @@ -329,6 +333,7 @@ mod tests {
}

/// Test logout from nonexistent server fails
#[serial]
#[test]
fn test_logout_from_nonexistent_server() {
let _temp_dir = setup_test_env();
Expand All @@ -341,6 +346,7 @@ mod tests {
}

/// Test logout from server with no matching URL
#[serial]
#[test]
fn test_logout_from_unknown_url() {
let _temp_dir = setup_test_env();
Expand All @@ -353,6 +359,7 @@ mod tests {
}

/// Test logout uses default server when none specified
#[serial]
#[test]
fn test_logout_uses_default_server() {
let _temp_dir = setup_test_env();
Expand All @@ -376,6 +383,7 @@ mod tests {
}

/// Test logout when already logged out from specified server
#[serial]
#[test]
fn test_logout_when_already_logged_out_from_server() {
let _temp_dir = setup_test_env();
Expand Down
16 changes: 11 additions & 5 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ pub async fn deploy(
anyhow::bail!("Path does not exist: {}", path.display());
}

// Resolve server configuration early so we can bail before the init dialog
// if the user has no API key configured
let server_config = config.resolve_server(server_ref)?;
let client = RicochetClient::new(&server_config)?;

client.preflight_key_check().await?;

// Check for _ricochet.toml
let toml_path = if path.is_dir() {
path.join("_ricochet.toml")
Expand All @@ -29,7 +36,10 @@ pub async fn deploy(
// Check if we're in an interactive terminal (not in tests or CI)
if !crate::utils::is_non_interactive() {
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("No _ricochet.toml found. Would you like to create one?")
.with_prompt(format!(
"No _ricochet.toml found. Would you like to create one? (deploying to {})",
server_config.url.as_str().trim_end_matches('/')
))
.default(true)
.interact()?;

Expand Down Expand Up @@ -75,10 +85,6 @@ pub async fn deploy(
);
pb.enable_steady_tick(std::time::Duration::from_millis(80));

// Resolve server configuration
let server_config = config.resolve_server(server_ref)?;
let client = RicochetClient::new(&server_config)?;

match client
.deploy(&path, content_id.clone(), &toml_path, &pb, debug)
.await
Expand Down
4 changes: 3 additions & 1 deletion src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ fn choose_entrypoint(content_type: &ContentType, dir: &PathBuf) -> anyhow::Resul
ContentType::Shiny => choose_shiny_entrypoint(dir),
ContentType::Rmd | ContentType::RmdShiny => find_candidate_entrypoints("Rmd", dir),
ContentType::Julia | ContentType::JuliaService => find_candidate_entrypoints("jl", dir),
ContentType::QuartoR | ContentType::QuartoRShiny | ContentType::QuartoJl
ContentType::QuartoR
| ContentType::QuartoRShiny
| ContentType::QuartoJl
| ContentType::QuartoPy => find_candidate_entrypoints("qmd", dir),
ContentType::Python
| ContentType::PythonService
Expand Down
21 changes: 20 additions & 1 deletion tests/deploy_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ mod deploy_tests {
}
}

fn mock_check_key(server: &mut Server) -> mockito::Mock {
server
.mock("GET", "/api/v0/check_key")
.match_header("authorization", Matcher::Regex("Key .*".to_string()))
.with_status(200)
.create()
}

fn create_test_project(dir: &Path, content_id: Option<&str>) -> std::io::Result<()> {
// Create _ricochet.toml
let toml_content = if let Some(id) = content_id {
Expand Down Expand Up @@ -91,6 +99,7 @@ shinyApp(ui = ui, server = server)"#,

// Create mock server
let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

// Mock the server response for new content deployment
let _m = server
Expand Down Expand Up @@ -147,6 +156,7 @@ shinyApp(ui = ui, server = server)"#,

// Create mock server
let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

// Mock the server response for updating existing content
let _m = server
Expand Down Expand Up @@ -199,9 +209,12 @@ shinyApp(ui = ui, server = server)"#,
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path();

let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

// Create test config
let config = ricochet_cli::config::Config::for_test(
Url::parse("http://localhost:3000").unwrap(),
Url::parse(&server.url()).unwrap(),
Some("test_api_key".to_string()),
);

Expand Down Expand Up @@ -272,6 +285,7 @@ key = "value"

// Create mock server
let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

// Mock the server response with 403 error
let _m = server
Expand Down Expand Up @@ -317,6 +331,7 @@ key = "value"

// Create mock server
let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

let _m = server
.mock("POST", "/api/v0/content/upload")
Expand Down Expand Up @@ -372,6 +387,7 @@ key = "value"

// Create mock server
let mut server = Server::new_async().await;
let _ck = mock_check_key(&mut server);

let _m = server
.mock("POST", "/api/v0/content/upload")
Expand Down Expand Up @@ -423,6 +439,7 @@ key = "value"

// Create mock server for staging
let mut staging_server = Server::new_async().await;
let _ck = mock_check_key(&mut staging_server);

// Mock the staging server response
let _m = staging_server
Expand Down Expand Up @@ -474,6 +491,7 @@ key = "value"

// Create mock server
let mut mock_server = Server::new_async().await;
let _ck = mock_check_key(&mut mock_server);
let mock_url = mock_server.url();

// Create config with mock server URL as staging
Expand Down Expand Up @@ -523,6 +541,7 @@ key = "value"

// Create mock server for prod (the default)
let mut prod_server = Server::new_async().await;
let _ck = mock_check_key(&mut prod_server);

// Mock the prod server response
let _m = prod_server
Expand Down