// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package consul

import (
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/netutil"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/sdk/testutil"
)

// Test basic creation
func TestIntentionApply_new(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			SourceNS:        structs.IntentionDefaultNamespace,
			SourceName:      "test",
			DestinationNS:   structs.IntentionDefaultNamespace,
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
			SourceType:      structs.IntentionSourceConsul,
			Meta:            map[string]string{},
		},
	}
	var reply string

	// Record now to check created at time
	now := time.Now()

	// Create
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	require.NotEmpty(t, reply)

	// Read
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
		actual := resp.Intentions[0]
		require.Equal(t, resp.Index, actual.ModifyIndex)
		require.WithinDuration(t, now, actual.CreatedAt, 5*time.Second)
		require.WithinDuration(t, now, actual.UpdatedAt, 5*time.Second)

		actual.CreateIndex, actual.ModifyIndex = 0, 0
		actual.CreatedAt = ixn.Intention.CreatedAt
		actual.UpdatedAt = ixn.Intention.UpdatedAt
		actual.Hash = ixn.Intention.Hash
		//nolint:staticcheck
		ixn.Intention.UpdatePrecedence()
		// Partition fields will be normalized on Intention.Get
		ixn.Intention.FillPartitionAndNamespace(nil, true)
		require.Equal(t, ixn.Intention, actual)
	}

	// Rename should fail
	t.Run("renaming the destination should fail", func(t *testing.T) {
		// Setup a basic record to create
		ixn2 := structs.IntentionRequest{
			Datacenter: "dc1",
			Op:         structs.IntentionOpUpdate,
			Intention: &structs.Intention{
				ID:              ixn.Intention.ID,
				SourceNS:        structs.IntentionDefaultNamespace,
				SourceName:      "test",
				DestinationNS:   structs.IntentionDefaultNamespace,
				DestinationName: "test-updated",
				Action:          structs.IntentionActionAllow,
				SourceType:      structs.IntentionSourceConsul,
				Meta:            map[string]string{},
			},
		}

		var reply string
		err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn2, &reply)
		testutil.RequireErrorContains(t, err, "Cannot modify Destination partition/namespace/name for an intention once it exists.")
	})
}

// Test the source type defaults
func TestIntentionApply_defaultSourceType(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			SourceNS:        structs.IntentionDefaultNamespace,
			SourceName:      "test",
			DestinationNS:   structs.IntentionDefaultNamespace,
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
		},
	}
	var reply string

	// Create
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	require.NotEmpty(t, reply)

	// Read
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
		actual := resp.Intentions[0]
		require.Equal(t, structs.IntentionSourceConsul, actual.SourceType)
	}
}

// Shouldn't be able to create with an ID set
func TestIntentionApply_createWithID(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			ID:              generateUUID(),
			SourceName:      "test",
			DestinationName: "test2",
		},
	}
	var reply string

	// Create
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.NotNil(t, err)
	require.Contains(t, err, "ID must be empty")
}

// Test basic updating
func TestIntentionApply_updateGood(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			SourceNS:        structs.IntentionDefaultNamespace,
			SourceName:      "test",
			DestinationNS:   structs.IntentionDefaultNamespace,
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
			SourceType:      structs.IntentionSourceConsul,
			Meta:            map[string]string{},
		},
	}
	var reply string

	// Create
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	require.NotEmpty(t, reply)

	// Read CreatedAt
	var createdAt time.Time
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
		actual := resp.Intentions[0]
		createdAt = actual.CreatedAt
	}

	// Sleep a bit so that the updated at will definitely be different, not much
	time.Sleep(1 * time.Millisecond)

	// Update
	ixn.Op = structs.IntentionOpUpdate
	ixn.Intention.ID = reply
	ixn.Intention.Description = "updated"
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Read
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
		actual := resp.Intentions[0]
		require.Equal(t, createdAt, actual.CreatedAt)
		require.WithinDuration(t, time.Now(), actual.UpdatedAt, 5*time.Second)

		actual.CreateIndex, actual.ModifyIndex = 0, 0
		actual.CreatedAt = ixn.Intention.CreatedAt
		actual.UpdatedAt = ixn.Intention.UpdatedAt
		actual.Hash = ixn.Intention.Hash
		//nolint:staticcheck
		ixn.Intention.UpdatePrecedence()
		// Partition fields will be normalized on Intention.Get
		ixn.Intention.FillPartitionAndNamespace(nil, true)
		require.Equal(t, ixn.Intention, actual)
	}
}

// TestIntentionApply_NoSourcePeer makes sure that no intention is created with a SourcePeer since this is not supported
func TestIntentionApply_NoSourcePeer(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	_, s1 := testServer(t)
	codec := rpcClient(t, s1)

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			SourceNS:        structs.IntentionDefaultNamespace,
			SourceName:      "test",
			SourcePeer:      "peer1",
			DestinationNS:   structs.IntentionDefaultNamespace,
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
			SourceType:      structs.IntentionSourceConsul,
			Meta:            map[string]string{},
		},
	}
	var reply string
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.Error(t, err)
	require.Contains(t, err, "SourcePeer field is not supported on this endpoint. Use config entries instead")
	require.Empty(t, reply)
}

// Shouldn't be able to update a non-existent intention
func TestIntentionApply_updateNonExist(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpUpdate,
		Intention: &structs.Intention{
			ID:         generateUUID(),
			SourceName: "test",
		},
	}
	var reply string

	// Create
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.NotNil(t, err)
	require.Contains(t, err, "Cannot modify non-existent intention")
}

// Test basic deleting
func TestIntentionApply_deleteGood(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention: &structs.Intention{
			SourceName:      "test",
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
		},
	}
	var reply string

	// Delete a non existent intention should return an error
	testutil.RequireErrorContains(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &structs.IntentionRequest{
		Op: structs.IntentionOpDelete,
		Intention: &structs.Intention{
			ID: generateUUID(),
		},
	}, &reply), "Cannot delete non-existent intention")

	// Create
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	require.NotEmpty(t, reply)

	// Delete
	ixn.Op = structs.IntentionOpDelete
	ixn.Intention.ID = reply
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Read
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
		require.NotNil(t, err)
		require.Contains(t, err, ErrIntentionNotFound.Error())
	}
}

func TestIntentionApply_WithoutIDs(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	defaultEntMeta := structs.DefaultEnterpriseMetaInDefaultPartition()

	// Force "test" to be L7-capable.
	{
		args := structs.ConfigEntryRequest{
			Datacenter: "dc1",
			Entry: &structs.ServiceConfigEntry{
				Kind:     structs.ServiceDefaults,
				Name:     "test",
				Protocol: "http",
			},
		}

		var out bool
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
		require.True(t, out)
	}

	opApply := func(req *structs.IntentionRequest) error {
		req.Datacenter = "dc1"
		var ignored string
		return msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &ignored)
	}

	opGet := func(req *structs.IntentionQueryRequest) (*structs.IndexedIntentions, error) {
		req.Datacenter = "dc1"
		var resp structs.IndexedIntentions
		if err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp); err != nil {
			return nil, err
		}
		return &resp, nil
	}

	opList := func() (*structs.IndexedIntentions, error) {
		req := &structs.IntentionListRequest{
			Datacenter:     "dc1",
			EnterpriseMeta: *structs.WildcardEnterpriseMetaInDefaultPartition(),
		}
		var resp structs.IndexedIntentions
		if err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp); err != nil {
			return nil, err
		}
		return &resp, nil
	}

	configEntryUpsert := func(entry *structs.ServiceIntentionsConfigEntry) error {
		req := &structs.ConfigEntryRequest{
			Datacenter: "dc1",
			Op:         structs.ConfigEntryUpsert,
			Entry:      entry,
		}
		var ignored bool
		return msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", req, &ignored)
	}

	getConfigEntry := func(kind, name string) (*structs.ServiceIntentionsConfigEntry, error) {
		state := s1.fsm.State()
		_, entry, err := state.ConfigEntry(nil, kind, name, defaultEntMeta)
		if err != nil {
			return nil, err
		}

		ixn, ok := entry.(*structs.ServiceIntentionsConfigEntry)
		if !ok {
			return nil, fmt.Errorf("unexpected type: %T", entry)
		}
		return ixn, nil
	}

	// Setup a basic record to create
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
		Intention: &structs.Intention{
			SourceName:      "test",
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
			Description:     "original",
		},
	}))

	// Read it back.
	{
		resp, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "test",
				DestinationName: "test",
			},
		})
		require.NoError(t, err)

		require.Len(t, resp.Intentions, 1)
		got := resp.Intentions[0]
		require.Equal(t, "original", got.Description)

		// L4
		require.Equal(t, structs.IntentionActionAllow, got.Action)
		require.Empty(t, got.Permissions)

		// Verify it is in the new-style.
		require.Empty(t, got.ID)
		require.True(t, got.CreatedAt.IsZero())
		require.True(t, got.UpdatedAt.IsZero())
	}

	// Double check that there's only 1.
	{
		resp, err := opList()
		require.NoError(t, err)
		require.Len(t, resp.Intentions, 1)
	}

	// Verify the config entry structure is expected.
	{
		entry, err := getConfigEntry(structs.ServiceIntentions, "test")
		require.NoError(t, err)
		require.NotNil(t, entry)

		expect := &structs.ServiceIntentionsConfigEntry{
			Kind:           structs.ServiceIntentions,
			Name:           "test",
			EnterpriseMeta: *defaultEntMeta,
			Sources: []*structs.SourceIntention{
				{
					Name:           "test",
					EnterpriseMeta: *defaultEntMeta,
					Action:         structs.IntentionActionAllow,
					Description:    "original",
					Precedence:     9,
					Type:           structs.IntentionSourceConsul,
				},
			},
			RaftIndex: entry.RaftIndex,
		}
		entry.Hash = 0
		require.Equal(t, expect, entry)
	}

	// Update in place.
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
		Intention: &structs.Intention{
			SourceName:      "test",
			DestinationName: "test",
			Action:          structs.IntentionActionAllow,
			Description:     "updated",
		},
	}))

	// Read it back.
	{
		resp, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "test",
				DestinationName: "test",
			},
		})
		require.NoError(t, err)

		require.Len(t, resp.Intentions, 1)
		got := resp.Intentions[0]
		require.Equal(t, "updated", got.Description)

		// L4
		require.Equal(t, structs.IntentionActionAllow, got.Action)
		require.Empty(t, got.Permissions)

		// Verify it is in the new-style.
		require.Empty(t, got.ID)
		require.True(t, got.CreatedAt.IsZero())
		require.True(t, got.UpdatedAt.IsZero())
	}

	// Double check that there's only 1.
	{
		resp, err := opList()
		require.NoError(t, err)
		require.Len(t, resp.Intentions, 1)
	}

	// Create a second one sharing the same destination
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
		Intention: &structs.Intention{
			SourceName:      "assay",
			DestinationName: "test",
			Description:     "original-2",
			Permissions: []*structs.IntentionPermission{
				{
					Action: structs.IntentionActionAllow,
					HTTP: &structs.IntentionHTTPPermission{
						PathExact: "/foo",
					},
				},
			},
		},
	}))

	// Read it back.
	{
		resp, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "assay",
				DestinationName: "test",
			},
		})
		require.NoError(t, err)

		require.Len(t, resp.Intentions, 1)
		got := resp.Intentions[0]
		require.Equal(t, "original-2", got.Description)

		// L7
		require.Empty(t, got.Action)
		require.Equal(t, []*structs.IntentionPermission{
			{
				Action: structs.IntentionActionAllow,
				HTTP: &structs.IntentionHTTPPermission{
					PathExact: "/foo",
				},
			},
		}, got.Permissions)

		// Verify it is in the new-style.
		require.Empty(t, got.ID)
		require.True(t, got.CreatedAt.IsZero())
		require.True(t, got.UpdatedAt.IsZero())
	}

	// Double check that there's 2 now.
	{
		resp, err := opList()
		require.NoError(t, err)
		require.Len(t, resp.Intentions, 2)
	}

	// Verify the config entry structure is expected.
	{
		entry, err := getConfigEntry(structs.ServiceIntentions, "test")
		require.NoError(t, err)
		require.NotNil(t, entry)

		expect := &structs.ServiceIntentionsConfigEntry{
			Kind:           structs.ServiceIntentions,
			Name:           "test",
			EnterpriseMeta: *defaultEntMeta,
			Sources: []*structs.SourceIntention{
				{
					Name:           "test",
					EnterpriseMeta: *defaultEntMeta,
					Action:         structs.IntentionActionAllow,
					Description:    "updated",
					Precedence:     9,
					Type:           structs.IntentionSourceConsul,
				},
				{
					Name:           "assay",
					EnterpriseMeta: *defaultEntMeta,
					Description:    "original-2",
					Precedence:     9,
					Type:           structs.IntentionSourceConsul,
					Permissions: []*structs.IntentionPermission{
						{
							Action: structs.IntentionActionAllow,
							HTTP: &structs.IntentionHTTPPermission{
								PathExact: "/foo",
							},
						},
					},
				},
			},
			RaftIndex: entry.RaftIndex,
		}
		entry.Hash = 0
		require.Equal(t, expect, entry)
	}

	// Delete a non existent intention should act like it did work
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpDelete,
		Intention: &structs.Intention{
			SourceName:      "ghost",
			DestinationName: "phantom",
		},
	}))

	// Delete the original
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpDelete,
		Intention: &structs.Intention{
			SourceName:      "test",
			DestinationName: "test",
		},
	}))

	// Read it back (not found)
	{
		_, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "test",
				DestinationName: "test",
			},
		})
		testutil.RequireErrorContains(t, err, ErrIntentionNotFound.Error())
	}

	// Double check that there's 1 again.
	{
		resp, err := opList()
		require.NoError(t, err)
		require.Len(t, resp.Intentions, 1)
	}

	// Verify the config entry structure is expected.
	{
		entry, err := getConfigEntry(structs.ServiceIntentions, "test")
		require.NoError(t, err)
		require.NotNil(t, entry)

		expect := &structs.ServiceIntentionsConfigEntry{
			Kind:           structs.ServiceIntentions,
			Name:           "test",
			EnterpriseMeta: *defaultEntMeta,
			Sources: []*structs.SourceIntention{
				{
					Name:           "assay",
					EnterpriseMeta: *defaultEntMeta,
					Description:    "original-2",
					Precedence:     9,
					Type:           structs.IntentionSourceConsul,
					Permissions: []*structs.IntentionPermission{
						{
							Action: structs.IntentionActionAllow,
							HTTP: &structs.IntentionHTTPPermission{
								PathExact: "/foo",
							},
						},
					},
				},
			},
			RaftIndex: entry.RaftIndex,
		}
		entry.Hash = 0
		require.Equal(t, expect, entry)
	}

	// Set metadata on the config entry directly.
	{
		require.NoError(t, configEntryUpsert(&structs.ServiceIntentionsConfigEntry{
			Kind:           structs.ServiceIntentions,
			Name:           "test",
			EnterpriseMeta: *defaultEntMeta,
			Meta: map[string]string{
				"foo": "bar",
				"zim": "gir",
			},
			Sources: []*structs.SourceIntention{
				{
					Name:           "assay",
					EnterpriseMeta: *defaultEntMeta,
					Action:         structs.IntentionActionDeny,
					Description:    "original-2",
					Precedence:     9,
					Type:           structs.IntentionSourceConsul,
				},
			},
		}))
	}

	// Attempt to create a new intention and set the metadata.
	{
		err := opApply(&structs.IntentionRequest{
			Op: structs.IntentionOpUpsert,
			Intention: &structs.Intention{
				SourceName:      "foo",
				DestinationName: "bar",
				Action:          structs.IntentionActionDeny,
				Meta:            map[string]string{"horseshoe": "crab"},
			},
		})
		testutil.RequireErrorContains(t, err, "Meta must not be specified")
	}

	// Attempt to update an intention and change the metadata.
	{
		err := opApply(&structs.IntentionRequest{
			Op: structs.IntentionOpUpsert,
			Intention: &structs.Intention{
				SourceName:      "assay",
				DestinationName: "test",
				Action:          structs.IntentionActionDeny,
				Description:     "original-3",
				Meta:            map[string]string{"horseshoe": "crab"},
			},
		})
		testutil.RequireErrorContains(t, err, "Meta must not be specified, or should be unchanged during an update.")
	}

	// Try again with the same metadata.
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
		Intention: &structs.Intention{
			SourceName:      "assay",
			DestinationName: "test",
			Action:          structs.IntentionActionDeny,
			Description:     "original-3",
			Meta: map[string]string{
				"foo": "bar",
				"zim": "gir",
			},
		},
	}))

	// Read it back.
	{
		resp, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "assay",
				DestinationName: "test",
			},
		})
		require.NoError(t, err)

		require.Len(t, resp.Intentions, 1)
		got := resp.Intentions[0]
		require.Equal(t, "original-3", got.Description)
		require.Equal(t, map[string]string{
			"foo": "bar",
			"zim": "gir",
		}, got.Meta)

		// Verify it is in the new-style.
		require.Empty(t, got.ID)
		require.True(t, got.CreatedAt.IsZero())
		require.True(t, got.UpdatedAt.IsZero())
	}

	// Try again with NO metadata.
	require.NoError(t, opApply(&structs.IntentionRequest{
		Op: structs.IntentionOpUpsert,
		Intention: &structs.Intention{
			SourceName:      "assay",
			DestinationName: "test",
			Action:          structs.IntentionActionDeny,
			Description:     "original-4",
		},
	}))

	// Read it back.
	{
		resp, err := opGet(&structs.IntentionQueryRequest{
			Exact: &structs.IntentionQueryExact{
				SourceName:      "assay",
				DestinationName: "test",
			},
		})
		require.NoError(t, err)

		require.Len(t, resp.Intentions, 1)
		got := resp.Intentions[0]
		require.Equal(t, "original-4", got.Description)
		require.Equal(t, map[string]string{
			"foo": "bar",
			"zim": "gir",
		}, got.Meta)

		// Verify it is in the new-style.
		require.Empty(t, got.ID)
		require.True(t, got.CreatedAt.IsZero())
		require.True(t, got.UpdatedAt.IsZero())
	}
}

// Test apply with a deny ACL
func TestIntentionApply_aclDeny(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	rules := `
service "foobar" {
	policy = "deny"
	intentions = "write"
}`
	token := createToken(t, codec, rules)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "foobar"

	// Create without a token should error since default deny
	var reply string
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Now add the token and try again.
	ixn.Token = token
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Read
	ixn.Intention.ID = reply
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:   "dc1",
			IntentionID:  ixn.Intention.ID,
			QueryOptions: structs.QueryOptions{Token: "root"},
		}
		var resp structs.IndexedIntentions
		require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
		actual := resp.Intentions[0]
		require.Equal(t, resp.Index, actual.ModifyIndex)

		actual.CreateIndex, actual.ModifyIndex = 0, 0
		actual.CreatedAt = ixn.Intention.CreatedAt
		actual.UpdatedAt = ixn.Intention.UpdatedAt
		actual.Hash = ixn.Intention.Hash
		//nolint:staticcheck
		ixn.Intention.UpdatePrecedence()
		require.Equal(t, ixn.Intention, actual)
	}
}

func TestIntention_WildcardACLEnforcement(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	_, srv, codec := testACLServerWithConfig(t, nil, false)
	waitForLeaderEstablishment(t, srv)

	// create some test policies.

	writeToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "write" }`)
	require.NoError(t, err)
	readToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "read" }`)
	require.NoError(t, err)
	exactToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "*" { policy = "deny" intentions = "write" }`)
	require.NoError(t, err)
	wildcardPrefixToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "*" { policy = "deny" intentions = "write" }`)
	require.NoError(t, err)
	fooToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "foo" { policy = "deny" intentions = "write" }`)
	require.NoError(t, err)
	denyToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "" { policy = "deny" intentions = "deny" }`)
	require.NoError(t, err)

	doIntentionCreate := func(t *testing.T, token string, dest string, deny bool) string {
		t.Helper()
		ixn := structs.IntentionRequest{
			Datacenter: "dc1",
			Op:         structs.IntentionOpCreate,
			Intention: &structs.Intention{
				SourceNS:        "default",
				SourceName:      "*",
				DestinationNS:   "default",
				DestinationName: dest,
				Action:          structs.IntentionActionAllow,
				SourceType:      structs.IntentionSourceConsul,
			},
			WriteRequest: structs.WriteRequest{Token: token},
		}
		var reply string
		err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
		if deny {
			require.Error(t, err)
			require.True(t, acl.IsErrPermissionDenied(err))
			return ""
		} else {
			require.NoError(t, err)
			require.NotEmpty(t, reply)
			return reply
		}
	}

	t.Run("deny-write-for-read-token", func(t *testing.T) {
		// This tests ensures that tokens with only read access to all intentions
		// cannot create a wildcard intention
		doIntentionCreate(t, readToken.SecretID, "*", true)
	})

	t.Run("deny-write-for-exact-wildcard-rule", func(t *testing.T) {
		// This test ensures that having a rules like:
		// service "*" {
		//    intentions = "write"
		// }
		// will not actually allow creating an intention with a wildcard service name
		doIntentionCreate(t, exactToken.SecretID, "*", true)
	})

	t.Run("deny-write-for-prefix-wildcard-rule", func(t *testing.T) {
		// This test ensures that having a rules like:
		// service_prefix "*" {
		//    intentions = "write"
		// }
		// will not actually allow creating an intention with a wildcard service name
		doIntentionCreate(t, wildcardPrefixToken.SecretID, "*", true)
	})

	var intentionID string
	allowWriteOk := t.Run("allow-write", func(t *testing.T) {
		// tests that a token with all the required privileges can create
		// intentions with a wildcard destination
		intentionID = doIntentionCreate(t, writeToken.SecretID, "*", false)
	})

	requireAllowWrite := func(t *testing.T) {
		t.Helper()
		if !allowWriteOk {
			t.Skip("Skipping because the allow-write subtest failed")
		}
	}

	doIntentionRead := func(t *testing.T, token string, deny bool) {
		t.Helper()
		requireAllowWrite(t)
		req := &structs.IntentionQueryRequest{
			Datacenter:   "dc1",
			IntentionID:  intentionID,
			QueryOptions: structs.QueryOptions{Token: token},
		}

		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
		if deny {
			require.Error(t, err)
			require.True(t, acl.IsErrPermissionDenied(err))
		} else {
			require.NoError(t, err)
			require.Len(t, resp.Intentions, 1)
			require.Equal(t, "*", resp.Intentions[0].DestinationName)
		}
	}

	t.Run("allow-read-for-write-token", func(t *testing.T) {
		doIntentionRead(t, writeToken.SecretID, false)
	})

	t.Run("allow-read-for-read-token", func(t *testing.T) {
		doIntentionRead(t, readToken.SecretID, false)
	})

	t.Run("allow-read-for-exact-wildcard-token", func(t *testing.T) {
		// this is allowed because, the effect of the policy is to grant
		// intention:write on the service named "*". When reading the
		// intention we will validate that the token has read permissions
		// for any intention that would match the wildcard.
		doIntentionRead(t, exactToken.SecretID, false)
	})

	t.Run("allow-read-for-prefix-wildcard-token", func(t *testing.T) {
		// this is allowed for the same reasons as for the
		// exact-wildcard-token case
		doIntentionRead(t, wildcardPrefixToken.SecretID, false)
	})

	t.Run("deny-read-for-deny-token", func(t *testing.T) {
		doIntentionRead(t, denyToken.SecretID, true)
	})

	doIntentionList := func(t *testing.T, token string, deny bool) {
		t.Helper()
		requireAllowWrite(t)
		req := &structs.IntentionListRequest{
			Datacenter:   "dc1",
			QueryOptions: structs.QueryOptions{Token: token},
		}

		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)
		// even with permission denied this should return success but with an empty list
		require.NoError(t, err)
		if deny {
			require.Empty(t, resp.Intentions)
		} else {
			require.Len(t, resp.Intentions, 1)
			require.Equal(t, "*", resp.Intentions[0].DestinationName)
		}
	}

	t.Run("allow-list-for-write-token", func(t *testing.T) {
		doIntentionList(t, writeToken.SecretID, false)
	})

	t.Run("allow-list-for-read-token", func(t *testing.T) {
		doIntentionList(t, readToken.SecretID, false)
	})

	t.Run("allow-list-for-exact-wildcard-token", func(t *testing.T) {
		doIntentionList(t, exactToken.SecretID, false)
	})

	t.Run("allow-list-for-prefix-wildcard-token", func(t *testing.T) {
		doIntentionList(t, wildcardPrefixToken.SecretID, false)
	})

	t.Run("deny-list-for-deny-token", func(t *testing.T) {
		doIntentionList(t, denyToken.SecretID, true)
	})

	doIntentionMatch := func(t *testing.T, token string, deny bool) {
		t.Helper()
		requireAllowWrite(t)
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Match: &structs.IntentionQueryMatch{
				Type: structs.IntentionMatchDestination,
				Entries: []structs.IntentionMatchEntry{
					{
						Namespace: "default",
						Name:      "*",
					},
				},
			},
			QueryOptions: structs.QueryOptions{Token: token},
		}

		var resp structs.IndexedIntentionMatches
		err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)
		if deny {
			require.Error(t, err)
			require.Empty(t, resp.Matches)
		} else {
			require.NoError(t, err)
			require.Len(t, resp.Matches, 1)
			require.Len(t, resp.Matches[0], 1)
			require.Equal(t, "*", resp.Matches[0][0].DestinationName)
		}
	}

	t.Run("allow-match-for-write-token", func(t *testing.T) {
		doIntentionMatch(t, writeToken.SecretID, false)
	})

	t.Run("allow-match-for-read-token", func(t *testing.T) {
		doIntentionMatch(t, readToken.SecretID, false)
	})

	t.Run("allow-match-for-exact-wildcard-token", func(t *testing.T) {
		doIntentionMatch(t, exactToken.SecretID, false)
	})

	t.Run("allow-match-for-prefix-wildcard-token", func(t *testing.T) {
		doIntentionMatch(t, wildcardPrefixToken.SecretID, false)
	})

	t.Run("deny-match-for-deny-token", func(t *testing.T) {
		doIntentionMatch(t, denyToken.SecretID, true)
	})

	// Since we can't rename the destination, create a new intention for the rest of this test.
	wildIntentionID := intentionID
	fooIntentionID := doIntentionCreate(t, writeToken.SecretID, "foo", false)

	doIntentionUpdate := func(t *testing.T, token string, intentionID, dest, description string, deny bool) {
		t.Helper()
		requireAllowWrite(t)
		ixn := structs.IntentionRequest{
			Datacenter: "dc1",
			Op:         structs.IntentionOpUpdate,
			Intention: &structs.Intention{
				ID:              intentionID,
				SourceNS:        "default",
				SourceName:      "*",
				DestinationNS:   "default",
				DestinationName: dest,
				Description:     description,
				Action:          structs.IntentionActionAllow,
				SourceType:      structs.IntentionSourceConsul,
			},
			WriteRequest: structs.WriteRequest{Token: token},
		}
		var reply string
		err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
		if deny {
			require.Error(t, err)
			require.True(t, acl.IsErrPermissionDenied(err))
		} else {
			require.NoError(t, err)
		}
	}

	t.Run("deny-update-for-foo-token", func(t *testing.T) {
		doIntentionUpdate(t, fooToken.SecretID, wildIntentionID, "*", "wild-desc", true)
	})

	t.Run("allow-update-for-prefix-token", func(t *testing.T) {
		// This tests that the prefix token can edit wildcard intentions and regular intentions.
		doIntentionUpdate(t, writeToken.SecretID, fooIntentionID, "foo", "foo-desc-two", false)
		doIntentionUpdate(t, writeToken.SecretID, wildIntentionID, "*", "wild-desc-two", false)
	})

	doIntentionDelete := func(t *testing.T, token string, intentionID string, deny bool) {
		t.Helper()
		requireAllowWrite(t)
		ixn := structs.IntentionRequest{
			Datacenter: "dc1",
			Op:         structs.IntentionOpDelete,
			Intention: &structs.Intention{
				ID: intentionID,
			},
			WriteRequest: structs.WriteRequest{Token: token},
		}
		var reply string
		err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
		if deny {
			require.Error(t, err)
			require.True(t, acl.IsErrPermissionDenied(err))
		} else {
			require.NoError(t, err)
		}
	}

	t.Run("deny-delete-for-read-token", func(t *testing.T) {
		doIntentionDelete(t, readToken.SecretID, fooIntentionID, true)
	})

	t.Run("deny-delete-for-exact-wildcard-rule", func(t *testing.T) {
		// This test ensures that having a rules like:
		// service "*" {
		//    intentions = "write"
		// }
		// will not actually allow deleting an intention with a wildcard service name
		doIntentionDelete(t, exactToken.SecretID, fooIntentionID, true)
	})

	t.Run("deny-delete-for-prefix-wildcard-rule", func(t *testing.T) {
		// This test ensures that having a rules like:
		// service_prefix "*" {
		//    intentions = "write"
		// }
		// will not actually allow creating an intention with a wildcard service name
		doIntentionDelete(t, wildcardPrefixToken.SecretID, fooIntentionID, true)
	})

	t.Run("allow-delete", func(t *testing.T) {
		// tests that a token with all the required privileges can delete
		// intentions with a wildcard destination
		doIntentionDelete(t, writeToken.SecretID, fooIntentionID, false)
	})
}

// Test apply with delete and a default deny ACL
func TestIntentionApply_aclDelete(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	rules := `
service "foobar" {
	policy = "deny"
	intentions = "write"
}`
	token := createToken(t, codec, rules)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "foobar"
	ixn.Token = token

	// Create
	var reply string
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Try to do a delete with no token; this should get rejected.
	ixn.Op = structs.IntentionOpDelete
	ixn.Intention.ID = reply
	ixn.Token = ""
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Try again with the original token. This should go through.
	ixn.Token = token
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Verify it is gone
	{
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}
		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
		require.NotNil(t, err)
		require.Contains(t, err.Error(), ErrIntentionNotFound.Error())
	}
}

// Test apply with update and a default deny ACL
func TestIntentionApply_aclUpdate(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	rules := `
service "foobar" {
	policy = "deny"
	intentions = "write"
}`
	token := createToken(t, codec, rules)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "foobar"
	ixn.Token = token

	// Create
	var reply string
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Try to do an update without a token; this should get rejected.
	ixn.Op = structs.IntentionOpUpdate
	ixn.Intention.ID = reply
	ixn.Token = ""
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Try again with the original token; this should go through.
	ixn.Token = token
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
}

// Test apply with a management token
func TestIntentionApply_aclManagement(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "foobar"
	ixn.Token = "root"

	// Create
	var reply string
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	ixn.Intention.ID = reply

	// Update
	ixn.Op = structs.IntentionOpUpdate
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Delete
	ixn.Op = structs.IntentionOpDelete
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
}

// Test update changing the name where an ACL won't allow it
func TestIntentionApply_aclUpdateChange(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	rules := `
service "foobar" {
	policy = "deny"
	intentions = "write"
}`
	token := createToken(t, codec, rules)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "bar"
	ixn.Token = "root"

	// Create
	var reply string
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))

	// Try to do an update without a token; this should get rejected.
	ixn.Op = structs.IntentionOpUpdate
	ixn.Intention.ID = reply
	ixn.Intention.DestinationName = "foo"
	ixn.Token = token
	err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
	require.True(t, acl.IsErrPermissionDenied(err))
}

// Test reading with ACLs
func TestIntentionGet_acl(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Create an ACL with service write permissions. This will grant
	// intentions read on either end of an intention.
	token, err := upsertTestTokenWithPolicyRules(codec, "root", "dc1", `
	service "foobar" {
		policy = "write"
	}`)
	require.NoError(t, err)

	// Setup a basic record to create
	ixn := structs.IntentionRequest{
		Datacenter: "dc1",
		Op:         structs.IntentionOpCreate,
		Intention:  structs.TestIntention(t),
	}
	ixn.Intention.DestinationName = "foobar"
	ixn.Token = "root"

	// Create
	var reply string
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	ixn.Intention.ID = reply

	t.Run("Read by ID without token should be error", func(t *testing.T) {
		req := &structs.IntentionQueryRequest{
			Datacenter:  "dc1",
			IntentionID: ixn.Intention.ID,
		}

		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
		require.True(t, acl.IsErrPermissionDenied(err))
		require.Len(t, resp.Intentions, 0)
	})

	t.Run("Read by ID with token should work", func(t *testing.T) {
		req := &structs.IntentionQueryRequest{
			Datacenter:   "dc1",
			IntentionID:  ixn.Intention.ID,
			QueryOptions: structs.QueryOptions{Token: token.SecretID},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
	})

	t.Run("Read by Exact without token should be error", func(t *testing.T) {
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Exact: &structs.IntentionQueryExact{
				SourceNS:        structs.IntentionDefaultNamespace,
				SourceName:      "api",
				DestinationNS:   structs.IntentionDefaultNamespace,
				DestinationName: "foobar",
			},
		}

		var resp structs.IndexedIntentions
		err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp)
		require.True(t, acl.IsErrPermissionDenied(err))
		require.Len(t, resp.Intentions, 0)
	})

	t.Run("Read by Exact with token should work", func(t *testing.T) {
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Exact: &structs.IntentionQueryExact{
				SourceNS:        structs.IntentionDefaultNamespace,
				SourceName:      "api",
				DestinationNS:   structs.IntentionDefaultNamespace,
				DestinationName: "foobar",
			},
			QueryOptions: structs.QueryOptions{Token: token.SecretID},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp))
		require.Len(t, resp.Intentions, 1)
	})
}

func TestIntentionList(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()

	codec := rpcClient(t, s1)
	defer codec.Close()
	waitForLeaderEstablishment(t, s1)

	// Test with no intentions inserted yet
	{
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
		}
		var resp structs.IndexedIntentions
		require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.NotNil(t, resp.Intentions)
		require.Len(t, resp.Intentions, 0)
	}
}

// Test listing with ACLs
func TestIntentionList_acl(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, testServerACLConfig)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service_prefix "foo" { policy = "write" }`)
	require.NoError(t, err)

	const (
		bexprMatch   = "DestinationName matches `f.*`"
		bexprNoMatch = "DestinationName matches `nomatch.*`"
	)

	// Create a few records
	for _, name := range []string{"foobar", "bar", "baz"} {
		ixn := structs.IntentionRequest{
			Datacenter: "dc1",
			Op:         structs.IntentionOpCreate,
			Intention:  structs.TestIntention(t),
		}
		ixn.Intention.SourceNS = "default"
		ixn.Intention.DestinationNS = "default"
		ixn.Intention.DestinationName = name
		ixn.Token = TestDefaultInitialManagementToken

		// Create
		var reply string
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
	}

	// Test with no token
	t.Run("no-token", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 0)
		require.False(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
	})

	// Test with management token
	t.Run("initial-management-token", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter:   "dc1",
			QueryOptions: structs.QueryOptions{Token: TestDefaultInitialManagementToken},
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 3)
		require.False(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
	})

	// Test with user token
	t.Run("user-token", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter:   "dc1",
			QueryOptions: structs.QueryOptions{Token: token.SecretID},
		}
		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 1)
		require.True(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
	})

	// maskResultsFilteredByACLs() in rpc.go sets ResultsFilteredByACLs to false if the token is an empty string
	// after resp.QueryMeta.ResultsFilteredByACLs has been determined to be true from filterACLs().
	t.Run("filtered with no token should return no results and ResultsFilteredByACLs equal to false", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
			QueryOptions: structs.QueryOptions{
				Filter: bexprMatch,
			},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 0)
		require.False(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
	})

	// has access to everything
	t.Run("filtered with initial management token should return 1 and ResultsFilteredByACLs equal to false", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
			QueryOptions: structs.QueryOptions{
				Token:  TestDefaultInitialManagementToken,
				Filter: bexprMatch,
			},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 1)
		require.False(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
	})

	// ResultsFilteredByACLs should reflect user does not have access to read all intentions but has access to some.
	t.Run("filtered with user token whose permissions match filter should return 1 and ResultsFilteredByACLs equal to true", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
			QueryOptions: structs.QueryOptions{
				Token:  token.SecretID,
				Filter: bexprMatch,
			},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 1)
		require.True(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
	})

	// ResultsFilteredByACLs need to act as though no filter was applied.
	t.Run("filtered with user token whose permissions do match filter should return 0 and ResultsFilteredByACLs equal to true", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
			QueryOptions: structs.QueryOptions{
				Token:  token.SecretID,
				Filter: bexprNoMatch,
			},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 0)
		require.True(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be true")
	})

	// ResultsFilteredByACLs should reflect user does not have access to read any intentions
	t.Run("filtered with anonymous token should return 0 and ResultsFilteredByACLs equal to true", func(t *testing.T) {
		req := &structs.IntentionListRequest{
			Datacenter: "dc1",
			QueryOptions: structs.QueryOptions{
				Token:  "anonymous",
				Filter: bexprMatch,
			},
		}

		var resp structs.IndexedIntentions
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
		require.Len(t, resp.Intentions, 0)
		require.False(t, resp.ResultsFilteredByACLs, "ResultsFilteredByACLs should be false")
	})
}

// Test basic matching. We don't need to exhaustively test inputs since this
// is tested in the agent/consul/state package.
func TestIntentionMatch_good(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServer(t)
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	// Create some records
	{
		insert := [][]string{
			{"default", "*", "default", "*"},
			{"default", "*", "default", "bar"},
			{"default", "*", "default", "baz"}, // shouldn't match
		}

		for _, v := range insert {
			ixn := structs.IntentionRequest{
				Datacenter: "dc1",
				Op:         structs.IntentionOpCreate,
				Intention: &structs.Intention{
					SourceNS:        v[0],
					SourceName:      v[1],
					DestinationNS:   v[2],
					DestinationName: v[3],
					Action:          structs.IntentionActionAllow,
				},
			}

			// Create
			var reply string
			require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
		}
	}

	// Match
	req := &structs.IntentionQueryRequest{
		Datacenter: "dc1",
		Match: &structs.IntentionQueryMatch{
			Type: structs.IntentionMatchDestination,
			Entries: []structs.IntentionMatchEntry{
				{Name: "bar"},
			},
		},
	}
	var resp structs.IndexedIntentionMatches
	require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
	require.Len(t, resp.Matches, 1)

	expected := [][]string{
		{"default", "*", "default", "bar"},
		{"default", "*", "default", "*"},
	}
	var actual [][]string
	for _, ixn := range resp.Matches[0] {
		actual = append(actual, []string{
			ixn.SourceNS,
			ixn.SourceName,
			ixn.DestinationNS,
			ixn.DestinationName,
		})
	}
	require.Equal(t, expected, actual)
}

func TestIntentionMatch_BlockOnNoChange(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	_, s1 := testServerWithConfig(t, func(c *Config) {
		c.DevMode = true // keep it in ram to make it 10x faster on macos
	})

	codec := rpcClient(t, s1)

	waitForLeaderEstablishment(t, s1)

	run := func(t *testing.T, dataPrefix string, expectMatches int) {
		rpcBlockingQueryTestHarness(t,
			func(minQueryIndex uint64) (*structs.QueryMeta, <-chan error) {
				args := &structs.IntentionQueryRequest{
					Datacenter: "dc1",
					Match: &structs.IntentionQueryMatch{
						Type: structs.IntentionMatchDestination,
						Entries: []structs.IntentionMatchEntry{
							{Name: "bar"},
						},
					},
				}
				args.MinQueryIndex = minQueryIndex

				var out structs.IndexedIntentionMatches
				errCh := channelCallRPC(s1, "Intention.Match", args, &out, func() error {
					if len(out.Matches) != 1 {
						return fmt.Errorf("expected 1 match got %d", len(out.Matches))
					}
					if len(out.Matches[0]) != expectMatches {
						return fmt.Errorf("expected %d inner matches got %d", expectMatches, len(out.Matches[0]))
					}
					return nil
				})
				return &out.QueryMeta, errCh
			},
			func(i int) <-chan error {
				var out string
				return channelCallRPC(s1, "Intention.Apply", &structs.IntentionRequest{
					Datacenter: "dc1",
					Op:         structs.IntentionOpCreate,
					Intention: &structs.Intention{
						// {"default", "*", "default", "baz"}, // shouldn't match
						SourceNS:        "default",
						SourceName:      "*",
						DestinationNS:   "default",
						DestinationName: fmt.Sprintf(dataPrefix+"%d", i),
						Action:          structs.IntentionActionAllow,
					},
				}, &out, nil)
			},
		)
	}

	testutil.RunStep(t, "test the errNotFound path", func(t *testing.T) {
		run(t, "other", 0)
	})

	// Create some records
	{
		insert := [][]string{
			{"default", "*", "default", "*"},
			{"default", "*", "default", "bar"},
			{"default", "*", "default", "baz"}, // shouldn't match
		}

		for _, v := range insert {
			var out string
			require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &structs.IntentionRequest{
				Datacenter: "dc1",
				Op:         structs.IntentionOpCreate,
				Intention: &structs.Intention{
					SourceNS:        v[0],
					SourceName:      v[1],
					DestinationNS:   v[2],
					DestinationName: v[3],
					Action:          structs.IntentionActionAllow,
				},
			}, &out))
		}
	}

	testutil.RunStep(t, "test the errNotChanged path", func(t *testing.T) {
		run(t, "completely-different-other", 2)
	})
}

// Test matching with ACLs
func TestIntentionMatch_acl(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	_, srv, codec := testACLServerWithConfig(t, nil, false)
	waitForLeaderEstablishment(t, srv)

	token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "bar" { policy = "write" }`)
	require.NoError(t, err)

	// Create some records
	{
		insert := []string{
			"*",
			"bar",
			"baz",
		}

		for _, v := range insert {
			ixn := structs.IntentionRequest{
				Datacenter: "dc1",
				Op:         structs.IntentionOpCreate,
				Intention:  structs.TestIntention(t),
			}
			ixn.Intention.DestinationName = v
			ixn.Token = TestDefaultInitialManagementToken

			// Create
			var reply string
			require.Nil(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
		}
	}

	// Test with no token
	{
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Match: &structs.IntentionQueryMatch{
				Type: structs.IntentionMatchDestination,
				Entries: []structs.IntentionMatchEntry{
					{
						Namespace: "default",
						Name:      "bar",
					},
				},
			},
		}
		var resp structs.IndexedIntentionMatches
		err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)
		require.Error(t, err)
		require.True(t, acl.IsErrPermissionDenied(err))
		require.Len(t, resp.Matches, 0)
	}

	// Test with proper token
	{
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Match: &structs.IntentionQueryMatch{
				Type: structs.IntentionMatchDestination,
				Entries: []structs.IntentionMatchEntry{
					{
						Namespace: "default",
						Name:      "bar",
					},
				},
			},
			QueryOptions: structs.QueryOptions{Token: token.SecretID},
		}
		var resp structs.IndexedIntentionMatches
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp))
		require.Len(t, resp.Matches, 1)

		expected := []string{"bar", "*"}
		var actual []string
		for _, ixn := range resp.Matches[0] {
			actual = append(actual, ixn.DestinationName)
		}

		require.ElementsMatch(t, expected, actual)
	}
}

func TestIntentionCheck(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	type testcase struct {
		aclsEnabled   bool
		defaultACL    string
		defaultIxn    string
		expectAllowed bool
	}
	tcs := map[string]testcase{
		"acls disabled, no default intention policy": {
			aclsEnabled:   false,
			expectAllowed: true,
		},
		"acls disabled, default intention allow": {
			aclsEnabled:   false,
			defaultIxn:    "allow",
			expectAllowed: true,
		},
		"acls disabled, default intention deny": {
			aclsEnabled:   false,
			defaultIxn:    "deny",
			expectAllowed: false,
		},
		"acls deny, no default intention policy": {
			aclsEnabled:   true,
			defaultACL:    "deny",
			expectAllowed: false,
		},
		"acls allow, no default intention policy": {
			aclsEnabled:   true,
			defaultACL:    "allow",
			expectAllowed: true,
		},
		"acls deny, default intention allow": {
			aclsEnabled:   true,
			defaultACL:    "deny",
			defaultIxn:    "allow",
			expectAllowed: true,
		},
		"acls allow, default intention deny": {
			aclsEnabled:   true,
			defaultACL:    "allow",
			defaultIxn:    "deny",
			expectAllowed: false,
		},
	}
	for name, tc := range tcs {
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			_, s1 := testServerWithConfig(t, func(c *Config) {
				if tc.aclsEnabled {
					c.PrimaryDatacenter = "dc1"
					c.ACLsEnabled = true
					c.ACLInitialManagementToken = "root"
					c.ACLResolverSettings.ACLDefaultPolicy = tc.defaultACL
				}
				c.DefaultIntentionPolicy = tc.defaultIxn
			})
			codec := rpcClient(t, s1)

			waitForLeaderEstablishment(t, s1)

			req := &structs.IntentionQueryRequest{
				Datacenter: "dc1",
				Check: &structs.IntentionQueryCheck{
					SourceName:      "bar",
					DestinationName: "qux",
					SourceType:      structs.IntentionSourceConsul,
				},
			}
			req.Token = "root"

			var resp structs.IntentionQueryCheckResponse
			require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
			require.Equal(t, tc.expectAllowed, resp.Allowed)
		})
	}
}

// Test the Check method requires service:read permission.
func TestIntentionCheck_aclDeny(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	dir1, s1 := testServerWithConfig(t, func(c *Config) {
		c.PrimaryDatacenter = "dc1"
		c.ACLsEnabled = true
		c.ACLInitialManagementToken = "root"
		c.ACLResolverSettings.ACLDefaultPolicy = "deny"
	})
	defer os.RemoveAll(dir1)
	defer s1.Shutdown()
	codec := rpcClient(t, s1)
	defer codec.Close()

	waitForLeaderEstablishment(t, s1)

	rules := `
service "bar" {
	policy = "read"
}`
	token := createToken(t, codec, rules)

	// Check
	req := &structs.IntentionQueryRequest{
		Datacenter: "dc1",
		Check: &structs.IntentionQueryCheck{
			SourceName:      "qux",
			DestinationName: "baz",
			SourceType:      structs.IntentionSourceConsul,
		},
	}
	req.Token = token
	var resp structs.IntentionQueryCheckResponse
	err := msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)
	require.True(t, acl.IsErrPermissionDenied(err))
}

// Test the Check method returns allow/deny properly.
func TestIntentionCheck_match(t *testing.T) {
	netutil.GetAgentBindAddrFunc = netutil.GetMockGetAgentBindAddrFunc("0.0.0.0")
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	_, srv, codec := testACLServerWithConfig(t, nil, false)
	waitForLeaderEstablishment(t, srv)

	token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultInitialManagementToken, "dc1", `service "api" { policy = "read" }`)
	require.NoError(t, err)

	// Create some intentions
	{
		insert := [][]string{
			{"web", "db"},
			{"api", "db"},
			{"web", "api"},
		}

		for _, v := range insert {
			ixn := structs.IntentionRequest{
				Datacenter: "dc1",
				Op:         structs.IntentionOpCreate,
				Intention: &structs.Intention{
					SourceNS:        "default",
					SourceName:      v[0],
					DestinationNS:   "default",
					DestinationName: v[1],
					Action:          structs.IntentionActionAllow,
				},
				WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
			}
			// Create
			var reply string
			require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
		}
	}

	// Check
	req := &structs.IntentionQueryRequest{
		Datacenter: "dc1",
		Check: &structs.IntentionQueryCheck{
			SourceNS:        "default",
			SourceName:      "web",
			DestinationNS:   "default",
			DestinationName: "api",
			SourceType:      structs.IntentionSourceConsul,
		},
		QueryOptions: structs.QueryOptions{Token: token.SecretID},
	}
	var resp structs.IntentionQueryCheckResponse
	require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
	require.True(t, resp.Allowed)

	// Test no match for sanity
	{
		req := &structs.IntentionQueryRequest{
			Datacenter: "dc1",
			Check: &structs.IntentionQueryCheck{
				SourceNS:        "default",
				SourceName:      "db",
				DestinationNS:   "default",
				DestinationName: "api",
				SourceType:      structs.IntentionSourceConsul,
			},
			QueryOptions: structs.QueryOptions{Token: token.SecretID},
		}
		var resp structs.IntentionQueryCheckResponse
		require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
		require.False(t, resp.Allowed)
	}
}

func TestEqualStringMaps(t *testing.T) {
	m1 := map[string]string{
		"foo": "a",
	}
	m2 := map[string]string{
		"foo": "a",
		"bar": "b",
	}
	var m3 map[string]string

	m4 := map[string]string{
		"dog": "",
	}

	m5 := map[string]string{
		"cat": "",
	}

	tests := []struct {
		a      map[string]string
		b      map[string]string
		result bool
	}{
		{m1, m1, true},
		{m2, m2, true},
		{m1, m2, false},
		{m2, m1, false},
		{m2, m2, true},
		{m3, m1, false},
		{m3, m3, true},
		{m4, m5, false},
	}

	for i, test := range tests {
		actual := equalStringMaps(test.a, test.b)
		if actual != test.result {
			t.Fatalf("case %d, expected %v, got %v", i, test.result, actual)
		}
	}
}
