mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 21:57:59 +00:00
1253 lines
35 KiB
Go
1253 lines
35 KiB
Go
// Copyright 2017 HootSuite Media Inc.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the License);
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an AS IS BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||
|
||
package events_test
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"testing"
|
||
|
||
"github.com/hashicorp/go-version"
|
||
. "github.com/petergtz/pegomock/v4"
|
||
"github.com/runatlantis/atlantis/server/core/config/valid"
|
||
"github.com/runatlantis/atlantis/server/core/runtime"
|
||
tmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
|
||
"github.com/runatlantis/atlantis/server/events"
|
||
"github.com/runatlantis/atlantis/server/events/command"
|
||
"github.com/runatlantis/atlantis/server/events/mocks"
|
||
"github.com/runatlantis/atlantis/server/events/models"
|
||
"github.com/runatlantis/atlantis/server/events/models/testdata"
|
||
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
|
||
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
|
||
"github.com/runatlantis/atlantis/server/logging"
|
||
. "github.com/runatlantis/atlantis/testing"
|
||
)
|
||
|
||
// Test that it runs the expected plan steps.
|
||
func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockPlan := mocks.NewMockStepRunner()
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockRun := mocks.NewMockCustomStepRunner()
|
||
realEnv := runtime.EnvStepRunner{}
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockCommandRequirementHandler := mocks.NewMockCommandRequirementHandler()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
PlanStepRunner: mockPlan,
|
||
ApplyStepRunner: mockApply,
|
||
RunStepRunner: mockRun,
|
||
EnvStepRunner: &realEnv,
|
||
PullApprovedChecker: nil,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: nil,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: mockCommandRequirementHandler,
|
||
}
|
||
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),
|
||
Any[string]())).ThenReturn(repoDir, false, nil)
|
||
When(mockLocker.TryLock(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.User](), Any[string](),
|
||
Any[models.Project](), AnyBool())).ThenReturn(&events.TryLockResponse{LockAcquired: true, LockKey: "lock-key"}, nil)
|
||
|
||
expEnvs := map[string]string{
|
||
"name": "value",
|
||
}
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "name",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
},
|
||
{
|
||
StepName: "apply",
|
||
},
|
||
{
|
||
StepName: "plan",
|
||
},
|
||
{
|
||
StepName: "init",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
}
|
||
// Each step will output its step name.
|
||
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
|
||
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
|
||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
|
||
When(mockRun.Run(ctx, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
|
||
res := runner.Plan(ctx)
|
||
|
||
Assert(t, res.PlanSuccess != nil, "exp plan success")
|
||
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
|
||
Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput)
|
||
expSteps := []string{"run", "apply", "plan", "init", "env"}
|
||
for _, step := range expSteps {
|
||
switch step {
|
||
case "init":
|
||
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "plan":
|
||
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "apply":
|
||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "run":
|
||
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs, true, "")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestProjectOutputWrapper(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "plan",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
}
|
||
|
||
cases := []struct {
|
||
Description string
|
||
Failure bool
|
||
Error bool
|
||
Success bool
|
||
CommandName command.Name
|
||
}{
|
||
{
|
||
Description: "plan success",
|
||
Success: true,
|
||
CommandName: command.Plan,
|
||
},
|
||
{
|
||
Description: "plan failure",
|
||
Failure: true,
|
||
CommandName: command.Plan,
|
||
},
|
||
{
|
||
Description: "plan error",
|
||
Error: true,
|
||
CommandName: command.Plan,
|
||
},
|
||
{
|
||
Description: "apply success",
|
||
Success: true,
|
||
CommandName: command.Apply,
|
||
},
|
||
{
|
||
Description: "apply failure",
|
||
Failure: true,
|
||
CommandName: command.Apply,
|
||
},
|
||
{
|
||
Description: "apply error",
|
||
Error: true,
|
||
CommandName: command.Apply,
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
t.Run(c.Description, func(t *testing.T) {
|
||
var prjResult command.ProjectResult
|
||
var expCommitStatus models.CommitStatus
|
||
|
||
mockJobURLSetter := mocks.NewMockJobURLSetter()
|
||
mockJobMessageSender := mocks.NewMockJobMessageSender()
|
||
mockProjectCommandRunner := mocks.NewMockProjectCommandRunner()
|
||
|
||
runner := &events.ProjectOutputWrapper{
|
||
JobURLSetter: mockJobURLSetter,
|
||
JobMessageSender: mockJobMessageSender,
|
||
ProjectCommandRunner: mockProjectCommandRunner,
|
||
}
|
||
|
||
if c.Success {
|
||
prjResult = command.ProjectResult{
|
||
PlanSuccess: &models.PlanSuccess{},
|
||
ApplySuccess: "exists",
|
||
}
|
||
expCommitStatus = models.SuccessCommitStatus
|
||
} else if c.Failure {
|
||
prjResult = command.ProjectResult{
|
||
Failure: "failure",
|
||
}
|
||
expCommitStatus = models.FailedCommitStatus
|
||
} else if c.Error {
|
||
prjResult = command.ProjectResult{
|
||
Error: errors.New("error"),
|
||
}
|
||
expCommitStatus = models.FailedCommitStatus
|
||
}
|
||
|
||
When(mockProjectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(prjResult)
|
||
When(mockProjectCommandRunner.Apply(Any[command.ProjectContext]())).ThenReturn(prjResult)
|
||
|
||
switch c.CommandName {
|
||
case command.Plan:
|
||
runner.Plan(ctx)
|
||
case command.Apply:
|
||
runner.Apply(ctx)
|
||
}
|
||
|
||
mockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, models.PendingCommitStatus, nil)
|
||
mockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, expCommitStatus, &prjResult)
|
||
|
||
switch c.CommandName {
|
||
case command.Plan:
|
||
mockProjectCommandRunner.VerifyWasCalledOnce().Plan(ctx)
|
||
case command.Apply:
|
||
mockProjectCommandRunner.VerifyWasCalledOnce().Apply(ctx)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// Test what happens if there's no working dir. This signals that the project
|
||
// was never planned.
|
||
func TestDefaultProjectCommandRunner_ApplyNotCloned(t *testing.T) {
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
}
|
||
ctx := command.ProjectContext{}
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn("", os.ErrNotExist)
|
||
|
||
res := runner.Apply(ctx)
|
||
ErrEquals(t, "project has not been cloned–did you run plan?", res.Error)
|
||
}
|
||
|
||
// Test that if approval is required and the PR isn't approved we give an error.
|
||
func TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
},
|
||
}
|
||
ctx := command.ProjectContext{
|
||
ApplyRequirements: []string{"approved"},
|
||
}
|
||
tmp := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, "Pull request must be approved according to the project's approval rules before running apply.", res.Failure)
|
||
}
|
||
|
||
// Test that if mergeable is required and the PR isn't mergeable we give an error.
|
||
func TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
},
|
||
}
|
||
ctx := command.ProjectContext{
|
||
PullReqStatus: models.PullReqStatus{
|
||
Mergeable: false,
|
||
},
|
||
ApplyRequirements: []string{"mergeable"},
|
||
}
|
||
tmp := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, "Pull request must be mergeable before running apply.", res.Failure)
|
||
}
|
||
|
||
// Test that if undiverged is required and the PR is diverged we give an error.
|
||
func TestDefaultProjectCommandRunner_ApplyDiverged(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
},
|
||
}
|
||
log := logging.NewNoopLogger(t)
|
||
ctx := command.ProjectContext{
|
||
Log: log,
|
||
ApplyRequirements: []string{"undiverged"},
|
||
}
|
||
tmp := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
|
||
When(mockWorkingDir.HasDiverged(ctx.Log, tmp)).ThenReturn(true)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, "Default branch must be rebased onto pull request before running apply.", res.Failure)
|
||
}
|
||
|
||
// Test that it runs the expected apply steps.
|
||
func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
|
||
cases := []struct {
|
||
description string
|
||
steps []valid.Step
|
||
applyReqs []string
|
||
|
||
expSteps []string
|
||
expOut string
|
||
expFailure string
|
||
pullMergeable bool
|
||
}{
|
||
{
|
||
description: "normal workflow",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
expSteps: []string{"apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "approval required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
applyReqs: []string{"approved"},
|
||
expSteps: []string{"approve", "apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "mergeable required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: true,
|
||
applyReqs: []string{"mergeable"},
|
||
expSteps: []string{"apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "mergeable required, pull not mergeable",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: false,
|
||
applyReqs: []string{"mergeable"},
|
||
expSteps: []string{""},
|
||
expOut: "",
|
||
expFailure: "Pull request must be mergeable before running apply.",
|
||
},
|
||
{
|
||
description: "mergeable and approved required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: true,
|
||
applyReqs: []string{"mergeable", "approved"},
|
||
expSteps: []string{"approved", "apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "workflow with custom apply stage",
|
||
steps: []valid.Step{
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "key",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
},
|
||
{
|
||
StepName: "apply",
|
||
},
|
||
{
|
||
StepName: "plan",
|
||
},
|
||
{
|
||
StepName: "init",
|
||
},
|
||
},
|
||
expSteps: []string{"env", "run", "apply", "plan", "init"},
|
||
expOut: "run\napply\nplan\ninit",
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
if c.description != "workflow with custom apply stage" {
|
||
continue
|
||
}
|
||
t.Run(c.description, func(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockPlan := mocks.NewMockStepRunner()
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockRun := mocks.NewMockCustomStepRunner()
|
||
mockEnv := mocks.NewMockEnvStepRunner()
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockSender := mocks.NewMockWebhooksSender()
|
||
applyReqHandler := &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
}
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
PlanStepRunner: mockPlan,
|
||
ApplyStepRunner: mockApply,
|
||
RunStepRunner: mockRun,
|
||
EnvStepRunner: mockEnv,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: mockSender,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: applyReqHandler,
|
||
}
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(
|
||
Any[models.Repo](),
|
||
Any[models.PullRequest](),
|
||
Any[string](),
|
||
)).ThenReturn(repoDir, nil)
|
||
When(mockLocker.TryLock(
|
||
Any[logging.SimpleLogging](),
|
||
Any[models.PullRequest](),
|
||
Any[models.User](),
|
||
Any[string](),
|
||
Any[models.Project](),
|
||
AnyBool(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: c.steps,
|
||
Workspace: "default",
|
||
ApplyRequirements: c.applyReqs,
|
||
RepoRelDir: ".",
|
||
PullReqStatus: models.PullReqStatus{
|
||
ApprovalStatus: models.ApprovalStatus{
|
||
IsApproved: true,
|
||
},
|
||
Mergeable: true,
|
||
},
|
||
}
|
||
expEnvs := map[string]string{
|
||
"key": "value",
|
||
}
|
||
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
|
||
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
|
||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
|
||
When(mockRun.Run(ctx, "", repoDir, expEnvs, true, "")).ThenReturn("run", nil)
|
||
When(mockEnv.Run(ctx, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, c.expOut, res.ApplySuccess)
|
||
Equals(t, c.expFailure, res.Failure)
|
||
|
||
for _, step := range c.expSteps {
|
||
switch step {
|
||
case "init":
|
||
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "plan":
|
||
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "apply":
|
||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "run":
|
||
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs, true, "")
|
||
case "env":
|
||
mockEnv.VerifyWasCalledOnce().Run(ctx, "", "value", repoDir, expEnvs)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// Test that it runs the expected apply steps.
|
||
func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockSender := mocks.NewMockWebhooksSender()
|
||
applyReqHandler := &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
}
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
ApplyStepRunner: mockApply,
|
||
WorkingDir: mockWorkingDir,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: applyReqHandler,
|
||
Webhooks: mockSender,
|
||
}
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(
|
||
Any[models.Repo](),
|
||
Any[models.PullRequest](),
|
||
Any[string](),
|
||
)).ThenReturn(repoDir, nil)
|
||
When(mockLocker.TryLock(
|
||
Any[logging.SimpleLogging](),
|
||
Any[models.PullRequest](),
|
||
Any[models.User](),
|
||
Any[string](),
|
||
Any[models.Project](),
|
||
AnyBool(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "apply",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
ApplyRequirements: []string{},
|
||
RepoRelDir: ".",
|
||
}
|
||
expEnvs := map[string]string{}
|
||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", fmt.Errorf("something went wrong"))
|
||
|
||
res := runner.Apply(ctx)
|
||
Assert(t, res.ApplySuccess == "", "exp apply failure")
|
||
|
||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
}
|
||
|
||
// Test run and env steps. We don't use mocks for this test since we're
|
||
// not running any Terraform.
|
||
func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
tfClient := tmocks.NewMockClient()
|
||
tfVersion, err := version.NewVersion("0.12.0")
|
||
Ok(t, err)
|
||
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
|
||
run := runtime.RunStepRunner{
|
||
TerraformExecutor: tfClient,
|
||
DefaultTFVersion: tfVersion,
|
||
ProjectCmdOutputHandler: projectCmdOutputHandler,
|
||
}
|
||
env := runtime.EnvStepRunner{
|
||
RunStepRunner: &run,
|
||
}
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockCommandRequirementHandler := mocks.NewMockCommandRequirementHandler()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
RunStepRunner: &run,
|
||
EnvStepRunner: &env,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: nil,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: mockCommandRequirementHandler,
|
||
}
|
||
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),
|
||
Any[string]())).ThenReturn(repoDir, false, nil)
|
||
When(mockLocker.TryLock(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.User](), Any[string](),
|
||
Any[models.Project](), AnyBool())).ThenReturn(&events.TryLockResponse{LockAcquired: true, LockKey: "lock-key"}, nil)
|
||
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo var=$var",
|
||
},
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "var",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo var=$var",
|
||
},
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "dynamic_var",
|
||
RunCommand: "echo dynamic_value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo dynamic_var=$dynamic_var",
|
||
},
|
||
// Test overriding the variable
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "dynamic_var",
|
||
EnvVarValue: "overridden",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo dynamic_var=$dynamic_var",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
}
|
||
res := runner.Plan(ctx)
|
||
Assert(t, res.PlanSuccess != nil, "exp plan success")
|
||
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
|
||
Equals(t, "var=\n\nvar=value\n\ndynamic_var=dynamic_value\n\ndynamic_var=overridden\n", res.PlanSuccess.TerraformOutput)
|
||
}
|
||
|
||
// Test that it runs the expected import steps.
|
||
func TestDefaultProjectCommandRunner_Import(t *testing.T) {
|
||
expEnvs := map[string]string{}
|
||
cases := []struct {
|
||
description string
|
||
steps []valid.Step
|
||
importReqs []string
|
||
pullReqStatus models.PullReqStatus
|
||
setup func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner)
|
||
|
||
expSteps []string
|
||
expOut *models.ImportSuccess
|
||
expFailure string
|
||
}{
|
||
{
|
||
description: "normal workflow",
|
||
steps: valid.DefaultImportStage.Steps,
|
||
importReqs: []string{"approved"},
|
||
pullReqStatus: models.PullReqStatus{
|
||
ApprovalStatus: models.ApprovalStatus{
|
||
IsApproved: true,
|
||
},
|
||
},
|
||
setup: func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner) {
|
||
When(mockLocker.TryLock(
|
||
Any[logging.SimpleLogging](),
|
||
Any[models.PullRequest](),
|
||
Any[models.User](),
|
||
Any[string](),
|
||
Any[models.Project](),
|
||
AnyBool(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
|
||
When(mockImport.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("import", nil)
|
||
},
|
||
expSteps: []string{"import"},
|
||
expOut: &models.ImportSuccess{
|
||
Output: "init\nimport",
|
||
RePlanCmd: "atlantis plan -d .",
|
||
},
|
||
},
|
||
{
|
||
description: "approval required",
|
||
steps: valid.DefaultImportStage.Steps,
|
||
importReqs: []string{"approved"},
|
||
pullReqStatus: models.PullReqStatus{
|
||
ApprovalStatus: models.ApprovalStatus{
|
||
IsApproved: false,
|
||
},
|
||
},
|
||
expFailure: "Pull request must be approved according to the project's approval rules before running import.",
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
t.Run(c.description, func(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockImport := mocks.NewMockStepRunner()
|
||
mockStateRm := mocks.NewMockStepRunner()
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockSender := mocks.NewMockWebhooksSender()
|
||
applyReqHandler := &events.DefaultCommandRequirementHandler{
|
||
WorkingDir: mockWorkingDir,
|
||
}
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
ImportStepRunner: mockImport,
|
||
StateRmStepRunner: mockStateRm,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: mockSender,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
CommandRequirementHandler: applyReqHandler,
|
||
}
|
||
ctx := command.ProjectContext{
|
||
Log: logging.NewNoopLogger(t),
|
||
Steps: c.steps,
|
||
Workspace: "default",
|
||
ImportRequirements: c.importReqs,
|
||
RepoRelDir: ".",
|
||
PullReqStatus: c.pullReqStatus,
|
||
RePlanCmd: "atlantis plan -d . -- addr id",
|
||
}
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),
|
||
Any[string]())).ThenReturn(repoDir, false, nil)
|
||
if c.setup != nil {
|
||
c.setup(repoDir, ctx, mockLocker, mockInit, mockImport)
|
||
}
|
||
|
||
res := runner.Import(ctx)
|
||
Equals(t, c.expOut, res.ImportSuccess)
|
||
Equals(t, c.expFailure, res.Failure)
|
||
|
||
for _, step := range c.expSteps {
|
||
switch step {
|
||
case "init":
|
||
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "import":
|
||
mockImport.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
type mockURLGenerator struct{}
|
||
|
||
func (m mockURLGenerator) GenerateLockURL(lockID string) string {
|
||
return "https://" + lockID
|
||
}
|
||
|
||
// Test approve policies logic.
|
||
func TestDefaultProjectCommandRunner_ApprovePolicies(t *testing.T) {
|
||
cases := []struct {
|
||
description string
|
||
|
||
policySetCfg valid.PolicySets
|
||
policySetStatus []models.PolicySetStatus
|
||
userTeams []string // Teams the user is a member of
|
||
targetedPolicy string // Policy to target when running approvals
|
||
clearPolicyApproval bool
|
||
|
||
expOut []models.PolicySetResult
|
||
expFailure string
|
||
hasErr bool
|
||
}{
|
||
{
|
||
description: "When user is not an owner at any level, approve policy fails.",
|
||
hasErr: true,
|
||
policySetCfg: valid.PolicySets{
|
||
Owners: valid.PolicyOwners{
|
||
Users: []string{"someotheruser1"},
|
||
},
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someotherteam"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
},
|
||
},
|
||
expFailure: "One or more policy sets require additional approval.",
|
||
},
|
||
{
|
||
description: "When user is a top-level owner, increment approval count on all policies.",
|
||
hasErr: false,
|
||
policySetCfg: valid.PolicySets{
|
||
Owners: valid.PolicyOwners{
|
||
Users: []string{testdata.User.Username},
|
||
},
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Name: "policy2",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 1,
|
||
},
|
||
},
|
||
expFailure: "One or more policy sets require additional approval.",
|
||
},
|
||
{
|
||
description: "When user is not a top-level owner, but an owner of a policy set, increment approval count only the policy set they are an owner of.",
|
||
hasErr: true,
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Users: []string{testdata.User.Username},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Name: "policy2",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 0,
|
||
},
|
||
},
|
||
expFailure: "One or more policy sets require additional approval.",
|
||
},
|
||
{
|
||
description: "When user is a top-level ownner through membership, increment approval on all policies.",
|
||
userTeams: []string{"someuserteam"},
|
||
policySetCfg: valid.PolicySets{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Name: "policy2",
|
||
ApproveCount: 1,
|
||
},
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
},
|
||
expFailure: "",
|
||
},
|
||
{
|
||
description: "When user is not a top-level owner, but is an owner of one policy set through nembership, increment approval only the policy to which they are an owner.",
|
||
hasErr: true,
|
||
userTeams: []string{"someuserteam"},
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Name: "policy2",
|
||
ApproveCount: 1,
|
||
},
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 0,
|
||
},
|
||
},
|
||
expFailure: "One or more policy sets require additional approval.",
|
||
},
|
||
{
|
||
description: "Do not increment or error on passing or fully-approved policy sets.",
|
||
userTeams: []string{"someuserteam"},
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Approvals: 2,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 2,
|
||
},
|
||
},
|
||
expFailure: ``,
|
||
hasErr: false,
|
||
},
|
||
{
|
||
description: "Policies should not fail if they pass.",
|
||
userTeams: []string{"someuserteam"},
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Passed: true,
|
||
Approvals: 0,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 0,
|
||
Passed: true,
|
||
},
|
||
},
|
||
expFailure: ``,
|
||
hasErr: false,
|
||
},
|
||
{
|
||
description: "Non-targeted failing policies should still trigger failure when a targeted policy is cleared.",
|
||
userTeams: []string{"someuserteam"},
|
||
targetedPolicy: "policy1",
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy2",
|
||
ApproveCount: 1,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Approvals: 0,
|
||
Passed: false,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
Approvals: 0,
|
||
Passed: false,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 0,
|
||
},
|
||
},
|
||
expFailure: `One or more policy sets require additional approval.`,
|
||
hasErr: false,
|
||
},
|
||
{
|
||
description: "Approval count should be zero if ClearPolicyApproval is set.",
|
||
userTeams: []string{"someuserteam"},
|
||
clearPolicyApproval: true,
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy2",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 0,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 0,
|
||
},
|
||
},
|
||
expFailure: `One or more policy sets require additional approval.`,
|
||
hasErr: false,
|
||
},
|
||
{
|
||
description: "Approval count should not clear if user is not owner and ClearPolicyApproval is set.",
|
||
userTeams: []string{"someuserteam"},
|
||
clearPolicyApproval: true,
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someotheruserteam"},
|
||
},
|
||
Name: "policy2",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 0,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 1,
|
||
},
|
||
},
|
||
expFailure: `One or more policy sets require additional approval.`,
|
||
hasErr: true,
|
||
},
|
||
{
|
||
description: "Approval count should only clear targeted policies when ClearPolicyApproval is set.",
|
||
userTeams: []string{"someuserteam"},
|
||
targetedPolicy: "policy2",
|
||
clearPolicyApproval: true,
|
||
policySetCfg: valid.PolicySets{
|
||
PolicySets: []valid.PolicySet{
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy1",
|
||
ApproveCount: 1,
|
||
},
|
||
{
|
||
Owners: valid.PolicyOwners{
|
||
Teams: []string{"someuserteam"},
|
||
},
|
||
Name: "policy2",
|
||
ApproveCount: 2,
|
||
},
|
||
},
|
||
},
|
||
policySetStatus: []models.PolicySetStatus{
|
||
{
|
||
PolicySetName: "policy1",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
Approvals: 1,
|
||
Passed: false,
|
||
},
|
||
},
|
||
expOut: []models.PolicySetResult{
|
||
{
|
||
PolicySetName: "policy1",
|
||
ReqApprovals: 1,
|
||
CurApprovals: 1,
|
||
},
|
||
{
|
||
PolicySetName: "policy2",
|
||
ReqApprovals: 2,
|
||
CurApprovals: 0,
|
||
},
|
||
},
|
||
expFailure: `One or more policy sets require additional approval.`,
|
||
hasErr: false,
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
t.Run(c.description, func(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockVcsClient := vcsmocks.NewMockClient()
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockPlan := mocks.NewMockStepRunner()
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockRun := mocks.NewMockCustomStepRunner()
|
||
mockEnv := mocks.NewMockEnvStepRunner()
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockSender := mocks.NewMockWebhooksSender()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
VcsClient: mockVcsClient,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
PlanStepRunner: mockPlan,
|
||
ApplyStepRunner: mockApply,
|
||
RunStepRunner: mockRun,
|
||
EnvStepRunner: mockEnv,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: mockSender,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
repoDir := t.TempDir()
|
||
When(mockWorkingDir.GetWorkingDir(
|
||
Any[models.Repo](),
|
||
Any[models.PullRequest](),
|
||
Any[string](),
|
||
)).ThenReturn(repoDir, nil)
|
||
When(mockLocker.TryLock(
|
||
Any[logging.SimpleLogging](),
|
||
Any[models.PullRequest](),
|
||
Any[models.User](),
|
||
Any[string](),
|
||
Any[models.Project](),
|
||
AnyBool(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
var projPolicyStatus []models.PolicySetStatus
|
||
if c.policySetStatus == nil {
|
||
for _, p := range c.policySetCfg.PolicySets {
|
||
projPolicyStatus = append(projPolicyStatus, models.PolicySetStatus{
|
||
PolicySetName: p.Name,
|
||
})
|
||
}
|
||
} else {
|
||
projPolicyStatus = c.policySetStatus
|
||
}
|
||
|
||
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
|
||
When(runner.VcsClient.GetTeamNamesForUser(testdata.GithubRepo, testdata.User)).ThenReturn(c.userTeams, nil)
|
||
ctx := command.ProjectContext{
|
||
User: testdata.User,
|
||
Log: logging.NewNoopLogger(t),
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
PolicySets: c.policySetCfg,
|
||
ProjectPolicyStatus: projPolicyStatus,
|
||
Pull: modelPull,
|
||
PolicySetTarget: c.targetedPolicy,
|
||
ClearPolicyApproval: c.clearPolicyApproval,
|
||
}
|
||
|
||
res := runner.ApprovePolicies(ctx)
|
||
Equals(t, c.expOut, res.PolicyCheckResults.PolicySetResults)
|
||
Equals(t, c.expFailure, res.Failure)
|
||
if c.hasErr == true {
|
||
Assert(t, res.Error != nil, "expecting error.")
|
||
} else {
|
||
Assert(t, res.Error == nil, "not expecting error.")
|
||
}
|
||
})
|
||
}
|
||
}
|