| index 57577dd5e..22c2fed0b 100644 |
| --- a/internal/termfence/fence_test.go |
| +++ b/internal/termfence/fence_test.go |
| @@ -1,20 +1,27 @@ |
| package termfence |
| |
| import ( |
| + "context" |
| "errors" |
| "testing" |
| "time" |
| |
| "github.com/aws/aws-sdk-go-v2/aws" |
| + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" |
| "github.com/aws/aws-sdk-go-v2/service/ec2" |
| "github.com/aws/aws-sdk-go-v2/service/ec2/types" |
| + "github.com/aws/smithy-go" |
| + smithymiddleware "github.com/aws/smithy-go/middleware" |
| |
| "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) |
| + box = Box{Ref: "annie-welding", Enrollment: "enr-annie-1", |
| + Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}} |
| + worker = Box{Ref: "bert-sawing", Enrollment: "enr-bert-1", |
| + Cloud: boxid.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0bbb"}} |
| + now = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) |
| ) |
| |
| func describe(owner, instance string, state types.InstanceStateName) *ec2.DescribeInstancesOutput { |
| @@ -24,20 +31,28 @@ func describe(owner, instance string, state types.InstanceStateName) *ec2.Descri |
| }}} |
| } |
| |
| +func withRequestID(out *ec2.DescribeInstancesOutput, id string) *ec2.DescribeInstancesOutput { |
| + var md smithymiddleware.Metadata |
| + awsmiddleware.SetRequestIDMetadata(&md, id) |
| + out.ResultMetadata = md |
| + return out |
| +} |
| + |
| 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) |
| + out := withRequestID(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "req-1") |
| + rec, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), 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) |
| + want := Record{V: 1, Box: "annie-welding", Enrollment: "enr-annie-1", Cloud: box.Cloud, State: "terminated", |
| + ObservedAt: "2026-10-06T12:00:00Z", Via: "describe", RequestID: "req-1", |
| + WrittenBy: WrittenBy{Box: "bert-sawing", Enrollment: "enr-bert-1"}} |
| + if rec != want { |
| + t.Fatalf("record = %+v\nwant %+v", rec, want) |
| } |
| } |
| |
| -// Review Focus 4: every lookalike is refused. |
| +// 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"), |
| @@ -56,39 +71,63 @@ func TestLookalikesAreNotEvidence(t *testing.T) { |
| } |
| 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) |
| + if rec, err := Verify(box, worker, o, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("record = %+v, err = %v; want ErrNotEvidence", rec, err) |
| } |
| }) |
| } |
| } |
| |
| -func TestAnUnverifiedIdentityIsNeverFenced(t *testing.T) { |
| +// Review Focus 5: the fence is a regional worker's own read in the box's |
| +// own region. A worker enrolled elsewhere is refused even when what it |
| +// read is a perfect terminated observation. |
| +func TestAWorkerInAnotherRegionNeverFences(t *testing.T) { |
| + far := worker |
| + far.Cloud.Region = "us-east-1" |
| + o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1") |
| + if _, err := Verify(box, far, o, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("err = %v, want ErrNotEvidence", err) |
| + } |
| +} |
| + |
| +func TestAnIncompleteRosterEntryIsNeverFenced(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. |
| + // it MATCHES an empty cloud field for field. Only the guard on the |
| + // entry 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) |
| + for name, b := range map[string]Box{ |
| + "empty cloud": {Ref: "annie-welding", Enrollment: "enr-annie-1"}, |
| + "no enrollment": {Ref: "annie-welding", Cloud: box.Cloud}, |
| + "no reference": {Enrollment: "enr-annie-1", Cloud: box.Cloud}, |
| + "not aws": {Ref: "annie-welding", Enrollment: "enr-annie-1", Cloud: boxid.Cloud{Provider: "gcp", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + full := Observation{Via: ViaDescribe, Found: true, InstanceID: b.Cloud.InstanceID, Account: b.Cloud.Account, Region: b.Cloud.Region, State: "terminated"} |
| + for _, obs := range []Observation{o, full} { |
| + if _, err := Verify(b, worker, obs, now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("err = %v, want ErrNotEvidence", 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) |
| + if _, err := Verify(box, Box{Ref: "bert-sawing"}, FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("a worker with no proven identity: err = %v", 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"}}` |
| +const terminatedEvent = `{"version":"0","id":"ev-123","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","time":"2026-10-06T11: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) |
| + rec, err := Verify(box, worker, o, now) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if f.Via != "event" || f.EventID != "ev-123" || f.State != "terminated" { |
| - t.Fatalf("fence = %+v", f) |
| + if rec.Via != "event" || rec.EventID != "ev-123" || rec.State != "terminated" || rec.RequestID != "" { |
| + t.Fatalf("record = %+v", rec) |
| } |
| } |
| |
| @@ -113,15 +152,58 @@ func TestAShuttingDownEventIsNotEvidence(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if _, err := Verify("i-0abc", id, o, now); !errors.Is(err, ErrNotEvidence) { |
| + if _, err := Verify(box, worker, 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) |
| +// fakeEC2 answers DescribeInstances and records the region each call was |
| +// forced into. |
| +type fakeEC2 struct { |
| + out *ec2.DescribeInstancesOutput |
| + err error |
| + regions []string |
| + ids [][]string |
| +} |
| + |
| +func (f *fakeEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { |
| + o := ec2.Options{Region: "client-default"} |
| + for _, fn := range optFns { |
| + fn(&o) |
| + } |
| + f.regions = append(f.regions, o.Region) |
| + f.ids = append(f.ids, in.InstanceIds) |
| + return f.out, f.err |
| +} |
| + |
| +func TestDescribeInstanceAsksInTheBoxsOwnRegion(t *testing.T) { |
| + f := &fakeEC2{out: describe("111122223333", "i-0abc", types.InstanceStateNameTerminated)} |
| + out, err := DescribeInstance(context.Background(), f, box.Cloud) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(f.regions) != 1 || f.regions[0] != "eu-west-1" || len(f.ids[0]) != 1 || f.ids[0][0] != "i-0abc" { |
| + t.Fatalf("called in %v for %v", f.regions, f.ids) |
| + } |
| + if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestDescribeInstanceNotFoundIsAbsenceAndOtherErrorsAreErrors(t *testing.T) { |
| + notFound := &fakeEC2{err: &smithy.GenericAPIError{Code: "InvalidInstanceID.NotFound", Message: "gone"}} |
| + out, err := DescribeInstance(context.Background(), notFound, box.Cloud) |
| + if err != nil { |
| + t.Fatalf("NotFound: err = %v", err) |
| + } |
| + if _, err := Verify(box, worker, FromDescribe(out, "i-0abc", "eu-west-1"), now); !errors.Is(err, ErrNotEvidence) { |
| + t.Fatalf("NotFound fenced the box: err = %v", err) |
| + } |
| + denied := &fakeEC2{err: &smithy.GenericAPIError{Code: "UnauthorizedOperation", Message: "no"}} |
| + if _, err := DescribeInstance(context.Background(), denied, box.Cloud); err == nil { |
| + t.Fatal("an access error read as an answer") |
| + } |
| + if _, err := DescribeInstance(context.Background(), &fakeEC2{}, boxid.Cloud{InstanceID: "i-0abc"}); err == nil { |
| + t.Fatal("described an incomplete cloud") |
| } |
| } |