Skip to content

Commit a2cca12

Browse files
committed
test: every error variant's code is checked without a hand-kept list of codes
Each of stack-profile, stack-auth, stack-kms and stack-encrypt carried a public ERROR_CODES list that only its own tests read: a test built every variant, required its code to be listed, and required every listed code to be produced. Adding or renaming a code meant editing the list too, and the list still could not catch a variant missing from the test's rows. The lists are gone. Each crate's test builds one of every variant and checks its code is in the crate's namespace and snake_case, or, for a variant that wraps another crate's error, in that crate's. The rows are written `pattern => value` through a small test macro whose patterns are the arms of a match with no wildcard, so a variant without a row fails to compile, and each row must build the variant its pattern names. That found two StackKmsBuilderError variants, Auth and InvalidConfig, the old test never built. AuthError::ERROR_CODES, the frozen list the TypeScript bindings and the Go auth guest read, is unchanged. A renamed code now shows only as the changed #[diagnostic(code(...))] line in review. Refs #1099 Claude-Session: https://claude.ai/code/session_01URtfKsTToFUCRwq3g7gCUf
1 parent 2c9bfaf commit a2cca12

12 files changed

Lines changed: 380 additions & 435 deletions

File tree

‎packages/stack-auth/src/error.rs‎

Lines changed: 41 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::access_key;
2626
/// [`error_code`](Self::error_code) is the frozen code the TypeScript
2727
/// bindings publish (`INVALID_CRN`), kept unchanged beside the miette
2828
/// [`code`](miette::Diagnostic::code) (`stack_auth::invalid_crn`) every error
29-
/// also carries; [`ERROR_CODES`](crate::ERROR_CODES) lists the miette codes.
29+
/// also carries.
3030
pub trait AuthErrorKind: std::error::Error + miette::Diagnostic {
3131
/// Stable machine-readable identifier surfaced across FFI boundaries
3232
/// (e.g. JS `Error.code`). Named `error_code` to avoid colliding with
@@ -985,44 +985,6 @@ impl ErrorPayload for AuthError {
985985
}
986986
}
987987

988-
/// Every miette code an error from this crate can carry: [`AuthError`]'s
989-
/// twenty own codes and [`InvalidAccessKey`](crate::InvalidAccessKey)'s
990-
/// four. `DeviceClientError` adds none: each of its variants carries the
991-
/// code of the [`AuthError`] it converts into. A store failure carries a
992-
/// `stack_profile` code ([`StoreError`]), so it is not here.
993-
///
994-
/// Not [`AuthError::ERROR_CODES`]: that is the frozen list of `INVALID_CRN`
995-
/// style codes the TypeScript bindings publish. A test maps each
996-
/// [`AuthError`] miette code to its frozen code, so the two cannot drift,
997-
/// and another builds every variant and checks its code is here, so renaming
998-
/// a code means editing this list on purpose.
999-
pub const ERROR_CODES: &[&str] = &[
1000-
"stack_auth::request_error",
1001-
"stack_auth::access_denied",
1002-
"stack_auth::invalid_grant",
1003-
"stack_auth::invalid_client",
1004-
"stack_auth::invalid_url",
1005-
"stack_auth::invalid_region",
1006-
"stack_auth::invalid_crn",
1007-
"stack_auth::workspace_mismatch",
1008-
"stack_auth::invalid_workspace_id",
1009-
"stack_auth::missing_workspace_crn",
1010-
"stack_auth::not_authenticated",
1011-
"stack_auth::expired_token",
1012-
"stack_auth::invalid_access_key",
1013-
"stack_auth::invalid_token",
1014-
"stack_auth::usage_limit_exceeded",
1015-
"stack_auth::org_not_provisioned",
1016-
"stack_auth::server_error",
1017-
"stack_auth::already_consumed",
1018-
"stack_auth::internal_error",
1019-
"stack_auth::custom",
1020-
"stack_auth::access_key_missing_prefix",
1021-
"stack_auth::access_key_missing_dot",
1022-
"stack_auth::access_key_empty_id",
1023-
"stack_auth::access_key_empty_secret",
1024-
];
1025-
1026988
// ---------------------------------------------------------------------------
1027989
// Ergonomic `From<Foreign>` impls — keep `?` working where call sites lift a
1028990
// foreign error straight into `AuthError` (the per-struct wrapping is internal).
@@ -1674,41 +1636,59 @@ mod tests {
16741636
);
16751637
}
16761638

1677-
/// Every code an error from this crate produces is in [`ERROR_CODES`],
1678-
/// in this crate's namespace, and every listed code is produced.
1639+
/// One row per variant of an enum, written `pattern => value`. The
1640+
/// patterns are the arms of a match with no wildcard, so a variant with
1641+
/// no row fails to compile, and each value must match its own pattern.
1642+
macro_rules! variants {
1643+
($($pattern:pat => $value:expr),+ $(,)?) => {{
1644+
let rows = vec![$({
1645+
let value = $value;
1646+
assert!(matches!(value, $pattern), "{value:?} is not {}", stringify!($pattern));
1647+
value
1648+
}),+];
1649+
for row in &rows {
1650+
match row {
1651+
$($pattern => {})+
1652+
}
1653+
}
1654+
rows
1655+
}};
1656+
}
1657+
1658+
/// Every variant has a code in this crate's namespace and `snake_case`,
1659+
/// save a store failure, whose code is its profile error's.
1660+
/// [`every_variant`] has a row for every frozen code, so every
1661+
/// [`AuthError`] variant is here; the access-key rows cover
1662+
/// [`InvalidAccessKey`](crate::InvalidAccessKey).
16791663
#[test]
1680-
fn every_variant_has_a_listed_code() {
1664+
fn every_variant_has_a_code_of_this_crate() {
1665+
use crate::InvalidAccessKey;
16811666
use miette::Diagnostic;
16821667
use stack_profile::diagnostic::is_code_of;
16831668
let mut errors: Vec<Box<dyn Diagnostic>> = every_variant()
16841669
.into_iter()
16851670
.map(|(error, _, _)| Box::new(error) as Box<dyn Diagnostic>)
16861671
.collect();
1687-
for key in ["CSAK", "nope", "CSAK.secret", "CSAKid."] {
1688-
errors.push(Box::new(
1689-
key.parse::<crate::access_key::AccessKey>().unwrap_err(),
1690-
));
1691-
}
1692-
let mut seen = std::collections::BTreeSet::new();
1672+
errors.extend(
1673+
variants![
1674+
InvalidAccessKey::MissingPrefix => InvalidAccessKey::MissingPrefix,
1675+
InvalidAccessKey::MissingDot => InvalidAccessKey::MissingDot,
1676+
InvalidAccessKey::EmptyKeyId => InvalidAccessKey::EmptyKeyId,
1677+
InvalidAccessKey::EmptySecret => InvalidAccessKey::EmptySecret,
1678+
]
1679+
.into_iter()
1680+
.map(|error| Box::new(error) as Box<dyn Diagnostic>),
1681+
);
16931682
for error in &errors {
16941683
let code = error
16951684
.code()
16961685
.unwrap_or_else(|| panic!("{error:?} has no code"))
16971686
.to_string();
1698-
if code.starts_with("stack_profile::") {
1699-
assert!(
1700-
stack_profile::ERROR_CODES.contains(&code.as_str()),
1701-
"{code}"
1702-
);
1703-
continue;
1704-
}
1705-
assert!(is_code_of("stack_auth", &code), "{code}");
1706-
assert!(ERROR_CODES.contains(&code.as_str()), "{code} is unlisted");
1707-
seen.insert(code);
1687+
assert!(
1688+
is_code_of("stack_auth", &code) || is_code_of("stack_profile", &code),
1689+
"{code}"
1690+
);
17081691
}
1709-
let listed: std::collections::BTreeSet<String> =
1710-
ERROR_CODES.iter().map(|code| code.to_string()).collect();
1711-
assert_eq!(seen, listed, "every listed code is produced");
17121692
}
17131693

17141694
/// A store failure's payload is the profile error's, so a binding

‎packages/stack-auth/src/lib.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ mod oidc_refresher;
7777
mod refresher;
7878

7979
pub use error::StoreError;
80-
pub use error::ERROR_CODES;
8180
pub use error::{
8281
AccessDenied, AlreadyConsumed, AuthError, AuthErrorKind, CustomError, InternalError,
8382
InvalidAccessKeyError, InvalidClient, InvalidCrn, InvalidGrant, InvalidToken, InvalidUrl,

‎packages/stack-encrypt/CHANGELOG.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
`stack_encrypt::foreign_keyset`. Help text explains how to resolve
1515
errors callers can fix. `ErrorPayload::payload()` provides details such
1616
as both keyset IDs for a wrong-keyset error or the field rejected by a
17-
plan. `ErrorPayload` is re-exported from `stack-profile`, and
18-
`ERROR_CODES` lists the crate's codes. Codes identify errors across
19-
language boundaries; Rust callers can continue matching enum variants.
17+
plan. `ErrorPayload` is re-exported from `stack-profile`. Codes
18+
identify errors across language boundaries; Rust callers can continue
19+
matching enum variants.
2020
The payload policy excludes plaintext, key material, tokens,
2121
ciphertext, search-index bytes, and raw context values.
2222
- **Data-driven input errors identify the field and the reason.**

‎packages/stack-encrypt/src/cipher.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ pub type StackCipherText = CipherText<SealedValue, BoxedPassthrough>;
120120

121121
/// Errors from sealing or opening a [`StackCipherText`].
122122
///
123-
/// Every variant has a miette code in [`ERROR_CODES`](crate::ERROR_CODES),
124-
/// or forwards the code of the error it carries ([`Kms`](Self::Kms),
125-
/// [`Term`](Self::Term), [`Plan`](Self::Plan)); its structured fields are its
123+
/// Every variant has a `stack_encrypt::` miette code, or forwards the code
124+
/// of the error it carries ([`Kms`](Self::Kms), [`Term`](Self::Term),
125+
/// [`Plan`](Self::Plan)); its structured fields are its
126126
/// [`ErrorPayload`](crate::ErrorPayload). No message carries a context's
127127
/// descriptor or another library's text (see the rule on
128128
/// [`ErrorPayload`](crate::ErrorPayload)).

0 commit comments

Comments
 (0)