diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d7a7e775..c39f0ae73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- The `dart-symbol-map upload` command now correctly resolves the organization from the auth token payload ([#3065](https://github.com/getsentry/sentry-cli/pull/3065)). + ## 3.0.2 ### Fixes diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 2784060806..ac16b9b9a7 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -113,26 +113,22 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Prepare chunked upload let api = Api::current(); - // Resolve org and project like logs: prefer args, fallback to defaults + // Resolve org and project, with org also checking auth token payload let config = Config::current(); - let (default_org, default_project) = config.get_org_and_project_defaults(); - let org = args - .org - .as_ref() - .or(default_org.as_ref()) - .ok_or_else(|| anyhow::anyhow!( - "No organization specified. Please specify an organization using the --org argument." - ))?; + let org = config.get_org_with_cli_input(args.org.as_deref())?; let project = args .project - .as_ref() - .or(default_project.as_ref()) - .ok_or_else(|| anyhow::anyhow!( - "No project specified. Use --project or set a default in config." - ))?; + .clone() + .or_else(|| std::env::var("SENTRY_PROJECT").ok()) + .or_else(|| config.get_project_default().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "No project specified. Use --project or set a default in config." + ) + })?; let chunk_upload_options = api .authenticated()? - .get_chunk_upload_options(org)?; + .get_chunk_upload_options(&org)?; if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) { bail!( @@ -153,7 +149,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { ); } - let options = ChunkOptions::new(chunk_upload_options, org, project) + let options = ChunkOptions::new(chunk_upload_options, &org, &project) .with_max_wait(DEFAULT_MAX_WAIT); let chunked = Chunked::from(object, options.server_options().chunk_size); diff --git a/src/config.rs b/src/config.rs index fd844f1236..4f62b444d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -320,13 +320,17 @@ impl Config { } } - /// Given a match object from clap, this returns the org from it. - pub fn get_org(&self, matches: &ArgMatches) -> Result { + /// Resolves the org from CLI input, environment variables, config, and auth token. + /// + /// The resolution order is: + /// 1. Org embedded in auth token (takes precedence, with warning if different from CLI) + /// 2. CLI argument or SENTRY_ORG environment variable + /// 3. Config file defaults + pub fn get_org_with_cli_input(&self, cli_org: Option<&str>) -> Result { let org_from_token = self.cached_token_data.as_ref().map(|t| &t.org); - let org_from_cli = matches - .get_one::("org") - .cloned() + let org_from_cli = cli_org + .map(str::to_owned) .or_else(|| env::var("SENTRY_ORG").ok()); match (org_from_token, org_from_cli) { @@ -352,6 +356,12 @@ impl Config { } } + /// Given a match object from clap, this returns the org from it. + pub fn get_org(&self, matches: &ArgMatches) -> Result { + let cli_org = matches.get_one::("org").map(String::as_str); + self.get_org_with_cli_input(cli_org) + } + /// Given a match object from clap, this returns the release from it. pub fn get_release(&self, matches: &ArgMatches) -> Result { matches diff --git a/tests/integration/test_utils/test_manager.rs b/tests/integration/test_utils/test_manager.rs index ba041108d2..7f37b094dd 100644 --- a/tests/integration/test_utils/test_manager.rs +++ b/tests/integration/test_utils/test_manager.rs @@ -205,6 +205,16 @@ impl AssertCmdTestManager { self } + /// Set a custom environment variable for the test. + pub fn env( + mut self, + key: impl AsRef, + value: impl AsRef, + ) -> Self { + self.command.env(key, value); + self + } + /// Run the command and perform assertions. /// /// This function asserts both the mocks and the command result. diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs index 784ceeeda5..af6c356563 100644 --- a/tests/integration/upload_dart_symbol_map.rs +++ b/tests/integration/upload_dart_symbol_map.rs @@ -3,6 +3,11 @@ use std::sync::atomic::{AtomicU8, Ordering}; use crate::integration::test_utils::AssertCommand; use crate::integration::{MockEndpointBuilder, TestManager}; +/// A test org auth token with org="wat-org" and empty URL. +/// Format: sntrys_{base64_payload}_{base64_secret} +/// Payload: {"iat":1704374159.069583,"url":"","region_url":"","org":"wat-org"} +const ORG_AUTH_TOKEN_WAT_ORG: &str = "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"; + #[test] fn command_upload_dart_symbol_map_missing_capability() { // Server does not advertise `dartsymbolmap` capability → command should bail early. @@ -102,3 +107,79 @@ fn command_upload_dart_symbol_map_invalid_mapping() { .with_default_token() .run_and_assert(AssertCommand::Failure); } + +#[test] +fn command_upload_dart_symbol_map_org_from_token() { + // When no --org is provided and SENTRY_ORG is not set, the org should be resolved + // from the org auth token. This test verifies the fix for CLI-260. + // + // The test uses an org auth token with org="wat-org" (matching mock server paths). + // By unsetting SENTRY_ORG and not providing --org, we verify the org is extracted + // from the token. + let call_count = AtomicU8::new(0); + + TestManager::new() + // Server advertises capability including `dartsymbolmap`. + // This endpoint uses "wat-org" in the path - if org resolution fails, + // the request would go to a different path and not match. + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("dart_symbol_map/get-chunk-upload.json"), + ) + // Accept chunk upload requests for the missing chunks + .mock_endpoint(MockEndpointBuilder::new( + "POST", + "/api/0/organizations/wat-org/chunk-upload/", + )) + // Assemble flow: 1) not_found (missingChunks), 2) created, 3) ok + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_response_fn(move |request| { + let body = request.body().expect("body should be readable"); + let body_json: serde_json::Value = + serde_json::from_slice(body).expect("request body should be valid JSON"); + + let (checksum, _obj) = body_json + .as_object() + .and_then(|m| m.iter().next()) + .map(|(k, v)| (k.clone(), v.clone())) + .expect("assemble request must contain at least one object"); + + match call_count.fetch_add(1, Ordering::Relaxed) { + 0 => format!( + "{{\"{checksum}\":{{\"state\":\"not_found\",\"missingChunks\":[\"{checksum}\"]}}}}" + ) + .into(), + 1 => format!( + "{{\"{checksum}\":{{\"state\":\"created\",\"missingChunks\":[]}}}}" + ) + .into(), + 2 => format!( + "{{\"{checksum}\":{{\"state\":\"ok\",\"detail\":null,\"missingChunks\":[],\"dif\":{{\"id\":\"1\",\"uuid\":\"00000000-0000-0000-0000-000000000000\",\"debugId\":\"00000000-0000-0000-0000-000000000000\",\"objectName\":\"dartsymbolmap.json\",\"cpuName\":\"any\",\"headers\":{{\"Content-Type\":\"application/octet-stream\"}},\"size\":1,\"sha1\":\"{checksum}\",\"dateCreated\":\"1776-07-04T12:00:00.000Z\",\"data\":{{}}}}}}}}" + ) + .into(), + n => panic!( + "Only 3 calls to the assemble endpoint expected, but there were {}.", + n + 1 + ), + } + }) + .expect(3), + ) + .assert_cmd([ + "dart-symbol-map", + "upload", + // No --org flag provided! + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + // Use org auth token with embedded org="wat-org" instead of default token + .env("SENTRY_AUTH_TOKEN", ORG_AUTH_TOKEN_WAT_ORG) + // Explicitly unset SENTRY_ORG to ensure org comes from token + .env("SENTRY_ORG", "") + .run_and_assert(AssertCommand::Success); +}