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  })