Skip to content

Commit a9cc8c0

Browse files
committed
unified auth token env var, fixed some error messages, removed need for toml file if env vars cover all of the necessary items
1 parent f588315 commit a9cc8c0

7 files changed

Lines changed: 715 additions & 113 deletions

File tree

src/auth.rs

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ struct Credentials {
2727
email: Option<String>,
2828
}
2929

30+
#[derive(Debug, PartialEq, Eq)]
3031
pub enum AuthSource {
3132
Environment,
3233
File,
@@ -118,26 +119,44 @@ impl AuthManager {
118119
}
119120

120121
pub fn get_auth_info(&self) -> AuthInfo {
121-
// Check environment first
122-
if let Ok(api_key) = std::env::var("WAVEDASH_TOKEN") {
123-
if !api_key.is_empty() {
122+
Self::resolve_auth(
123+
std::env::var(config::ENV_TOKEN).ok(),
124+
self.read_file_credentials(),
125+
)
126+
}
127+
128+
/// Precedence and blank handling, split out from [`Self::get_auth_info`] so
129+
/// it's testable without mutating the process environment or reading the
130+
/// real credentials file.
131+
///
132+
/// Blank counts as unset, the same rule every other `WAVEDASH_*` variable
133+
/// follows: an unpopulated CI secret falls back to stored credentials rather
134+
/// than being sent as a bare `Bearer `, which returns a 401 telling the user
135+
/// to run an interactive login they can't run. Trimming matters just as
136+
/// much — a `WAVEDASH_TOKEN=$(cat key)` trailing newline is not a valid
137+
/// header value, and reqwest rejects it as "failed to parse header value"
138+
/// with nothing to say it was the token.
139+
fn resolve_auth(env_token: Option<String>, file: Option<Credentials>) -> AuthInfo {
140+
if let Some(api_key) = env_token.and_then(config::non_blank) {
141+
return AuthInfo {
142+
source: AuthSource::Environment,
143+
api_key: Some(api_key),
144+
email: None, // No email available from env var
145+
};
146+
}
147+
148+
// A blank key on disk is a corrupt or half-written credentials file, and
149+
// is no more usable than a blank variable.
150+
if let Some(creds) = file {
151+
if let Some(api_key) = config::non_blank(creds.api_key) {
124152
return AuthInfo {
125-
source: AuthSource::Environment,
153+
source: AuthSource::File,
126154
api_key: Some(api_key),
127-
email: None, // No email available from env var
155+
email: creds.email,
128156
};
129157
}
130158
}
131159

132-
// Check file
133-
if let Some(creds) = self.read_file_credentials() {
134-
return AuthInfo {
135-
source: AuthSource::File,
136-
api_key: Some(creds.api_key),
137-
email: creds.email,
138-
};
139-
}
140-
141160
AuthInfo {
142161
source: AuthSource::None,
143162
api_key: None,
@@ -496,6 +515,63 @@ pub async fn login_with_browser() -> Result<LoginResult> {
496515
mod tests {
497516
use super::*;
498517

518+
fn stored(api_key: &str) -> Option<Credentials> {
519+
Some(Credentials {
520+
api_key: api_key.to_string(),
521+
email: Some("dev@wavedash.com".to_string()),
522+
})
523+
}
524+
525+
#[test]
526+
fn env_token_wins_over_stored_credentials() {
527+
let info = AuthManager::resolve_auth(Some("from_env".into()), stored("from_file"));
528+
529+
assert_eq!(info.source, AuthSource::Environment);
530+
assert_eq!(info.api_key.as_deref(), Some("from_env"));
531+
}
532+
533+
/// The CI shape: an unpopulated secret expands to "", which must not shadow
534+
/// the stored credentials or be sent as a bare `Bearer `.
535+
#[test]
536+
fn a_blank_env_token_falls_back_to_stored_credentials() {
537+
for blank in ["", " ", "\t", "\n"] {
538+
let info = AuthManager::resolve_auth(Some(blank.into()), stored("from_file"));
539+
540+
assert_eq!(info.source, AuthSource::File, "blank: {:?}", blank);
541+
assert_eq!(info.api_key.as_deref(), Some("from_file"));
542+
}
543+
}
544+
545+
#[test]
546+
fn a_blank_env_token_with_nothing_stored_is_unauthenticated() {
547+
let info = AuthManager::resolve_auth(Some(" ".into()), None);
548+
549+
assert_eq!(info.source, AuthSource::None);
550+
assert!(info.api_key.is_none());
551+
}
552+
553+
/// `WAVEDASH_TOKEN=$(cat key.txt)` keeps the trailing newline, which is not a
554+
/// legal header value — trim it here rather than fail opaquely at send time.
555+
#[test]
556+
fn an_env_token_is_trimmed_so_it_survives_becoming_a_header() {
557+
let info = AuthManager::resolve_auth(Some(" wdcli_abc123\n".into()), None);
558+
559+
assert_eq!(info.api_key.as_deref(), Some("wdcli_abc123"));
560+
assert!(reqwest::header::HeaderValue::from_str(&format!(
561+
"Bearer {}",
562+
info.api_key.unwrap()
563+
))
564+
.is_ok());
565+
}
566+
567+
#[test]
568+
fn a_blank_stored_key_is_unauthenticated() {
569+
let info = AuthManager::resolve_auth(None, stored(" "));
570+
571+
assert_eq!(info.source, AuthSource::None);
572+
assert!(info.api_key.is_none());
573+
}
574+
499575
#[test]
500576
fn derives_rfc7636_s256_pkce_challenge() {
501577
let challenge = pkce_challenge_from_verifier("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");

src/builds.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ pub async fn handle_build_push(
147147
let config_dir = config_path
148148
.parent()
149149
.ok_or_else(|| anyhow::anyhow!("Config file has no parent directory"))?;
150-
let upload_dir = config_dir.join(wavedash_config.upload_dir());
150+
let upload_dir = config_dir.join(wavedash_config.upload_dir()?);
151151

152152
// Verify source directory exists
153153
if !upload_dir.exists() {
@@ -170,7 +170,7 @@ pub async fn handle_build_push(
170170
let engine_kind = wavedash_config.engine_type()?;
171171
let creds = get_temp_credentials(
172172
BuildUploadInfo {
173-
game_id: wavedash_config.game_id(),
173+
game_id: wavedash_config.game_id()?,
174174
engine: engine_kind.map(|e| e.as_label()),
175175
engine_version: wavedash_config.engine_version(),
176176
entrypoint: wavedash_config.entrypoint(),
@@ -198,7 +198,7 @@ pub async fn handle_build_push(
198198

199199
// Notify the server that upload is complete
200200
let result =
201-
notify_upload_complete(wavedash_config.game_id(), &creds.game_build_id, &api_key).await?;
201+
notify_upload_complete(wavedash_config.game_id()?, &creds.game_build_id, &api_key).await?;
202202

203203
// Print the play URL
204204
let site_host = config::get("open_browser_website_host")?;

0 commit comments

Comments
 (0)