Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
Add a basic implementation of the applyCampaign mutation (#11934)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrnugget committed Jul 20, 2020
1 parent cfbb955 commit d0de822
Show file tree
Hide file tree
Showing 7 changed files with 592 additions and 33 deletions.
52 changes: 51 additions & 1 deletion enterprise/internal/campaigns/resolvers/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,61 @@ func (r *Resolver) CreateCampaign(ctx context.Context, args *graphqlbackend.Crea
return nil, err
}

// TODO: This mutation is not done.
return &campaignResolver{store: r.store, httpFactory: r.httpFactory, Campaign: campaign}, nil
}

func (r *Resolver) ApplyCampaign(ctx context.Context, args *graphqlbackend.ApplyCampaignArgs) (graphqlbackend.CampaignResolver, error) {
return nil, errors.New("TODO: not implemented")
var err error
tr, ctx := trace.New(ctx, "Resolver.ApplyCampaign", fmt.Sprintf("Namespace %s, CampaignSpec %s", args.Namespace, args.CampaignSpec))
defer func() {
tr.SetError(err)
tr.Finish()
}()

user, err := db.Users.GetByCurrentAuthUser(ctx)
if err != nil {
return nil, errors.Wrapf(err, "%v", backend.ErrNotAuthenticated)
}

// 🚨 SECURITY: Only site admins may create a campaign for now.
if !user.SiteAdmin {
return nil, backend.ErrMustBeSiteAdmin
}

opts := ee.ApplyCampaignOpts{}
switch relay.UnmarshalKind(args.Namespace) {
case "User":
err = relay.UnmarshalSpec(args.Namespace, &opts.NamespaceUserID)
case "Org":
err = relay.UnmarshalSpec(args.Namespace, &opts.NamespaceOrgID)
default:
err = errors.Errorf("Invalid namespace %q", args.Namespace)
}

if err != nil {
return nil, err
}

opts.CampaignSpecRandID, err = unmarshalCampaignSpecID(args.CampaignSpec)
if err != nil {
return nil, err
}

if args.EnsureCampaign != nil {
opts.EnsureCampaignID, err = campaigns.UnmarshalCampaignID(*args.EnsureCampaign)
if err != nil {
return nil, err
}
}

svc := ee.NewService(r.store, r.httpFactory)
campaign, err := svc.ApplyCampaign(ctx, opts)
if err != nil {
return nil, err
}

return &campaignResolver{store: r.store, httpFactory: r.httpFactory, Campaign: campaign}, nil
}

func (r *Resolver) CreateCampaignSpec(ctx context.Context, args *graphqlbackend.CreateCampaignSpecArgs) (graphqlbackend.CampaignSpecResolver, error) {
Expand Down
138 changes: 138 additions & 0 deletions enterprise/internal/campaigns/resolvers/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1024,3 +1024,141 @@ mutation($changesetSpec: String!){
}
}
`

func TestApplyCampaign(t *testing.T) {
if testing.Short() {
t.Skip()
}

ctx := context.Background()
dbtesting.SetupGlobalTestDB(t)

userID := insertTestUser(t, dbconn.Global, "apply-campaign", true)

store := ee.NewStore(dbconn.Global)
reposStore := repos.NewDBStore(dbconn.Global, sql.TxOptions{})

repo := newGitHubTestRepo("github.com/sourcegraph/sourcegraph", 1)
if err := reposStore.UpsertRepos(ctx, repo); err != nil {
t.Fatal(err)
}

repoApiID := graphqlbackend.MarshalRepositoryID(repo.ID)

changesetSpec := &campaigns.ChangesetSpec{
RawSpec: ct.NewRawChangesetSpec(repoApiID),
Spec: campaigns.ChangesetSpecFields{
RepoID: repoApiID,
},
RepoID: repo.ID,
UserID: userID,
}
if err := store.CreateChangesetSpec(ctx, changesetSpec); err != nil {
t.Fatal(err)
}

campaignSpec := &campaigns.CampaignSpec{
RawSpec: ct.TestRawCampaignSpec,
Spec: campaigns.CampaignSpecFields{
Name: "my-campaign",
Description: "My description",
ChangesetTemplate: campaigns.ChangesetTemplate{
Title: "Hello there",
Body: "This is the body",
Branch: "my-branch",
Commit: campaigns.CommitTemplate{
Message: "Add hello world",
},
Published: false,
},
},
UserID: userID,
NamespaceUserID: userID,
}
if err := store.CreateCampaignSpec(ctx, campaignSpec); err != nil {
t.Fatal(err)
}

r := &Resolver{store: store}
s, err := graphqlbackend.NewSchema(r, nil, nil)
if err != nil {
t.Fatal(err)
}

userApiID := string(graphqlbackend.MarshalUserID(userID))
input := map[string]interface{}{
// TODO: Do we need the namespace in this mutation?
"namespace": userApiID,
"campaignSpec": string(marshalCampaignSpecRandID(campaignSpec.RandID)),
}

var response struct{ ApplyCampaign apitest.Campaign }
actorCtx := actor.WithActor(ctx, actor.FromUser(userID))
apitest.MustExec(actorCtx, t, s, input, &response, mutationApplyCampaign)

have := response.ApplyCampaign
want := apitest.Campaign{
ID: have.ID,
Name: campaignSpec.Spec.Name,
Description: campaignSpec.Spec.Description,
Branch: campaignSpec.Spec.ChangesetTemplate.Branch,
Namespace: apitest.UserOrg{
ID: userApiID,
DatabaseID: userID,
SiteAdmin: true,
},
Author: apitest.User{
ID: userApiID,
DatabaseID: userID,
SiteAdmin: true,
},
// TODO: Test for CampaignSpec/ChangesetSpecs once they're defined in
// the schema.
}

if diff := cmp.Diff(want, have); diff != "" {
t.Fatalf("unexpected response (-want +got):\n%s", diff)
}

// Now we execute it again and make sure we get the same campaign back
apitest.MustExec(actorCtx, t, s, input, &response, mutationApplyCampaign)
have2 := response.ApplyCampaign
if diff := cmp.Diff(want, have2); diff != "" {
t.Fatalf("unexpected response (-want +got):\n%s", diff)
}

// Execute it again with ensureCampaign set to correct campaign's ID
input["ensureCampaign"] = have2.ID
apitest.MustExec(actorCtx, t, s, input, &response, mutationApplyCampaign)
have3 := response.ApplyCampaign
if diff := cmp.Diff(want, have3); diff != "" {
t.Fatalf("unexpected response (-want +got):\n%s", diff)
}

// Execute it again but ensureCampaign set to wrong campaign ID
campaignID, err := campaigns.UnmarshalCampaignID(graphql.ID(have3.ID))
if err != nil {
t.Fatal(err)
}
input["ensureCampaign"] = campaigns.MarshalCampaignID(campaignID + 999)
errs := apitest.Exec(actorCtx, t, s, input, &response, mutationApplyCampaign)
if len(errs) == 0 {
t.Fatalf("expected errors, got none")
}
}

const mutationApplyCampaign = `
fragment u on User { id, databaseID, siteAdmin }
fragment o on Org { id, name }
mutation($namespace: ID!, $campaignSpec: ID!, $ensureCampaign: ID){
applyCampaign(namespace: $namespace, campaignSpec: $campaignSpec, ensureCampaign: $ensureCampaign) {
id, name, description, branch
author { ...u }
namespace {
... on User { ...u }
... on Org { ...o }
}
}
}
`
93 changes: 93 additions & 0 deletions enterprise/internal/campaigns/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,99 @@ func (e *changesetSpecNotFoundErr) Error() string {

func (e *changesetSpecNotFoundErr) NotFound() bool { return true }

type ApplyCampaignOpts struct {
CampaignSpecRandID string

NamespaceUserID int32
NamespaceOrgID int32

EnsureCampaignID int64
}

func (o ApplyCampaignOpts) String() string {
return fmt.Sprintf(
"CampaignSpec %s, NamespaceOrgID %d, NamespaceUserID %d, EnsureCampaignID %d",
o.CampaignSpecRandID,
o.NamespaceOrgID,
o.NamespaceUserID,
o.EnsureCampaignID,
)
}

// ApplyCampaign creates the CampaignSpec.
func (s *Service) ApplyCampaign(ctx context.Context, opts ApplyCampaignOpts) (campaign *campaigns.Campaign, err error) {
tr, ctx := trace.New(ctx, "Service.ApplyCampaign", opts.String())
defer func() {
tr.SetError(err)
tr.Finish()
}()

tx, err := s.store.Transact(ctx)
if err != nil {
return nil, err
}
defer tx.Done(&err)

campaignSpec, err := tx.GetCampaignSpec(ctx, GetCampaignSpecOpts{
RandID: opts.CampaignSpecRandID,
})
if err != nil {
return nil, err
}

getOpts := GetCampaignOpts{CampaignSpecName: campaignSpec.Spec.Name}
if opts.NamespaceUserID != 0 {
getOpts.NamespaceUserID = opts.NamespaceUserID
} else if opts.NamespaceOrgID != 0 {
getOpts.NamespaceOrgID = opts.NamespaceOrgID
} else {
return nil, errors.New("no namespace specified")
}

campaign, err = tx.GetCampaign(ctx, getOpts)
if err != nil {
if err != ErrNoResults {
return nil, err
}
err = nil
}
if campaign == nil {
campaign = &campaigns.Campaign{}
}

if opts.EnsureCampaignID != 0 && campaign.ID != opts.EnsureCampaignID {
return nil, ErrEnsureCampaignFailed
}

if campaign.CampaignSpecID == campaignSpec.ID {
return campaign, nil
}

campaign.CampaignSpecID = campaignSpec.ID

// Do we still need AuthorID on Campaign?
campaign.AuthorID = campaignSpec.UserID

// TODO Do we need these fields on Campaign or is it enough that
// we have them on CampaignSpec?
campaign.NamespaceOrgID = opts.NamespaceOrgID
campaign.NamespaceUserID = opts.NamespaceUserID
campaign.Branch = campaignSpec.Spec.ChangesetTemplate.Branch
campaign.Name = campaignSpec.Spec.Name
campaign.Description = campaignSpec.Spec.Description

if campaign.ID == 0 {
return campaign, tx.CreateCampaign(ctx, campaign)
}

return campaign, tx.UpdateCampaign(ctx, campaign)
}

// ErrEnsureCampaignFailed is returned by ApplyCampaign when a ensureCampaignID
// is provided but a campaign with the name specified the campaignSpec exists
// in the given namespace but has a different ID.
var ErrEnsureCampaignFailed = errors.New("a campaign in the given namespace and with the given name exists but does not match the given ID")

// ErrNoPatches is returned by CreateCampaign or UpdateCampaign if a
// PatchSetID was specified but the PatchSet does not have any
// (finished) Patches.
Expand Down
Loading

0 comments on commit d0de822

Please sign in to comment.