diff --git a/enterprise/internal/campaigns/resolvers/resolver.go b/enterprise/internal/campaigns/resolvers/resolver.go index e7c96df4c139e..6e338acdb262c 100644 --- a/enterprise/internal/campaigns/resolvers/resolver.go +++ b/enterprise/internal/campaigns/resolvers/resolver.go @@ -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) { diff --git a/enterprise/internal/campaigns/resolvers/resolver_test.go b/enterprise/internal/campaigns/resolvers/resolver_test.go index 523d87b00b454..35b79cbd34cad 100644 --- a/enterprise/internal/campaigns/resolvers/resolver_test.go +++ b/enterprise/internal/campaigns/resolvers/resolver_test.go @@ -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 } + } + } +} +` diff --git a/enterprise/internal/campaigns/service.go b/enterprise/internal/campaigns/service.go index b4d70e1f78695..08bb010384bf6 100644 --- a/enterprise/internal/campaigns/service.go +++ b/enterprise/internal/campaigns/service.go @@ -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. diff --git a/enterprise/internal/campaigns/service_test.go b/enterprise/internal/campaigns/service_test.go index 34637e8a7a60e..468de95403963 100644 --- a/enterprise/internal/campaigns/service_test.go +++ b/enterprise/internal/campaigns/service_test.go @@ -703,6 +703,160 @@ func TestService(t *testing.T) { } }) }) + + t.Run("ApplyCampaign", func(t *testing.T) { + svc := NewServiceWithClock(store, cf, clock) + + createCampaignSpec := func(t *testing.T, name string, userID int32) *campaigns.CampaignSpec { + t.Helper() + + s := &campaigns.CampaignSpec{ + UserID: userID, + NamespaceUserID: userID, + Spec: campaigns.CampaignSpecFields{ + Name: name, + Description: "the description", + ChangesetTemplate: campaigns.ChangesetTemplate{ + Branch: "branch-name", + }, + }, + } + + if err := store.CreateCampaignSpec(ctx, s); err != nil { + t.Fatal(err) + } + + return s + } + + t.Run("new campaign", func(t *testing.T) { + campaignSpec := createCampaignSpec(t, "campaign-name", user.ID) + campaign, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec.RandID, + }) + if err != nil { + t.Fatal(err) + } + + if campaign.ID == 0 { + t.Fatalf("campaign ID is 0") + } + + want := &campaigns.Campaign{ + Name: campaignSpec.Spec.Name, + Description: campaignSpec.Spec.Description, + Branch: campaignSpec.Spec.ChangesetTemplate.Branch, + AuthorID: campaignSpec.UserID, + ChangesetIDs: []int64{}, + NamespaceUserID: campaignSpec.NamespaceUserID, + CampaignSpecID: campaignSpec.ID, + + // Ignore these fields + ID: campaign.ID, + UpdatedAt: campaign.UpdatedAt, + CreatedAt: campaign.CreatedAt, + } + + if diff := cmp.Diff(want, campaign); diff != "" { + t.Fatalf("wrong spec fields (-want +got):\n%s", diff) + } + }) + + t.Run("existing campaign", func(t *testing.T) { + campaignSpec := createCampaignSpec(t, "campaign-name", user.ID) + campaign, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec.RandID, + }) + if err != nil { + t.Fatal(err) + } + + if campaign.ID == 0 { + t.Fatalf("campaign ID is 0") + } + + t.Run("apply same campaignSpec", func(t *testing.T) { + campaign2, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec.RandID, + }) + if err != nil { + t.Fatal(err) + } + + if have, want := campaign2.ID, campaign.ID; have != want { + t.Fatalf("campaign ID is wrong. want=%d, have=%d", want, have) + } + }) + + t.Run("apply campaign spec with same name", func(t *testing.T) { + campaignSpec2 := createCampaignSpec(t, "campaign-name", user.ID) + campaign2, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec2.RandID, + }) + if err != nil { + t.Fatal(err) + } + + if have, want := campaign2.ID, campaign.ID; have != want { + t.Fatalf("campaign ID is wrong. want=%d, have=%d", want, have) + } + }) + + t.Run("apply campaign spec with same name but different namespace", func(t *testing.T) { + user2 := createTestUser(ctx, t) + campaignSpec2 := createCampaignSpec(t, "campaign-name", user2.ID) + + campaign2, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user2.ID, + CampaignSpecRandID: campaignSpec2.RandID, + }) + if err != nil { + t.Fatal(err) + } + + if campaign2.ID == 0 { + t.Fatalf("campaign2 ID is 0") + } + + if campaign2.ID == campaign.ID { + t.Fatalf("campaign IDs are the same, but want different") + } + }) + + t.Run("campaign spec with same name and same ensureCampaignID", func(t *testing.T) { + campaignSpec2 := createCampaignSpec(t, "campaign-name", user.ID) + + campaign2, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec2.RandID, + EnsureCampaignID: campaign.ID, + }) + if err != nil { + t.Fatal(err) + } + if have, want := campaign2.ID, campaign.ID; have != want { + t.Fatalf("campaign has wrong ID. want=%d, have=%d", want, have) + } + }) + + t.Run("campaign spec with same name but different ensureCampaignID", func(t *testing.T) { + campaignSpec2 := createCampaignSpec(t, "campaign-name", user.ID) + + _, err := svc.ApplyCampaign(ctx, ApplyCampaignOpts{ + NamespaceUserID: user.ID, + CampaignSpecRandID: campaignSpec2.RandID, + EnsureCampaignID: campaign.ID + 999, + }) + if err != ErrEnsureCampaignFailed { + t.Fatalf("wrong error: %s", err) + } + }) + }) + }) } var testUser = db.NewUser{ diff --git a/enterprise/internal/campaigns/store.go b/enterprise/internal/campaigns/store.go index 278716bbf1388..49a36e15e29a7 100644 --- a/enterprise/internal/campaigns/store.go +++ b/enterprise/internal/campaigns/store.go @@ -1206,9 +1206,10 @@ INSERT INTO campaigns ( updated_at, changeset_ids, patch_set_id, - closed_at + closed_at, + campaign_spec_id ) -VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id, name, @@ -1221,7 +1222,8 @@ RETURNING updated_at, changeset_ids, patch_set_id, - closed_at + closed_at, + campaign_spec_id ` func (s *Store) createCampaignQuery(c *campaigns.Campaign) (*sqlf.Query, error) { @@ -1251,6 +1253,7 @@ func (s *Store) createCampaignQuery(c *campaigns.Campaign) (*sqlf.Query, error) changesetIDs, nullInt64Column(c.PatchSetID), nullTimeColumn(c.ClosedAt), + nullInt64Column(c.CampaignSpecID), ), nil } @@ -1308,8 +1311,9 @@ SET ( updated_at, changeset_ids, patch_set_id, - closed_at -) = (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + closed_at, + campaign_spec_id +) = (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) WHERE id = %s RETURNING id, @@ -1323,7 +1327,8 @@ RETURNING updated_at, changeset_ids, patch_set_id, - closed_at + closed_at, + campaign_spec_id ` func (s *Store) updateCampaignQuery(c *campaigns.Campaign) (*sqlf.Query, error) { @@ -1346,6 +1351,7 @@ func (s *Store) updateCampaignQuery(c *campaigns.Campaign) (*sqlf.Query, error) changesetIDs, nullInt64Column(c.PatchSetID), nullTimeColumn(c.ClosedAt), + nullInt64Column(c.CampaignSpecID), c.ID, ), nil } @@ -1428,6 +1434,12 @@ func countCampaignsQuery(opts *CountCampaignsOpts) *sqlf.Query { type GetCampaignOpts struct { ID int64 PatchSetID int64 + + NamespaceUserID int32 + NamespaceOrgID int32 + + CampaignSpecID int64 + CampaignSpecName string } // GetCampaign gets a campaign matching the given options. @@ -1449,22 +1461,26 @@ func (s *Store) GetCampaign(ctx context.Context, opts GetCampaignOpts) (*campaig return &c, nil } -var getCampaignsQueryFmtstr = ` +var getCampaignsQueryFmtstrPre = ` -- source: enterprise/internal/campaigns/store.go:GetCampaign SELECT - id, - name, - description, - branch, - author_id, - namespace_user_id, - namespace_org_id, - created_at, - updated_at, - changeset_ids, - patch_set_id, - closed_at + campaigns.id, + campaigns.name, + campaigns.description, + campaigns.branch, + campaigns.author_id, + campaigns.namespace_user_id, + campaigns.namespace_org_id, + campaigns.created_at, + campaigns.updated_at, + campaigns.changeset_ids, + campaigns.patch_set_id, + campaigns.closed_at, + campaigns.campaign_spec_id FROM campaigns +` + +var getCampaignsQueryFmtstrPost = ` WHERE %s LIMIT 1 ` @@ -1472,18 +1488,40 @@ LIMIT 1 func getCampaignQuery(opts *GetCampaignOpts) *sqlf.Query { var preds []*sqlf.Query if opts.ID != 0 { - preds = append(preds, sqlf.Sprintf("id = %s", opts.ID)) + preds = append(preds, sqlf.Sprintf("campaigns.id = %s", opts.ID)) } if opts.PatchSetID != 0 { - preds = append(preds, sqlf.Sprintf("patch_set_id = %s", opts.PatchSetID)) + preds = append(preds, sqlf.Sprintf("campaigns.patch_set_id = %s", opts.PatchSetID)) + } + + if opts.CampaignSpecID != 0 { + preds = append(preds, sqlf.Sprintf("campaigns.campaign_spec_id = %s", opts.CampaignSpecID)) + } + + if opts.NamespaceUserID != 0 { + preds = append(preds, sqlf.Sprintf("campaigns.namespace_user_id = %s", opts.NamespaceUserID)) + } + + if opts.NamespaceOrgID != 0 { + preds = append(preds, sqlf.Sprintf("campaigns.namespace_org_id = %s", opts.NamespaceOrgID)) } if len(preds) == 0 { preds = append(preds, sqlf.Sprintf("TRUE")) } - return sqlf.Sprintf(getCampaignsQueryFmtstr, sqlf.Join(preds, "\n AND ")) + var joinClause string + if opts.CampaignSpecName != "" { + joinClause = "JOIN campaign_specs ON campaigns.campaign_spec_id = campaign_specs.id" + cond := fmt.Sprintf(`campaign_specs.spec @> '{"name": %q}'`, opts.CampaignSpecName) + preds = append(preds, sqlf.Sprintf(cond)) + + } + return sqlf.Sprintf( + getCampaignsQueryFmtstrPre+joinClause+getCampaignsQueryFmtstrPost, + sqlf.Join(preds, "\n AND "), + ) } // ListCampaignsOpts captures the query options needed for @@ -1534,7 +1572,8 @@ SELECT updated_at, changeset_ids, patch_set_id, - closed_at + closed_at, + campaign_spec_id FROM campaigns WHERE %s ORDER BY id ASC @@ -2963,6 +3002,7 @@ func scanCampaign(c *campaigns.Campaign, s scanner) error { &dbutil.JSONInt64Set{Set: &c.ChangesetIDs}, &dbutil.NullInt64{N: &c.PatchSetID}, &dbutil.NullTime{Time: &c.ClosedAt}, + &dbutil.NullInt64{N: &c.CampaignSpecID}, ) } diff --git a/enterprise/internal/campaigns/store_test.go b/enterprise/internal/campaigns/store_test.go index d462959136d90..a82275847705e 100644 --- a/enterprise/internal/campaigns/store_test.go +++ b/enterprise/internal/campaigns/store_test.go @@ -62,23 +62,25 @@ func testStoreCampaigns(t *testing.T, ctx context.Context, s *Store, _ repos.Sto t.Run("Create", func(t *testing.T) { for i := 0; i < cap(campaigns); i++ { c := &cmpgn.Campaign{ - Name: fmt.Sprintf("Upgrade ES-Lint %d", i), - Description: "All the Javascripts are belong to us", - Branch: "upgrade-es-lint", - AuthorID: int32(i) + 50, - ChangesetIDs: []int64{int64(i) + 1}, - PatchSetID: 42 + int64(i), - ClosedAt: clock.now(), + Name: fmt.Sprintf("Upgrade ES-Lint %d", i), + Description: "All the Javascripts are belong to us", + Branch: "upgrade-es-lint", + AuthorID: int32(i) + 50, + ChangesetIDs: []int64{int64(i) + 1}, + PatchSetID: 42 + int64(i), + CampaignSpecID: 1742 + int64(i), + ClosedAt: clock.now(), } if i == 0 { - // don't have a patch set for the first one + // don't have associations for the first one c.PatchSetID = 0 + c.CampaignSpecID = 0 // Don't close the first one c.ClosedAt = time.Time{} } if i%2 == 0 { - c.NamespaceOrgID = 23 + c.NamespaceOrgID = int32(i) + 23 } else { c.NamespaceUserID = c.AuthorID } @@ -401,6 +403,87 @@ func testStoreCampaigns(t *testing.T, ctx context.Context, s *Store, _ repos.Sto } }) + t.Run("ByCampaignSpecID", func(t *testing.T) { + want := campaigns[0] + opts := GetCampaignOpts{CampaignSpecID: want.CampaignSpecID} + + have, err := s.GetCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(have, want); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ByCampaignSpecName", func(t *testing.T) { + want := campaigns[0] + + campaignSpec := &cmpgn.CampaignSpec{ + Spec: cmpgn.CampaignSpecFields{Name: "the-name"}, + NamespaceOrgID: want.NamespaceOrgID, + } + if err := s.CreateCampaignSpec(ctx, campaignSpec); err != nil { + t.Fatal(err) + } + + want.CampaignSpecID = campaignSpec.ID + if err := s.UpdateCampaign(ctx, want); err != nil { + t.Fatal(err) + } + + opts := GetCampaignOpts{CampaignSpecName: campaignSpec.Spec.Name} + have, err := s.GetCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(have, want); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("ByNamespaceUserID", func(t *testing.T) { + for _, c := range campaigns { + if c.NamespaceUserID == 0 { + continue + } + + want := c + opts := GetCampaignOpts{NamespaceUserID: c.NamespaceUserID} + + have, err := s.GetCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(have, want); diff != "" { + t.Fatal(diff) + } + } + }) + + t.Run("ByNamespaceOrgID", func(t *testing.T) { + for _, c := range campaigns { + if c.NamespaceOrgID == 0 { + continue + } + + want := c + opts := GetCampaignOpts{NamespaceOrgID: c.NamespaceOrgID} + + have, err := s.GetCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(have, want); diff != "" { + t.Fatal(diff) + } + } + }) + t.Run("NoResults", func(t *testing.T) { opts := GetCampaignOpts{ID: 0xdeadbeef} diff --git a/internal/campaigns/types.go b/internal/campaigns/types.go index e2a9a023d708d..3749a378b4a11 100644 --- a/internal/campaigns/types.go +++ b/internal/campaigns/types.go @@ -143,6 +143,7 @@ type Campaign struct { ChangesetIDs []int64 PatchSetID int64 ClosedAt time.Time + CampaignSpecID int64 } // Clone returns a clone of a Campaign.