sourcegraph/internal/database/code_monitors.go
Erik Seliger 1d3b9f969c
Move record encrypter to worker package (#61900)
This was exposed from internal/database but there is no other routine that needs these tools so moving it to a more contained package.
A small improvement on making the database package a _little bit_ less crazy big.

Test plan:

Tests are still passing after moving the code.
2024-04-16 10:16:58 +02:00

289 lines
11 KiB
Go

package database
import (
"context"
"testing"
"time"
"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
"github.com/keegancsmith/sqlf"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/log"
"github.com/sourcegraph/log/logtest"
"github.com/sourcegraph/sourcegraph/internal/actor"
"github.com/sourcegraph/sourcegraph/internal/api"
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
"github.com/sourcegraph/sourcegraph/internal/search/result"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/internal/types"
)
// CodeMonitorStore is an interface for interacting with the code monitor tables in the database
type CodeMonitorStore interface {
basestore.ShareableStore
Transact(context.Context) (CodeMonitorStore, error)
Done(error) error
Now() time.Time
Clock() func() time.Time
Exec(ctx context.Context, query *sqlf.Query) error
CreateMonitor(ctx context.Context, args MonitorArgs) (*Monitor, error)
UpdateMonitor(ctx context.Context, id int64, args MonitorArgs) (*Monitor, error)
UpdateMonitorEnabled(ctx context.Context, id int64, enabled bool) (*Monitor, error)
DeleteMonitor(ctx context.Context, id int64) error
GetMonitor(ctx context.Context, monitorID int64) (*Monitor, error)
ListMonitors(context.Context, ListMonitorsOpts) ([]*Monitor, error)
CountMonitors(ctx context.Context, opts ListMonitorsOpts) (int32, error)
CreateQueryTrigger(ctx context.Context, monitorID int64, query string) (*QueryTrigger, error)
UpdateQueryTrigger(ctx context.Context, id int64, query string) error
GetQueryTriggerForMonitor(ctx context.Context, monitorID int64) (*QueryTrigger, error)
ResetQueryTriggerTimestamps(ctx context.Context, queryID int64) error
SetQueryTriggerNextRun(ctx context.Context, triggerQueryID int64, next time.Time, latestResults time.Time) error
GetQueryTriggerForJob(ctx context.Context, triggerJob int32) (*QueryTrigger, error)
EnqueueQueryTriggerJobs(context.Context) ([]*TriggerJob, error)
ListQueryTriggerJobs(context.Context, ListTriggerJobsOpts) ([]*TriggerJob, error)
CountQueryTriggerJobs(ctx context.Context, queryID int64) (int32, error)
UpdateTriggerJobWithResults(ctx context.Context, triggerJobID int32, queryString string, results []*result.CommitMatch) error
DeleteOldTriggerJobs(ctx context.Context, retentionInDays int) error
UpdateTriggerJobWithLogs(ctx context.Context, triggerJobID int32, entry TriggerJobLogs) error
UpdateEmailAction(_ context.Context, id int64, _ *EmailActionArgs) (*EmailAction, error)
CreateEmailAction(ctx context.Context, monitorID int64, _ *EmailActionArgs) (*EmailAction, error)
DeleteEmailActions(ctx context.Context, actionIDs []int64, monitorID int64) error
GetEmailAction(ctx context.Context, emailID int64) (*EmailAction, error)
ListEmailActions(context.Context, ListActionsOpts) ([]*EmailAction, error)
UpdateWebhookAction(_ context.Context, id int64, enabled, includeResults bool, url string) (*WebhookAction, error)
CreateWebhookAction(ctx context.Context, monitorID int64, enabled, includeResults bool, url string) (*WebhookAction, error)
DeleteWebhookActions(ctx context.Context, monitorID int64, ids ...int64) error
CountWebhookActions(ctx context.Context, monitorID int64) (int, error)
GetWebhookAction(ctx context.Context, id int64) (*WebhookAction, error)
ListWebhookActions(context.Context, ListActionsOpts) ([]*WebhookAction, error)
UpdateSlackWebhookAction(_ context.Context, id int64, enabled, includeResults bool, url string) (*SlackWebhookAction, error)
CreateSlackWebhookAction(ctx context.Context, monitorID int64, enabled, includeResults bool, url string) (*SlackWebhookAction, error)
DeleteSlackWebhookActions(ctx context.Context, monitorID int64, ids ...int64) error
CountSlackWebhookActions(ctx context.Context, monitorID int64) (int, error)
GetSlackWebhookAction(ctx context.Context, id int64) (*SlackWebhookAction, error)
ListSlackWebhookActions(context.Context, ListActionsOpts) ([]*SlackWebhookAction, error)
CreateRecipient(ctx context.Context, emailID int64, userID, orgID *int32) (*Recipient, error)
DeleteRecipients(ctx context.Context, emailID int64) error
ListRecipients(context.Context, ListRecipientsOpts) ([]*Recipient, error)
CountRecipients(ctx context.Context, emailID int64) (int32, error)
ListActionJobs(context.Context, ListActionJobsOpts) ([]*ActionJob, error)
CountActionJobs(context.Context, ListActionJobsOpts) (int, error)
GetActionJobMetadata(ctx context.Context, jobID int32) (*ActionJobMetadata, error)
GetActionJob(ctx context.Context, jobID int32) (*ActionJob, error)
EnqueueActionJobsForMonitor(ctx context.Context, monitorID int64, triggerJob int32) ([]*ActionJob, error)
// HasAnyLastSearched returns whether there have ever been any repo-aware code monitor
// searches executed for this code monitor. This should only be needed during the transition
// version so that we don't detect every repo as a new repo and search their entire history
// when a code monitor transitions from non-repo-aware to repo-aware.
HasAnyLastSearched(ctx context.Context, monitorID int64) (bool, error)
UpsertLastSearched(ctx context.Context, monitorID int64, repoID api.RepoID, lastSearched []string) error
GetLastSearched(ctx context.Context, monitorID int64, repoID api.RepoID) ([]string, error)
}
// codeMonitorStore exposes methods to read and write codemonitors domain models
// from persistent storage.
type codeMonitorStore struct {
*basestore.Store
userStore UserStore
now func() time.Time
}
var _ CodeMonitorStore = (*codeMonitorStore)(nil)
// CodeMonitorsWith returns a new Store backed by the given database.
func CodeMonitorsWith(other basestore.ShareableStore) *codeMonitorStore {
return CodeMonitorsWithClock(other, timeutil.Now)
}
// CodeMonitorsWithClock returns a new Store backed by the given database and
// clock for timestamps.
func CodeMonitorsWithClock(other basestore.ShareableStore, clock func() time.Time) *codeMonitorStore {
handle := basestore.NewWithHandle(other.Handle())
return &codeMonitorStore{Store: handle, userStore: UsersWith(log.Scoped("codemonitors"), handle), now: clock}
}
// Clock returns the clock of the underlying store.
func (s *codeMonitorStore) Clock() func() time.Time {
return s.now
}
func (s *codeMonitorStore) Now() time.Time {
return s.now()
}
// Transact creates a new transaction.
// It's required to implement this method and wrap the Transact method of the
// underlying basestore.Store.
func (s *codeMonitorStore) Transact(ctx context.Context) (CodeMonitorStore, error) {
txBase, err := s.Store.Transact(ctx)
if err != nil {
return nil, err
}
return &codeMonitorStore{Store: txBase, now: s.now}, nil
}
type JobTable int
const (
TriggerJobs JobTable = iota
ActionJobs
)
type JobState int
const (
Queued JobState = iota
Processing
Completed
Errored
Failed
)
const setStatusFmtStr = `
UPDATE %s
SET state = %s,
started_at = %s,
finished_at = %s
WHERE id = %s;
`
func (s *TestStore) SetJobStatus(ctx context.Context, table JobTable, state JobState, id int) error {
st := []string{"queued", "processing", "completed", "errored", "failed"}[state]
t := []string{"cm_trigger_jobs", "cm_action_jobs"}[table]
return s.Exec(ctx, sqlf.Sprintf(setStatusFmtStr, sqlf.Sprintf(t), st, s.Now(), s.Now(), id))
}
type TestStore struct {
CodeMonitorStore
}
func (s *TestStore) InsertTestMonitor(ctx context.Context, t *testing.T) (*Monitor, error) {
t.Helper()
actions := []*EmailActionArgs{
{
Enabled: true,
IncludeResults: false,
Priority: "NORMAL",
Header: "test header 1",
},
{
Enabled: true,
IncludeResults: false,
Priority: "CRITICAL",
Header: "test header 2",
},
}
// Create monitor.
uid := actor.FromContext(ctx).UID
m, err := s.CreateMonitor(ctx, MonitorArgs{
Description: testDescription,
Enabled: true,
NamespaceUserID: &uid,
})
if err != nil {
return nil, err
}
// Create trigger.
_, err = s.CreateQueryTrigger(ctx, m.ID, testQuery)
if err != nil {
return nil, err
}
for _, a := range actions {
e, err := s.CreateEmailAction(ctx, m.ID, &EmailActionArgs{
Enabled: a.Enabled,
IncludeResults: a.IncludeResults,
Priority: a.Priority,
Header: a.Header,
})
if err != nil {
return nil, err
}
_, err = s.CreateRecipient(ctx, e.ID, &uid, nil)
if err != nil {
return nil, err
}
// TODO(camdencheek): add other action types (webhooks) here
}
return m, nil
}
func namespaceScopeQuery(user *types.User) *sqlf.Query {
namespaceScope := sqlf.Sprintf("cm_monitors.namespace_user_id = %s", user.ID)
if user.SiteAdmin {
namespaceScope = sqlf.Sprintf("TRUE")
}
return namespaceScope
}
func NewTestStore(t *testing.T, db DB) (context.Context, *TestStore) {
ctx := actor.WithInternalActor(context.Background())
now := time.Now().Truncate(time.Microsecond)
return ctx, &TestStore{CodeMonitorsWithClock(db, func() time.Time { return now })}
}
func NewTestUser(ctx context.Context, t *testing.T, db dbutil.DB) (name string, id int32, namespace graphql.ID, userContext context.Context) {
t.Helper()
name = "cm-user1"
id = insertTestUser(ctx, t, db, name, true)
namespace = relay.MarshalID("User", id)
ctx = actor.WithActor(ctx, actor.FromUser(id))
return name, id, namespace, ctx
}
const (
//nolint:unused // used in tests
testQuery = "repo:github\\.com/sourcegraph/sourcegraph func type:diff patternType:literal"
//nolint:unused // used in tests
testDescription = "test description"
)
//nolint:unused // used in tests
func newTestStore(t *testing.T) (context.Context, DB, *codeMonitorStore) {
logger := logtest.Scoped(t)
ctx := actor.WithInternalActor(context.Background())
db := NewDB(logger, dbtest.NewDB(t))
now := time.Now().Truncate(time.Microsecond)
return ctx, db, CodeMonitorsWithClock(db, func() time.Time { return now })
}
//nolint:unused // used in tests
func newTestUser(ctx context.Context, t *testing.T, db dbutil.DB) (name string, id int32, userContext context.Context) {
t.Helper()
name = "cm-user1"
id = insertTestUser(ctx, t, db, name, true)
_ = relay.MarshalID("User", id)
ctx = actor.WithActor(ctx, actor.FromUser(id))
return name, id, ctx
}
//nolint:unused // used in tests
func insertTestUser(ctx context.Context, t *testing.T, db dbutil.DB, name string, isAdmin bool) (userID int32) {
t.Helper()
q := sqlf.Sprintf("INSERT INTO users (username, site_admin) VALUES (%s, %t) RETURNING id", name, isAdmin)
err := db.QueryRowContext(ctx, q.Query(sqlf.PostgresBindVar), q.Args()...).Scan(&userID)
require.NoError(t, err)
return userID
}