diff --git a/integrations/access/opsgenie/opsgenie_test.go b/integrations/access/opsgenie/opsgenie_test.go
deleted file mode 100644
index 5a86d8cb177ef..0000000000000
--- a/integrations/access/opsgenie/opsgenie_test.go
+++ /dev/null
@@ -1,574 +0,0 @@
-/*
- * Teleport
- * Copyright (C) 2023 Gravitational, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-package opsgenie
-
-import (
- "os/user"
- "runtime"
- "testing"
- "time"
-
- "github.com/google/uuid"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "github.com/stretchr/testify/suite"
-
- "github.com/gravitational/teleport/api/client/proto"
- "github.com/gravitational/teleport/api/types"
- "github.com/gravitational/teleport/api/types/wrappers"
- "github.com/gravitational/teleport/integrations/lib"
- "github.com/gravitational/teleport/integrations/lib/logger"
- "github.com/gravitational/teleport/integrations/lib/testing/integration"
-)
-
-const (
- NotifyServiceName = "Teleport Notifications"
- NotifyServiceAnnotation = types.TeleportNamespace + types.ReqAnnotationNotifyServicesLabel
- ResponderName1 = "Responder 1"
- ResponderName2 = "Responder 2"
- ResponderName3 = "Responder 3"
-)
-
-type OpsgenieSuite struct {
- integration.Suite
- appConfig Config
- currentRequestor string
- userNames struct {
- ruler string
- reviewer1 string
- reviewer2 string
- requestor string
- approver string
- racer1 string
- racer2 string
- plugin string
- }
- raceNumber int
- fakeOpsgenie *FakeOpsgenie
-
- ogNotifyResponder Responder
- ogResponder1 Responder
- ogResponder2 Responder
- ogResponder3 Responder
-
- clients map[string]*integration.Client
- teleportFeatures *proto.Features
- teleportConfig lib.TeleportConfig
-}
-
-func TestOpsgenieSuite(t *testing.T) { suite.Run(t, &OpsgenieSuite{}) }
-
-func (s *OpsgenieSuite) SetupSuite() {
- var err error
- t := s.T()
-
- logger.Init()
- err = logger.Setup(logger.Config{Severity: "debug"})
- require.NoError(t, err)
- s.raceNumber = 2 * runtime.GOMAXPROCS(0)
- me, err := user.Current()
- require.NoError(t, err)
-
- // We set such a big timeout because integration.NewFromEnv could start
- // downloading a Teleport *-bin.tar.gz file which can take a long time.
- ctx := s.SetContextTimeout(2 * time.Minute)
-
- teleport, err := integration.NewFromEnv(ctx)
- require.NoError(t, err)
- require.NotNil(t, teleport)
- t.Cleanup(teleport.Close)
-
- auth, err := teleport.NewAuthService()
- require.NoError(t, err)
- require.NotNil(t, auth)
- s.StartApp(auth)
-
- s.clients = make(map[string]*integration.Client)
-
- // Set up the user who has an access to all kinds of resources.
-
- s.userNames.ruler = me.Username + "-ruler@example.com"
- client, err := teleport.MakeAdmin(ctx, auth, s.userNames.ruler)
- require.NoError(t, err)
- s.clients[s.userNames.ruler] = client
-
- // Get the server features.
-
- pong, err := client.Ping(ctx)
- require.NoError(t, err)
- teleportFeatures := pong.GetServerFeatures()
-
- var bootstrap integration.Bootstrap
-
- // Set up user who can request the access to role "editor".
-
- conditions := types.RoleConditions{
- Request: &types.AccessRequestConditions{
- Roles: []string{"editor"},
- Annotations: wrappers.Traits{
- NotifyServiceAnnotation: []string{NotifyServiceName},
- },
- },
- }
- if teleportFeatures.AdvancedAccessWorkflows {
- conditions.Request.Thresholds = []types.AccessReviewThreshold{{Approve: 2, Deny: 2}}
- }
- // This is the role for testing notification alert creation.
- role, err := bootstrap.AddRole("foo", types.RoleSpecV6{Allow: conditions})
- require.NoError(t, err)
-
- user, err := bootstrap.AddUserWithRoles(me.Username+"@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.requestor = user.GetName()
-
- if teleportFeatures.AdvancedAccessWorkflows {
- // Set up TWO users who can review access requests to role "editor".
-
- role, err = bootstrap.AddRole("foo-reviewer", types.RoleSpecV6{
- Allow: types.RoleConditions{
- ReviewRequests: &types.AccessReviewConditions{Roles: []string{"editor"}},
- },
- })
- require.NoError(t, err)
-
- user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer1@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.reviewer1 = user.GetName()
-
- user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer2@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.reviewer2 = user.GetName()
-
- // This is the role that needs exactly one approval review for an access request to be approved.
- // It's handy to test auto-approval scenarios so we also put "opsgenie_services" annotation.
- role, err = bootstrap.AddRole("bar", types.RoleSpecV6{
- Allow: types.RoleConditions{
- Request: &types.AccessRequestConditions{
- Roles: []string{"editor"},
- Annotations: wrappers.Traits{
- NotifyServiceAnnotation: []string{ResponderName1, ResponderName2},
- },
- },
- },
- })
- require.NoError(t, err)
-
- user, err = bootstrap.AddUserWithRoles(me.Username+"-approver@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.approver = user.GetName()
-
- // This is the role with a maximum possible setup: both "opsgenie_notify_service" and
- // "opsgenie_services" annotations and threshold.
- role, err = bootstrap.AddRole("foo-bar", types.RoleSpecV6{
- Allow: types.RoleConditions{
- Request: &types.AccessRequestConditions{
- Roles: []string{"editor"},
- Annotations: wrappers.Traits{
- NotifyServiceAnnotation: []string{NotifyServiceName},
- // ServicesDefaultAnnotation: []string{ServiceName1, ServiceName2}, // TODO: FIX THIS
- },
- Thresholds: []types.AccessReviewThreshold{types.AccessReviewThreshold{Approve: 2, Deny: 2}},
- },
- },
- })
- require.NoError(t, err)
-
- user, err = bootstrap.AddUserWithRoles(me.Username+"-racer1@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.racer1 = user.GetName()
-
- user, err = bootstrap.AddUserWithRoles(me.Username+"-racer2@example.com", role.GetName())
- require.NoError(t, err)
- s.userNames.racer2 = user.GetName()
- }
-
- conditions = types.RoleConditions{
- Rules: []types.Rule{
- types.NewRule("access_request", []string{"list", "read"}),
- types.NewRule("access_plugin_data", []string{"update"}),
- },
- }
- if teleportFeatures.AdvancedAccessWorkflows {
- conditions.ReviewRequests = &types.AccessReviewConditions{Roles: []string{"editor"}}
- }
-
- // Set up plugin user.
-
- role, err = bootstrap.AddRole("access-opsgenie", types.RoleSpecV6{Allow: conditions})
- require.NoError(t, err)
-
- user, err = bootstrap.AddUserWithRoles("access-opsgenie", role.GetName())
- require.NoError(t, err)
- s.userNames.plugin = user.GetName()
-
- // Bake all the resources.
-
- err = teleport.Bootstrap(ctx, auth, bootstrap.Resources())
- require.NoError(t, err)
-
- // Initialize the clients.
-
- client, err = teleport.NewClient(ctx, auth, s.userNames.requestor)
- require.NoError(t, err)
- s.clients[s.userNames.requestor] = client
-
- if teleportFeatures.AdvancedAccessWorkflows {
- client, err = teleport.NewClient(ctx, auth, s.userNames.approver)
- require.NoError(t, err)
- s.clients[s.userNames.approver] = client
-
- client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer1)
- require.NoError(t, err)
- s.clients[s.userNames.reviewer1] = client
-
- client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer2)
- require.NoError(t, err)
- s.clients[s.userNames.reviewer2] = client
-
- client, err = teleport.NewClient(ctx, auth, s.userNames.racer1)
- require.NoError(t, err)
- s.clients[s.userNames.racer1] = client
-
- client, err = teleport.NewClient(ctx, auth, s.userNames.racer2)
- require.NoError(t, err)
- s.clients[s.userNames.racer2] = client
- }
-
- identityPath, err := teleport.Sign(ctx, auth, s.userNames.plugin)
- require.NoError(t, err)
-
- s.teleportConfig.Addr = auth.AuthAddr().String()
- s.teleportConfig.Identity = identityPath
- s.teleportFeatures = teleportFeatures
-}
-
-func (s *OpsgenieSuite) SetupTest() {
- t := s.T()
-
- err := logger.Setup(logger.Config{Severity: "debug"})
- require.NoError(t, err)
-
- fakeOpsgenie := NewFakeOpsgenie(s.raceNumber)
- t.Cleanup(fakeOpsgenie.Close)
- s.fakeOpsgenie = fakeOpsgenie
-
- s.ogNotifyResponder = s.fakeOpsgenie.StoreResponder(Responder{
- Name: NotifyServiceName,
- })
- s.ogResponder1 = s.fakeOpsgenie.StoreResponder(Responder{
- Name: ResponderName1,
- })
- s.ogResponder2 = s.fakeOpsgenie.StoreResponder(Responder{
- Name: ResponderName2,
- })
- s.ogResponder3 = s.fakeOpsgenie.StoreResponder(Responder{
- Name: ResponderName3,
- })
-
- var conf Config
- conf.Teleport = s.teleportConfig
- conf.ClientConfig.APIEndpoint = s.fakeOpsgenie.URL()
-
- s.appConfig = conf
- s.currentRequestor = s.userNames.requestor
- s.SetContextTimeout(5 * time.Second)
-}
-
-func (s *OpsgenieSuite) startApp() {
- t := s.T()
- t.Helper()
-
- app, err := NewOpsgenieApp(s.Context(), &s.appConfig)
- require.NoError(t, err)
-
- s.StartApp(app)
-}
-
-func (s *OpsgenieSuite) ruler() *integration.Client {
- return s.clients[s.userNames.ruler]
-}
-
-func (s *OpsgenieSuite) requestor() *integration.Client {
- return s.clients[s.currentRequestor]
-}
-
-func (s *OpsgenieSuite) reviewer1() *integration.Client {
- return s.clients[s.userNames.reviewer1]
-}
-
-func (s *OpsgenieSuite) reviewer2() *integration.Client {
- return s.clients[s.userNames.reviewer2]
-}
-
-func (s *OpsgenieSuite) newAccessRequest() types.AccessRequest {
- t := s.T()
- t.Helper()
-
- req, err := types.NewAccessRequest(uuid.New().String(), s.currentRequestor, "editor")
- req.SetSystemAnnotations(map[string][]string{
- NotifyServiceAnnotation: {NotifyServiceName},
- })
- require.NoError(s.T(), err)
- return req
-}
-
-func (s *OpsgenieSuite) createAccessRequest() types.AccessRequest {
- t := s.T()
- t.Helper()
-
- req := s.newAccessRequest()
- out, err := s.requestor().CreateAccessRequestV2(s.Context(), req)
- require.NoError(t, err)
- return out
-}
-
-func (s *OpsgenieSuite) checkPluginData(reqID string, cond func(PluginData) bool) PluginData {
- t := s.T()
- t.Helper()
-
- for {
- rawData, err := s.ruler().PollAccessRequestPluginData(s.Context(), "opsgenie", reqID)
- require.NoError(t, err)
- if data := DecodePluginData(rawData); cond(data) {
- return data
- }
- }
-}
-
-func (s *OpsgenieSuite) TestAlertCreation() {
- t := s.T()
-
- s.startApp()
-
- req := s.createAccessRequest()
- pluginData := s.checkPluginData(req.GetName(), func(data PluginData) bool {
- return data.AlertID != ""
- })
-
- alert, err := s.fakeOpsgenie.CheckNewAlert(s.Context())
- require.NoError(t, err, "no new alerts stored")
-
- assert.Equal(t, alert.ID, pluginData.AlertID)
-}
-
-func (s *OpsgenieSuite) TestApproval() {
- t := s.T()
-
- s.startApp()
-
- req := s.createAccessRequest()
-
- alert, err := s.fakeOpsgenie.CheckNewAlert(s.Context())
- require.NoError(t, err, "no new alerts stored")
-
- err = s.ruler().ApproveAccessRequest(s.Context(), req.GetName(), "okay")
- require.NoError(t, err)
-
- note, err := s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, "Access request has been approved")
- assert.Contains(t, note.Note, "Reason: okay")
-
- alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(s.Context())
- require.NoError(t, err)
- assert.Equal(t, "resolved", alertUpdate.Status)
-}
-
-func (s *OpsgenieSuite) TestDenial() {
- t := s.T()
-
- s.startApp()
-
- req := s.createAccessRequest()
-
- alert, err := s.fakeOpsgenie.CheckNewAlert(s.Context())
- require.NoError(t, err, "no new alerts stored")
-
- err = s.ruler().DenyAccessRequest(s.Context(), req.GetName(), "not okay")
- require.NoError(t, err)
-
- note, err := s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, "Access request has been denied")
- assert.Contains(t, note.Note, "Reason: not okay")
-
- alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(s.Context())
- require.NoError(t, err)
- assert.Equal(t, "resolved", alertUpdate.Status)
-}
-
-func (s *OpsgenieSuite) TestReviewNotes() {
- t := s.T()
-
- if !s.teleportFeatures.AdvancedAccessWorkflows {
- t.Skip("Doesn't work in OSS version")
- }
-
- s.startApp()
-
- req := s.createAccessRequest()
-
- err := s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer1,
- ProposedState: types.RequestState_APPROVED,
- Created: time.Now(),
- Reason: "okay",
- })
- require.NoError(t, err)
-
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer2,
- ProposedState: types.RequestState_APPROVED,
- Created: time.Now(),
- Reason: "not okay",
- })
- require.NoError(t, err)
-
- pluginData := s.checkPluginData(req.GetName(), func(data PluginData) bool {
- return data.AlertID != "" && data.ReviewsCount == 2
- })
-
- note, err := s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, pluginData.AlertID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer1+" reviewed the request", "note must contain a review author")
- assert.Contains(t, note.Note, "Resolution: APPROVED", "note must contain an approval resolution")
- assert.Contains(t, note.Note, "Reason: okay", "note must contain an approval reason")
-
- note, err = s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, pluginData.AlertID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer2+" reviewed the request", "note must contain a review author")
- assert.Contains(t, note.Note, "Resolution: APPROVED", "note must contain a approval resolution")
- assert.Contains(t, note.Note, "Reason: not okay", "note must contain a denial reason")
-}
-
-func (s *OpsgenieSuite) TestApprovalByReview() {
- t := s.T()
-
- if !s.teleportFeatures.AdvancedAccessWorkflows {
- t.Skip("Doesn't work in OSS version")
- }
-
- s.startApp()
-
- req := s.createAccessRequest()
-
- alert, err := s.fakeOpsgenie.CheckNewAlert(s.Context())
- require.NoError(t, err, "no new alerts stored")
-
- err = s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer1,
- ProposedState: types.RequestState_APPROVED,
- Created: time.Now(),
- Reason: "okay",
- })
- require.NoError(t, err)
-
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer2,
- ProposedState: types.RequestState_APPROVED,
- Created: time.Now(),
- Reason: "finally okay",
- })
- require.NoError(t, err)
-
- note, err := s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer1+" reviewed the request", "note must contain a review author")
-
- note, err = s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer2+" reviewed the request", "note must contain a review author")
-
- data := s.checkPluginData(req.GetName(), func(data PluginData) bool {
- return data.ReviewsCount == 2 && data.Resolution.Tag != Unresolved
- })
- assert.Equal(t, Resolution{Tag: ResolvedApproved, Reason: "finally okay"}, data.Resolution)
-
- note, err = s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, "Access request has been approved")
- assert.Contains(t, note.Note, "Reason: finally okay")
-
- alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(s.Context())
- require.NoError(t, err)
- assert.Equal(t, "resolved", alertUpdate.Status)
-}
-
-func (s *OpsgenieSuite) TestDenialByReview() {
- t := s.T()
-
- if !s.teleportFeatures.AdvancedAccessWorkflows {
- t.Skip("Doesn't work in OSS version")
- }
-
- s.startApp()
-
- req := s.createAccessRequest()
-
- alert, err := s.fakeOpsgenie.CheckNewAlert(s.Context())
- require.NoError(t, err, "no new alerts stored")
-
- err = s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer1,
- ProposedState: types.RequestState_DENIED,
- Created: time.Now(),
- Reason: "not okay",
- })
- require.NoError(t, err)
-
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
- Author: s.userNames.reviewer2,
- ProposedState: types.RequestState_DENIED,
- Created: time.Now(),
- Reason: "finally not okay",
- })
- require.NoError(t, err)
-
- note, err := s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer1+" reviewed the request", "note must contain a review author")
-
- note, err = s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, s.userNames.reviewer2+" reviewed the request", "note must contain a review author")
-
- data := s.checkPluginData(req.GetName(), func(data PluginData) bool {
- return data.ReviewsCount == 2 && data.Resolution.Tag != Unresolved
- })
- assert.Equal(t, Resolution{Tag: ResolvedDenied, Reason: "finally not okay"}, data.Resolution)
-
- note, err = s.fakeOpsgenie.CheckNewAlertNote(s.Context())
- require.NoError(t, err)
- assert.Equal(t, alert.ID, note.AlertID)
- assert.Contains(t, note.Note, "Access request has been denied")
- assert.Contains(t, note.Note, "Reason: finally not okay")
-
- alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(s.Context())
- require.NoError(t, err)
- assert.Equal(t, "resolved", alertUpdate.Status)
-}
diff --git a/integrations/access/opsgenie/fake_opsgenie_test.go b/integrations/access/opsgenie/testlib/fake_opsgenie.go
similarity index 68%
rename from integrations/access/opsgenie/fake_opsgenie_test.go
rename to integrations/access/opsgenie/testlib/fake_opsgenie.go
index 87fcf06ac2507..2d63adf5c06b0 100644
--- a/integrations/access/opsgenie/fake_opsgenie_test.go
+++ b/integrations/access/opsgenie/testlib/fake_opsgenie.go
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package opsgenie
+package testlib
import (
"context"
@@ -35,6 +35,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/integrations/access/opsgenie"
"github.com/gravitational/teleport/integrations/lib/stringset"
)
@@ -44,8 +45,8 @@ type FakeOpsgenie struct {
objects sync.Map
// Alerts
alertIDCounter uint64
- newAlerts chan Alert
- alertUpdates chan Alert
+ newAlerts chan opsgenie.Alert
+ alertUpdates chan opsgenie.Alert
// Alert notes
newAlertNotes chan FakeAlertNote
// Responders
@@ -72,15 +73,15 @@ type fakeResponderByNameKey string
type FakeAlertNote struct {
AlertID string
- AlertNote
+ opsgenie.AlertNote
}
func NewFakeOpsgenie(concurrency int) *FakeOpsgenie {
router := httprouter.New()
- opsgenie := &FakeOpsgenie{
- newAlerts: make(chan Alert, concurrency),
- alertUpdates: make(chan Alert, concurrency),
+ mock := &FakeOpsgenie{
+ newAlerts: make(chan opsgenie.Alert, concurrency),
+ alertUpdates: make(chan opsgenie.Alert, concurrency),
newAlertNotes: make(chan FakeAlertNote, concurrency*3), // for any alert there could be 1-3 notes
srv: httptest.NewServer(router),
}
@@ -89,40 +90,40 @@ func NewFakeOpsgenie(concurrency int) *FakeOpsgenie {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusCreated)
- var alert Alert
+ var alert opsgenie.Alert
err := json.NewDecoder(r.Body).Decode(&alert)
panicIf(err)
- alert.ID = fmt.Sprintf("alert-%v", atomic.AddUint64(&opsgenie.alertIDCounter, 1))
+ alert.ID = fmt.Sprintf("alert-%v", atomic.AddUint64(&mock.alertIDCounter, 1))
alert.Status = types.RequestState_PENDING.String()
- opsgenie.StoreAlert(alert)
- opsgenie.newAlerts <- alert
+ mock.StoreAlert(alert)
+ mock.newAlerts <- alert
- err = json.NewEncoder(rw).Encode(AlertResult{Alert: alert})
+ err = json.NewEncoder(rw).Encode(opsgenie.AlertResult{Alert: alert})
panicIf(err)
})
router.POST("/v2/alerts/:alertID/close", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
alertID := ps.ByName("alertID")
- var body AlertNote
+ var body opsgenie.AlertNote
err := json.NewDecoder(r.Body).Decode(&body)
panicIf(err)
- note := opsgenie.StoreAlertNote(alertID, AlertNote{Note: body.Note})
+ note := mock.StoreAlertNote(alertID, opsgenie.AlertNote{Note: body.Note})
- opsgenie.newAlertNotes <- FakeAlertNote{AlertNote: note, AlertID: alertID}
+ mock.newAlertNotes <- FakeAlertNote{AlertNote: note, AlertID: alertID}
err = json.NewEncoder(rw).Encode(note)
panicIf(err)
- alert, found := opsgenie.GetAlert(alertID)
+ alert, found := mock.GetAlert(alertID)
if !found {
rw.WriteHeader(http.StatusNotFound)
return
}
alert.Status = "resolved"
- opsgenie.StoreAlert(alert)
- opsgenie.alertUpdates <- alert
+ mock.StoreAlert(alert)
+ mock.alertUpdates <- alert
})
router.POST("/v2/alerts/:alertID/notes", func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
@@ -131,18 +132,18 @@ func NewFakeOpsgenie(concurrency int) *FakeOpsgenie {
alertID := ps.ByName("alertID")
- var body AlertNote
+ var body opsgenie.AlertNote
err := json.NewDecoder(r.Body).Decode(&body)
panicIf(err)
- note := opsgenie.StoreAlertNote(alertID, AlertNote{Note: body.Note})
+ note := mock.StoreAlertNote(alertID, opsgenie.AlertNote{Note: body.Note})
- opsgenie.newAlertNotes <- FakeAlertNote{AlertNote: note, AlertID: alertID}
+ mock.newAlertNotes <- FakeAlertNote{AlertNote: note, AlertID: alertID}
err = json.NewEncoder(rw).Encode(note)
panicIf(err)
})
- return opsgenie
+ return mock
}
func (s *FakeOpsgenie) URL() string {
@@ -156,27 +157,27 @@ func (s *FakeOpsgenie) Close() {
close(s.newAlertNotes)
}
-func (s *FakeOpsgenie) GetResponder(id string) (Responder, bool) {
+func (s *FakeOpsgenie) GetResponder(id string) (opsgenie.Responder, bool) {
if obj, ok := s.objects.Load(id); ok {
- responder, ok := obj.(Responder)
+ responder, ok := obj.(opsgenie.Responder)
return responder, ok
}
- return Responder{}, false
+ return opsgenie.Responder{}, false
}
-func (s *FakeOpsgenie) GetResponderByName(name string) (Responder, bool) {
+func (s *FakeOpsgenie) GetResponderByName(name string) (opsgenie.Responder, bool) {
if obj, ok := s.objects.Load(fakeResponderByNameKey(strings.ToLower(name))); ok {
- responder, ok := obj.(Responder)
+ responder, ok := obj.(opsgenie.Responder)
return responder, ok
}
- return Responder{}, false
+ return opsgenie.Responder{}, false
}
-func (s *FakeOpsgenie) StoreResponder(responder Responder) Responder {
+func (s *FakeOpsgenie) StoreResponder(responder opsgenie.Responder) opsgenie.Responder {
byNameKey := fakeResponderByNameKey(strings.ToLower(responder.Name))
if responder.ID == "" {
if obj, ok := s.objects.Load(byNameKey); ok {
- responder.ID = obj.(Responder).ID
+ responder.ID = obj.(opsgenie.Responder).ID
} else {
responder.ID = fmt.Sprintf("responder-%v", atomic.AddUint64(&s.responderIDCounter, 1))
}
@@ -186,15 +187,15 @@ func (s *FakeOpsgenie) StoreResponder(responder Responder) Responder {
return responder
}
-func (s *FakeOpsgenie) GetAlert(id string) (Alert, bool) {
+func (s *FakeOpsgenie) GetAlert(id string) (opsgenie.Alert, bool) {
if obj, ok := s.objects.Load(id); ok {
- alert, ok := obj.(Alert)
+ alert, ok := obj.(opsgenie.Alert)
return alert, ok
}
- return Alert{}, false
+ return opsgenie.Alert{}, false
}
-func (s *FakeOpsgenie) StoreAlert(alert Alert) Alert {
+func (s *FakeOpsgenie) StoreAlert(alert opsgenie.Alert) opsgenie.Alert {
if alert.ID == "" {
alert.ID = fmt.Sprintf("alert-%v", atomic.AddUint64(&s.alertIDCounter, 1))
}
@@ -202,26 +203,26 @@ func (s *FakeOpsgenie) StoreAlert(alert Alert) Alert {
return alert
}
-func (s *FakeOpsgenie) StoreAlertNote(alertID string, note AlertNote) AlertNote {
+func (s *FakeOpsgenie) StoreAlertNote(alertID string, note opsgenie.AlertNote) opsgenie.AlertNote {
s.objects.Store(alertID+note.Note, note)
return note
}
-func (s *FakeOpsgenie) CheckNewAlert(ctx context.Context) (Alert, error) {
+func (s *FakeOpsgenie) CheckNewAlert(ctx context.Context) (opsgenie.Alert, error) {
select {
case alert := <-s.newAlerts:
return alert, nil
case <-ctx.Done():
- return Alert{}, trace.Wrap(ctx.Err())
+ return opsgenie.Alert{}, trace.Wrap(ctx.Err())
}
}
-func (s *FakeOpsgenie) CheckAlertUpdate(ctx context.Context) (Alert, error) {
+func (s *FakeOpsgenie) CheckAlertUpdate(ctx context.Context) (opsgenie.Alert, error) {
select {
case alert := <-s.alertUpdates:
return alert, nil
case <-ctx.Done():
- return Alert{}, trace.Wrap(ctx.Err())
+ return opsgenie.Alert{}, trace.Wrap(ctx.Err())
}
}
diff --git a/integrations/access/opsgenie/testlib/helpers.go b/integrations/access/opsgenie/testlib/helpers.go
new file mode 100644
index 0000000000000..a600fedc52f7c
--- /dev/null
+++ b/integrations/access/opsgenie/testlib/helpers.go
@@ -0,0 +1,40 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package testlib
+
+import (
+ "context"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/integrations/access/opsgenie"
+)
+
+func (s *OpsgenieSuite) checkPluginData(ctx context.Context, reqID string, cond func(opsgenie.PluginData) bool) opsgenie.PluginData {
+ t := s.T()
+ t.Helper()
+
+ for {
+ rawData, err := s.Ruler().PollAccessRequestPluginData(ctx, "opsgenie", reqID)
+ require.NoError(t, err)
+ if data := opsgenie.DecodePluginData(rawData); cond(data) {
+ return data
+ }
+ }
+}
diff --git a/integrations/access/opsgenie/testlib/oss_integration_test.go b/integrations/access/opsgenie/testlib/oss_integration_test.go
new file mode 100644
index 0000000000000..2b2846285fe79
--- /dev/null
+++ b/integrations/access/opsgenie/testlib/oss_integration_test.go
@@ -0,0 +1,36 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package testlib
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+
+ "github.com/gravitational/teleport/integrations/lib/testing/integration"
+)
+
+func TestOpsgeniePluginOSS(t *testing.T) {
+ opsgenieSuite := &OpsgenieSuite{
+ AccessRequestSuite: &integration.AccessRequestSuite{
+ AuthHelper: &integration.OSSAuthHelper{},
+ },
+ }
+ suite.Run(t, opsgenieSuite)
+}
diff --git a/integrations/access/opsgenie/testlib/suite.go b/integrations/access/opsgenie/testlib/suite.go
new file mode 100644
index 0000000000000..183422c0100f7
--- /dev/null
+++ b/integrations/access/opsgenie/testlib/suite.go
@@ -0,0 +1,384 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package testlib
+
+import (
+ "context"
+ "runtime"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/integrations/access/opsgenie"
+ "github.com/gravitational/teleport/integrations/access/pagerduty"
+ "github.com/gravitational/teleport/integrations/lib/logger"
+ "github.com/gravitational/teleport/integrations/lib/testing/integration"
+)
+
+const (
+ NotifyServiceName = "Teleport Notifications"
+ NotifyServiceAnnotation = types.TeleportNamespace + types.ReqAnnotationNotifyServicesLabel
+ ResponderName1 = "Responder 1"
+ ResponderName2 = "Responder 2"
+ ResponderName3 = "Responder 3"
+)
+
+// OpsgenieSuite is the OpsGenie access plugin test suite.
+// It implements the testify.TestingSuite interface.
+type OpsgenieSuite struct {
+ *integration.AccessRequestSuite
+ appConfig opsgenie.Config
+ raceNumber int
+ fakeOpsgenie *FakeOpsgenie
+
+ ogNotifyResponder opsgenie.Responder
+ ogResponder1 opsgenie.Responder
+ ogResponder2 opsgenie.Responder
+ ogResponder3 opsgenie.Responder
+}
+
+// SetupTest starts a fake OpsGenie and generates the plugin configuration.
+// It also configures the role notifications for OpsGenie notifications and
+// automatic approval.
+// It is run for each test.
+func (s *OpsgenieSuite) SetupTest() {
+ t := s.T()
+ ctx := context.Background()
+
+ err := logger.Setup(logger.Config{Severity: "debug"})
+ require.NoError(t, err)
+ s.raceNumber = 2 * runtime.GOMAXPROCS(0)
+
+ s.fakeOpsgenie = NewFakeOpsgenie(s.raceNumber)
+ t.Cleanup(s.fakeOpsgenie.Close)
+
+ // This service should be notified for every access request.
+ s.ogNotifyResponder = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{
+ Name: NotifyServiceName,
+ })
+ s.AnnotateRequesterRoleAccessRequests(
+ ctx,
+ NotifyServiceAnnotation,
+ []string{NotifyServiceName},
+ )
+
+ // Responder 1 and 2 are on-call and should be automatically approved.
+ // Responder 3 is not.
+ s.ogResponder1 = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{
+ Name: ResponderName1,
+ })
+ s.ogResponder2 = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{
+ Name: ResponderName2,
+ })
+ s.ogResponder3 = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{
+ Name: ResponderName3,
+ })
+ s.AnnotateRequesterRoleAccessRequests(
+ ctx,
+ pagerduty.ServicesDefaultAnnotation,
+ []string{ResponderName1, ResponderName2},
+ )
+
+ var conf opsgenie.Config
+ conf.Teleport = s.TeleportConfig()
+ conf.ClientConfig.APIEndpoint = s.fakeOpsgenie.URL()
+
+ s.appConfig = conf
+}
+
+// startApp starts the OpsGenie plugin, waits for it to become ready and returns.
+func (s *OpsgenieSuite) startApp() {
+ t := s.T()
+ t.Helper()
+
+ app, err := opsgenie.NewOpsgenieApp(context.Background(), &s.appConfig)
+ require.NoError(t, err)
+ s.RunAndWaitReady(t, app)
+}
+
+// TestAlertCreation validates that an alert is created to the service
+// specified in the role's annotation.
+func (s *OpsgenieSuite) TestAlertCreation() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ s.startApp()
+
+ // Test execution: create an access request
+ req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil)
+
+ // Validate the alert has been created in OpsGenie and its ID is stored in
+ // the plugin_data.
+ pluginData := s.checkPluginData(ctx, req.GetName(), func(data opsgenie.PluginData) bool {
+ return data.AlertID != ""
+ })
+
+ alert, err := s.fakeOpsgenie.CheckNewAlert(ctx)
+ require.NoError(t, err, "no new alerts stored")
+
+ assert.Equal(t, alert.ID, pluginData.AlertID)
+}
+
+// TestApproval tests that when a request is approved, its corresponding alert
+// is updated to reflect the new request state and a note is added to the alert.
+func (s *OpsgenieSuite) TestApproval() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ s.startApp()
+
+ // Test setup: we create an access request and wait for its alert.
+ req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil)
+
+ alert, err := s.fakeOpsgenie.CheckNewAlert(ctx)
+ require.NoError(t, err, "no new alerts stored")
+
+ // Test execution: we approve the request
+ err = s.Ruler().ApproveAccessRequest(ctx, req.GetName(), "okay")
+ require.NoError(t, err)
+
+ // Validating the plugin added a note to the alert describing the review.
+ note, err := s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, "Access request has been approved")
+ assert.Contains(t, note.Note, "Reason: okay")
+
+ // Validating the plugin resolved the alert.
+ alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, "resolved", alertUpdate.Status)
+}
+
+// TestDenial tests that when a request is denied, its corresponding alert
+// is updated to reflect the new request state and a note is added to the alert.
+func (s *OpsgenieSuite) TestDenial() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ s.startApp()
+
+ // Test setup: we create an access request and wait for its alert.
+ req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil)
+
+ alert, err := s.fakeOpsgenie.CheckNewAlert(ctx)
+ require.NoError(t, err, "no new alerts stored")
+
+ // Test execution: we deny the request
+ err = s.Ruler().DenyAccessRequest(ctx, req.GetName(), "not okay")
+ require.NoError(t, err)
+
+ // Validating the plugin added a note to the alert describing the review.
+ note, err := s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, "Access request has been denied")
+ assert.Contains(t, note.Note, "Reason: not okay")
+
+ // Validating the plugin resolved the alert.
+ alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, "resolved", alertUpdate.Status)
+}
+
+// TestReviewNotes tests that alert notes are sent after the access request
+// is reviewed. Each review should create a new note.
+func (s *OpsgenieSuite) TestReviewNotes() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ if !s.TeleportFeatures().AdvancedAccessWorkflows {
+ t.Skip("Doesn't work in OSS version")
+ }
+
+ s.startApp()
+
+ // Test setup: we create an access request and wait for its alert.
+ req := s.CreateAccessRequest(ctx, integration.Requester1UserName, nil)
+
+ // Test execution: we submit two reviews
+ err := s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer1UserName,
+ ProposedState: types.RequestState_APPROVED,
+ Created: time.Now(),
+ Reason: "okay",
+ })
+ require.NoError(t, err)
+
+ err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer2UserName,
+ ProposedState: types.RequestState_APPROVED,
+ Created: time.Now(),
+ Reason: "not okay",
+ })
+ require.NoError(t, err)
+
+ // Validate alert notes were sent with the correct content.
+ pluginData := s.checkPluginData(ctx, req.GetName(), func(data opsgenie.PluginData) bool {
+ return data.AlertID != "" && data.ReviewsCount == 2
+ })
+
+ note, err := s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, pluginData.AlertID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer1UserName+" reviewed the request", "note must contain a review author")
+ assert.Contains(t, note.Note, "Resolution: APPROVED", "note must contain an approval resolution")
+ assert.Contains(t, note.Note, "Reason: okay", "note must contain an approval reason")
+
+ note, err = s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, pluginData.AlertID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer2UserName+" reviewed the request", "note must contain a review author")
+ assert.Contains(t, note.Note, "Resolution: APPROVED", "note must contain a approval resolution")
+ assert.Contains(t, note.Note, "Reason: not okay", "note must contain a denial reason")
+}
+
+// TestApprovalByReview tests that the alert is annotated and resolved after the
+// access request approval threshold is reached.
+func (s *OpsgenieSuite) TestApprovalByReview() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ if !s.TeleportFeatures().AdvancedAccessWorkflows {
+ t.Skip("Doesn't work in OSS version")
+ }
+
+ s.startApp()
+
+ // Test setup: we create an access request and wait for its alert.
+ req := s.CreateAccessRequest(ctx, integration.Requester1UserName, nil)
+
+ alert, err := s.fakeOpsgenie.CheckNewAlert(ctx)
+ require.NoError(t, err, "no new alerts stored")
+
+ // Test execution: we submit two reviews
+ err = s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer1UserName,
+ ProposedState: types.RequestState_APPROVED,
+ Created: time.Now(),
+ Reason: "okay",
+ })
+ require.NoError(t, err)
+
+ err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer2UserName,
+ ProposedState: types.RequestState_APPROVED,
+ Created: time.Now(),
+ Reason: "finally okay",
+ })
+ require.NoError(t, err)
+
+ // Validate alert notes were sent with the correct content.
+ note, err := s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer1UserName+" reviewed the request", "note must contain a review author")
+
+ note, err = s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer2UserName+" reviewed the request", "note must contain a review author")
+
+ // Validate the alert got resolved.
+ data := s.checkPluginData(ctx, req.GetName(), func(data opsgenie.PluginData) bool {
+ return data.ReviewsCount == 2 && data.Resolution.Tag != opsgenie.Unresolved
+ })
+ assert.Equal(t, opsgenie.Resolution{Tag: opsgenie.ResolvedApproved, Reason: "finally okay"}, data.Resolution)
+
+ note, err = s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, "Access request has been approved")
+ assert.Contains(t, note.Note, "Reason: finally okay")
+
+ alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, "resolved", alertUpdate.Status)
+}
+
+// TestDenialByReview tests that the alert is annotated and resolved after the
+// access request denial threshold is reached.
+func (s *OpsgenieSuite) TestDenialByReview() {
+ t := s.T()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ t.Cleanup(cancel)
+
+ if !s.TeleportFeatures().AdvancedAccessWorkflows {
+ t.Skip("Doesn't work in OSS version")
+ }
+
+ s.startApp()
+
+ // Test setup: we create an access request and wait for its alert.
+ req := s.CreateAccessRequest(ctx, integration.Requester1UserName, nil)
+
+ alert, err := s.fakeOpsgenie.CheckNewAlert(ctx)
+ require.NoError(t, err, "no new alerts stored")
+
+ // Test execution: we submit two reviews
+ err = s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer1UserName,
+ ProposedState: types.RequestState_DENIED,
+ Created: time.Now(),
+ Reason: "not okay",
+ })
+ require.NoError(t, err)
+
+ err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
+ Author: integration.Reviewer2UserName,
+ ProposedState: types.RequestState_DENIED,
+ Created: time.Now(),
+ Reason: "finally not okay",
+ })
+ require.NoError(t, err)
+
+ // Validate alert notes were sent with the correct content.
+ note, err := s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer1UserName+" reviewed the request", "note must contain a review author")
+
+ note, err = s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, integration.Reviewer2UserName+" reviewed the request", "note must contain a review author")
+
+ // Validate the alert got resolved.
+ data := s.checkPluginData(ctx, req.GetName(), func(data opsgenie.PluginData) bool {
+ return data.ReviewsCount == 2 && data.Resolution.Tag != opsgenie.Unresolved
+ })
+ assert.Equal(t, opsgenie.Resolution{Tag: opsgenie.ResolvedDenied, Reason: "finally not okay"}, data.Resolution)
+
+ note, err = s.fakeOpsgenie.CheckNewAlertNote(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, alert.ID, note.AlertID)
+ assert.Contains(t, note.Note, "Access request has been denied")
+ assert.Contains(t, note.Note, "Reason: finally not okay")
+
+ alertUpdate, err := s.fakeOpsgenie.CheckAlertUpdate(ctx)
+ require.NoError(t, err)
+ assert.Equal(t, "resolved", alertUpdate.Status)
+}