| new file mode 100644 |
| index 000000000..57577dd5e |
| --- /dev/null |
| +++ b/internal/termfence/fence_test.go |
| @@ -0,0 +1,127 @@ |
| +package termfence |
| + |
| +import ( |
| + "errors" |
| + "testing" |
| + "time" |
| + |
| + "github.com/aws/aws-sdk-go-v2/aws" |
| + "github.com/aws/aws-sdk-go-v2/service/ec2" |
| + "github.com/aws/aws-sdk-go-v2/service/ec2/types" |
| + |
| + "github.com/carlosframework/platform/internal/boxid" |
| +) |
| + |
| +var ( |
| + id = boxid.Identity{AccountID: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"} |
| + now = time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) |
| +) |
| + |
| +func describe(owner, instance string, state types.InstanceStateName) *ec2.DescribeInstancesOutput { |
| + return &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| + OwnerId: aws.String(owner), |
| + Instances: []types.Instance{{InstanceId: aws.String(instance), State: &types.InstanceState{Name: state}}}, |
| + }}} |
| +} |
| + |
| +func TestATerminatedInstanceOfThisBoxIsAFence(t *testing.T) { |
| + o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1") |
| + f, err := Verify("i-0abc", id, o, now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want := Fence{Box: "i-0abc", InstanceID: "i-0abc", Account: "111122223333", AWSRegion: "eu-west-1", |
| + State: "terminated", ObservedAt: "2026-10-05T12:00:00Z", Via: "describe"} |
| + if f != want { |
| + t.Fatalf("fence = %+v\nwant %+v", f, want) |
| + } |
| +} |
| + |
| +// Review Focus 4: every lookalike is refused. |
| +func TestLookalikesAreNotEvidence(t *testing.T) { |
| + cases := map[string]Observation{ |
| + "stopped": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopped), "i-0abc", "eu-west-1"), |
| + "stopping": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopping), "i-0abc", "eu-west-1"), |
| + "shutting-down": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameShuttingDown), "i-0abc", "eu-west-1"), |
| + "running": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameRunning), "i-0abc", "eu-west-1"), |
| + "another account": FromDescribe(describe("999999999999", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), |
| + "another region": FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "us-east-1"), |
| + "another instance": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0zzz", "eu-west-1"), |
| + "not in the reply": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), |
| + "empty reply": FromDescribe(&ec2.DescribeInstancesOutput{}, "i-0abc", "eu-west-1"), |
| + "nil reply": FromDescribe(nil, "i-0abc", "eu-west-1"), |
| + "no state": FromDescribe(&ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("111122223333"), Instances: []types.Instance{{InstanceId: aws.String("i-0abc")}}}}}, "i-0abc", "eu-west-1"), |
| + "unknown source": {Via: "cloudtrail", Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"}, |
| + "zero observation": {}, |
| + } |
| + for name, o := range cases { |
| + t.Run(name, func(t *testing.T) { |
| + if f, err := Verify("i-0abc", id, o, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("fence = %+v, err = %v; want ErrNotEvidence", f, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestAnUnverifiedIdentityIsNeverFenced(t *testing.T) { |
| + // A found, terminated observation whose every coordinate is empty, so |
| + // it MATCHES an empty identity field for field. Only the guard on the |
| + // identity itself can refuse it. |
| + o := Observation{Via: ViaDescribe, Found: true, State: "terminated"} |
| + if _, err := Verify("i-0abc", boxid.Identity{}, o, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("err = %v: an empty identity matched an empty observation", err) |
| + } |
| + if _, err := Verify("", id, Observation{Via: ViaDescribe, Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"}, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("err = %v: a fence with no box name", err) |
| + } |
| +} |
| + |
| +const terminatedEvent = `{"version":"0","id":"ev-123","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","time":"2026-10-05T11:59:00Z","region":"eu-west-1","resources":["arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc"],"detail":{"instance-id":"i-0abc","state":"terminated"}}` |
| + |
| +func TestATerminatedEventIsAFence(t *testing.T) { |
| + o, err := FromEvent([]byte(terminatedEvent)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f, err := Verify("i-0abc", id, o, now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if f.Via != "event" || f.EventID != "ev-123" || f.State != "terminated" { |
| + t.Fatalf("fence = %+v", f) |
| + } |
| +} |
| + |
| +func TestEventsThatAreNotEC2StateChangesAreRefused(t *testing.T) { |
| + for name, body := range map[string]string{ |
| + "not JSON": `terminated`, |
| + "another source": `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"my.app","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, |
| + "another detail-type": `{"id":"e","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, |
| + "no event id": `{"detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + if _, err := FromEvent([]byte(body)); err == nil { |
| + t.Fatal("accepted") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestAShuttingDownEventIsNotEvidence(t *testing.T) { |
| + body := `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"shutting-down"}}` |
| + o, err := FromEvent([]byte(body)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := Verify("i-0abc", id, o, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("err = %v, want ErrNotEvidence", err) |
| + } |
| +} |
| + |
| +func TestALegacyLabelFencesByItsBoundInstance(t *testing.T) { |
| + o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1") |
| + f, err := Verify("annie-welding", id, o, now) |
| + if err != nil || f.Box != "annie-welding" || f.InstanceID != "i-0abc" { |
| + t.Fatalf("fence = %+v, err = %v", f, err) |
| + } |
| +} |