github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/pluginswatcher/plugins_test.go (about) 1 // Copyright (c) 2021-2023, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package pluginswatcher 6 7 import ( 8 "crypto/ed25519" 9 "crypto/rand" 10 "encoding/base64" 11 "encoding/hex" 12 "os" 13 "path/filepath" 14 "testing" 15 "time" 16 17 "github.com/choria-io/go-choria/aagent/model" 18 iu "github.com/choria-io/go-choria/internal/util" 19 "github.com/ghodss/yaml" 20 "github.com/golang/mock/gomock" 21 . "github.com/onsi/ginkgo/v2" 22 . "github.com/onsi/gomega" 23 ) 24 25 func TestMachine(t *testing.T) { 26 RegisterFailHandler(Fail) 27 RunSpecs(t, "AAgent/Watchers/PluginsWatcher") 28 } 29 30 var _ = Describe("AAgent/Watchers/PluginsWatcher", func() { 31 var ( 32 w *Watcher 33 machine *model.MockMachine 34 mockctl *gomock.Controller 35 td string 36 err error 37 ) 38 39 BeforeEach(func() { 40 td, err = os.MkdirTemp("", "") 41 Expect(err).ToNot(HaveOccurred()) 42 mockctl = gomock.NewController(GinkgoT()) 43 44 machine = model.NewMockMachine(mockctl) 45 machine.EXPECT().Directory().Return(td).AnyTimes() 46 machine.EXPECT().SignerKey().Return("").AnyTimes() 47 48 wi, err := New(machine, "machines", nil, "", "", "1m", time.Hour, map[string]any{ 49 "data_item": "spec", 50 }) 51 Expect(err).ToNot(HaveOccurred()) 52 w = wi.(*Watcher) 53 }) 54 55 AfterEach(func() { 56 mockctl.Finish() 57 os.RemoveAll(td) 58 }) 59 60 Describe("Specification/Encode", func() { 61 It("Should correctly encode the specification", func() { 62 pub, priv, err := iu.Ed25519KeyPair() 63 Expect(err).ToNot(HaveOccurred()) 64 65 data, err := os.ReadFile("testdata/plugins.json") 66 Expect(err).ToNot(HaveOccurred()) 67 68 spec := &Specification{Plugins: string(data)} 69 _, err = spec.Encode(hex.EncodeToString(priv)) 70 Expect(err).ToNot(HaveOccurred()) 71 72 sig, err := hex.DecodeString(spec.Signature) 73 Expect(err).ToNot(HaveOccurred()) 74 75 ok, err := iu.Ed25519Verify(pub, data, sig) 76 Expect(err).ToNot(HaveOccurred()) 77 Expect(ok).To(BeTrue()) 78 }) 79 }) 80 81 Describe("setProperties", func() { 82 It("Should support defaulting manager machine prefix", func() { 83 err = w.setProperties(map[string]any{}) 84 Expect(err).ToNot(HaveOccurred()) 85 Expect(w.properties.ManagerMachinePrefix).To(Equal("mm")) 86 }) 87 88 It("Should not allow underscores", func() { 89 err = w.setProperties(map[string]any{"manager_machine_prefix": "plugin_foo"}) 90 Expect(err).To(MatchError("manager_machine_prefix may not contain underscore")) 91 }) 92 93 It("Should support custom manager machine prefix", func() { 94 err = w.setProperties(map[string]any{"manager_machine_prefix": "plugin"}) 95 Expect(err).ToNot(HaveOccurred()) 96 97 Expect(w.properties.ManagerMachinePrefix).To(Equal("plugin")) 98 r, err := w.renderMachine(&ManagedPlugin{ 99 Name: "x", 100 NamePrefix: "plugin", 101 }) 102 Expect(err).ToNot(HaveOccurred()) 103 104 var m map[string]any 105 Expect(yaml.Unmarshal(r, &m)).To(Succeed()) 106 107 Expect(m["name"]).To(Equal("plugin_x")) 108 actual := w.targetDirForManagerMachine("x") 109 expected := filepath.Join(filepath.Dir(w.machine.Directory()), "plugin_x") 110 Expect(actual).To(Equal(expected)) 111 }) 112 }) 113 114 Describe("targetDirForManagerMachine", func() { 115 It("Should set the correct target", func() { 116 err = w.setProperties(map[string]any{"manager_machine_prefix": "plugin"}) 117 Expect(err).ToNot(HaveOccurred()) 118 actual := w.targetDirForManagerMachine("x") 119 expected := filepath.Join(filepath.Dir(w.machine.Directory()), "plugin_x") 120 Expect(actual).To(Equal(expected)) 121 }) 122 }) 123 Describe("loadAndValidateData", func() { 124 var ( 125 data *Specification 126 pri ed25519.PrivateKey 127 pub ed25519.PublicKey 128 spec string 129 ) 130 131 BeforeEach(func() { 132 pub, pri, err = ed25519.GenerateKey(rand.Reader) 133 Expect(err).ToNot(HaveOccurred()) 134 spec = base64.StdEncoding.EncodeToString([]byte("[]")) 135 data = &Specification{Plugins: string(spec)} 136 data.Signature = hex.EncodeToString(ed25519.Sign(pri, []byte(spec))) 137 machine.EXPECT().DataGet(gomock.Eq("spec")).Return(data, true).AnyTimes() 138 }) 139 140 It("Should function without a signature", func() { 141 data.Signature = "" 142 machine.EXPECT().SignerKey().Return("").AnyTimes() 143 spec, err := w.loadAndValidateData() 144 Expect(err).ToNot(HaveOccurred()) 145 Expect(spec).ToNot(BeNil()) 146 }) 147 148 It("Should handle data with no signatures when signature is required", func() { 149 err = w.setProperties(map[string]any{ 150 "data_item": "spec", 151 "public_key": "x", 152 }) 153 Expect(err).ToNot(HaveOccurred()) 154 data.Signature = "" 155 machine.EXPECT().DataDelete(gomock.Eq("spec")) 156 machine.EXPECT().Errorf(gomock.Any(), gomock.Eq("No signature found in specification, removing data")) 157 spec, err := w.loadAndValidateData() 158 Expect(err).To(MatchError("invalid data_item")) 159 Expect(spec).To(BeNil()) 160 }) 161 162 It("Should handle data with corrupt signatures", func() { 163 err = w.setProperties(map[string]any{ 164 "data_item": "spec", 165 "public_key": hex.EncodeToString(pub), 166 }) 167 Expect(err).ToNot(HaveOccurred()) 168 data.Signature = "x" 169 170 machine.EXPECT().DataDelete(gomock.Eq("spec")) 171 machine.EXPECT().Errorf(gomock.Any(), gomock.Eq("Signature in data_item %s did not verify using configured public key '%s', removing data"), gomock.Eq("spec"), gomock.Eq(hex.EncodeToString(pub))) 172 spec, err := w.loadAndValidateData() 173 Expect(err).To(MatchError("invalid data_item")) 174 Expect(spec).To(BeNil()) 175 }) 176 177 It("Should handle data with invalid signatures", func() { 178 err = w.setProperties(map[string]any{ 179 "data_item": "spec", 180 "public_key": hex.EncodeToString(pub), 181 }) 182 Expect(err).ToNot(HaveOccurred()) 183 data.Signature = hex.EncodeToString(ed25519.Sign(pri, []byte("wrong"))) 184 185 machine.EXPECT().DataDelete(gomock.Eq("spec")) 186 machine.EXPECT().Errorf(gomock.Any(), gomock.Eq("Signature in data_item %s did not verify using configured public key '%s', removing data"), gomock.Eq("spec"), gomock.Eq(hex.EncodeToString(pub))) 187 spec, err := w.loadAndValidateData() 188 Expect(err).To(MatchError("invalid data_item")) 189 Expect(spec).To(BeNil()) 190 }) 191 192 It("Should allow overrides from config", func() { 193 machine := model.NewMockMachine(mockctl) 194 machine.EXPECT().Directory().Return(td).AnyTimes() 195 machine.EXPECT().SignerKey().Return(hex.EncodeToString(pub)).AnyTimes() 196 machine.EXPECT().DataGet(gomock.Eq("spec")).Return(data, true).AnyTimes() 197 198 wi, err := New(machine, "machines", nil, "", "", "1m", time.Hour, map[string]any{ 199 "data_item": "spec", 200 "public_key": "other", 201 }) 202 Expect(err).ToNot(HaveOccurred()) 203 w = wi.(*Watcher) 204 205 Expect(w.properties.PublicKey).To(Equal(hex.EncodeToString(pub))) 206 207 spec, err := w.loadAndValidateData() 208 Expect(err).ToNot(HaveOccurred()) 209 Expect(spec).To(Equal([]byte("[]"))) 210 }) 211 212 It("Should handle valid signatures", func() { 213 err = w.setProperties(map[string]any{ 214 "data_item": "spec", 215 "public_key": hex.EncodeToString(pub), 216 }) 217 Expect(err).ToNot(HaveOccurred()) 218 219 spec, err := w.loadAndValidateData() 220 Expect(err).ToNot(HaveOccurred()) 221 Expect(spec).To(Equal([]byte("[]"))) 222 }) 223 }) 224 })