Add startHTTPServer() support function
Centralize httpd creation and add automatic retry to try to deal with NS_ERROR_SOCKET_ADDRESS_IN_USE errors in CI.
This commit is contained in:
parent
17daf9fe8d
commit
fb96cd595d
5 changed files with 75 additions and 83 deletions
|
@ -1094,3 +1094,26 @@ function setHTTPResponse(server, baseURL, response, responses, username, passwor
|
||||||
server.respondWith(response.method, baseURL + response.url, responseArray);
|
server.respondWith(response.method, baseURL + response.url, responseArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let httpdServerPort = 16213;
|
||||||
|
/**
|
||||||
|
* @param {Number} [port] - Port number to use. If not provided, one is picked automatically.
|
||||||
|
* @return {Promise<{ httpd: Object, port: Number }>}
|
||||||
|
*/
|
||||||
|
async function startHTTPServer(port = null) {
|
||||||
|
if (!port) {
|
||||||
|
port = httpdServerPort;
|
||||||
|
}
|
||||||
|
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||||
|
var httpd = new HttpServer();
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
httpd.start(port);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
await Zotero.Promise.delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { httpd, port };
|
||||||
|
}
|
||||||
|
|
|
@ -326,31 +326,20 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
describe("#importFromDocument()", function () {
|
describe("#importFromDocument()", function () {
|
||||||
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
|
||||||
var testServerPath, httpd, prefix;
|
var testServerPath, httpd, prefix;
|
||||||
var testServerPortMin = 16213;
|
var testServerPort;
|
||||||
var testServerPortMax = testServerPortMin + 20;
|
|
||||||
var testServerPort = testServerPortMin;
|
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
Zotero.Prefs.set("httpServer.enabled", true);
|
Zotero.Prefs.set("httpServer.enabled", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
// Cycle through ports to prevent NS_ERROR_SOCKET_ADDRESS_IN_USE errors from server
|
|
||||||
// not always fully stopping in time
|
|
||||||
if (testServerPort < testServerPortMax) {
|
|
||||||
testServerPort++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
testServerPort = testServerPortMin;
|
|
||||||
}
|
|
||||||
// Use random prefix because httpd does not actually stop between tests
|
// Use random prefix because httpd does not actually stop between tests
|
||||||
prefix = Zotero.Utilities.randomString();
|
prefix = Zotero.Utilities.randomString();
|
||||||
|
({ httpd, port: testServerPort } = await startHTTPServer());
|
||||||
testServerPath = 'http://127.0.0.1:' + testServerPort + '/' + prefix;
|
testServerPath = 'http://127.0.0.1:' + testServerPort + '/' + prefix;
|
||||||
httpd = new HttpServer();
|
|
||||||
httpd.start(testServerPort);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function () {
|
afterEach(async function () {
|
||||||
|
@ -586,7 +575,6 @@ describe("Zotero.Attachments", function() {
|
||||||
var pageURL9 = 'http://website/article9';
|
var pageURL9 = 'http://website/article9';
|
||||||
var pageURL10 = 'http://website/refresh';
|
var pageURL10 = 'http://website/refresh';
|
||||||
|
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
|
||||||
var httpd;
|
var httpd;
|
||||||
var port = 16213;
|
var port = 16213;
|
||||||
var baseURL = `http://localhost:${port}/`;
|
var baseURL = `http://localhost:${port}/`;
|
||||||
|
@ -821,8 +809,7 @@ describe("Zotero.Attachments", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
httpd = new HttpServer();
|
({ httpd } = await startHTTPServer(port));
|
||||||
httpd.start(port);
|
|
||||||
httpd.registerFile(
|
httpd.registerFile(
|
||||||
pdfURL.substr(baseURL.length - 1),
|
pdfURL.substr(baseURL.length - 1),
|
||||||
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.pdf'))
|
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.pdf'))
|
||||||
|
|
|
@ -4,17 +4,12 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
//
|
//
|
||||||
// Setup
|
// Setup
|
||||||
//
|
//
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
const davScheme = "http";
|
||||||
|
const davBasePath = "/webdav/";
|
||||||
|
const davUsername = "user";
|
||||||
|
const davPassword = "password";
|
||||||
|
|
||||||
var davScheme = "http";
|
var win, controller, server, requestCount, httpd, davHostPath, davURL;
|
||||||
var davPort = 16214;
|
|
||||||
var davBasePath = "/webdav/";
|
|
||||||
var davHostPath = `localhost:${davPort}${davBasePath}`;
|
|
||||||
var davUsername = "user";
|
|
||||||
var davPassword = "password";
|
|
||||||
var davURL = `${davScheme}://${davHostPath}`;
|
|
||||||
|
|
||||||
var win, controller, server, requestCount;
|
|
||||||
var responses = {};
|
var responses = {};
|
||||||
|
|
||||||
function setResponse(response) {
|
function setResponse(response) {
|
||||||
|
@ -46,8 +41,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function* () {
|
beforeEach(async function () {
|
||||||
yield resetDB({
|
await resetDB({
|
||||||
thisArg: this,
|
thisArg: this,
|
||||||
skipBundledFiles: true
|
skipBundledFiles: true
|
||||||
});
|
});
|
||||||
|
@ -56,11 +51,13 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
server = sinon.fakeServer.create();
|
server = sinon.fakeServer.create();
|
||||||
server.autoRespond = true;
|
server.autoRespond = true;
|
||||||
|
|
||||||
this.httpd = new HttpServer();
|
var port;
|
||||||
this.httpd.start(davPort);
|
({ httpd, port } = await startHTTPServer());
|
||||||
|
davHostPath = `localhost:${port}${davBasePath}`;
|
||||||
|
davURL = `${davScheme}://${davHostPath}`;
|
||||||
|
|
||||||
yield Zotero.Users.setCurrentUserID(1);
|
await Zotero.Users.setCurrentUserID(1);
|
||||||
yield Zotero.Users.setCurrentUsername("testuser");
|
await Zotero.Users.setCurrentUsername("testuser");
|
||||||
|
|
||||||
Zotero.Sync.Storage.Local.setModeForLibrary(Zotero.Libraries.userLibraryID, 'webdav');
|
Zotero.Sync.Storage.Local.setModeForLibrary(Zotero.Libraries.userLibraryID, 'webdav');
|
||||||
controller = new Zotero.Sync.Storage.Mode.WebDAV;
|
controller = new Zotero.Sync.Storage.Mode.WebDAV;
|
||||||
|
@ -124,7 +121,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async function () {
|
afterEach(async function () {
|
||||||
await new Promise(request => this.httpd.stop(request));
|
await new Promise(request => httpd.stop(request));
|
||||||
})
|
})
|
||||||
|
|
||||||
after(function* () {
|
after(function* () {
|
||||||
|
@ -248,7 +245,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
+ '<hash>8286300a280f64a4b5cfaac547c21d32</hash>'
|
+ '<hash>8286300a280f64a4b5cfaac547c21d32</hash>'
|
||||||
+ '</properties>'
|
+ '</properties>'
|
||||||
});
|
});
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/${item.key}.zip`,
|
`${davBasePath}zotero/${item.key}.zip`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -312,7 +309,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
+ `<hash>${md5}</hash>`
|
+ `<hash>${md5}</hash>`
|
||||||
+ '</properties>'
|
+ '</properties>'
|
||||||
});
|
});
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/${item.key}.zip`,
|
`${davBasePath}zotero/${item.key}.zip`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -621,7 +618,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
yield OS.File.remove(zipPath);
|
yield OS.File.remove(zipPath);
|
||||||
|
|
||||||
// OPTIONS request to cache credentials
|
// OPTIONS request to cache credentials
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/`,
|
`${davBasePath}zotero/`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -644,7 +641,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/${item.key}.prop`,
|
`${davBasePath}zotero/${item.key}.prop`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -672,7 +669,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/${item.key}.zip`,
|
`${davBasePath}zotero/${item.key}.zip`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -777,7 +774,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
|
|
||||||
it("should show an error for a 403", function* () {
|
it("should show an error for a 403", function* () {
|
||||||
Zotero.HTTP.mock = null;
|
Zotero.HTTP.mock = null;
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/`,
|
`${davBasePath}zotero/`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -816,7 +813,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
Zotero.HTTP.mock = null;
|
Zotero.HTTP.mock = null;
|
||||||
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
||||||
|
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/`,
|
`${davBasePath}zotero/`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -831,7 +828,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}`,
|
`${davBasePath}`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -870,7 +867,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
|
|
||||||
it("should show an error for a 200 for a nonexistent file", async function () {
|
it("should show an error for a 200 for a nonexistent file", async function () {
|
||||||
Zotero.HTTP.mock = null;
|
Zotero.HTTP.mock = null;
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/`,
|
`${davBasePath}zotero/`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -891,7 +888,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`${davBasePath}zotero/nonexistent.prop`,
|
`${davBasePath}zotero/nonexistent.prop`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
|
|
@ -4,13 +4,9 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
//
|
//
|
||||||
// Setup
|
// Setup
|
||||||
//
|
//
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
|
||||||
|
|
||||||
var apiKey = Zotero.Utilities.randomString(24);
|
var apiKey = Zotero.Utilities.randomString(24);
|
||||||
var port = 16213;
|
|
||||||
var baseURL = `http://localhost:${port}/`;
|
|
||||||
|
|
||||||
var win, server, requestCount;
|
var win, server, requestCount, httpd, baseURL;
|
||||||
var responses = {};
|
var responses = {};
|
||||||
|
|
||||||
function setResponse(response) {
|
function setResponse(response) {
|
||||||
|
@ -45,22 +41,23 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
//
|
//
|
||||||
// Tests
|
// Tests
|
||||||
//
|
//
|
||||||
beforeEach(function* () {
|
beforeEach(async function () {
|
||||||
yield resetDB({
|
await resetDB({
|
||||||
thisArg: this,
|
thisArg: this,
|
||||||
skipBundledFiles: true
|
skipBundledFiles: true
|
||||||
});
|
});
|
||||||
win = yield loadZoteroPane();
|
win = await loadZoteroPane();
|
||||||
|
|
||||||
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
||||||
server = sinon.fakeServer.create();
|
server = sinon.fakeServer.create();
|
||||||
server.autoRespond = true;
|
server.autoRespond = true;
|
||||||
|
|
||||||
this.httpd = new HttpServer();
|
var port;
|
||||||
this.httpd.start(port);
|
({ httpd, port } = await startHTTPServer());
|
||||||
|
baseURL = `http://localhost:${port}/`;
|
||||||
|
|
||||||
yield Zotero.Users.setCurrentUserID(1);
|
await Zotero.Users.setCurrentUserID(1);
|
||||||
yield Zotero.Users.setCurrentUsername("testuser");
|
await Zotero.Users.setCurrentUsername("testuser");
|
||||||
|
|
||||||
Zotero.Sync.Storage.Local.setModeForLibrary(Zotero.Libraries.userLibraryID, 'zfs');
|
Zotero.Sync.Storage.Local.setModeForLibrary(Zotero.Libraries.userLibraryID, 'zfs');
|
||||||
|
|
||||||
|
@ -103,7 +100,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
|
|
||||||
afterEach(function* () {
|
afterEach(function* () {
|
||||||
var defer = new Zotero.Promise.defer();
|
var defer = new Zotero.Promise.defer();
|
||||||
this.httpd.stop(() => defer.resolve());
|
httpd.stop(() => defer.resolve());
|
||||||
yield defer.promise;
|
yield defer.promise;
|
||||||
win.close();
|
win.close();
|
||||||
})
|
})
|
||||||
|
@ -148,7 +145,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
item.attachmentSyncState = "to_download";
|
item.attachmentSyncState = "to_download";
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
|
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -214,7 +211,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
item.attachmentSyncState = "to_download";
|
item.attachmentSyncState = "to_download";
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
|
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -251,7 +248,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
var md5 = Zotero.Utilities.Internal.md5(text)
|
var md5 = Zotero.Utilities.Internal.md5(text)
|
||||||
|
|
||||||
var s3Path = `pretend-s3/${item.key}`;
|
var s3Path = `pretend-s3/${item.key}`;
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -272,7 +269,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
"/" + s3Path,
|
"/" + s3Path,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -313,7 +310,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
var md5 = Zotero.Utilities.Internal.md5(text);
|
var md5 = Zotero.Utilities.Internal.md5(text);
|
||||||
|
|
||||||
var s3Path = `pretend-s3/${item.key}`;
|
var s3Path = `pretend-s3/${item.key}`;
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -325,7 +322,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
"/" + s3Path,
|
"/" + s3Path,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
@ -687,7 +684,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
var md5 = Zotero.Utilities.Internal.md5(file)
|
var md5 = Zotero.Utilities.Internal.md5(file)
|
||||||
|
|
||||||
var s3Path = `pretend-s3/${item.key}`;
|
var s3Path = `pretend-s3/${item.key}`;
|
||||||
this.httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
handle: function (request, response) {
|
handle: function (request, response) {
|
||||||
|
|
|
@ -141,11 +141,7 @@ describe("ZoteroPane", function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#viewAttachment", function () {
|
describe("#viewAttachment", function () {
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
|
||||||
var apiKey = Zotero.Utilities.randomString(24);
|
var apiKey = Zotero.Utilities.randomString(24);
|
||||||
var testServerPortMin = 16213;
|
|
||||||
var testServerPortMax = testServerPortMin + 20;
|
|
||||||
var testServerPort = testServerPortMin;
|
|
||||||
var baseURL;
|
var baseURL;
|
||||||
var httpd;
|
var httpd;
|
||||||
|
|
||||||
|
@ -202,23 +198,15 @@ describe("ZoteroPane", function() {
|
||||||
before(function () {
|
before(function () {
|
||||||
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
||||||
})
|
})
|
||||||
beforeEach(function* () {
|
beforeEach(async function () {
|
||||||
// Cycle through ports to prevent NS_ERROR_SOCKET_ADDRESS_IN_USE errors from server
|
var port;
|
||||||
// not always fully stopping in time
|
({ httpd, port } = await startHTTPServer());
|
||||||
if (testServerPort < testServerPortMax) {
|
baseURL = `http://localhost:${port}/`;
|
||||||
testServerPort++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
testServerPort = testServerPortMin;
|
|
||||||
}
|
|
||||||
baseURL = `http://localhost:${testServerPort}/`;
|
|
||||||
Zotero.Prefs.set("api.url", baseURL);
|
Zotero.Prefs.set("api.url", baseURL);
|
||||||
httpd = new HttpServer();
|
|
||||||
httpd.start(testServerPort);
|
|
||||||
|
|
||||||
Zotero.Sync.Runner.apiKey = apiKey;
|
Zotero.Sync.Runner.apiKey = apiKey;
|
||||||
yield Zotero.Users.setCurrentUserID(1);
|
await Zotero.Users.setCurrentUserID(1);
|
||||||
yield Zotero.Users.setCurrentUsername("testuser");
|
await Zotero.Users.setCurrentUsername("testuser");
|
||||||
})
|
})
|
||||||
afterEach(function* () {
|
afterEach(function* () {
|
||||||
var defer = new Zotero.Promise.defer();
|
var defer = new Zotero.Promise.defer();
|
||||||
|
|
Loading…
Add table
Reference in a new issue