<commit_before>package filters

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/abesto/easyssh/target"
	"github.com/abesto/easyssh/util"
	"github.com/maraino/go-mock"
)

type dummyEc2InstanceIdParser struct {
	shouldMatch bool
}

func (p dummyEc2InstanceIdParser) Parse(input string) string {
	if p.shouldMatch {
		return input + ".instanceid"
	}
	return ""
}

func TestEc2InstanceIdLookupStringViaMake(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		input := "(ec2-instance-id test-region)"
		structs := "[ec2-instance-id test-region]"
		final := "<ec2-instance-id test-region>"
		l.ExpectDebugf("MakeFromString %s -> %s", input, structs)
		l.ExpectDebugf("Make %s -> %s", structs, final)
		Make(input)
	})
}

func TestEc2InstanceIdLookupMakeWithoutArgument(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id)", "[ec2-instance-id]")
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 0: []",
			func() { Make("(ec2-instance-id)") })
	})
}

func TestEc2InstanceIdLookupFilterWithoutSetArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 0: []",
			func() { (&ec2InstanceIdLookup{}).Filter([]target.Target{}) })
	})
}

func TestEc2InstanceIdSetTooManyArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id foo bar)", "[ec2-instance-id foo bar]")
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 2: [foo bar]",
			func() { Make("(ec2-instance-id foo bar)") })
	})
}

func TestEc2InstanceIdSetArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id foo)", "[ec2-instance-id foo]").Times(1)
		l.ExpectDebugf("Make %s -> %s", "[ec2-instance-id foo]", "<ec2-instance-id foo>").Times(1)
		f := Make("(ec2-instance-id foo)").(*ec2InstanceIdLookup)
		if f.region != "foo" {
			t.Errorf("Expected region to be foo, was %s", f.region)
		}
		if len(f.args) != 1 || fmt.Sprintf("%s", f.args[0]) != "foo" {
			t.Error(len(f.args), f.args)
		}
	})
}

func TestEc2InstanceIdParser(t *testing.T) {
	cases := map[string]string{
		"foo-i-deadbeef.subnet.private": "i-deadbeef",
		"i-foo":      "",
		"abesto.net": "",
	}
	parser := realEc2InstanceIdParser{}
	for input, expected := range cases {
		actual := parser.Parse(input)
		if actual != expected {
			t.Errorf("Parsed '%s' into EC2 instance id '%s', expected '%s'", input, actual, expected)
		}
	}
}

func givenAnEc2InstanceIdLookupWithMockedParserAndRunner(shouldMatch bool) (*util.MockCommandRunner, *ec2InstanceIdLookup) {
	r := &util.MockCommandRunner{}
	idParser := dummyEc2InstanceIdParser{shouldMatch}
	f := &ec2InstanceIdLookup{idParser: idParser, commandRunner: r, region: "dummy-region", args: []interface{}{"dummy-region"}}
	return r, f
}

func awsReturns(r *util.MockCommandRunner, instanceId string, region string, output string, err error) *mock.MockFunction {
	return r.When("RunGetOutput", "aws", []string{"ec2", "describe-instances", "--instance-id", instanceId, "--region", region}).Return([]byte(output), err)
}

func assertFilterResults(t *testing.T, f *ec2InstanceIdLookup, input []target.Target, expectedOutput []target.Target) {
	actualOutput := f.Filter(input)
	if len(input) != len(actualOutput) {
		t.Fail()
	}
	for i := 0; i < len(input); i++ {
		if expectedOutput[0] != actualOutput[0] {
			t.Errorf("Target %d was expected to be %s, found %s", i, expectedOutput[0], actualOutput[0])
		}
	}
}

func TestEc2InstanceIdLookupFails(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		r, f := givenAnEc2InstanceIdLookupWithMockedParserAndRunner(true)
		msg := "A client error (InvalidInstanceID.NotFound) occurred when calling the DescribeInstances operation: The instance ID 'i-deadbeef' does not exist"
		host := "dummy-instance-id"
		instanceId := host + ".instanceid"
		l.ExpectInfof("EC2 Instance lookup: %s in %s", instanceId, f.region).Times(2)
		l.ExpectDebugf("Response from AWS API: %s", msg).Times(2)
		l.ExpectInfof("EC2 Instance lookup failed for %s (%s) in region %s (aws command failed): %s", host, instanceId, f.region, msg).Times(2)
		// When the aws cli tool fails
		awsReturns(r, instanceId, f.region, msg, util.DummyError{"test fails aws"}).Times(2)
		// Filtering doesn't touch the target list
		targets := target.GivenTargets(host, host)
		assertFilterResults(t, f, targets, targets)
		util.VerifyMocks(t, r)
		// And no panic happened on JSON parsing, even though the CLI tools output was not valid JSON, because we don't even try to parse the output.
	})
}

func TestEc2InstanceIdLookupInvalidJson(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		invalidJson := "HAH! not a valid JSON"
		host := "dummy-instance-id"
		instanceId := host + ".instanceid"
		r, f := givenAnEc2InstanceIdLookupWithMockedParserAndRunner(true)
		l.ExpectDebugf("Response from AWS API: %s", invalidJson)
		l.ExpectInfof("EC2 Instance lookup: %s in %s", instanceId, f.region)
		// When the AWS API returns invalid JSON
		awsReturns(r, instanceId, f.region, invalidJson, nil).Times(1)
		// I get a fatal error for filtering
		util.ExpectPanic(t, fmt.Sprintf("Invalid JSON returned by AWS API.\nError: invalid character 'H' looking for beginning of value\nJSON follows this line\n%s", invalidJson),
			func() { f.Filter([]target.Target{target.FromString(host)}) })
		util.VerifyMocks(t, r)
	})
}

func jsonWithoutReservations() string {
	bytes, _ := json.Marshal(ec2DescribeInstanceApiResponse{Reservations: []ec2Reservation{}})
	return string(bytes)<commit_msg>Small fix in test code<commit_after>package filters

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/abesto/easyssh/target"
	"github.com/abesto/easyssh/util"
	"github.com/maraino/go-mock"
)

type dummyEc2InstanceIdParser struct {
	shouldMatch bool
}

func (p dummyEc2InstanceIdParser) Parse(input string) string {
	if p.shouldMatch {
		return input + ".instanceid"
	}
	return ""
}

func TestEc2InstanceIdLookupStringViaMake(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		input := "(ec2-instance-id test-region)"
		structs := "[ec2-instance-id test-region]"
		final := "<ec2-instance-id test-region>"
		l.ExpectDebugf("MakeFromString %s -> %s", input, structs)
		l.ExpectDebugf("Make %s -> %s", structs, final)
		Make(input)
	})
}

func TestEc2InstanceIdLookupMakeWithoutArgument(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id)", "[ec2-instance-id]")
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 0: []",
			func() { Make("(ec2-instance-id)") })
	})
}

func TestEc2InstanceIdLookupFilterWithoutSetArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 0: []",
			func() { (&ec2InstanceIdLookup{}).Filter([]target.Target{}) })
	})
}

func TestEc2InstanceIdSetTooManyArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id foo bar)", "[ec2-instance-id foo bar]")
		util.ExpectPanic(t, "<ec2-instance-id > requires exactly 1 argument(s), got 2: [foo bar]",
			func() { Make("(ec2-instance-id foo bar)") })
	})
}

func TestEc2InstanceIdSetArgs(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		l.ExpectDebugf("MakeFromString %s -> %s", "(ec2-instance-id foo)", "[ec2-instance-id foo]").Times(1)
		l.ExpectDebugf("Make %s -> %s", "[ec2-instance-id foo]", "<ec2-instance-id foo>").Times(1)
		f := Make("(ec2-instance-id foo)").(*ec2InstanceIdLookup)
		if f.region != "foo" {
			t.Errorf("Expected region to be foo, was %s", f.region)
		}
		if len(f.args) != 1 || fmt.Sprintf("%s", f.args[0]) != "foo" {
			t.Error(len(f.args), f.args)
		}
	})
}

func TestEc2InstanceIdParser(t *testing.T) {
	cases := map[string]string{
		"foo-i-deadbeef.subnet.private": "i-deadbeef",
		"i-foo":      "",
		"abesto.net": "",
	}
	parser := realEc2InstanceIdParser{}
	for input, expected := range cases {
		actual := parser.Parse(input)
		if actual != expected {
			t.Errorf("Parsed '%s' into EC2 instance id '%s', expected '%s'", input, actual, expected)
		}
	}
}

func givenAnEc2InstanceIdLookupWithMockedParserAndRunner(shouldMatch bool) (*util.MockCommandRunner, *ec2InstanceIdLookup) {
	r := &util.MockCommandRunner{}
	idParser := dummyEc2InstanceIdParser{shouldMatch}
	f := &ec2InstanceIdLookup{idParser: idParser, commandRunner: r, region: "dummy-region", args: []interface{}{"dummy-region"}}
	return r, f
}

func awsReturns(r *util.MockCommandRunner, instanceId string, region string, output string, err error) *mock.MockFunction {
	return r.When("RunGetOutput", "aws", []string{"ec2", "describe-instances", "--instance-id", instanceId, "--region", region}).Return([]byte(output), err)
}

func assertFilterResults(t *testing.T, f *ec2InstanceIdLookup, input []target.Target, expectedOutput []target.Target) {
	actualOutput := f.Filter(input)
	if len(input) != len(actualOutput) {
		t.Fail()
	}
	for i := 0; i < len(input); i++ {
		if expectedOutput[0] != actualOutput[0] {
			t.Errorf("Target %d was expected to be %s, found %s", i, expectedOutput[0], actualOutput[0])
		}
	}
}

func TestEc2InstanceIdLookupFails(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		r, f := givenAnEc2InstanceIdLookupWithMockedParserAndRunner(true)
		msg := "A client error (InvalidInstanceID.NotFound) occurred when calling the DescribeInstances operation: The instance ID 'i-deadbeef' does not exist"
		host := "dummy-instance-id"
		instanceId := host + ".instanceid"
		l.ExpectInfof("EC2 Instance lookup: %s in %s", instanceId, f.region).Times(2)
		l.ExpectDebugf("Response from AWS API: %s", msg).Times(2)
		l.ExpectInfof("EC2 Instance lookup failed for %s (%s) in region %s (aws command failed): %s", host, instanceId, f.region, msg).Times(2)
		// When the aws cli tool fails
		awsReturns(r, instanceId, f.region, msg, util.DummyError{Msg: "test fails aws"}).Times(2)
		// Filtering doesn't touch the target list
		targets := target.GivenTargets(host, host)
		assertFilterResults(t, f, targets, targets)
		util.VerifyMocks(t, r)
		// And no panic happened on JSON parsing, even though the CLI tools output was not valid JSON, because we don't even try to parse the output.
	})
}

func TestEc2InstanceIdLookupInvalidJson(t *testing.T) {
	util.WithLogAssertions(t, func(l *util.MockLogger) {
		invalidJson := "HAH! not a valid JSON"
		host := "dummy-instance-id"
		instanceId := host + ".instanceid"
		r, f := givenAnEc2InstanceIdLookupWithMockedParserAndRunner(true)
		l.ExpectDebugf("Response from AWS API: %s", invalidJson)
		l.ExpectInfof("EC2 Instance lookup: %s in %s", instanceId, f.region)
		// When the AWS API returns invalid JSON
		awsReturns(r, instanceId, f.region, invalidJson, nil).Times(1)
		// I get a fatal error for filtering
		util.ExpectPanic(t, fmt.Sprintf("Invalid JSON returned by AWS API.\nError: invalid character 'H' looking for beginning of value\nJSON follows this line\n%s", invalidJson),
			func() { f.Filter([]target.Target{target.FromString(host)}) })
		util.VerifyMocks(t, r)
	})
}

func jsonWithoutReservations() string {
	bytes, _ := json.Marshal(ec2DescribeInstanceApiResponse{Reservations: []ec2Reservation{}})
	return string(bytes)
{'fraction_non_alphanumeric': 0.15945899379738113, 'fraction_numerical': 0.011026878015161957, 'mean_word_length': 4.984020618556701, 'pattern_counts': {'":': 6, '<': 15, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3816835', 'n_tokens_mistral': 4250, 'n_tokens_neox': 4050, 'n_words': 952}