diff --git a/go.mod b/go.mod index d5bcb67..e30d81d 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/black-desk/cgtproxy go 1.21 require ( - github.com/black-desk/lib/go v0.0.0-20231021105122-e4106bdb89f9 + github.com/black-desk/lib/go v0.0.0-20231023094454-94c87a910679 github.com/deniswernert/go-fstab v0.0.0-20141204152952-eb4090f26517 github.com/go-playground/validator/v10 v10.15.5 - github.com/google/nftables v0.1.1-0.20231021201155-6df7a82bbd85 + github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717 github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.28.1 github.com/sourcegraph/conc v0.3.0 @@ -31,7 +31,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect github.com/google/wire v0.5.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect @@ -48,5 +48,3 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.14.0 // indirect ) - -replace github.com/google/nftables => github.com/black-desk/nftables v0.0.0-20231024023000-507e72d30b17 diff --git a/go.sum b/go.sum index 0774ea6..e9defe6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/black-desk/lib/go v0.0.0-20231021105122-e4106bdb89f9 h1:z4ENGB/6+zF3nzO0qi5ud1kxZDu6lw74+I7h5OLTmrQ= github.com/black-desk/lib/go v0.0.0-20231021105122-e4106bdb89f9/go.mod h1:M8o2UXm8jAG7bech4xc/SeNxbHquqfagVvHdKyDoRNk= +github.com/black-desk/lib/go v0.0.0-20231023094454-94c87a910679 h1:TlnFjdFKIvetgeV2OXGH/k0czjSf1puuvjtWhEhdb4s= +github.com/black-desk/lib/go v0.0.0-20231023094454-94c87a910679/go.mod h1:M8o2UXm8jAG7bech4xc/SeNxbHquqfagVvHdKyDoRNk= github.com/black-desk/nftables v0.0.0-20231024023000-507e72d30b17 h1:/mmacHTt9zP1NRUMz6Y3ko7uafsOv2v6aLjItzGfxxU= github.com/black-desk/nftables v0.0.0-20231024023000-507e72d30b17/go.mod h1:FODgEv85GcCEyoUYZ27mPWQBSU1f67bzgNu2IITA9k8= github.com/black-desk/zap-journal v0.0.0-20230529080551-a8e82d81454b h1:fiO3y68dfa5ctXhgZKCniY0IfPl0U+EoyADUgG2zP+s= @@ -29,8 +31,16 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/nftables v0.0.0-20220607152305-1f0380f5c76e h1:4195/gpB4BjPlC0WqOjh9+WsiDToMJewxTozgCnbu1Y= +github.com/google/nftables v0.0.0-20220607152305-1f0380f5c76e/go.mod h1:b97ulCCFipUC+kSin+zygkvUVpx0vyIAwxXFdY3PlNc= +github.com/google/nftables v0.1.1-0.20231021201155-6df7a82bbd85 h1:eJvIqeddy69xqWdapD/AZVNhJD5/kPSx2RyO1rjPaeI= +github.com/google/nftables v0.1.1-0.20231021201155-6df7a82bbd85/go.mod h1:FODgEv85GcCEyoUYZ27mPWQBSU1f67bzgNu2IITA9k8= +github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717 h1:U0bCHvg4y7uTBQWiyE5iOC/R3Dw9Dve1TjIok0BfkbQ= +github.com/google/nftables v0.1.1-0.20231024065723-32bfbb662717/go.mod h1:FODgEv85GcCEyoUYZ27mPWQBSU1f67bzgNu2IITA9k8= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/pkg/cgtproxy/config/config_test.go b/pkg/cgtproxy/config/config_test.go index a10bc8e..e750d1e 100644 --- a/pkg/cgtproxy/config/config_test.go +++ b/pkg/cgtproxy/config/config_test.go @@ -16,73 +16,77 @@ import ( ) var _ = Describe("Configuration", func() { - ContextTable("load from valid configuration (%s)", func(path string) { - var ( - err error - file *os.File - content []byte - ) + ContextTable("load from valid configuration (%s)", + ContextTableEntry("../../../misc/config/example.yaml"), + ContextTableEntry("../../../test/data/example_config.yaml"), + func(path string) { + var ( + err error + file *os.File + content []byte + ) - BeforeEach(func() { - file, err = os.Open(path) - if err != nil { - Fail(fmt.Sprintf("Failed to open configuration %s: %s", path, err.Error())) - } + BeforeEach(func() { + file, err = os.Open(path) + if err != nil { + Fail(fmt.Sprintf("Failed to open configuration %s: %s", path, err.Error())) + } - content, err = io.ReadAll(file) - if err != nil { - Fail(fmt.Sprintf("Failed to read configuration %s: %s", path, err.Error())) - } + content, err = io.ReadAll(file) + if err != nil { + Fail(fmt.Sprintf("Failed to read configuration %s: %s", path, err.Error())) + } - _, err = config.New(config.WithContent(content)) - }) - AfterEach(func() { - file.Close() - }) - It("should success.", func() { - Expect(err).To(BeNil()) + _, err = config.New(config.WithContent(content)) + }) + AfterEach(func() { + file.Close() + }) + It("should success.", func() { + Expect(err).To(BeNil()) + }) }) - }, - ContextTableEntry("../../../misc/config/example.yaml"), - ContextTableEntry("../../../test/data/example_config.yaml"), - ) - ContextTable("load from invalid configuration (%s)", func(path string, expectErr error, errString string) { - var ( - err error - file *os.File - content []byte - ) + ContextTable("load from invalid configuration (%s)", + ContextTableEntry( + "../../../test/data/wrong_type.yaml", + new(yaml.TypeError), "yaml.TypeError", + ).WithFmt("../../../test/data/wrong_type.yaml"), + ContextTableEntry( + "../../../test/data/validation_fail.yaml", + validator.ValidationErrors{}, "validator.ValidationErrors", + ).WithFmt("../../../test/data/validation_fail.yaml"), + func(path string, expectErr error, errString string) { + var ( + err error + file *os.File + content []byte + ) - BeforeEach(func() { - file, err = os.Open(path) - if err != nil { - Fail(fmt.Sprintf("Failed to open configuration %s: %s", path, err.Error())) - } + BeforeEach(func() { + file, err = os.Open(path) + if err != nil { + Fail(fmt.Sprintf("Failed to open configuration %s: %s", path, err.Error())) + } - content, err = io.ReadAll(file) - if err != nil { - Fail(fmt.Sprintf("Failed to read configuration %s: %s", path, err.Error())) - } + content, err = io.ReadAll(file) + if err != nil { + Fail(fmt.Sprintf("Failed to read configuration %s: %s", path, err.Error())) + } - _, err = config.New(config.WithContent(content)) - }) + _, err = config.New(config.WithContent(content)) + }) - AfterEach(func() { - if file != nil { - file.Close() - } - }) + AfterEach(func() { + if file != nil { + file.Close() + } + }) - It(fmt.Sprintf("should fail with error: %s", errString), func() { - Expect(err).To(MatchErr(expectErr)) + It(fmt.Sprintf("should fail with error: %s", errString), func() { + Expect(err).To(MatchErr(expectErr)) + }) }) - }, - ContextTableEntry("../../../test/data/wrong_type.yaml", new(yaml.TypeError), "yaml.TypeError"). - WithFmt("../../../test/data/wrong_type.yaml"), - ContextTableEntry("../../../test/data/validation_fail.yaml", validator.ValidationErrors{}, "validator.ValidationErrors"). - WithFmt("../../../test/data/validation_fail.yaml"), - ) }) func TestConfig(t *testing.T) { diff --git a/pkg/nftman/new.go b/pkg/nftman/new.go index 8514e08..fdfc74b 100644 --- a/pkg/nftman/new.go +++ b/pkg/nftman/new.go @@ -46,7 +46,6 @@ type NFTManager struct { type Opt = (func(*NFTManager) (*NFTManager, error)) //go:generate go run github.com/rjeczalik/interfaces/cmd/interfacer@v0.3.0 -for github.com/black-desk/cgtproxy/pkg/nftman.NFTManager -as interfaces.NFTManager -o ../interfaces/nftman.go - func New(opts ...Opt) (ret *NFTManager, err error) { defer Wrap(&err, "create nft table mananger") @@ -81,7 +80,7 @@ func New(opts ...Opt) (ret *NFTManager, err error) { ret = t - t.log.Debugw("Create a nft table.") + t.log.Debugw("NFTManager created.") return } diff --git a/pkg/nftman/nftman_test.go b/pkg/nftman/nftman_test.go index a224dd2..47d6549 100644 --- a/pkg/nftman/nftman_test.go +++ b/pkg/nftman/nftman_test.go @@ -39,236 +39,232 @@ var _ = Describe("Netfliter table", Ordered, func() { cgroupRoot = os.Getenv("CGTPROXY_TEST_CGROUP_ROOT") ) - ContextTable("with %s", func(injectedNFTManager func(cGroupRoot config.CGroupRoot) (*NFTManager, error)) { - BeforeEach(func() { - By("Create a Table object.", func() { - nft, err = injectedNFTManager(config.CGroupRoot(cgroupRoot)) - Expect(err).To(Succeed()) - }) - }) - - AfterEach(func() { - By("Clear nftable content.", func() { - if nft == nil { - return - } - err = nft.Clear() - Expect(err).To(Succeed()) - }) - }) - - Context("then call Table.Clear()", func() { - BeforeEach(func() { - err = nft.Clear() - }) - - It("should clear the nft table with no error", func() { - Expect(err).To(Succeed()) - - result = getNFTableRules() - Expect(result).To(BeEmpty()) - }) - }) - - type TproxyCase struct { - t *config.TProxy - expects []string - } - - ContextTable("with some tproxies", func(tps []*TproxyCase) { - + ContextTable("with %s", + ContextTableEntry(injectedNFTManagerWithLastingConnector).WithFmt("lasting connector"), + ContextTableEntry(injectedNFTManagerWithConnector).WithFmt("connector"), + func(injectedNFTManager func(cGroupRoot config.CGroupRoot) (*NFTManager, error)) { BeforeEach(func() { - By("Initialize table with tproxies.", func() { - for _, tp := range tps { - err = nft.AddChainAndRulesForTProxy(tp.t) - Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) - } + By("Create a Table object.", func() { + nft, err = injectedNFTManager(config.CGroupRoot(cgroupRoot)) + Expect(err).To(Succeed()) }) }) - It("should produce expected nftable rules", func() { - result = getNFTableRules() - - Expect(result).To(ContainSubstring(NftTableName)) - for _, tp := range tps { - for _, expect := range tp.expects { - Expect(result).To(ContainSubstring(expect)) + AfterEach(func() { + By("Clear nftable content.", func() { + if nft == nil { + return } - } - + err = nft.Clear() + Expect(err).To(Succeed()) + }) }) - Context("then add some cgroups", func() { + Context("then call Table.Clear()", func() { BeforeEach(func() { - err = os.MkdirAll(cgroupRoot+"/test/a", 0755) - Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) - err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/a", - Target: types.Target{Op: types.TargetTProxy, Chain: tps[rand.Intn(len(tps))].t.Name + "-MARK"}}}) - Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) - - err = os.MkdirAll(cgroupRoot+"/test/b", 0755) - Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) - err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/b", - Target: types.Target{Op: types.TargetTProxy, Chain: tps[rand.Intn(len(tps))].t.Name + "-MARK"}}}) - Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) - - err = os.MkdirAll(cgroupRoot+"/test/c", 0755) - Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) - err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/c", - Target: types.Target{Op: types.TargetDrop}}}) - Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) - - err = os.MkdirAll(cgroupRoot+"/test/d/d", 0755) - Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) - err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/d/d", - Target: types.Target{Op: types.TargetDirect}}}) - - Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + err = nft.Clear() }) - AfterEach(func() { - err = syscall.Rmdir(cgroupRoot + "/test/d/d") - Expect(err).To(Succeed()) - err = syscall.Rmdir(cgroupRoot + "/test/d") - Expect(err).To(Succeed()) - err = syscall.Rmdir(cgroupRoot + "/test/c") + It("should clear the nft table with no error", func() { Expect(err).To(Succeed()) - err = syscall.Rmdir(cgroupRoot + "/test/b") - Expect(err).To(Succeed()) - err = syscall.Rmdir(cgroupRoot + "/test/a") - Expect(err).To(Succeed()) - err = syscall.Rmdir(cgroupRoot + "/test") - Expect(err).To(Succeed()) - }) - It("should produce expected nftable rules", func() { result = getNFTableRules() - { - Expect(result).To(ContainSubstring(NftTableName)) - for _, tp := range tps { - Expect(result).To(ContainSubstring(tp.t.Name)) - } - Expect(result).To( - ContainSubstring("socket cgroupv2 level 3 vmap @cgroup-vmap"), - ) - Expect(result).To( - ContainSubstring("socket cgroupv2 level 2 vmap @cgroup-vmap"), - ) - - Expect(result).To( - ContainSubstring(`test/d/d`), - ) - - Expect(result).To( - ContainSubstring(`test/a" : goto tproxy`), - ) - - Expect(result).To( - ContainSubstring(`test/b" : goto tproxy`), - ) - - Expect(result).To( - ContainSubstring(`test/c" : drop`), - ) - - Expect(result).To( - ContainSubstring(`test/d/d"`), - ) - } + Expect(result).To(BeEmpty()) }) + }) - Context("and remove them later", func() { + type TproxyCase struct { + t *config.TProxy + expects []string + } + + ContextTable("with some tproxies", + ContextTableEntry([]*TproxyCase{ + { + t: &config.TProxy{ + Name: "tproxy1", + NoUDP: true, + NoIPv6: false, + Port: 7893, + Mark: 100, + }, + expects: []string{ + "chain tproxy1", + "meta l4proto tcp tproxy to :7893", + }, + }, + { + t: &config.TProxy{ + Name: "tproxy2", + NoUDP: false, + NoIPv6: true, + Port: 7894, + Mark: 101, + }, + expects: []string{ + "chain tproxy2", + "meta l4proto { tcp, udp } tproxy ip to :7894", + }, + }, + { + t: &config.TProxy{ + Name: "tproxy3", + NoUDP: false, + NoIPv6: false, + Port: 7895, + Mark: 103, + }, + expects: []string{ + "chain tproxy3", + "meta l4proto { tcp, udp } tproxy to :7895", + }, + }, + { + t: &config.TProxy{ + Name: "tproxy4", + NoUDP: true, + NoIPv6: true, + Port: 7896, + Mark: 104, + }, + expects: []string{ + "chain tproxy4", + "meta l4proto tcp tproxy ip to :7896", + }, + }, + }).WithFmt(), + func(tps []*TproxyCase) { BeforeEach(func() { - nft.RemoveCgroups([]string{cgroupRoot + "/test/a"}) - nft.RemoveCgroups([]string{cgroupRoot + "/test/b"}) - nft.RemoveCgroups([]string{cgroupRoot + "/test/c"}) - nft.RemoveCgroups([]string{cgroupRoot + "/test/d/d"}) + By("Initialize table with tproxies.", func() { + for _, tp := range tps { + err = nft.AddChainAndRulesForTProxy(tp.t) + Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + } + }) }) It("should produce expected nftable rules", func() { result = getNFTableRules() - Expect(result).ToNot(ContainSubstring("drop")) + + Expect(result).To(ContainSubstring(NftTableName)) + for _, tp := range tps { + for _, expect := range tp.expects { + Expect(result).To(ContainSubstring(expect)) + } + } + }) - Context("then add some of them back", func() { + Context("then add some cgroups", func() { BeforeEach(func() { + err = os.MkdirAll(cgroupRoot+"/test/a", 0755) + Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/a", Target: types.Target{Op: types.TargetTProxy, Chain: tps[rand.Intn(len(tps))].t.Name + "-MARK"}}}) Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + + err = os.MkdirAll(cgroupRoot+"/test/b", 0755) + Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) + err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/b", + Target: types.Target{Op: types.TargetTProxy, Chain: tps[rand.Intn(len(tps))].t.Name + "-MARK"}}}) + Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + + err = os.MkdirAll(cgroupRoot+"/test/c", 0755) + Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) + err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/c", + Target: types.Target{Op: types.TargetDrop}}}) + Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + + err = os.MkdirAll(cgroupRoot+"/test/d/d", 0755) + Expect(err).To(Or(Succeed(), MatchError(os.ErrExist))) + err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/d/d", + Target: types.Target{Op: types.TargetDirect}}}) + + Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + }) + + AfterEach(func() { + err = syscall.Rmdir(cgroupRoot + "/test/d/d") + Expect(err).To(Succeed()) + err = syscall.Rmdir(cgroupRoot + "/test/d") + Expect(err).To(Succeed()) + err = syscall.Rmdir(cgroupRoot + "/test/c") + Expect(err).To(Succeed()) + err = syscall.Rmdir(cgroupRoot + "/test/b") + Expect(err).To(Succeed()) + err = syscall.Rmdir(cgroupRoot + "/test/a") + Expect(err).To(Succeed()) + err = syscall.Rmdir(cgroupRoot + "/test") + Expect(err).To(Succeed()) }) It("should produce expected nftable rules", func() { result = getNFTableRules() - Expect(result).To(ContainSubstring("goto")) - Expect(result).ToNot(ContainSubstring("drop")) + { + Expect(result).To(ContainSubstring(NftTableName)) + for _, tp := range tps { + Expect(result).To(ContainSubstring(tp.t.Name)) + } + Expect(result).To( + ContainSubstring("socket cgroupv2 level 3 vmap @cgroup-vmap"), + ) + Expect(result).To( + ContainSubstring("socket cgroupv2 level 2 vmap @cgroup-vmap"), + ) + + Expect(result).To( + ContainSubstring(`test/d/d`), + ) + + Expect(result).To( + ContainSubstring(`test/a" : goto tproxy`), + ) + + Expect(result).To( + ContainSubstring(`test/b" : goto tproxy`), + ) + + Expect(result).To( + ContainSubstring(`test/c" : drop`), + ) + + Expect(result).To( + ContainSubstring(`test/d/d"`), + ) + } + }) + + Context("and remove them later", func() { + BeforeEach(func() { + nft.RemoveCgroups([]string{cgroupRoot + "/test/a"}) + nft.RemoveCgroups([]string{cgroupRoot + "/test/b"}) + nft.RemoveCgroups([]string{cgroupRoot + "/test/c"}) + nft.RemoveCgroups([]string{cgroupRoot + "/test/d/d"}) + }) + + It("should produce expected nftable rules", func() { + result = getNFTableRules() + Expect(result).ToNot(ContainSubstring("drop")) + }) + + Context("then add some of them back", func() { + BeforeEach(func() { + err = nft.AddRoutes([]types.Route{{Path: cgroupRoot + "/test/a", + Target: types.Target{Op: types.TargetTProxy, Chain: tps[rand.Intn(len(tps))].t.Name + "-MARK"}}}) + Expect(err).To(Succeed(), "nft:\n%s", getNFTableRules()) + }) + + It("should produce expected nftable rules", func() { + result = getNFTableRules() + Expect(result).To(ContainSubstring("goto")) + Expect(result).ToNot(ContainSubstring("drop")) + }) + }) }) }) }) - }) - }, - ContextTableEntry([]*TproxyCase{ - { - t: &config.TProxy{ - Name: "tproxy1", - NoUDP: true, - NoIPv6: false, - Port: 7893, - Mark: 100, - }, - expects: []string{ - "chain tproxy1", - "meta l4proto tcp tproxy to :7893", - }, - }, - { - t: &config.TProxy{ - Name: "tproxy2", - NoUDP: false, - NoIPv6: true, - Port: 7894, - Mark: 101, - }, - expects: []string{ - "chain tproxy2", - "meta l4proto { tcp, udp } tproxy ip to :7894", - }, - }, - { - t: &config.TProxy{ - Name: "tproxy3", - NoUDP: false, - NoIPv6: false, - Port: 7895, - Mark: 103, - }, - expects: []string{ - "chain tproxy3", - "meta l4proto { tcp, udp } tproxy to :7895", - }, - }, - { - t: &config.TProxy{ - Name: "tproxy4", - NoUDP: true, - NoIPv6: true, - Port: 7896, - Mark: 104, - }, - expects: []string{ - "chain tproxy4", - "meta l4proto tcp tproxy ip to :7896", - }, - }, - }).WithFmt(), - ) - - }, - ContextTableEntry(injectedNFTManagerWithLastingConnector).WithFmt("lasting connector"), - ContextTableEntry(injectedNFTManagerWithConnector).WithFmt("connector"), - ) - + }) }) - }) func TestTable(t *testing.T) {