Bug 226890 - Thunderbird doesn't handle news URIs properly, part 2: Implement news URIs tests. r=Standard8
--- a/mailnews/news/src/nsNNTPArticleList.cpp
+++ b/mailnews/news/src/nsNNTPArticleList.cpp
@@ -108,17 +108,19 @@ nsNNTPArticleList::AddArticleKey(PRInt32
return NS_OK;
}
NS_IMETHODIMP
nsNNTPArticleList::FinishAddingArticleKeys()
{
// if the last n messages in the group are cancelled, they won't have gotten removed
// so we have to go and remove them now.
- m_idsDeleted.AppendElements(&m_idsInDB[m_dbIndex], m_idsInDB.Length() - m_dbIndex);
+ if (m_dbIndex < m_idsInDB.Length())
+ m_idsDeleted.AppendElements(&m_idsInDB[m_dbIndex],
+ m_idsInDB.Length() - m_dbIndex);
if (m_idsDeleted.Length())
m_newsFolder->RemoveMessages(m_idsDeleted);
#ifdef DEBUG
// make sure none of the deleted turned up on the idsOnServer list
for (PRUint32 i = 0; i < m_idsDeleted.Length(); i++) {
NS_ASSERTION(m_idsOnServer.IndexOf((nsMsgKey)(m_idsDeleted[i]), 0) == nsMsgViewIndex_None, "a deleted turned up on the idsOnServer list");
--- a/mailnews/news/test/unit/head_server_setup.js
+++ b/mailnews/news/test/unit/head_server_setup.js
@@ -13,16 +13,17 @@ const kSimpleNewsArticle =
"Message-ID: <TSS1@nntp.test>\n"+
"\n"+
"What does the acronym H2G2 stand for? I've seen it before...\n";
// The groups to set up on the fake server.
// It is an array of tuples, where the first element is the group name and the
// second element is whether or not we should subscribe to it.
var groups = [
+ ["misc.test", false],
["test.empty", false],
["test.subscribe.empty", true],
["test.subscribe.simple", true],
["test.filter", true]
];
// Sets up the NNTP daemon object for use in fake server
function setupNNTPDaemon() {
var daemon = new nntpDaemon();
new file mode 100644
--- /dev/null
+++ b/mailnews/news/test/unit/postings/post2.eml
@@ -0,0 +1,6 @@
+From: "Demo User" <nobody@example.net>
+Newsgroups: misc.test
+Subject: I am just a test article
+Organization: An Example Net
+
+This is just a test article.
--- a/mailnews/news/test/unit/tail_news.js
+++ b/mailnews/news/test/unit/tail_news.js
@@ -1,1 +1,5 @@
load("../../../resources/mailShutdown.js");
+
+if (_server)
+ _server.QueryInterface(Components.interfaces.nsISubscribableServer)
+ .subscribeCleanup();
new file mode 100644
--- /dev/null
+++ b/mailnews/news/test/unit/test_internalUris.js
@@ -0,0 +1,208 @@
+/* Tests internal URIs generated by various methods in the code base.
+ * If you manually generate a news URI somewhere, please add it to this test.
+ */
+
+load("../../../resources/logHelper.js");
+load("../../../resources/asyncTestUtils.js");
+load("../../../resources/alertTestUtils.js");
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let dummyMsgWindow = {
+ get statusFeedback() {
+ return {
+ startMeteors: function () {},
+ stopMeteors: function () {
+ async_driver();
+ },
+ showProgress: function () {}
+ };
+ },
+ get promptDialog() {
+ return alertUtilsPrompts;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMsgWindow,
+ Ci.nsISupportsWeakReference])
+};
+var daemon, localserver, server;
+
+var nntpService = Cc["@mozilla.org/messenger/nntpservice;1"]
+ .getService(Components.interfaces.nsINntpService);
+
+let tests = [
+ test_newMsgs,
+ test_cancel,
+ test_fetchMessage,
+ test_search,
+ test_grouplist,
+ test_postMessage,
+ cleanUp
+];
+
+function test_newMsgs() {
+ // This tests nsMsgNewsFolder::GetNewsMessages via getNewMessages
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ do_check_eq(folder.getTotalMessages(false), 0);
+ folder.getNewMessages(null, asyncUrlListener);
+ yield false;
+ do_check_eq(folder.getTotalMessages(false), 7);
+ yield true;
+}
+
+// Prompts for cancel
+function alert(title, text) {}
+function confirmEx(title, text, flags) { return 0; }
+
+function test_cancel() {
+ // This tests nsMsgNewsFolder::CancelMessage
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ let db = folder.msgDatabase;
+ let hdr = db.GetMsgHdrForKey(4);
+
+ let mailSession = Cc['@mozilla.org/messenger/services/session;1']
+ .getService(Ci.nsIMsgMailSession);
+ let atomService = Cc['@mozilla.org/atom-service;1']
+ .getService(Ci.nsIAtomService);
+ let kDeleteAtom = atomService.getAtom("DeleteOrMoveMsgCompleted");
+ let folderListener = {
+ OnItemEvent: function(aEventFolder, aEvent) {
+ if (aEvent == kDeleteAtom) {
+ mailSession.RemoveFolderListener(this);
+ }
+ }
+ };
+ mailSession.AddFolderListener(folderListener, Ci.nsIFolderListener.event);
+ folder.QueryInterface(Ci.nsIMsgNewsFolder)
+ .cancelMessage(hdr, dummyMsgWindow);
+ yield false;
+
+ do_check_eq(folder.getTotalMessages(false), 6);
+ yield true;
+}
+
+function test_fetchMessage() {
+ // Tests nsNntpService::CreateMessageIDURL via FetchMessage
+ var statuscode = -1;
+ let streamlistener = {
+ onDataAvailable: function() {},
+ onStartRequest: function() {
+ },
+ onStopRequest: function (aRequest, aContext, aStatus) {
+ statuscode = aStatus;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener,
+ Ci.nsIRequestObserver])
+ };
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ nntpService.fetchMessage(folder, 2, null, streamlistener, asyncUrlListener);
+ yield false;
+ do_check_eq(statuscode, Components.results.NS_OK);
+ yield true;
+}
+
+function test_search() {
+ // This tests nsNntpService::Search
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ var searchSession = Cc["@mozilla.org/messenger/searchSession;1"]
+ .createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.news, folder);
+
+ let searchTerm = searchSession.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.Subject;
+ let value = searchTerm.value;
+ value.str = 'First';
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ searchTerm.booleanAnd = false;
+ searchSession.appendTerm(searchTerm);
+
+ let hitCount;
+ let searchListener = {
+ onSearchHit: function (dbHdr, folder) { hitCount++; },
+ onSearchDone: function (status) {
+ searchSession.unregisterListener(this);
+ async_driver();
+ },
+ onNewSearch: function() { hitCount = 0; }
+ };
+ searchSession.registerListener(searchListener);
+
+ searchSession.search(null);
+ yield false;
+
+ do_check_eq(hitCount, 1);
+ yield true;
+}
+
+function test_grouplist() {
+ // This tests nsNntpService::GetListOfGroupsOnServer
+ let subserver = localserver.QueryInterface(Ci.nsISubscribableServer);
+ let subscribeListener = {
+ OnDonePopulating: function () { async_driver(); }
+ };
+ subserver.subscribeListener = subscribeListener;
+
+ function enumGroups(rootUri) {
+ let hierarchy = subserver.getChildren(rootUri);
+ let groups = [];
+ while (hierarchy.hasMoreElements()) {
+ let element = hierarchy.getNext().QueryInterface(Ci.nsIRDFResource);
+ let name = element.ValueUTF8;
+ name = name.slice(name.lastIndexOf("/") + 1);
+ if (subserver.isSubscribable(name))
+ groups.push(name);
+ if (subserver.hasChildren(name))
+ groups = groups.concat(enumGroups(name));
+ }
+ return groups;
+ }
+
+ nntpService.getListOfGroupsOnServer(localserver, null, false);
+ yield false;
+
+ let groups = enumGroups("");
+ for (let group in daemon._groups)
+ do_check_true(groups.indexOf(group) >= 0);
+ yield true;
+}
+
+function test_postMessage() {
+ // This tests nsNntpService::SetUpNntpUrlForPosting via PostMessage
+ nntpService.postMessage(do_get_file("postings/post2.eml"), "misc.test",
+ localserver.key, asyncUrlListener, null);
+ yield false;
+ do_check_eq(daemon.getGroup("misc.test").keys.length, 1);
+ yield true;
+}
+
+// Not tested because it requires UI, and this is insufficient, I think.
+function test_forwardInline() {
+ // This tests mime_parse_stream_complete via forwarding inline
+ let composeSvc = Cc['@mozilla.org/messengercompose;1']
+ .getService(Ci.nsIMsgComposeService);
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ let hdr = folder.msgDatabase.GetMsgHdrForKey(1);
+ composeSvc.forwardMessage("a@b.c", hdr, null,
+ localserver, Ci.nsIMsgComposeService.kForwardInline);
+}
+
+function run_test() {
+ daemon = setupNNTPDaemon();
+ localserver = setupLocalServer(NNTP_PORT);
+ server = new nsMailServer(new NNTP_RFC2980_handler(daemon));
+ server.start(NNTP_PORT);
+
+ // Set up an identity for posting
+ var acctmgr = Cc["@mozilla.org/messenger/account-manager;1"]
+ .getService(Ci.nsIMsgAccountManager);
+ let identity = acctmgr.createIdentity();
+ identity.fullName = "Normal Person";
+ identity.email = "fake@acme.invalid";
+ acctmgr.FindAccountForServer(localserver).addIdentity(identity);
+
+ async_run_tests(tests);
+}
+
+function cleanUp() {
+ localserver.closeCachedConnections();
+}
--- a/mailnews/news/test/unit/test_nntpPassword2.js
+++ b/mailnews/news/test/unit/test_nntpPassword2.js
@@ -77,16 +77,18 @@ function run_test() {
// Now set up and run the tests
setupProtocolTest(NNTP_PORT, prefix+"*", incomingServer);
server.performTest();
var transaction = server.playTransaction();
do_check_transaction(transaction, ["MODE READER", "LIST",
"AUTHINFO user testnews",
"AUTHINFO pass newstest", "LIST"]);
+ incomingServer.QueryInterface(Components.interfaces.nsISubscribableServer)
+ .subscribeCleanup();
} catch (e) {
dump("NNTP Protocol test "+test+" failed for type RFC 977:\n");
try {
var trans = server.playTransaction();
if (trans)
dump("Commands called: "+trans.them+"\n");
} catch (exp) {}
--- a/mailnews/news/test/unit/test_server.js
+++ b/mailnews/news/test/unit/test_server.js
@@ -40,43 +40,42 @@ function testRFC977() {
// Test - group subscribe listing
test = "news:*";
setupProtocolTest(NNTP_PORT, prefix+"*");
server.performTest();
transaction = server.playTransaction();
do_check_transaction(transaction, ["MODE READER", "LIST"]);
- // GROUP_WANTED fails without UI
// Test - getting group headers
- /*test = "news:test.empty";
+ test = "news:test.subscribe.empty";
server.resetTest();
- setupProtocolTest(NNTP_PORT, prefix+"test.empty");
+ setupProtocolTest(NNTP_PORT, prefix+"test.subscribe.empty");
server.performTest();
transaction = server.playTransaction();
- do_check_transaction(transaction, []);*/
+ do_check_transaction(transaction, ["MODE READER",
+ "GROUP test.subscribe.empty"]);
// Test - getting an article
test = "news:MESSAGE_ID";
server.resetTest();
setupProtocolTest(NNTP_PORT, prefix+"TSS1@nntp.test");
server.performTest();
transaction = server.playTransaction();
do_check_transaction(transaction, ["MODE READER",
"ARTICLE <TSS1@nntp.test>"]);
- // Broken because of folder brokenness
// Test - news expiration
- /*test = "news:GROUP/?list-ids";
+ test = "news:GROUP?list-ids";
server.resetTest();
- setupProtocolTest(NNTP_PORT, prefix+"test.subscribe.empty/?list-ids");
+ setupProtocolTest(NNTP_PORT, prefix+"test.filter?list-ids");
server.performTest();
transaction = server.playTransaction();
do_check_transaction(transaction, ["MODE READER",
- "LISTGROUP test.subscribe.empty"]);*/
+ "listgroup test.filter"]);
// Test - posting
test = "news with post";
server.resetTest();
var url = create_post(prefix, "postings/post1.eml");
setupProtocolTest(NNTP_PORT, url);
server.performTest();
transaction = server.playTransaction();
@@ -101,17 +100,18 @@ function testConnectionLimit() {
var handler = new NNTP_RFC977_handler(daemon);
var server = new nsMailServer(handler);
server.start(NNTP_PORT);
var prefix = "news://localhost:"+NNTP_PORT+"/";
var transaction;
// To test make connections limit, we run two URIs simultaneously.
- setupProtocolTest(NNTP_PORT, prefix+"*");
+ var url = URLCreator.newURI(prefix+"*", null, null);
+ _server.loadNewsUrl(url, null, null);
setupProtocolTest(NNTP_PORT, prefix+"TSS1@nntp.test");
server.performTest();
// We should have length one... which means this must be a transaction object,
// containing only us and them
do_check_true('us' in server.playTransaction());
server.stop();
var thread = gThreadManager.currentThread;
--- a/mailnews/test/fakeserver/nntpd.js
+++ b/mailnews/test/fakeserver/nntpd.js
@@ -98,16 +98,37 @@ function newsArticle(text) {
let lines = this.body.split('\n').length;
this.headers["lines"] = lines;
preamble += "Lines: "+lines;
}
this.fulltext = preamble + '\n' + this.body;
}
+/**
+ * This function converts an NNTP wildmat into a regular expression.
+ *
+ * I don't know how accurate it is wrt i18n characters, but it's primary usage
+ * right now is just XPAT, where i18n effects are utterly unspecified, so I am
+ * not too concerned.
+ *
+ * This also neglects cases where special characters are in [] blocks.
+ */
+function wildmat2regex(wildmat) {
+ // Special characters in regex that aren't special in wildmat
+ wildmat = wildmat.replace(/[$+.()|{}^]/, function (str) {
+ return "\\" + str;
+ });
+ wildmat = wildmat.replace(/(\\*)([*?])/, function (str, p1, p2) {
+ if (p1.length % 2 == 0)
+ return p2 == '*' ? '.*' : '.';
+ });
+ return new RegExp(wildmat);
+}
+
// NNTP FLAGS
const NNTP_POSTABLE = 0x0001;
const NNTP_REAL_LENGTH = 0x0100;
function hasFlag(flags, flag) {
return (flags & flag) == flag;
}
@@ -412,17 +433,17 @@ subclass(NNTP_RFC2980_handler, NNTP_RFC9
if (!found)
return "420 No such article";
response += '.';
return response;
},
XOVER : function (args) {
if (!this.group)
return "412 No group selected";
-
+
var response = "224 List of articles\n";
for each (var key in this.group.keys) {
response += key + "\t";
var article = this.group[key];
response += article.headers["subject"] + "\t" +
article.headers["from"] + "\t" +
article.headers["date"] + "\t" +
article.headers["message-id"] + "\t" +
@@ -431,17 +452,47 @@ subclass(NNTP_RFC2980_handler, NNTP_RFC9
article.fullText.replace(/\r?\n/,'\r\n').length + "\t" +
article.body.split(/\r?\n/).length + "\t" +
article.headers["xref"] + "\n";
}
response += '.\n';
return response;
},
XPAT : function (args) {
- return "502 Command not implemented";
+ if (!this.group)
+ return "412 No group selected";
+
+ /* XPAT header range ... */
+ args = args.split(/ +/, 3);
+ let header = args[0].toLowerCase();
+ let regex = wildmat2regex(args[2]);
+
+ let response = "221 Results follow\n";
+ for each (let key in this._filterRange(args[1], this.group.keys)) {
+ let article = this.group[key];
+ if (header in article.headers && regex.test(article.headers[header])) {
+ response += key + ' ' + article.headers[header] + '\n';
+ }
+ }
+ return response + '.';
+ },
+
+ _filterRange: function (range, keys) {
+ let dash = range.indexOf('-');
+ let low, high;
+ if (dash < 0) {
+ low = high = parseInt(range);
+ } else {
+ low = parseInt(range.substring(0, dash));
+ if (dash < range.length - 1)
+ high = range.substring(dash + 1);
+ else
+ high = 1.0 / 0.0; // Everything is less than this
+ }
+ return keys.filter(function (e) { return low <= e && e <= high; });
}
});
function NNTP_Giganews_handler(daemon) {
subconstructor(this, NNTP_RFC2980_handler, daemon);
}
subclass(NNTP_Giganews_handler, NNTP_RFC2980_handler, {
XHDR : function (args) {