Bug 226890 - Thunderbird doesn't handle news URIs properly, part 2: Implement news URIs tests. r=Standard8
authorJoshua Cranmer <Pidgeot18@gmail.com>
Mon, 15 Nov 2010 18:43:43 -0500
changeset 7098 78440aa35ff6451d684f32212d4f25a69ed5b3ea
parent 7097 3bd81d3e9ddf19c577b795a40cb032bce908d4bb
child 7099 058149fc39307ee8ed5499ba5a9a9b2419856af0
push idunknown
push userunknown
push dateunknown
reviewersStandard8
bugs226890
Bug 226890 - Thunderbird doesn't handle news URIs properly, part 2: Implement news URIs tests. r=Standard8
mailnews/news/src/nsNNTPArticleList.cpp
mailnews/news/test/unit/head_server_setup.js
mailnews/news/test/unit/postings/post2.eml
mailnews/news/test/unit/tail_news.js
mailnews/news/test/unit/test_internalUris.js
mailnews/news/test/unit/test_nntpPassword2.js
mailnews/news/test/unit/test_server.js
mailnews/test/fakeserver/nntpd.js
--- 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) {