Skip to main content

fakecloud_ec2/
defaults.rs

1//! Default-VPC bootstrap.
2//!
3//! Every real AWS account has, per region, a *default VPC* (`172.31.0.0/16`)
4//! with an attached internet gateway, a main route table that sends
5//! `0.0.0.0/0` at the gateway, one default subnet per Availability Zone, a
6//! `default` security group, and a default network ACL. Callers that never
7//! touch the VPC APIs (the common case — `RunInstances` with no `SubnetId`)
8//! still expect their instances to land in that default VPC and come back from
9//! `DescribeInstances` with a real `vpc-…` / `subnet-…`.
10//!
11//! fakecloud builds the same fixtures the first time an account's EC2 state is
12//! constructed ([`Ec2State::new`](crate::state::Ec2State::new)). The resource
13//! ids are **deterministic** functions of the account id and a role string
14//! (region-independent — see [`deterministic_id`]), so the throwaway empty
15//! states that the read paths synthesize as a "not found" fallback report the
16//! *same* ids as the persisted account state regardless of the caller's region.
17//!
18//! Per-VPC packet isolation (issue #1745 phase 2+) keys off this topology: a
19//! subnet whose route table has a `0.0.0.0/0 -> igw-…` route is public and gets
20//! a routable backing network; a subnet without one is private (`internal`).
21
22use crate::state::{
23    Ec2State, Image, InternetGateway, NetworkAcl, NetworkAclAssoc, NetworkAclEntry, Route,
24    RouteTable, RouteTableAssociation, SecurityGroup, SecurityGroupRule, Subnet, Vpc,
25};
26
27/// CIDR of the default VPC, matching AWS.
28const DEFAULT_VPC_CIDR: &str = "172.31.0.0/16";
29
30/// The Availability Zone suffixes that receive a default subnet. AWS creates a
31/// default subnet in every AZ; three covers the cardinality every realistic
32/// test exercises (and keeps the deterministic CIDR layout simple).
33const DEFAULT_AZ_SUFFIXES: [&str; 3] = ["a", "b", "c"];
34
35/// Deterministic EC2 resource id: `<prefix>-<17 hex>` derived from the account
36/// and a per-resource `role`. Deliberately **region-independent**: a
37/// `MultiAccountState` partitions by account and pins a single region per
38/// server, but read handlers build a throwaway `Ec2State::new(account,
39/// req.region)` for accounts that don't exist yet — where `req.region` is the
40/// caller's SigV4 scope, not the server's region. Seeding the id on the region
41/// made those throwaway-derived ids disagree with the persisted account's ids
42/// whenever the client region differed from the server's, so a no-subnet launch
43/// stamped the instance with a subnet/VPC id that didn't exist in its own
44/// account (bug-hunt 2026-06-18 finding 1.1). Dropping region from the seed
45/// makes both paths agree; the AZ/CIDR cosmetics below still use the region.
46pub(crate) fn deterministic_id(prefix: &str, account: &str, role: &str) -> String {
47    let seed = format!("{account}/{role}");
48    let h1 = fnv1a64(seed.as_bytes());
49    let h2 = fnv1a64(format!("{seed}/salt").as_bytes());
50    // 16 hex from the first hash + 1 nibble from the second = the 17 hex chars
51    // a modern EC2 long-id carries.
52    format!("{prefix}-{:016x}{:01x}", h1, h2 & 0xf)
53}
54
55/// FNV-1a 64-bit. A tiny, dependency-free, stable hash — we only need
56/// determinism, not cryptographic strength.
57fn fnv1a64(bytes: &[u8]) -> u64 {
58    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
59    for b in bytes {
60        h ^= u64::from(*b);
61        h = h.wrapping_mul(0x0000_0100_0000_01b3);
62    }
63    h
64}
65
66/// AWS-style AZ-id prefix for a region: `us-east-1 -> use1`. Falls back to the
67/// region with dashes stripped for non-`a-b-N` shapes.
68fn az_id_prefix(region: &str) -> String {
69    let parts: Vec<&str> = region.split('-').collect();
70    if parts.len() == 3 && !parts[1].is_empty() {
71        format!(
72            "{}{}{}",
73            parts[0],
74            parts[1].chars().next().unwrap_or('x'),
75            parts[2]
76        )
77    } else {
78        region.replace('-', "")
79    }
80}
81
82/// The default VPC id for an account (also exposed so request handlers can
83/// resolve the implicit default without re-deriving the seed by hand).
84pub(crate) fn default_vpc_id(account: &str) -> String {
85    deterministic_id("vpc", account, "default-vpc")
86}
87
88/// The default security-group id for an account.
89pub(crate) fn default_security_group_id(account: &str) -> String {
90    deterministic_id("sg", account, "default-sg")
91}
92
93/// Populate `state` with the default VPC topology. Called once at state
94/// construction; idempotent in practice because the deterministic ids collide
95/// on re-entry.
96/// Seed a small catalogue of public AMIs the way every real AWS account sees
97/// them, so Terraform's `aws_ami` / `aws_ami_ids` data sources — which resolve
98/// an image via `owners = ["amazon"|"099720109477"|…]` + a `name` wildcard +
99/// `most_recent = true` — return a result instead of empty. Without this,
100/// `DescribeImages` only ever returned user-registered AMIs, so the common
101/// `data "aws_ami" "al2" { most_recent = true; owners = ["amazon"]; filter { … } }`
102/// pattern (and everything that chains off it, e.g. an `aws_instance` or an
103/// ELBv2 target-group attachment) could not be planned.
104///
105/// Ids and `creationDate`s are deterministic and version-stable; the distinct
106/// dates make `most_recent` ordering well-defined. These are public, read-only
107/// fixtures owned by Amazon/Canonical — not user data — and share the
108/// deterministic-id property of the default network so the throwaway empty
109/// states the read paths build report the same catalogue.
110/// One seeded public-AMI row: `(image_id, name, description, architecture,
111/// owner_id, owner_alias, creation_date, root_device_name, platform)`.
112type AmiSeed = (
113    &'static str,
114    &'static str,
115    &'static str,
116    &'static str,
117    &'static str,
118    Option<&'static str>,
119    &'static str,
120    &'static str,
121    Option<&'static str>,
122);
123
124pub(crate) fn seed_public_images(state: &mut Ec2State) {
125    const AMAZON: &str = "137112412989";
126    const CANONICAL: &str = "099720109477";
127    const AMAZON_WINDOWS: &str = "801119661308";
128    let seeds: &[AmiSeed] = &[
129        // Amazon Linux 2 (x86_64 + arm64). The `amzn2-ami-minimal-hvm-*` name +
130        // `root-device-type = ebs` shape is exactly what the standard
131        // terraform-provider-aws acctest helper
132        // `ConfigLatestAmazonLinux2HVMEBSX8664AMI()` / `…ARM64AMI()` filters on,
133        // so every acceptance test that launches an instance via that helper
134        // (aws_instance, the ELBv2 target-group attachment, autoscaling, …)
135        // resolves a real AMI from this catalogue.
136        (
137            "ami-0a1b2c3d4e5f60001",
138            "amzn2-ami-minimal-hvm-2.0.20240306.2-x86_64-ebs",
139            "Amazon Linux 2 AMI 2.0.20240306.2 x86_64 Minimal HVM ebs",
140            "x86_64",
141            AMAZON,
142            Some("amazon"),
143            "2024-03-06T12:00:00.000Z",
144            "/dev/xvda",
145            None,
146        ),
147        (
148            "ami-0a1b2c3d4e5f60007",
149            "amzn2-ami-minimal-hvm-2.0.20240306.2-arm64-ebs",
150            "Amazon Linux 2 AMI 2.0.20240306.2 arm64 Minimal HVM ebs",
151            "arm64",
152            AMAZON,
153            Some("amazon"),
154            "2024-03-06T12:00:00.000Z",
155            "/dev/xvda",
156            None,
157        ),
158        (
159            "ami-0a1b2c3d4e5f60002",
160            "al2023-ami-2023.4.20240319.1-kernel-6.1-x86_64",
161            "Amazon Linux 2023 AMI 2023.4.20240319.1 x86_64 HVM kernel-6.1",
162            "x86_64",
163            AMAZON,
164            Some("amazon"),
165            "2024-03-19T12:00:00.000Z",
166            "/dev/xvda",
167            None,
168        ),
169        (
170            "ami-0a1b2c3d4e5f60003",
171            "al2023-ami-2023.4.20240319.1-kernel-6.1-arm64",
172            "Amazon Linux 2023 AMI 2023.4.20240319.1 arm64 HVM kernel-6.1",
173            "arm64",
174            AMAZON,
175            Some("amazon"),
176            "2024-03-19T12:00:00.000Z",
177            "/dev/xvda",
178            None,
179        ),
180        (
181            "ami-0a1b2c3d4e5f60004",
182            "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20240319",
183            "Canonical, Ubuntu, 22.04 LTS, amd64 jammy image build on 2024-03-19",
184            "x86_64",
185            CANONICAL,
186            None,
187            "2024-03-19T06:00:00.000Z",
188            "/dev/sda1",
189            None,
190        ),
191        (
192            "ami-0a1b2c3d4e5f60005",
193            "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-20240423",
194            "Canonical, Ubuntu, 24.04 LTS, amd64 noble image build on 2024-04-23",
195            "x86_64",
196            CANONICAL,
197            None,
198            "2024-04-23T06:00:00.000Z",
199            "/dev/sda1",
200            None,
201        ),
202        // Canonical instance-store Ubuntu (the `ubuntu/images/hvm-instance/*`
203        // shape the `aws_ami_ids` acctest filters on, distinct from the EBS
204        // `hvm-ssd*` images above).
205        (
206            "ami-0a1b2c3d4e5f60008",
207            "ubuntu/images/hvm-instance/ubuntu-jammy-22.04-amd64-server-20240319",
208            "Canonical, Ubuntu, 22.04 LTS, amd64 jammy instance-store image build on 2024-03-19",
209            "x86_64",
210            CANONICAL,
211            None,
212            "2024-03-19T05:00:00.000Z",
213            "/dev/sda1",
214            None,
215        ),
216        (
217            "ami-0a1b2c3d4e5f60006",
218            "Windows_Server-2022-English-Full-Base-2024.03.13",
219            "Microsoft Windows Server 2022 Full Locale English AMI provided by Amazon",
220            "x86_64",
221            AMAZON_WINDOWS,
222            Some("amazon"),
223            "2024-03-13T12:00:00.000Z",
224            "/dev/sda1",
225            Some("windows"),
226        ),
227    ];
228    for (id, name, desc, arch, owner, alias, created, root_dev, platform) in seeds {
229        state.images.insert(
230            (*id).to_string(),
231            Image {
232                image_id: (*id).to_string(),
233                name: (*name).to_string(),
234                description: (*desc).to_string(),
235                state: "available".to_string(),
236                architecture: (*arch).to_string(),
237                public: true,
238                source_instance_id: None,
239                in_recycle_bin: false,
240                deprecation_time: None,
241                deregistration_protection: false,
242                launch_permission_users: Vec::new(),
243                launch_permission_groups: vec!["all".to_string()],
244                boot_mode: None,
245                owner_id: Some((*owner).to_string()),
246                owner_alias: alias.map(str::to_string),
247                creation_date: Some((*created).to_string()),
248                root_device_name: Some((*root_dev).to_string()),
249                platform: platform.map(str::to_string),
250            },
251        );
252    }
253}
254
255pub(crate) fn bootstrap_default_network(state: &mut Ec2State) {
256    let account = state.account_id.clone();
257    let region = if state.region.is_empty() {
258        "us-east-1".to_string()
259    } else {
260        state.region.clone()
261    };
262
263    let vpc_id = default_vpc_id(&account);
264    let igw_id = deterministic_id("igw", &account, "default-igw");
265    let rtb_id = deterministic_id("rtb", &account, "default-rtb");
266    let acl_id = deterministic_id("acl", &account, "default-acl");
267    let sg_id = default_security_group_id(&account);
268
269    // --- default VPC ---
270    state.vpcs.insert(
271        vpc_id.clone(),
272        Vpc {
273            vpc_id: vpc_id.clone(),
274            cidr_block: DEFAULT_VPC_CIDR.to_string(),
275            state: "available".to_string(),
276            dhcp_options_id: "default".to_string(),
277            instance_tenancy: "default".to_string(),
278            is_default: true,
279            enable_dns_support: true,
280            enable_dns_hostnames: true,
281            cidr_associations: Vec::new(),
282            ipv6_cidr_block: None,
283        },
284    );
285
286    // --- internet gateway, attached to the default VPC ---
287    state.internet_gateways.insert(
288        igw_id.clone(),
289        InternetGateway {
290            internet_gateway_id: igw_id.clone(),
291            attachments: vec![(vpc_id.clone(), "available".to_string())],
292        },
293    );
294
295    // --- default subnets, one per AZ ---
296    let az_prefix = az_id_prefix(&region);
297    let mut subnet_ids = Vec::new();
298    for (idx, suffix) in DEFAULT_AZ_SUFFIXES.iter().enumerate() {
299        let subnet_id = deterministic_id("subnet", &account, &format!("default-subnet-{suffix}"));
300        let az = format!("{region}{suffix}");
301        state.subnets.insert(
302            subnet_id.clone(),
303            Subnet {
304                subnet_id: subnet_id.clone(),
305                vpc_id: vpc_id.clone(),
306                // /20 blocks carved from 172.31.0.0/16: .0, .16, .32 …
307                cidr_block: format!("172.31.{}.0/20", idx * 16),
308                availability_zone: az,
309                availability_zone_id: format!("{az_prefix}-az{}", idx + 1),
310                state: "available".to_string(),
311                available_ip_address_count: 4091,
312                default_for_az: true,
313                // Default subnets auto-assign public IPs, matching AWS.
314                map_public_ip_on_launch: true,
315                assign_ipv6_address_on_creation: false,
316                map_customer_owned_ip_on_launch: false,
317                enable_dns64: false,
318                private_dns_hostname_type: "ip-name".to_string(),
319                ipv6_cidr_block: None,
320            },
321        );
322        subnet_ids.push(subnet_id);
323    }
324
325    // --- main route table: local + default route at the IGW (public) ---
326    let mut associations = vec![RouteTableAssociation {
327        association_id: deterministic_id("rtbassoc", &account, "default-rtb-main"),
328        route_table_id: rtb_id.clone(),
329        subnet_id: None,
330        gateway_id: None,
331        main: true,
332    }];
333    for sid in &subnet_ids {
334        associations.push(RouteTableAssociation {
335            association_id: deterministic_id("rtbassoc", &account, &format!("default-rtb-{sid}")),
336            route_table_id: rtb_id.clone(),
337            subnet_id: Some(sid.clone()),
338            gateway_id: None,
339            main: false,
340        });
341    }
342    state.route_tables.insert(
343        rtb_id.clone(),
344        RouteTable {
345            route_table_id: rtb_id.clone(),
346            vpc_id: vpc_id.clone(),
347            routes: vec![
348                Route {
349                    destination_cidr_block: Some(DEFAULT_VPC_CIDR.to_string()),
350                    gateway_id: Some("local".to_string()),
351                    ..Default::default()
352                },
353                Route {
354                    destination_cidr_block: Some("0.0.0.0/0".to_string()),
355                    gateway_id: Some(igw_id.clone()),
356                    ..Default::default()
357                },
358            ],
359            associations,
360        },
361    );
362
363    // --- default security group: allow all from self, allow all egress ---
364    state.security_groups.insert(
365        sg_id.clone(),
366        SecurityGroup {
367            group_id: sg_id.clone(),
368            group_name: "default".to_string(),
369            description: "default VPC security group".to_string(),
370            vpc_id: vpc_id.clone(),
371            rules: vec![
372                SecurityGroupRule {
373                    rule_id: deterministic_id("sgr", &account, "default-sg-ingress"),
374                    group_id: sg_id.clone(),
375                    is_egress: false,
376                    ip_protocol: "-1".to_string(),
377                    from_port: -1,
378                    to_port: -1,
379                    cidr_ipv4: None,
380                    cidr_ipv6: None,
381                    prefix_list_id: None,
382                    referenced_group_id: Some(sg_id.clone()),
383                    referenced_group_name: None,
384                    referenced_user_id: None,
385                    description: String::new(),
386                },
387                SecurityGroupRule {
388                    rule_id: deterministic_id("sgr", &account, "default-sg-egress"),
389                    group_id: sg_id.clone(),
390                    is_egress: true,
391                    ip_protocol: "-1".to_string(),
392                    from_port: -1,
393                    to_port: -1,
394                    cidr_ipv4: Some("0.0.0.0/0".to_string()),
395                    cidr_ipv6: None,
396                    prefix_list_id: None,
397                    referenced_group_id: None,
398                    referenced_group_name: None,
399                    referenced_user_id: None,
400                    description: String::new(),
401                },
402            ],
403        },
404    );
405
406    // --- default network ACL: allow-all, associated with every default subnet ---
407    let nacl_associations = subnet_ids
408        .iter()
409        .map(|sid| NetworkAclAssoc {
410            association_id: deterministic_id("aclassoc", &account, &format!("default-acl-{sid}")),
411            subnet_id: sid.clone(),
412        })
413        .collect();
414    state.network_acls.insert(
415        acl_id.clone(),
416        NetworkAcl {
417            network_acl_id: acl_id.clone(),
418            vpc_id: vpc_id.clone(),
419            is_default: true,
420            entries: vec![
421                allow_all_entry(false),
422                deny_all_entry(false),
423                allow_all_entry(true),
424                deny_all_entry(true),
425            ],
426            associations: nacl_associations,
427        },
428    );
429}
430
431/// Create the implicit resources AWS provisions for every newly-created VPC: a
432/// `default` security group, a default network ACL, and a main route table
433/// (with the `local` route). The `aws_vpc` resource reads back
434/// `default_security_group_id`, `default_network_acl_id`,
435/// `default_route_table_id`, and `main_route_table_id`, all derived from these.
436/// Ids are deterministic functions of the VPC id so read-path fallbacks agree.
437pub(crate) fn create_vpc_default_resources(state: &mut Ec2State, vpc_id: &str, cidr: &str) {
438    let account = state.account_id.clone();
439    let rtb_id = deterministic_id("rtb", &account, &format!("{vpc_id}-main-rtb"));
440    let acl_id = deterministic_id("acl", &account, &format!("{vpc_id}-default-acl"));
441    let sg_id = deterministic_id("sg", &account, &format!("{vpc_id}-default-sg"));
442
443    state
444        .route_tables
445        .entry(rtb_id.clone())
446        .or_insert_with(|| RouteTable {
447            route_table_id: rtb_id.clone(),
448            vpc_id: vpc_id.to_string(),
449            routes: vec![Route {
450                destination_cidr_block: Some(cidr.to_string()),
451                gateway_id: Some("local".to_string()),
452                ..Default::default()
453            }],
454            associations: vec![RouteTableAssociation {
455                association_id: deterministic_id("rtbassoc", &account, &format!("{vpc_id}-main")),
456                route_table_id: rtb_id.clone(),
457                subnet_id: None,
458                gateway_id: None,
459                main: true,
460            }],
461        });
462
463    state
464        .security_groups
465        .entry(sg_id.clone())
466        .or_insert_with(|| SecurityGroup {
467            group_id: sg_id.clone(),
468            group_name: "default".to_string(),
469            description: "default VPC security group".to_string(),
470            vpc_id: vpc_id.to_string(),
471            rules: vec![
472                SecurityGroupRule {
473                    rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-ingress")),
474                    group_id: sg_id.clone(),
475                    is_egress: false,
476                    ip_protocol: "-1".to_string(),
477                    from_port: -1,
478                    to_port: -1,
479                    cidr_ipv4: None,
480                    cidr_ipv6: None,
481                    prefix_list_id: None,
482                    referenced_group_id: Some(sg_id.clone()),
483                    referenced_group_name: None,
484                    referenced_user_id: None,
485                    description: String::new(),
486                },
487                SecurityGroupRule {
488                    rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-egress")),
489                    group_id: sg_id.clone(),
490                    is_egress: true,
491                    ip_protocol: "-1".to_string(),
492                    from_port: -1,
493                    to_port: -1,
494                    cidr_ipv4: Some("0.0.0.0/0".to_string()),
495                    cidr_ipv6: None,
496                    prefix_list_id: None,
497                    referenced_group_id: None,
498                    referenced_group_name: None,
499                    referenced_user_id: None,
500                    description: String::new(),
501                },
502            ],
503        });
504
505    state
506        .network_acls
507        .entry(acl_id.clone())
508        .or_insert_with(|| NetworkAcl {
509            network_acl_id: acl_id.clone(),
510            vpc_id: vpc_id.to_string(),
511            is_default: true,
512            entries: vec![
513                allow_all_entry(false),
514                deny_all_entry(false),
515                allow_all_entry(true),
516                deny_all_entry(true),
517            ],
518            associations: Vec::new(),
519        });
520}
521
522fn allow_all_entry(egress: bool) -> NetworkAclEntry {
523    NetworkAclEntry {
524        rule_number: 100,
525        protocol: "-1".to_string(),
526        rule_action: "allow".to_string(),
527        egress,
528        cidr_block: Some("0.0.0.0/0".to_string()),
529        ipv6_cidr_block: None,
530        port_range: None,
531        icmp_type_code: None,
532    }
533}
534
535fn deny_all_entry(egress: bool) -> NetworkAclEntry {
536    NetworkAclEntry {
537        rule_number: 32767,
538        protocol: "-1".to_string(),
539        rule_action: "deny".to_string(),
540        egress,
541        cidr_block: Some("0.0.0.0/0".to_string()),
542        ipv6_cidr_block: None,
543        port_range: None,
544        icmp_type_code: None,
545    }
546}
547
548/// True when `subnet_id` resolves to a subnet whose route table carries a
549/// `0.0.0.0/0` route at an internet gateway — i.e. a *public* subnet. A subnet
550/// without such a route is private and (phase 2) backs onto an `internal`
551/// network. Subnets default to their VPC's main route table when not
552/// explicitly associated.
553// Drives per-subnet networking (chooses `internal` vs routable backing
554// networks) from `service/mod.rs` and `instance.rs`.
555pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
556    let Some(subnet) = state.subnets.get(subnet_id) else {
557        return false;
558    };
559    // An explicit association wins; otherwise fall back to the VPC's main table.
560    let explicit = state.route_tables.values().find(|rt| {
561        rt.associations
562            .iter()
563            .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
564    });
565    let main = state
566        .route_tables
567        .values()
568        .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
569    let rt = explicit.or(main);
570    rt.map(route_table_has_igw_default).unwrap_or(false)
571}
572
573fn route_table_has_igw_default(rt: &RouteTable) -> bool {
574    rt.routes.iter().any(|r| {
575        r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
576            && r.gateway_id
577                .as_deref()
578                .map(|g| g.starts_with("igw-"))
579                .unwrap_or(false)
580    })
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::state::Ec2State;
587
588    #[test]
589    fn deterministic_id_is_stable_and_shaped() {
590        let a = deterministic_id("vpc", "123456789012", "default-vpc");
591        let b = deterministic_id("vpc", "123456789012", "default-vpc");
592        assert_eq!(a, b);
593        assert!(a.starts_with("vpc-"));
594        // 17 hex chars after the prefix, matching EC2 long-ids.
595        let hex = a.strip_prefix("vpc-").unwrap();
596        assert_eq!(hex.len(), 17);
597        assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
598    }
599
600    #[test]
601    fn deterministic_id_varies_by_account_and_role() {
602        let base = deterministic_id("vpc", "111111111111", "default-vpc");
603        assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
604        assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
605    }
606
607    #[test]
608    fn deterministic_id_is_region_independent() {
609        // The id seed deliberately excludes region so read-path (req.region)
610        // and persisted (server region) states agree (finding 1.1). Region is
611        // not a parameter anymore — this documents the contract by asserting
612        // default_vpc_id depends only on the account.
613        assert_eq!(
614            default_vpc_id("111111111111"),
615            deterministic_id("vpc", "111111111111", "default-vpc")
616        );
617    }
618
619    #[test]
620    fn az_id_prefix_matches_aws_shape() {
621        assert_eq!(az_id_prefix("us-east-1"), "use1");
622        assert_eq!(az_id_prefix("eu-west-2"), "euw2");
623        assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
624    }
625
626    #[test]
627    fn bootstrap_creates_full_default_topology() {
628        let state = Ec2State::new("123456789012", "us-east-1");
629        // exactly one VPC, marked default
630        assert_eq!(state.vpcs.len(), 1);
631        let vpc = state.vpcs.values().next().unwrap();
632        assert!(vpc.is_default);
633        assert_eq!(vpc.cidr_block, "172.31.0.0/16");
634        // one subnet per AZ suffix, all default_for_az + public
635        assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
636        assert!(state.subnets.values().all(|s| s.default_for_az));
637        assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
638        // IGW attached to the default VPC
639        assert_eq!(state.internet_gateways.len(), 1);
640        let igw = state.internet_gateways.values().next().unwrap();
641        assert_eq!(igw.attachments[0].0, vpc.vpc_id);
642        // default SG + default NACL
643        let sg = state.security_groups.values().next().unwrap();
644        assert_eq!(sg.group_name, "default");
645        assert!(state.network_acls.values().next().unwrap().is_default);
646    }
647
648    #[test]
649    fn default_subnets_are_public() {
650        let state = Ec2State::new("123456789012", "us-east-1");
651        for sid in state.subnets.keys() {
652            assert!(
653                subnet_is_public(&state, sid),
654                "subnet {sid} should be public"
655            );
656        }
657    }
658
659    #[test]
660    fn ids_match_across_fresh_states() {
661        // The throwaway "empty" states read paths build must agree with the
662        // persisted account state on the default VPC id.
663        let a = Ec2State::new("123456789012", "us-east-1");
664        let b = Ec2State::new("123456789012", "us-east-1");
665        let a_vpc: Vec<_> = a.vpcs.keys().collect();
666        let b_vpc: Vec<_> = b.vpcs.keys().collect();
667        assert_eq!(a_vpc, b_vpc);
668        assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
669    }
670
671    #[test]
672    fn default_vpc_id_agrees_across_regions() {
673        // The crux of finding 1.1: a read-path empty built with the caller's
674        // region must derive the SAME default VPC id as the persisted account
675        // state built with the server's region.
676        let read_path = Ec2State::new("123456789012", "eu-west-1");
677        let persisted = Ec2State::new("123456789012", "us-east-1");
678        let read_vpc = read_path.vpcs.keys().next().unwrap();
679        let persisted_vpc = persisted.vpcs.keys().next().unwrap();
680        assert_eq!(read_vpc, persisted_vpc);
681        // subnets too (so a no-subnet launch resolves a subnet that exists in
682        // the persisted account regardless of the caller's region).
683        let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
684        let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
685        assert_eq!(read_subnets, persisted_subnets);
686    }
687}