Skip to content
Draft
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 12 additions & 16 deletions src/commands/dart_symbol_map/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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);
Expand Down
20 changes: 15 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
/// 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<String> {
let org_from_token = self.cached_token_data.as_ref().map(|t| &t.org);

let org_from_cli = matches
.get_one::<String>("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) {
Expand All @@ -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<String> {
let cli_org = matches.get_one::<String>("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<String> {
matches
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/test_utils/test_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ impl AssertCmdTestManager {
self
}

/// Set a custom environment variable for the test.
pub fn env(
mut self,
key: impl AsRef<std::ffi::OsStr>,
value: impl AsRef<std::ffi::OsStr>,
) -> Self {
self.command.env(key, value);
self
}

/// Run the command and perform assertions.
///
/// This function asserts both the mocks and the command result.
Expand Down
81 changes: 81 additions & 0 deletions tests/integration/upload_dart_symbol_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}