Example Scenario

Let’s say you have business logic that dictates that the component lead(s) must be added as watchers if the priority is Blocker.

You ignore the sensible option, just for the purpose of my example, and decide to write a script to add the component leads as watchers on the Start Progress transition.

Your script might look like:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue

// this comes to us in the binding... so this line is unnecessary other than to give my IDE "type" information
Issue issue = issue

// get some components we need
def watcherManager = ComponentAccessor.getWatcherManager()
def userUtil = ComponentAccessor.getUserUtil()

// would be better to use componentLead rather than lead, but not available until 6.3
issue.componentObjects*.lead.each {String username ->
    def applicationUser = userUtil.getUserByKey(username)
    watcherManager.startWatching(applicationUser, issue)
}
CODE

We will write some tests for the above script, and throw various combinations at it. The test inherits from AbstractWorkflowSpecification which will take care of setting us up a project, workflow and workflow scheme, ready for us to start editing it.

The entire test class can be found below:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.IssueInputParametersImpl
import com.atlassian.jira.project.AssigneeTypes
import com.atlassian.jira.project.Project
import com.atlassian.jira.project.type.ProjectTypeKey
import com.atlassian.jira.scheme.Scheme
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.workflow.JiraWorkflow
import com.onresolve.jira.groovy.GroovyFunctionPlugin
import com.onresolve.scriptrunner.canned.jira.utils.CustomScriptDelegate
import com.onresolve.scriptrunner.canned.jira.workflow.postfunctions.CustomScriptFunction
import com.opensymphony.workflow.loader.ActionDescriptor
import com.opensymphony.workflow.loader.DescriptorFactory
import spock.lang.Shared
import spock.lang.Specification

import static com.atlassian.jira.project.AssigneeTypes.PROJECT_DEFAULT

class TestAddComponentLeadsAsWatchers extends Specification {

    private static final String IN_PROGRESS = "In Progress"
    private static final String JOE_LEAD = "joelead"

    @Shared
    def projectService = ComponentAccessor.getComponent(ProjectService)

    @Shared
    def projectComponentManager = ComponentAccessor.projectComponentManager

    @Shared
    def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser

    @Shared
    def workflowManager = ComponentAccessor.workflowManager

    @Shared
    def workflowSchemeManager = ComponentAccessor.workflowSchemeManager

    @Shared
    def userService = ComponentAccessor.getComponent(UserService)

    @Shared
    def userManager = ComponentAccessor.userManager

    @Shared
    def userUtil = ComponentAccessor.userUtil

    def watcherManager = ComponentAccessor.watcherManager
    def issueService = ComponentAccessor.issueService
    def constantsManager = ComponentAccessor.constantsManager

    @Shared
    Project project

    /**
     * Create the workflow
     * add the post-function at the Start Progress step
     * Add some components, some with leads, some without
     */
    def setupSpec() {
        project = createTestSoftwareProject()

        def draftWorkflow = createDraftWorkflowForProject(project)

        addPostFunctionToDraftWorkflow(draftWorkflow, IN_PROGRESS, [
            "canned-script"                           : CustomScriptFunction.name,
            (CustomScriptDelegate.FIELD_SCRIPT_FILE)  : "com/acme/scriptrunner/scripts/AddComponentLeadsAsWatchers.groovy",
            "class.name"                              : GroovyFunctionPlugin.name
        ])

        publishDraftWorkflow(draftWorkflow)

        createUser(JOE_LEAD)

        projectComponentManager.create("Comp1", "has an assignee", JOE_LEAD, PROJECT_DEFAULT, project.id)
        projectComponentManager.create("Comp2", "no assignee", null, PROJECT_DEFAULT, project.id)
    }

    def cleanupSpec() {
        removeTestSoftwareProject()
        removeUser(JOE_LEAD)
    }

    def "test script with workflow transitions"() {
        given:
        def issue = createIssueWithComponents(project, componentNames)

        when:
        watcherManager.startWatching(currentUser, issue)
        issue = transitionIssue(issue, IN_PROGRESS)

        then:
        // do a comparison as a Set because we don't care about the order of the watcher names
        watcherManager.getCurrentWatcherUsernames(issue) as Set == expectedWatchers as Set

        // note the current user is always expected to be a watcher, as it's jira's behaviour
        // (provided you haven't turned off autowatch for the user running the test)
        // a useful improvement to this would be to set it

        where:
        componentNames     | expectedWatchers
        []                 | [currentUser.name]
        ["Comp1"]          | [currentUser.name, JOE_LEAD]
        ["Comp1", "Comp2"] | [currentUser.name, JOE_LEAD]
    }

    private Issue createIssueWithComponents(Project project, List<String> componentNames) {
        def inputParameters = new IssueInputParametersImpl().with {
            setProjectId(project.id)
            setSummary("my summary")
            setReporterId(currentUser.name)
            setIssueTypeId(constantsManager.allIssueTypeObjects.find { it.name == "Bug" }.id)
            setComponentIds(componentNames.collect { name -> projectComponentManager.findByComponentName(project.id, name).id } as Long[])
        }

        def createValidationResult = issueService.validateCreate(currentUser, inputParameters)
        assert !createValidationResult.errorCollection.hasAnyErrors()

        issueService.create(currentUser, createValidationResult).issue
    }

    private Issue transitionIssue(Issue issue, String actionName) {
        def workflow = getWorkflowForProject(project)
        def action = getActionInWorkflow(workflow, actionName)

        def transitionValidationResult = issueService.validateTransition(currentUser, issue.id, action.id, new IssueInputParametersImpl())
        issueService.transition(currentUser, transitionValidationResult).issue
    }

    private ApplicationUser createUser(String username) {
        def password = "password"
        def emailAddress = "${username}@example.com"

        def createUserRequest = UserService.CreateUserRequest.withUserDetails(currentUser, username, password, emailAddress, username)
        def validationResult = userService.validateCreateUser(createUserRequest)
        assert !validationResult.errorCollection.hasAnyErrors()

        userService.createUser(validationResult)
    }

    private void removeUser(String username) {
        def userToRemove = userManager.getUserByName(username)

        def validationResult = userService.validateDeleteUser(currentUser, userToRemove)
        assert !validationResult.errorCollection.hasAnyErrors()

        userUtil.removeUser(currentUser, userToRemove)
        assert !userUtil.userExists(userToRemove.name)
    }

    private Project createTestSoftwareProject() {
        def creationData = new ProjectCreationData.Builder().with {
            withName("Test Project")
            withKey("TEST")
            withLead(currentUser)
            withAssigneeType(AssigneeTypes.PROJECT_LEAD)
            withType(new ProjectTypeKey('software'))
            withProjectTemplateKey("com.pyxis.greenhopper.jira:basic-software-development-template")
        }

        def result = projectService.validateCreateProject(currentUser, creationData.build())
        assert !result.errorCollection.hasAnyErrors()

        projectService.createProject(result)
    }

    private void removeTestSoftwareProject() {
        def deleteProjectValidationResult = projectService.validateDeleteProject(currentUser, project.key)
        assert !deleteProjectValidationResult.errorCollection.hasAnyErrors()

        projectService.deleteProject(currentUser, deleteProjectValidationResult)

        def scheme = workflowSchemeManager.getSchemeFor(project)
        removeWorkflowSchemeIfUnused(scheme)
    }

    private void removeWorkflowSchemeIfUnused(Scheme scheme) {
        def projects = workflowSchemeManager.getProjects(scheme)
        if (!projects) {
            def workflows = workflowManager.getWorkflowsFromScheme(scheme).toUnique()
            workflowSchemeManager.deleteScheme(scheme.id)
            workflows.each { workflow ->
                def schemes = workflowSchemeManager.getSchemesForWorkflow(workflow)
                if (!schemes && !workflow.systemWorkflow) {
                    workflowManager.deleteWorkflow(workflow)
                }
            }
        }
    }

    private JiraWorkflow getWorkflowForProject(Project project) {
        def workflowName = getProjectWorkflowName(project)
        workflowManager.getWorkflow(workflowName)
    }

    private JiraWorkflow createDraftWorkflowForProject(Project project) {
        def workflowName = getProjectWorkflowName(project)
        def workflow = workflowManager.createDraftWorkflow(currentUser, workflowName)
        workflow
    }

    private void addPostFunctionToDraftWorkflow(JiraWorkflow draftWorkflow, String actionName, Map<String, String> args) {
        def action = getActionInWorkflow(draftWorkflow, actionName)

        def postFunction = DescriptorFactory.factory.createFunctionDescriptor()
        postFunction.type = 'class'
        postFunction.args << args

        action.unconditionalResult.postFunctions.add(postFunction)
    }

    private void publishDraftWorkflow(JiraWorkflow draftWorkflow) {
        workflowManager.updateWorkflow(currentUser, draftWorkflow)
        workflowManager.overwriteActiveWorkflow(currentUser, draftWorkflow.name)
    }

    private ActionDescriptor getActionInWorkflow(JiraWorkflow workflow, String actionName) {
        workflow.allActions.find { ad -> ad.name == actionName } as ActionDescriptor
    }

    private String getProjectWorkflowName(Project project) {
        def workflowScheme = workflowSchemeManager.getWorkflowSchemeObj(project)
        workflowScheme.actualDefaultWorkflow
    }
}

Then we write a single test. I used the Spock data table idiom. Whilst initially hard to setup, it’s very easy to add new test combinations.

In testing adding a component that doesn’t have a lead, I got an error from my script, and a test failure…​

ERROR [scriptrunner.jira.workflow.ScriptWorkflowFunction] Script function failed on issue: SRTESTPRJ-3, actionId: 4, file: examples/docs/AddComponentLeadsAsWatchers.groovy
groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method com.atlassian.jira.issue.watchers.DefaultWatcherManager#startWatching.
Cannot resolve which method to invoke for [null, class com.atlassian.jira.issue.IssueImpl] due to overlapping prototypes between:
    [interface com.atlassian.crowd.embedded.api.User, interface com.atlassian.jira.issue.Issue]
    [interface com.atlassian.jira.user.ApplicationUser, interface com.atlassian.jira.issue.Issue]
    at com.atlassian.jira.issue.watchers.WatcherManager$startWatching$1.call(Unknown Source)
    at examples.docs.AddComponentLeadsAsWatchers$_run_closure1.doCall(AddComponentLeadsAsWatchers.groovy:16)
CODE

This happened because I was passing null (the component without a lead) to startWatching, and so required a small change to the script to handle the case where the component lead is not set, namely:

...
if (applicationUser) {
    watcherManager.startWatching(applicationUser, issue)
}
...
CODE