Files
Johan Fylling 5464b005e8 Bumping golangci-lint to v1.59.1 (#6817)
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
2024-06-19 15:13:43 +02:00

1122 lines
28 KiB
Go

// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package status
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/plugins"
"github.com/open-policy-agent/opa/plugins/bundle"
inmem "github.com/open-policy-agent/opa/storage/inmem/test"
"github.com/open-policy-agent/opa/util"
"github.com/open-policy-agent/opa/util/test"
"github.com/open-policy-agent/opa/version"
lstat "github.com/open-policy-agent/opa/plugins/logs/status"
)
func TestMain(m *testing.M) {
if version.Version == "" {
version.Version = "unit-test"
}
os.Exit(m.Run())
}
func TestPluginPrometheus(t *testing.T) {
fixture := newTestFixture(t, nil, func(c *Config) {
c.Prometheus = true
})
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
<-fixture.server.ch
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
<-fixture.server.ch
registerMock := fixture.manager.PrometheusRegister().(*prometheusRegisterMock)
assertOpInformationGauge(t, registerMock)
if registerMock.Collectors[pluginStatus] != true {
t.Fatalf("Plugin status metric was not registered on prometheus")
}
if registerMock.Collectors[loaded] != true {
t.Fatalf("Loaded metric was not registered on prometheus")
}
if registerMock.Collectors[failLoad] != true {
t.Fatalf("FailLoad metric was not registered on prometheus")
}
if registerMock.Collectors[lastRequest] != true {
t.Fatalf("Last request metric was not registered on prometheus")
}
if registerMock.Collectors[lastSuccessfulActivation] != true {
t.Fatalf("Last Successful Activation metric was not registered on prometheus")
}
if registerMock.Collectors[lastSuccessfulDownload] != true {
t.Fatalf("Last Successful Download metric was not registered on prometheus")
}
if registerMock.Collectors[lastSuccessfulRequest] != true {
t.Fatalf("Last Successful Request metric was not registered on prometheus")
}
if registerMock.Collectors[bundleLoadDuration] != true {
t.Fatalf("Bundle Load Duration metric was not registered on prometheus")
}
if len(registerMock.Collectors) != 9 {
t.Fatalf("Number of collectors expected (%v), got %v", 9, len(registerMock.Collectors))
}
lastRequestMetricResult := time.UnixMilli(int64(testutil.ToFloat64(lastRequest) / 1e6))
if !lastRequestMetricResult.Equal(status.LastRequest) {
t.Fatalf("Last request expected (%v), got %v", status.LastRequest.UTC(), lastRequestMetricResult.UTC())
}
lastSuccessfulRequestMetricResult := time.UnixMilli(int64(testutil.ToFloat64(lastSuccessfulRequest) / 1e6))
if !lastSuccessfulRequestMetricResult.Equal(status.LastSuccessfulRequest) {
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulRequest.UTC(), lastSuccessfulRequestMetricResult.UTC())
}
lastSuccessfulDownloadMetricResult := time.UnixMilli(int64(testutil.ToFloat64(lastSuccessfulDownload) / 1e6))
if !lastSuccessfulDownloadMetricResult.Equal(status.LastSuccessfulDownload) {
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulDownload.UTC(), lastSuccessfulDownloadMetricResult.UTC())
}
lastSuccessfulActivationMetricResult := time.UnixMilli(int64(testutil.ToFloat64(lastSuccessfulActivation) / 1e6))
if !lastSuccessfulActivationMetricResult.Equal(status.LastSuccessfulActivation) {
t.Fatalf("Last request expected (%v), got %v", status.LastSuccessfulActivation.UTC(), lastSuccessfulActivationMetricResult.UTC())
}
bundlesLoaded := testutil.CollectAndCount(loaded)
if bundlesLoaded != 1 {
t.Fatalf("Unexpected number of bundle loads (%v), got %v", 1, bundlesLoaded)
}
bundlesFailedToLoad := testutil.CollectAndCount(failLoad)
if bundlesFailedToLoad != 0 {
t.Fatalf("Unexpected number of bundle fails load (%v), got %v", 0, bundlesFailedToLoad)
}
pluginsStatus := testutil.CollectAndCount(pluginStatus)
if pluginsStatus != 1 {
t.Fatalf("Unexpected number of plugins (%v), got %v", 1, pluginsStatus)
}
// Assert that metrics are purged when prometheus is disabled
prometheusDisabledConfig := newConfig(fixture.manager, func(c *Config) {
c.Prometheus = false
})
fixture.plugin.Reconfigure(ctx, prometheusDisabledConfig)
eventually(t, func() bool { return fixture.plugin.config.Prometheus == false })
if len(registerMock.Collectors) != 0 {
t.Fatalf("Number of collectors expected (%v), got %v", 0, len(registerMock.Collectors))
}
// Assert that metrics are re-registered when prometheus is re-enabled
prometheusReenabledConfig := newConfig(fixture.manager, func(c *Config) {
c.Prometheus = true
})
fixture.plugin.Reconfigure(ctx, prometheusReenabledConfig)
eventually(t, func() bool { return fixture.plugin.config.Prometheus == true })
if len(registerMock.Collectors) != 9 {
t.Fatalf("Number of collectors expected (%v), got %v", 9, len(registerMock.Collectors))
}
}
func eventually(t *testing.T, predicate func() bool) {
t.Helper()
if !test.Eventually(t, 1*time.Second, predicate) {
t.Fatal("check took too long")
}
}
func assertOpInformationGauge(t *testing.T, registerMock *prometheusRegisterMock) {
gauges := filterGauges(registerMock)
if len(gauges) != 1 {
t.Fatalf("Expected one registered gauge on prometheus but got %v", len(gauges))
}
gauge := gauges[0]
fqName := getName(gauge)
if fqName != "opa_info" {
t.Fatalf("Expected gauge to have name opa_info but was %s", fqName)
}
labels := getConstLabels(gauge)
versionAct := labels["version"]
if versionAct != version.Version {
t.Fatalf("Expected gauge to have version label with value %s but was %s", version.Version, versionAct)
}
}
func getName(gauge prometheus.Gauge) string {
desc := reflect.Indirect(reflect.ValueOf(gauge.Desc()))
fqName := desc.FieldByName("fqName").String()
return fqName
}
func getConstLabels(gauge prometheus.Gauge) prometheus.Labels {
desc := reflect.Indirect(reflect.ValueOf(gauge.Desc()))
constLabelPairs := desc.FieldByName("constLabelPairs")
// put all label pairs into a map for easier comparison.
labels := make(prometheus.Labels, constLabelPairs.Len())
for i := 0; i < constLabelPairs.Len(); i++ {
name := constLabelPairs.Index(i).Elem().FieldByName("Name").Elem().String()
value := constLabelPairs.Index(i).Elem().FieldByName("Value").Elem().String()
labels[name] = value
}
return labels
}
func filterGauges(registerMock *prometheusRegisterMock) []prometheus.Gauge {
fltd := make([]prometheus.Gauge, 0)
for m := range registerMock.Collectors {
switch metric := m.(type) {
case prometheus.Gauge:
fltd = append(fltd, metric)
}
}
return fltd
}
func TestMetricsBundleWithoutRevision(t *testing.T) {
fixture := newTestFixture(t, nil, func(c *Config) {
c.Prometheus = true
})
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
<-fixture.server.ch
status := testStatus()
status.ActiveRevision = ""
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
<-fixture.server.ch
bundlesLoaded := testutil.CollectAndCount(loaded)
if bundlesLoaded != 1 {
t.Fatalf("Unexpected number of bundle loads (%v), got %v", 1, bundlesLoaded)
}
bundlesFailedToLoad := testutil.CollectAndCount(failLoad)
if bundlesFailedToLoad != 0 {
t.Fatalf("Unexpected number of bundle fails load (%v), got %v", 0, bundlesFailedToLoad)
}
}
func TestPluginStart(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Start will trigger a status update when the plugin state switches
// from "not ready" to "ok".
result := <-fixture.server.ch
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
Plugins: map[string]*plugins.Status{
"status": {State: plugins.StateOK},
},
}
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
result = <-fixture.server.ch
exp.Bundles = map[string]*bundle.Status{"test": status}
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
}
func TestPluginNoLogging(t *testing.T) {
// Given no custom plugin, no service(s) and no console logging configured,
// this should not be an error, but neither do we need to initiate the plugin
cases := []struct {
note string
config []byte
}{
{
note: "no plugin attributes",
config: []byte(`{}`),
},
{
note: "empty plugin configuration",
config: []byte(`{"status": {}}`),
},
{
note: "only disabled console logger",
config: []byte(`{"status": {"console": "false"}}`),
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
config, err := ParseConfig(tc.config, []string{}, nil)
if err != nil {
t.Errorf("expected no error: %v", err)
}
if config != nil {
t.Errorf("excected no config for a no-op logging plugin")
}
})
}
}
func TestPluginStartTriggerManual(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
tr := plugins.TriggerManual
fixture.plugin.config.Trigger = &tr
// Start will trigger a status update when the plugin state switches
// from "not ready" to "ok". This status update will be sent only after a manual trigger
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// trigger the status update
go func() {
_ = fixture.plugin.Trigger(ctx)
}()
result := <-fixture.server.ch
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
}
if !reflect.DeepEqual(result.Labels, exp.Labels) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
// trigger the status update
go func() {
_ = fixture.plugin.Trigger(ctx)
}()
result = <-fixture.server.ch
exp.Bundles = map[string]*bundle.Status{"test": status}
if !reflect.DeepEqual(result.Bundles, exp.Bundles) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
}
func TestPluginStartTriggerManualMultiple(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
tr := plugins.TriggerManual
fixture.plugin.config.Trigger = &tr
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"test": status})
fixture.plugin.UpdateDiscoveryStatus(*status)
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
Plugins: map[string]*plugins.Status{
"status": {State: plugins.StateOK},
},
}
// trigger the status update
go func() {
_ = fixture.plugin.Trigger(ctx)
}()
result := <-fixture.server.ch
exp.Bundles = map[string]*bundle.Status{"test": status}
exp.Discovery = status
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
}
func TestPluginStartTriggerManualWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
s := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
time.Sleep(3 * time.Second) // this should cause the context deadline to exceed
}))
managerConfig := []byte(fmt.Sprintf(`{
"labels": {
"app": "example-app"
},
"services": [
{
"name": "example",
"url": %q
}
]}`, s.URL))
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New())
if err != nil {
t.Fatal(err)
}
pluginConfig := []byte(`{
"service": "example",
}`)
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
tr := plugins.TriggerManual
config.Trigger = &tr
p := New(config, manager)
// Start will trigger a status update when the plugin state switches
// from "not ready" to "ok". This status update will be sent only after a manual trigger
err = p.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer p.Stop(ctx)
// trigger the status update
done := make(chan struct{})
go func() {
// this call should block till the context deadline exceeds
_ = p.Trigger(ctx)
close(done)
}()
<-done
if ctx.Err() == nil {
t.Fatal("Expected error but got nil")
}
exp := "context deadline exceeded"
if ctx.Err().Error() != exp {
t.Fatalf("Expected error %v but got %v", exp, ctx.Err().Error())
}
}
func TestPluginStartTriggerManualWithError(t *testing.T) {
ctx := context.Background()
managerConfig := []byte(`{
"labels": {
"app": "example-app"
},
"services": [
{
"name": "example",
"url": "http://localhost:12345"
}
]}`)
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New())
if err != nil {
t.Fatal(err)
}
pluginConfig := []byte(`{
"service": "example",
}`)
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
tr := plugins.TriggerManual
config.Trigger = &tr
p := New(config, manager)
// Start will trigger a status update when the plugin state switches
// from "not ready" to "ok". This status update will be sent only after a manual trigger
err = p.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer p.Stop(ctx)
// trigger the status update
// this call should result in an error from the bad service config
err = p.Trigger(ctx)
if err == nil {
t.Fatal("Expected error but got nil")
}
exp := "connection refused"
if !strings.Contains(err.Error(), exp) {
t.Fatalf("Unexpected error message %v", err.Error())
}
}
func TestPluginStartBulkUpdate(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Start will trigger a status update when the plugin state switches
// from "not ready" to "ok".
<-fixture.server.ch // Discard first request.
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
Plugins: map[string]*plugins.Status{
"status": {State: plugins.StateOK},
},
}
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{status.Name: status})
result := <-fixture.server.ch
exp.Bundles = map[string]*bundle.Status{status.Name: status}
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %v but got: %v", exp, result)
}
}
func TestPluginStartBulkUpdateMultiple(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Ignore the plugin updating its status (tested elsewhere)
<-fixture.server.ch
statuses := map[string]*bundle.Status{}
tDownload, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:00.0000000Z")
tActivate, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:01.0000000Z")
for i := 0; i < 20; i++ {
name := fmt.Sprintf("test-bundle-%d", i)
statuses[name] = &bundle.Status{
Name: name,
ActiveRevision: fmt.Sprintf("v%d", i),
LastSuccessfulDownload: tDownload,
LastSuccessfulActivation: tActivate,
}
}
fixture.plugin.BulkUpdateBundleStatus(statuses)
result := <-fixture.server.ch
expLabels := map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
}
if !reflect.DeepEqual(result.Labels, expLabels) {
t.Fatalf("Unexpected status labels: %+v", result.Labels)
}
if len(result.Bundles) != len(statuses) {
t.Fatalf("Expected %d statuses, got %d", len(statuses), len(result.Bundles))
}
for name, s := range statuses {
actualStatus := result.Bundles[name]
if actualStatus.Name != s.Name ||
actualStatus.LastSuccessfulActivation != s.LastSuccessfulActivation ||
actualStatus.LastSuccessfulDownload != s.LastSuccessfulDownload ||
actualStatus.ActiveRevision != s.ActiveRevision {
t.Errorf("Bundle %s has unexpected status:\n\n %v\n\nExpected:\n%v\n\n", name, actualStatus, s)
}
}
}
func TestPluginStartDiscovery(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Ignore the plugin updating its status (tested elsewhere)
<-fixture.server.ch
status := testStatus()
fixture.plugin.UpdateDiscoveryStatus(*status)
result := <-fixture.server.ch
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
Discovery: status,
Plugins: map[string]*plugins.Status{
"status": {State: plugins.StateOK},
},
}
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %+v but got: %+v", exp, result)
}
}
func TestPluginStartDecisionLogs(t *testing.T) {
fixture := newTestFixture(t, nil)
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Ignore the plugin updating its status (tested elsewhere)
<-fixture.server.ch
status := &lstat.Status{
Code: "decision_log_error",
Message: "Upload Failed",
HTTPCode: "400",
}
fixture.plugin.UpdateDecisionLogsStatus(*status)
result := <-fixture.server.ch
exp := UpdateRequestV1{
Labels: map[string]string{
"id": "test-instance-id",
"app": "example-app",
"version": version.Version,
},
DecisionLogs: status,
Plugins: map[string]*plugins.Status{
"status": {State: plugins.StateOK},
},
}
if !reflect.DeepEqual(result, exp) {
t.Fatalf("Expected: %+v but got: %+v", exp, result)
}
}
func TestPluginBadAuth(t *testing.T) {
fixture := newTestFixture(t, nil)
ctx := context.Background()
fixture.server.expCode = 401
defer fixture.server.stop()
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
err := fixture.plugin.oneShot(ctx)
if err == nil {
t.Fatal("Expected error")
}
if err.Error() != "status update failed, server replied with HTTP 401 Unauthorized" {
t.Fatalf("Unexpected error contents: %v", err)
}
}
func TestPluginBadPath(t *testing.T) {
fixture := newTestFixture(t, nil)
ctx := context.Background()
fixture.server.expCode = 404
defer fixture.server.stop()
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
err := fixture.plugin.oneShot(ctx)
if err == nil {
t.Fatal("Expected error")
}
if err.Error() != "status update failed, server replied with HTTP 404 Not Found" {
t.Fatalf("Unexpected error contents: %v", err)
}
}
func TestPluginBadStatus(t *testing.T) {
fixture := newTestFixture(t, nil)
ctx := context.Background()
fixture.server.expCode = 500
defer fixture.server.stop()
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
err := fixture.plugin.oneShot(ctx)
if err == nil {
t.Fatal("Expected error")
}
if err.Error() != "status update failed, server replied with HTTP 500 Internal Server Error" {
t.Fatalf("Unexpected error contents: %v", err)
}
}
func TestPluginNonstandardStatus(t *testing.T) {
fixture := newTestFixture(t, nil)
ctx := context.Background()
fixture.server.expCode = 599
defer fixture.server.stop()
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
err := fixture.plugin.oneShot(ctx)
if err == nil {
t.Fatal("Expected error")
}
if err.Error() != "status update failed, server replied with HTTP 599 " {
t.Fatalf("Unexpected error contents: %v", err)
}
}
func TestPlugin2xxStatus(t *testing.T) {
fixture := newTestFixture(t, nil)
ctx := context.Background()
fixture.server.expCode = 204
defer fixture.server.stop()
fixture.plugin.lastBundleStatuses = map[string]*bundle.Status{}
err := fixture.plugin.oneShot(ctx)
if err != nil {
t.Fatal("Expected no error")
}
}
func TestPluginReconfigure(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t, nil)
defer fixture.server.stop()
if err := fixture.plugin.Start(ctx); err != nil {
t.Fatal(err)
}
pluginConfig := []byte(`{
"service": "example",
"partition_name": "test"
}`)
config, _ := ParseConfig(pluginConfig, fixture.manager.Services(), nil)
fixture.plugin.Reconfigure(ctx, config)
fixture.plugin.Stop(ctx)
if fixture.plugin.config.PartitionName != "test" {
t.Fatalf("Expected partition name: test but got %v", fixture.plugin.config.PartitionName)
}
}
func TestMetrics(t *testing.T) {
fixture := newTestFixture(t, metrics.New())
fixture.server.ch = make(chan UpdateRequestV1)
defer fixture.server.stop()
ctx := context.Background()
err := fixture.plugin.Start(ctx)
if err != nil {
t.Fatal(err)
}
defer fixture.plugin.Stop(ctx)
// Ignore the plugin updating its status (tested elsewhere)
<-fixture.server.ch
status := testStatus()
fixture.plugin.BulkUpdateBundleStatus(map[string]*bundle.Status{"bundle": status})
result := <-fixture.server.ch
exp := map[string]interface{}{"<built-in>": map[string]interface{}{}}
if !reflect.DeepEqual(result.Metrics, exp) {
t.Fatalf("Expected %v but got %v", exp, result.Metrics)
}
}
func TestParseConfigUseDefaultServiceNoConsole(t *testing.T) {
services := []string{
"s0",
"s1",
"s3",
}
loggerConfig := []byte(`{
"console": false
}`)
config, err := ParseConfig(loggerConfig, services, nil)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if config.Service != services[0] {
t.Errorf("Expected %s service in config, actual = '%s'", services[0], config.Service)
}
}
func TestParseConfigDefaultServiceWithConsole(t *testing.T) {
services := []string{
"s0",
"s1",
"s3",
}
loggerConfig := []byte(`{
"console": true
}`)
config, err := ParseConfig(loggerConfig, services, nil)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if config.Service != "" {
t.Errorf("Expected no service in config, actual = '%s'", config.Service)
}
}
func TestParseConfigTriggerMode(t *testing.T) {
cases := []struct {
note string
config []byte
expected plugins.TriggerMode
wantErr bool
err error
}{
{
note: "default trigger mode",
config: []byte(`{}`),
expected: plugins.DefaultTriggerMode,
},
{
note: "manual trigger mode",
config: []byte(`{"trigger": "manual"}`),
expected: plugins.TriggerManual,
},
{
note: "trigger mode mismatch",
config: []byte(`{"trigger": "manual"}`),
expected: plugins.TriggerPeriodic,
wantErr: true,
err: fmt.Errorf("invalid status config: trigger mode mismatch: periodic and manual (hint: check discovery configuration)"),
},
{
note: "bad trigger mode",
config: []byte(`{"trigger": "foo"}`),
expected: "foo",
wantErr: true,
err: fmt.Errorf("invalid status config: invalid trigger mode \"foo\" (want \"periodic\" or \"manual\")"),
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
c, err := NewConfigBuilder().WithBytes(tc.config).WithServices([]string{"s0"}).WithTriggerMode(&tc.expected).Parse()
if tc.wantErr {
if err == nil {
t.Fatal("Expected error but got nil")
}
if tc.err != nil && tc.err.Error() != err.Error() {
t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
}
} else {
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if *c.Trigger != tc.expected {
t.Fatalf("Expected trigger mode %v but got %v", tc.expected, *c.Trigger)
}
}
})
}
}
type testFixture struct {
manager *plugins.Manager
plugin *Plugin
server *testServer
}
type testPluginCustomizer func(c *Config)
func newTestFixture(t *testing.T, m metrics.Metrics, options ...testPluginCustomizer) testFixture {
ts := testServer{
t: t,
expCode: 200,
}
ts.start()
managerConfig := []byte(fmt.Sprintf(`{
"labels": {
"app": "example-app"
},
"services": [
{
"name": "example",
"url": %q,
"credentials": {
"bearer": {
"scheme": "Bearer",
"token": "secret"
}
}
}
]}`, ts.server.URL))
registerMock := &prometheusRegisterMock{
Collectors: map[prometheus.Collector]bool{},
}
manager, err := plugins.New(managerConfig, "test-instance-id", inmem.New(), plugins.WithPrometheusRegister(registerMock))
if err != nil {
t.Fatal(err)
}
config := newConfig(manager, options...)
p := New(config, manager).WithMetrics(m)
return testFixture{
manager: manager,
plugin: p,
server: &ts,
}
}
func newConfig(manager *plugins.Manager, options ...testPluginCustomizer) *Config {
pluginConfig := []byte(`{
"service": "example",
}`)
config, _ := ParseConfig(pluginConfig, manager.Services(), nil)
for _, option := range options {
option(config)
}
return config
}
type testServer struct {
t *testing.T
expCode int
server *httptest.Server
ch chan UpdateRequestV1
}
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
status := UpdateRequestV1{}
if err := util.NewJSONDecoder(r.Body).Decode(&status); err != nil {
t.t.Fatal(err)
}
if t.ch != nil {
t.ch <- status
}
w.WriteHeader(t.expCode)
}
func (t *testServer) start() {
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
}
func (t *testServer) stop() {
t.server.Close()
}
func testStatus() *bundle.Status {
tDownload, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:00.0000000Z")
tActivate, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:01.0000000Z")
tSuccessfulRequest, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:02.0000000Z")
tRequest, _ := time.Parse(time.RFC3339Nano, "2018-01-01T00:00:03.0000000Z")
status := bundle.Status{
Name: "example/authz",
ActiveRevision: "quickbrawnfaux",
LastSuccessfulDownload: tDownload,
LastSuccessfulActivation: tActivate,
LastRequest: tRequest,
LastSuccessfulRequest: tSuccessfulRequest,
}
return &status
}
type testPlugin struct {
reqs []UpdateRequestV1
}
func (*testPlugin) Start(context.Context) error {
return nil
}
func (p *testPlugin) Stop(context.Context) {
}
func (p *testPlugin) Reconfigure(context.Context, interface{}) {
}
func (p *testPlugin) Log(_ context.Context, req *UpdateRequestV1) error {
p.reqs = append(p.reqs, *req)
return nil
}
func TestPluginCustomBackend(t *testing.T) {
ctx := context.Background()
manager, _ := plugins.New(nil, "test-instance-id", inmem.New())
backend := &testPlugin{}
manager.Register("test_plugin", backend)
config, err := ParseConfig([]byte(`{"plugin": "test_plugin"}`), nil, []string{"test_plugin"})
if err != nil {
t.Fatal(err)
}
plugin := New(config, manager)
err = plugin.oneShot(ctx)
if err != nil {
t.Fatal(err)
}
err = plugin.oneShot(ctx)
if err != nil {
t.Fatal(err)
}
if len(backend.reqs) != 2 {
t.Fatalf("Unexpected number of reqs: expected 2, got %d: %v", len(backend.reqs), backend.reqs)
}
}
type prometheusRegisterMock struct {
Collectors map[prometheus.Collector]bool
}
func (p prometheusRegisterMock) Register(collector prometheus.Collector) error {
p.Collectors[collector] = true
return nil
}
func (p prometheusRegisterMock) MustRegister(collector ...prometheus.Collector) {
for _, c := range collector {
p.Collectors[c] = true
}
}
func (p prometheusRegisterMock) Unregister(collector prometheus.Collector) bool {
delete(p.Collectors, collector)
return true
}