Skip to main content

tfparser_core/provider/
resolver.rs

1//! `DefaultProviderResolver` — the last fill phase before the exporter.
2//!
3//! Per [16-provider-resolver.md § 4]. The resolver walks each component,
4//! resolves `provider = aws.<alias>` references to a [`ProviderBlock`], and
5//! for every [`Resource`] populates `account_id`, `account_name`, and
6//! `region`. It also fills `state_account_id` / `state_region` on the
7//! component's [`StateBackend`].
8//!
9//! Resolution is **deterministic** given `(Workspace, ProviderContext)` —
10//! I-PROV-1. The resolver is `Send + Sync`; mutation is performed on the
11//! borrowed `Workspace` in place — I-PROV-5 (the resolver writes only the
12//! fields it owns).
13//!
14//! [16-provider-resolver.md § 4]: ../../../specs/16-provider-resolver.md
15
16use std::{
17    collections::{BTreeSet, HashSet},
18    sync::Arc,
19};
20
21use crate::{
22    Result,
23    diagnostic::{Diagnostic, Severity},
24    ir::{
25        AccountId, AssumeRole, Component, Expression, ProviderBlock, ProviderRef, Region,
26        StateBackend, Value, Workspace,
27    },
28    provider::{
29        error::ProviderError,
30        profile_map::{ProfileEntry, ProfileMap},
31    },
32};
33
34/// Per-resolution context — `Arc<ProfileMap>` plus operator-driven flags.
35///
36/// Per spec § 2.
37#[derive(Clone, Debug)]
38#[non_exhaustive]
39pub struct ProviderContext {
40    /// Profile-name → account/region/role map.
41    pub profile_map: Arc<ProfileMap>,
42    /// Default region used when no other resolution succeeds. CLI binds
43    /// `--region` here; otherwise `None`.
44    pub default_region: Option<Region>,
45    /// When `true`, any profile referenced from source that is **not** in
46    /// the profile map raises [`ProviderError::StrictUnresolved`] from
47    /// [`DefaultProviderResolver::resolve`]; otherwise we emit a single
48    /// diagnostic per distinct profile (I-PROV-2).
49    pub strict: bool,
50}
51
52impl ProviderContext {
53    /// Construct a context with the spec defaults (`strict = false`,
54    /// no default region).
55    #[must_use]
56    pub fn new(profile_map: Arc<ProfileMap>) -> Self {
57        Self {
58            profile_map,
59            default_region: None,
60            strict: false,
61        }
62    }
63}
64
65/// Trait the orchestrator (Phase 9 CLI) calls. Phase 7 ships exactly one
66/// implementation, [`DefaultProviderResolver`]; downstream tests may
67/// swap in a stub.
68pub trait ProviderResolver: Send + Sync + std::fmt::Debug {
69    /// Fill `account_id` / `account_name` / `region` (resources) and
70    /// `state_account_id` / `state_region` (state backends) per spec § 4.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`ProviderError::StrictUnresolved`] only when
75    /// `ctx.strict == true` and at least one profile cannot be mapped.
76    /// All other anomalies (alias-not-found, malformed role ARN) attach
77    /// to `ws.diagnostics` and the call returns `Ok(())`.
78    fn resolve(&self, ws: &mut Workspace, ctx: &ProviderContext) -> Result<()>;
79}
80
81/// Default implementation per spec § 4. Stateless.
82#[derive(Clone, Copy, Debug, Default)]
83pub struct DefaultProviderResolver;
84
85impl DefaultProviderResolver {
86    /// Construct a resolver. Free-standing convenience over `default()`.
87    #[must_use]
88    pub const fn new() -> Self {
89        Self
90    }
91}
92
93impl ProviderResolver for DefaultProviderResolver {
94    fn resolve(&self, ws: &mut Workspace, ctx: &ProviderContext) -> Result<()> {
95        // Workspace-scoped deduplication: spec § 6 — emit one
96        // `MissingProfileMapping` per distinct unresolved profile name,
97        // not per resource site.
98        let mut unresolved: BTreeSet<Arc<str>> = BTreeSet::new();
99        let mut new_diags: Vec<Diagnostic> = Vec::new();
100
101        for component in &mut ws.components {
102            resolve_component(component, ctx, &mut unresolved, &mut new_diags);
103        }
104
105        // Emit one diagnostic per distinct unresolved profile.
106        for profile in &unresolved {
107            new_diags.push(
108                Diagnostic::new(
109                    Severity::Warn,
110                    "TF1601",
111                    format!("missing profile-map entry for `{profile}`"),
112                )
113                .with_suggestion(Arc::<str>::from(
114                    "add the profile to your profile-map.yaml or to ~/.aws/config",
115                )),
116            );
117        }
118
119        // Strict mode short-circuits — the workspace is in a consistent
120        // post-resolve state, but the operator asked us to fail.
121        if ctx.strict && !unresolved.is_empty() {
122            let first = unresolved
123                .iter()
124                .next()
125                .cloned()
126                .unwrap_or_else(|| Arc::from("<unknown>"));
127            // Diagnostics already attached; surface the strict-mode
128            // failure separately so the caller can branch.
129            ws.diagnostics.extend(new_diags);
130            return Err(ProviderError::StrictUnresolved {
131                count: unresolved.len(),
132                first,
133            }
134            .into());
135        }
136        ws.diagnostics.extend(new_diags);
137        Ok(())
138    }
139}
140
141/// Component-level resolution. Iterates resources once.
142fn resolve_component(
143    component: &mut Component,
144    ctx: &ProviderContext,
145    unresolved: &mut BTreeSet<Arc<str>>,
146    diagnostics: &mut Vec<Diagnostic>,
147) {
148    // Pre-index providers by alias (and default = `None`) so per-resource
149    // lookups are O(1).
150    let providers: Vec<ProviderBlock> = component.providers.clone();
151    let mut alias_not_found_emitted: HashSet<Arc<str>> = HashSet::new();
152
153    let cascade_region_v: Option<Region> = component
154        .terragrunt
155        .as_ref()
156        .and_then(cascade_region)
157        .or_else(|| ctx.default_region.clone());
158
159    let cascade_account_v: Option<AccountId> =
160        component.terragrunt.as_ref().and_then(cascade_account);
161
162    for resource in &mut component.resources {
163        let provider = pick_provider(&providers, resource.provider_ref.as_ref());
164
165        if let Some(local) = resource
166            .provider_ref
167            .as_ref()
168            .filter(|_| provider.is_none())
169            .map(|r| Arc::clone(&r.local_name))
170        {
171            let alias = resource
172                .provider_ref
173                .as_ref()
174                .and_then(|r| r.alias.clone())
175                .unwrap_or_else(|| Arc::from("default"));
176            if alias_not_found_emitted.insert(Arc::clone(&alias)) {
177                diagnostics.push(Diagnostic::new(
178                    Severity::Warn,
179                    "TF1602",
180                    format!(
181                        "provider alias `{local}.{alias}` referenced by `{}` not declared in \
182                         component `{}`",
183                        resource.address.as_str(),
184                        component.path.display()
185                    ),
186                ));
187            }
188        }
189
190        let region = first_resolved_region(
191            provider,
192            cascade_region_v.as_ref(),
193            ctx.default_region.as_ref(),
194        );
195        let (account_id, account_name) = first_resolved_account(
196            provider,
197            ctx,
198            unresolved,
199            diagnostics,
200            cascade_account_v.as_ref(),
201        );
202
203        resource.region = region;
204        resource.account_id = account_id;
205        resource.account_name = account_name;
206    }
207
208    // State-backend fill per spec § 4 trailing block.
209    if let Some(backend) = component.state_backend.as_mut() {
210        fill_state_backend(backend, ctx, unresolved, diagnostics);
211    }
212    if let Some(tg) = component.terragrunt.as_mut()
213        && let Some(backend) = tg.state_backend.as_mut()
214    {
215        fill_state_backend(backend, ctx, unresolved, diagnostics);
216    }
217}
218
219/// Pick the matching `provider` block for the resource's reference.
220fn pick_provider<'a>(
221    providers: &'a [ProviderBlock],
222    pref: Option<&ProviderRef>,
223) -> Option<&'a ProviderBlock> {
224    match pref {
225        Some(r) => providers.iter().find(|p| {
226            p.local_name == r.local_name && p.alias.as_ref().map(Arc::as_ref) == r.alias.as_deref()
227        }),
228        None => providers
229            .iter()
230            .find(|p| p.alias.is_none() && &*p.local_name == "aws")
231            .or_else(|| providers.iter().find(|p| p.alias.is_none())),
232    }
233}
234
235fn first_resolved_region(
236    provider: Option<&ProviderBlock>,
237    cascade: Option<&Region>,
238    default: Option<&Region>,
239) -> Option<Region> {
240    if let Some(p) = provider
241        && let Some(r) = p.region_expr.as_ref().and_then(literal_region)
242    {
243        return Some(r);
244    }
245    cascade.cloned().or_else(|| default.cloned())
246}
247
248/// Resolve `(account_id, account_name)` per spec § 4 / 4.1 / 4.2.
249fn first_resolved_account(
250    provider: Option<&ProviderBlock>,
251    ctx: &ProviderContext,
252    unresolved: &mut BTreeSet<Arc<str>>,
253    _diagnostics: &mut [Diagnostic],
254    cascade: Option<&AccountId>,
255) -> (Option<AccountId>, Option<Arc<str>>) {
256    // 1. assume_role.role_arn
257    if let Some(p) = provider
258        && let Some(arn_str) = p.assume_role.as_ref().and_then(assume_role_arn)
259        && let Some(id) = extract_account_id(arn_str.as_ref())
260    {
261        // Account name: look up by ARN in the profile map (typical
262        // operator labels their roles with the same account name). When
263        // multiple profiles share an account id, take the lexicographically
264        // smallest profile name so the result is deterministic
265        // (I-PROV-1) rather than `HashMap`-iteration-order-dependent.
266        let name = lookup_name_by_account(&ctx.profile_map, &id);
267        return (Some(id), name);
268    }
269
270    // 2. provider.profile_expr → profile_map.lookup
271    if let Some(p) = provider
272        && let Some(profile) = p.profile_expr.as_ref().and_then(literal_str)
273    {
274        if let Some(entry) = ctx.profile_map.lookup(&profile) {
275            return (
276                Some(entry.account_id.clone()),
277                Some(Arc::clone(&entry.account_name)),
278            );
279        }
280        unresolved.insert(profile);
281    }
282
283    // 3. cascade locals (Terragrunt-supplied `aws_account_id`)
284    if let Some(id) = cascade.cloned() {
285        let name = lookup_name_by_account(&ctx.profile_map, &id);
286        return (Some(id), name);
287    }
288
289    (None, None)
290}
291
292/// Reverse-lookup an account-name from the profile map by `AccountId`. When
293/// multiple profiles map to the same account, returns the
294/// lexicographically-smallest profile's `account_name`. Deterministic by
295/// construction — required by I-PROV-1.
296fn lookup_name_by_account(map: &ProfileMap, id: &AccountId) -> Option<Arc<str>> {
297    let mut hits: Vec<(&Arc<str>, &Arc<str>)> = map
298        .iter()
299        .filter(|(_, entry)| &entry.account_id == id)
300        .map(|(profile, entry)| (profile, &entry.account_name))
301        .collect();
302    hits.sort_by(|a, b| a.0.cmp(b.0));
303    hits.first().map(|(_, name)| Arc::clone(name))
304}
305
306fn fill_state_backend(
307    backend: &mut StateBackend,
308    ctx: &ProviderContext,
309    unresolved: &mut BTreeSet<Arc<str>>,
310    diagnostics: &mut [Diagnostic],
311) {
312    let attrs = &backend.attributes;
313
314    // 1. profile → profile_map
315    if backend.state_account_id.is_none()
316        && let Some(profile) = attr_str(attrs, "profile")
317    {
318        if let Some(entry) = ctx.profile_map.lookup(&profile) {
319            backend.state_account_id = Some(entry.account_id.clone());
320        } else {
321            unresolved.insert(profile);
322        }
323    }
324
325    // 2. role_arn → extract_account_id
326    if backend.state_account_id.is_none()
327        && let Some(arn) = attr_str(attrs, "role_arn")
328        && let Some(id) = extract_account_id(&arn)
329    {
330        backend.state_account_id = Some(id);
331    }
332
333    // 3. region (direct)
334    if backend.state_region.is_none()
335        && let Some(r) = attr_str(attrs, "region")
336        && let Ok(parsed) = Region::new(&r)
337    {
338        backend.state_region = Some(parsed);
339    }
340
341    let _ = diagnostics; // diagnostics for malformed values: future hook (P-068).
342}
343
344// ----------------------------------------------------------------------------
345// Helpers
346// ----------------------------------------------------------------------------
347
348/// Parse an `arn:aws:iam::<12 digits>:role/<...>` and return the 12-digit
349/// account id. Returns `None` for any other shape — including
350/// `assume_role`-less providers and malformed ARNs.
351///
352/// Per spec § 4.1.
353#[must_use]
354pub fn extract_account_id(role_arn: &str) -> Option<AccountId> {
355    let rest = role_arn.strip_prefix("arn:aws:iam::")?;
356    let (account_id, _) = rest.split_once(":role/")?;
357    AccountId::new(account_id).ok()
358}
359
360fn literal_str(expr: &Expression) -> Option<Arc<str>> {
361    match expr {
362        Expression::Literal(Value::Str(s)) => Some(Arc::clone(s)),
363        _ => None,
364    }
365}
366
367fn literal_region(expr: &Expression) -> Option<Region> {
368    let s = literal_str(expr)?;
369    Region::new(s.as_ref()).ok()
370}
371
372fn assume_role_arn(ar: &AssumeRole) -> Option<Arc<str>> {
373    literal_str(&ar.role_arn_expr)
374}
375
376fn attr_str(attrs: &crate::ir::AttributeMap, key: &str) -> Option<Arc<str>> {
377    attrs
378        .iter()
379        .find(|(k, _)| k.as_ref() == key)
380        .and_then(|(_, v)| literal_str(v))
381}
382
383fn cascade_region(tg: &crate::ir::TerragruntConfig) -> Option<Region> {
384    tg.effective_locals
385        .iter()
386        .find(|(k, _)| k.as_ref() == "aws_region")
387        .and_then(|(_, v)| match v {
388            Value::Str(s) => Region::new(s.as_ref()).ok(),
389            _ => None,
390        })
391}
392
393fn cascade_account(tg: &crate::ir::TerragruntConfig) -> Option<AccountId> {
394    tg.effective_locals
395        .iter()
396        .find(|(k, _)| k.as_ref() == "aws_account_id")
397        .and_then(|(_, v)| match v {
398            Value::Str(s) => AccountId::new(s.as_ref()).ok(),
399            _ => None,
400        })
401}
402
403#[allow(dead_code)]
404fn _profile_entry_dbg(e: &ProfileEntry) -> &ProfileEntry {
405    e
406}
407
408#[cfg(test)]
409#[allow(
410    clippy::unwrap_used,
411    clippy::expect_used,
412    clippy::panic,
413    clippy::indexing_slicing
414)]
415mod tests {
416    use std::{
417        collections::HashMap,
418        path::{Path, PathBuf},
419    };
420
421    use super::*;
422    use crate::{
423        ir::{
424            Address, AssumeRole, AttributeMap, Component, ComponentId, ComponentKind, Expression,
425            ProviderBlock, ProviderRef, Resource, ResourceKind, Span, TerragruntConfig, Workspace,
426        },
427        provider::profile_map::{ProfileMap, empty},
428    };
429
430    // ARN tests
431
432    #[test]
433    fn test_should_extract_account_id_from_role_arn() {
434        let id = extract_account_id("arn:aws:iam::123456789012:role/admin").unwrap();
435        assert_eq!(id.as_str(), "123456789012");
436    }
437
438    #[test]
439    fn test_should_reject_malformed_arn() {
440        assert!(extract_account_id("not-an-arn").is_none());
441        assert!(extract_account_id("arn:aws:iam::abc:role/x").is_none());
442        assert!(extract_account_id("arn:aws:s3:::bucket").is_none());
443    }
444
445    // Resolver: provider chain → role_arn
446
447    fn synth_profile_map_with(profile: &str, account: &str, region: &str) -> Arc<ProfileMap> {
448        let f = tempfile::NamedTempFile::new().unwrap();
449        std::fs::write(
450            f.path(),
451            format!(
452                "profiles:\n  {profile}:\n    account_id: \"{account}\"\n    account_name: \
453                 \"{profile}\"\n    region: \"{region}\"\n"
454            ),
455        )
456        .unwrap();
457        crate::provider::profile_map::load_yaml_profile_map(f.path()).unwrap()
458    }
459
460    fn span() -> Span {
461        Span::synthetic()
462    }
463
464    fn resource_with_provider(addr: &str, alias: Option<&str>) -> Resource {
465        Resource::builder()
466            .address(Address::new(addr).unwrap())
467            .kind(ResourceKind::Managed)
468            .type_(Arc::<str>::from("aws_iam_role"))
469            .name(Arc::<str>::from("r"))
470            .provider_ref(alias.map(|a| {
471                ProviderRef::builder()
472                    .local_name(Arc::<str>::from("aws"))
473                    .alias(Some(Arc::<str>::from(a)))
474                    .span(span())
475                    .build()
476            }))
477            .span(span())
478            .build()
479    }
480
481    fn component_with(
482        path: &str,
483        providers: Vec<ProviderBlock>,
484        resources: Vec<Resource>,
485    ) -> Component {
486        Component::builder()
487            .id(ComponentId::from_index(0))
488            .path(Arc::<Path>::from(PathBuf::from(path)))
489            .kind(ComponentKind::Component)
490            .providers(providers)
491            .resources(resources)
492            .build()
493    }
494
495    fn workspace_with(component: Component) -> Workspace {
496        Workspace::builder()
497            .root(Arc::<Path>::from(PathBuf::from("/tmp/repo")))
498            .components(vec![component])
499            .build()
500    }
501
502    fn provider_with(
503        alias: Option<&str>,
504        profile: Option<&str>,
505        region: Option<&str>,
506    ) -> ProviderBlock {
507        ProviderBlock::builder()
508            .local_name(Arc::<str>::from("aws"))
509            .alias(alias.map(Arc::<str>::from))
510            .profile_expr(profile.map(|p| Expression::Literal(Value::Str(Arc::from(p)))))
511            .region_expr(region.map(|r| Expression::Literal(Value::Str(Arc::from(r)))))
512            .span(span())
513            .build()
514    }
515
516    #[test]
517    fn test_should_resolve_account_from_provider_profile() {
518        let map = synth_profile_map_with("primary", "100000000001", "us-west-2");
519        let resource = resource_with_provider("aws_iam_role.r", Some("main"));
520        let provider = provider_with(Some("main"), Some("primary"), Some("us-west-2"));
521        let component = component_with("svc", vec![provider], vec![resource]);
522        let mut ws = workspace_with(component);
523
524        DefaultProviderResolver
525            .resolve(&mut ws, &ProviderContext::new(map))
526            .unwrap();
527        let r = &ws.components[0].resources[0];
528        assert_eq!(
529            r.account_id.as_ref().map(AccountId::as_str),
530            Some("100000000001")
531        );
532        assert_eq!(r.region.as_ref().map(Region::as_str), Some("us-west-2"));
533        assert_eq!(r.account_name.as_deref(), Some("primary"));
534    }
535
536    #[test]
537    fn test_should_prefer_assume_role_over_profile() {
538        let map = synth_profile_map_with("primary", "100000000001", "us-west-2");
539        let mut provider = provider_with(Some("main"), Some("primary"), Some("us-west-2"));
540        provider.assume_role = Some(
541            AssumeRole::builder()
542                .role_arn_expr(Expression::Literal(Value::Str(Arc::from(
543                    "arn:aws:iam::999999999999:role/x",
544                ))))
545                .span(span())
546                .build(),
547        );
548        let resource = resource_with_provider("aws_iam_role.r", Some("main"));
549        let component = component_with("svc", vec![provider], vec![resource]);
550        let mut ws = workspace_with(component);
551
552        DefaultProviderResolver
553            .resolve(&mut ws, &ProviderContext::new(map))
554            .unwrap();
555        let r = &ws.components[0].resources[0];
556        assert_eq!(
557            r.account_id.as_ref().map(AccountId::as_str),
558            Some("999999999999")
559        );
560    }
561
562    #[test]
563    fn test_should_fall_through_to_terragrunt_cascade_account() {
564        let map = empty();
565        let resource = resource_with_provider("aws_iam_role.r", None);
566        let mut component = component_with("svc", vec![], vec![resource]);
567        component.terragrunt = Some(
568            TerragruntConfig::builder()
569                .component_dir(Arc::<Path>::from(PathBuf::from("/repo/svc")))
570                .effective_locals(vec![
571                    (
572                        Arc::from("aws_account_id"),
573                        Value::Str(Arc::from("200000000002")),
574                    ),
575                    (Arc::from("aws_region"), Value::Str(Arc::from("eu-west-1"))),
576                ])
577                .build(),
578        );
579        let mut ws = workspace_with(component);
580        DefaultProviderResolver
581            .resolve(&mut ws, &ProviderContext::new(map))
582            .unwrap();
583        let r = &ws.components[0].resources[0];
584        assert_eq!(
585            r.account_id.as_ref().map(AccountId::as_str),
586            Some("200000000002")
587        );
588        assert_eq!(r.region.as_ref().map(Region::as_str), Some("eu-west-1"));
589    }
590
591    #[test]
592    fn test_should_emit_missing_profile_diagnostic_once() {
593        let map = empty();
594        let mut providers = HashMap::<String, ProviderBlock>::new();
595        providers.insert("a".into(), provider_with(Some("a"), Some("missing"), None));
596        providers.insert("b".into(), provider_with(Some("b"), Some("missing"), None));
597        let provider_a = providers.remove("a").unwrap();
598        let provider_b = providers.remove("b").unwrap();
599        let r1 = resource_with_provider("aws_iam_role.r1", Some("a"));
600        let r2 = resource_with_provider("aws_iam_role.r2", Some("b"));
601        let component = component_with("svc", vec![provider_a, provider_b], vec![r1, r2]);
602        let mut ws = workspace_with(component);
603
604        DefaultProviderResolver
605            .resolve(&mut ws, &ProviderContext::new(map))
606            .unwrap();
607        let missing: Vec<_> = ws
608            .diagnostics
609            .iter()
610            .filter(|d| d.code.as_ref() == "TF1601")
611            .collect();
612        assert_eq!(missing.len(), 1, "expected dedup; got {missing:?}");
613    }
614
615    #[test]
616    fn test_should_emit_alias_not_found_diagnostic_per_alias() {
617        let map = empty();
618        let resource = resource_with_provider("aws_iam_role.r", Some("ghost"));
619        let component = component_with("svc", vec![], vec![resource]);
620        let mut ws = workspace_with(component);
621        DefaultProviderResolver
622            .resolve(&mut ws, &ProviderContext::new(map))
623            .unwrap();
624        assert!(
625            ws.diagnostics.iter().any(|d| d.code.as_ref() == "TF1602"),
626            "{:?}",
627            ws.diagnostics
628        );
629    }
630
631    #[test]
632    fn test_strict_mode_returns_error_on_unresolved_profile() {
633        let map = empty();
634        let provider = provider_with(Some("main"), Some("ghost"), None);
635        let resource = resource_with_provider("aws_iam_role.r", Some("main"));
636        let component = component_with("svc", vec![provider], vec![resource]);
637        let mut ws = workspace_with(component);
638        let mut ctx = ProviderContext::new(map);
639        ctx.strict = true;
640        let err = DefaultProviderResolver.resolve(&mut ws, &ctx).unwrap_err();
641        assert!(
642            format!("{err}").contains("ghost"),
643            "expected strict error to mention profile: {err}"
644        );
645    }
646
647    #[test]
648    fn test_should_fill_state_backend_account_from_profile() {
649        let map = synth_profile_map_with("backend-profile", "300000000003", "ap-southeast-1");
650        let attrs: AttributeMap = vec![
651            (
652                Arc::from("profile"),
653                Expression::Literal(Value::Str(Arc::from("backend-profile"))),
654            ),
655            (
656                Arc::from("region"),
657                Expression::Literal(Value::Str(Arc::from("ap-southeast-1"))),
658            ),
659        ];
660        let mut backend = StateBackend::builder()
661            .kind(Arc::<str>::from("s3"))
662            .attributes(attrs)
663            .span(span())
664            .build();
665        let mut unresolved: BTreeSet<Arc<str>> = BTreeSet::new();
666        let mut diags: Vec<Diagnostic> = Vec::new();
667        fill_state_backend(
668            &mut backend,
669            &ProviderContext::new(map),
670            &mut unresolved,
671            &mut diags,
672        );
673        assert_eq!(
674            backend.state_account_id.as_ref().map(AccountId::as_str),
675            Some("300000000003")
676        );
677        assert_eq!(
678            backend.state_region.as_ref().map(Region::as_str),
679            Some("ap-southeast-1")
680        );
681    }
682
683    #[test]
684    fn test_should_fill_state_backend_account_from_role_arn() {
685        let map = empty();
686        let attrs: AttributeMap = vec![(
687            Arc::from("role_arn"),
688            Expression::Literal(Value::Str(Arc::from(
689                "arn:aws:iam::444444444444:role/state-writer",
690            ))),
691        )];
692        let mut backend = StateBackend::builder()
693            .kind(Arc::<str>::from("s3"))
694            .attributes(attrs)
695            .span(span())
696            .build();
697        let mut unresolved: BTreeSet<Arc<str>> = BTreeSet::new();
698        let mut diags: Vec<Diagnostic> = Vec::new();
699        fill_state_backend(
700            &mut backend,
701            &ProviderContext::new(map),
702            &mut unresolved,
703            &mut diags,
704        );
705        assert_eq!(
706            backend.state_account_id.as_ref().map(AccountId::as_str),
707            Some("444444444444")
708        );
709    }
710}