From 58bcb29d1a700386e40a986bc175e166db335730 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Mon, 14 Oct 2024 10:16:08 -0700 Subject: [PATCH] feat(server): added API to manipulate notification profiles in the UI (#4171) --- internal/server/api_notification_profile.go | 62 +++++++++++++ .../server/api_notification_profile_test.go | 87 +++++++++++++++++++ internal/server/server.go | 5 ++ 3 files changed, 154 insertions(+) create mode 100644 internal/server/api_notification_profile.go create mode 100644 internal/server/api_notification_profile_test.go diff --git a/internal/server/api_notification_profile.go b/internal/server/api_notification_profile.go new file mode 100644 index 000000000..845dcf0dd --- /dev/null +++ b/internal/server/api_notification_profile.go @@ -0,0 +1,62 @@ +package server + +import ( + "context" + "encoding/json" + + "github.com/kopia/kopia/internal/serverapi" + "github.com/kopia/kopia/notification/notifyprofile" + "github.com/kopia/kopia/repo" +) + +func handleNotificationProfileCreate(ctx context.Context, rc requestContext) (any, *apiError) { + var cfg notifyprofile.Config + + if err := json.Unmarshal(rc.body, &cfg); err != nil { + return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request body: "+string(rc.body)) + } + + if err := repo.WriteSession(ctx, rc.rep, repo.WriteSessionOptions{ + Purpose: "NotificationProfileCreate", + }, func(ctx context.Context, w repo.RepositoryWriter) error { + return notifyprofile.SaveProfile(ctx, w, cfg) + }); err != nil { + return nil, internalServerError(err) + } + + return &serverapi.Empty{}, nil +} + +func handleNotificationProfileGet(ctx context.Context, rc requestContext) (any, *apiError) { + cfg, ok, err := notifyprofile.GetProfile(ctx, rc.rep, rc.muxVar("profileName")) + if err != nil { + return nil, internalServerError(err) + } + + if !ok { + return nil, notFoundError("profile not found") + } + + return cfg, nil +} + +func handleNotificationProfileDelete(ctx context.Context, rc requestContext) (any, *apiError) { + if err := repo.WriteSession(ctx, rc.rep, repo.WriteSessionOptions{ + Purpose: "NotificationProfileDelete", + }, func(ctx context.Context, w repo.RepositoryWriter) error { + return notifyprofile.DeleteProfile(ctx, w, rc.muxVar("profileName")) + }); err != nil { + return nil, internalServerError(err) + } + + return &serverapi.Empty{}, nil +} + +func handleNotificationProfileList(ctx context.Context, rc requestContext) (any, *apiError) { + profiles, err := notifyprofile.ListProfiles(ctx, rc.rep) + if err != nil { + return nil, internalServerError(err) + } + + return profiles, nil +} diff --git a/internal/server/api_notification_profile_test.go b/internal/server/api_notification_profile_test.go new file mode 100644 index 000000000..e35c025de --- /dev/null +++ b/internal/server/api_notification_profile_test.go @@ -0,0 +1,87 @@ +package server_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kopia/kopia/internal/apiclient" + "github.com/kopia/kopia/internal/repotesting" + "github.com/kopia/kopia/internal/serverapi" + "github.com/kopia/kopia/internal/servertesting" + "github.com/kopia/kopia/notification/notifyprofile" + "github.com/kopia/kopia/notification/sender" + "github.com/kopia/kopia/notification/sender/testsender" +) + +func TestNotificationProfile(t *testing.T) { + ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant) + srvInfo := servertesting.StartServer(t, env, false) + + cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{ + BaseURL: srvInfo.BaseURL, + TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint, + Username: servertesting.TestUIUsername, + Password: servertesting.TestUIPassword, + }) + + require.NoError(t, err) + + require.NoError(t, cli.FetchCSRFTokenForTesting(ctx)) + + var profiles []notifyprofile.Config + + require.NoError(t, cli.Get(ctx, "notificationProfiles", nil, &profiles)) + require.Empty(t, profiles) + + // define new profile + require.NoError(t, cli.Post(ctx, "notificationProfiles", ¬ifyprofile.Config{ + ProfileName: "profile1", + MethodConfig: sender.MethodConfig{ + Type: "testsender", + Config: testsender.Options{ + Format: "txt", + }, + }, + MinSeverity: 3, + }, &serverapi.Empty{})) + + // define invalid profile + require.ErrorContains(t, cli.Post(ctx, "notificationProfiles", ¬ifyprofile.Config{ + ProfileName: "profile2", + MethodConfig: sender.MethodConfig{ + Type: "no-such-type", + Config: testsender.Options{ + Format: "txt", + }, + }, + MinSeverity: 3, + }, &serverapi.Empty{}), "malformed request body") + + var cfg notifyprofile.Config + + // get profile and verify + require.NoError(t, cli.Get(ctx, "notificationProfiles/profile1", nil, &cfg)) + require.Equal(t, "profile1", cfg.ProfileName) + require.Equal(t, sender.Method("testsender"), cfg.MethodConfig.Type) + + opt, ok := cfg.MethodConfig.Config.(map[string]any) + require.True(t, ok) + require.Equal(t, "txt", opt["format"]) + + // get non-existent profile + require.ErrorContains(t, cli.Get(ctx, "notificationProfiles/profile2", nil, &cfg), "profile not found") + + // list profiles + require.NoError(t, cli.Get(ctx, "notificationProfiles", nil, &profiles)) + require.Len(t, profiles, 1) + require.Equal(t, "profile1", profiles[0].ProfileName) + + // delete the profile, ensure idempotent + require.NoError(t, cli.Delete(ctx, "notificationProfiles/profile1", nil, nil, &serverapi.Empty{})) + require.NoError(t, cli.Delete(ctx, "notificationProfiles/profile1", nil, nil, &serverapi.Empty{})) + + // verify it's gone + require.NoError(t, cli.Get(ctx, "notificationProfiles", nil, &profiles)) + require.Empty(t, profiles) +} diff --git a/internal/server/server.go b/internal/server/server.go index 7d2e6d5ae..a0e4e5169 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -158,6 +158,11 @@ func (s *Server) SetupHTMLUIAPIHandlers(m *mux.Router) { m.HandleFunc("/api/v1/tasks/{taskID}", s.handleUIPossiblyNotConnected(handleTaskInfo)).Methods(http.MethodGet) m.HandleFunc("/api/v1/tasks/{taskID}/logs", s.handleUIPossiblyNotConnected(handleTaskLogs)).Methods(http.MethodGet) m.HandleFunc("/api/v1/tasks/{taskID}/cancel", s.handleUIPossiblyNotConnected(handleTaskCancel)).Methods(http.MethodPost) + + m.HandleFunc("/api/v1/notificationProfiles", s.handleUI(handleNotificationProfileCreate)).Methods(http.MethodPost) + m.HandleFunc("/api/v1/notificationProfiles/{profileName}", s.handleUI(handleNotificationProfileDelete)).Methods(http.MethodDelete) + m.HandleFunc("/api/v1/notificationProfiles/{profileName}", s.handleUI(handleNotificationProfileGet)).Methods(http.MethodGet) + m.HandleFunc("/api/v1/notificationProfiles", s.handleUI(handleNotificationProfileList)).Methods(http.MethodGet) } // SetupControlAPIHandlers registers control API handlers.