I'm encountering a really strange issue with the Jira Rest API.
Whever I try to create an issue using POST request to jira/rest/api/latest/issue/ I receive Error code 400 from Python 2.7 requests but it succeeds from Powershell's Invoke Web Request.
I've tried a number of troubleshooting suggestions from similar topics on this site already:
- I've verified that the Authorisation is correct (it's also required for the GET requests which all work)
- I'm testing with an account that has administrator level access to the Jira instance
- I've stripped the json back to just the fields and format matching the REST API documentation here: jira-rest-api-examples/#creating-an-issue-examples
- I've ensured that all the relevant session cookie and header data is stored and added to the follw-up requests
- I've verified through Issue/createmeta that I have the capabilities to create that issue type (and as I pointed out earlier - it works in Powershell)
- I've tried using Issuetype name and id as well as project key and id as the identifier, neither one changes anything
- I've even tried both omitting and including the trailing slash on the /issue path in case that was important
Json Body (Raw):
{"fields": {"issuetype": {"id": "10702"}, "project": {"id": "10061"}, "description": "Execution for Issue: SDBX-859", "summary": "Execution for Issue: SDBX-859"}}(Formatted for Legibility):
{
"fields": {
"issuetype": {
"id": "10702"
},
"project": {
"id": "10061"
},
"description": "Execution for Issue: SDBX-859",
"summary": "Execution for Issue: SDBX-859"
}
}
The process flow starts with this class:
class Migrator(object):
RestURLs = {
"projects": "api/latest/project",
"issuetype": "api/latest/issuetype",
"fields": "api/latest/field",
"tests": "api/latest/search?maxResults={limit}&expand=meta&jql=IssueType='{testType}'+and+project={projectKey}",
"zSteps": "zapi/latest/teststep/{issueId}",
"zExecutions": "zapi/latest/zql/executeSearch?zqlQuery=project={projectKey}",
"zCycles": "zapi/latest/cycle?projectId={projectId}",
"issue": "api/latest/issue/{issueKey}",
"xSteps": "raven/1.0/api/test/{issueKey}/step/{stepId}",
"xSet": "raven/1.0/api/testset/{issueKey}/test",
"xExecution": "raven/1.0/api/testexec/{issueKey}/test"
}
CustomFields = {
"Zephyr Teststep": "",
"Manual Test Steps": "",
"Test Type": ""
}
IssueNames = {
"zephyr":"Zephyr - Test",
"xray":"Test",
"set":"Test Set",
"execution":"Test Execution"
}
IssueTypes = {}
def __init__(self):
self.results = []
print("new Migrator initialised")
self.restHandler = RestHandler()
self.baseURL = "http://127.0.0.1/jira/rest/"
self.authentication = ""
self.commonHeaders = {}
self.projectList = []
self.project = None
self.testList = []
self.executionList = {}
self.versionList = set()
self.cycleList = {}
self.setList = []
def connect(self, username, password, serverUrl="http://127.0.0.1"):
# 1 - connect to jira
if serverUrl[-1] != '/':
serverUrl += '/'
self.baseURL = str.format("{0}jira/rest/", serverUrl)
self.authentication = "Basic " + base64.b64encode(username + ":" + password)
self.commonHeaders = {"Authorization": self.authentication}
print("Connecting to Server: " + self.baseURL)
headers = self.commonHeaders
projList = self.restHandler.perform(method=HTTP.GET,url=self.baseURL,path=Migrator.RestURLs["projects"],headers=headers)
# 2 - populate projects list
for projDict in projList:
self.projectList.append(Project().fromDict(projDict))
from this method:
def migrateExecutions(self, project):
print "working..."
for execution in self.executionList:
# Restricting it only to my issues for testing...
if execution.assigneeUserName == "boydnic":
headers = self.commonHeaders
execData = {"fields":{}}
execData["fields"]["issuetype"] = {"id":self.IssueTypes[self.IssueNames["execution"]].id}
execData["fields"]["project"] = {"id":project.id}
# execData["fields"]["reporter"] = {"name": userName}
# execData["fields"]["assignee"] = {"name": execution.assigneeUserName}
execData["fields"]["summary"] = "Execution for Issue: " + execution.issueKey
execData["fields"]["description"] = execution.comment if execution.comment else execData["fields"]["summary"]
xrayExec = self.createIssue(execData)
self.results.append(self.restHandler.perform(method=HTTP.POST, url=self.baseURL,
path=self.RestURLs["xExecution"], urlData={"issueKey":xrayExec.key},
headers=headers, body={"add":[execution.issueKey]}))
to this Method:
def createIssue(self, issueTemplate):
result = self.restHandler.perform(method=HTTP.POST, url=self.baseURL, path=Migrator.RestURLs["issue"], urlData={"issueKey":""}, headers=self.commonHeaders, body=issueTemplate)
issue = Issue()
issue.id = result["id"]
issue.key = result["key"]
issue.self = result["self"]
print("Created Issue: "+issue.key)
return issue
Which itself calls this class:
class RestHandler(object):
def __init__(self):
self.headerStore = {'X-CITNET-USER':"",
'X-ASEN':"",
'X-ASESSIONID':"",
'X-AUSERNAME':""}
self.cookieJar = requests.cookies.RequestsCookieJar()
def perform(self, method, url, path, headers={}, urlData={"projectId": "", "projectKey": "", "issueId": "", "issueKey": ""},
formData=dict(), body=""):
resultData = "{}"
path = url + path.format(**urlData)
body = body if isinstance(body, str) else json.dumps(body)
if self.headerStore:
headers.update(self.headerStore)
jar = self.cookieJar
print(str(method))
print(path)
if method is HTTP.GET:
resultData = requests.get(path, headers=headers, cookies = jar)
elif method is HTTP.POST:
print (body)
path = path.rstrip('/')
resultData = requests.post(path, json=body, headers=headers, cookies = jar)
elif method is HTTP.PUT:
print (body)
resultData = requests.put(path, json=body, headers=headers, cookies = jar)
elif method is HTTP.DELETE:
request = "DELETE request to " + path
else:
raise TypeError
print("\n\n===============================\nRest Call Debugging\n===============================")
print(resultData)
print(resultData.url)
print(resultData.status_code)
print(resultData.headers)
print(resultData.content)
print("\n\n===============================\n/Rest Call Debugging\n==============================")
if 199 < resultData.status_code < 300:
for hKey, hValue in resultData.headers.iteritems():
if hKey in self.headerStore.keys():
self.headerStore[hKey] = hValue
self.cookieJar.update(resultData.cookies)
print "testing breakpoint"
return json.loads(resultData.content)
else:
raise IOError(resultData.reason)
I'm getting incredibly frustrated by this, not least because it's blocking the completion of this migration script and seems to make no sense.