16

I need to test a protractor test case in which a user signs up, receives an email, goes to the link provided in the email and fills up his/her details in activation signup form.

The problem is how can I get the redeem token from the email. My email has a link to the activation page which has the auth token like following:

http://127.0.0.1:3000/#/signup/redeem/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlOTRhYzY3MC1kYTNlLTQyYTUtODVkZS02NDU4ZjVmZGMwYjAiLCJzdWIiOiJ0ZXN0QGNvZWYuY28iLCJpYXQiOjE0Mjc0OTM5MDMsImV4cCI6MTQyODA5ODcwM30.

But how do I fetch that token so that I can build the url or how can I click that button in my email so that I can complete the flow ? I am using mailcatcher to simulate email.

alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
Ashish Gaur
  • 2,030
  • 2
  • 18
  • 32
  • disposable webmail systems like Inbucket will also help to solve this scenario. You can fetch email by using it's REST API - https://github.com/jhillyerd/inbucket/wiki/REST-API – jithinkmatthew Apr 05 '18 at 14:19

3 Answers3

32

This is something I've solved recently. Hope the solution would also apply for your use-case.

Prerequisites:

Step by step instructions:

  1. Install mail-listener2:

    npm install mail-listener2 --save-dev
    
  2. In your protractor config initialize Mail Listener and make it available globally:

    onPrepare: function () {
        var MailListener = require("mail-listener2");
    
        // here goes your email connection configuration
        var mailListener = new MailListener({
            username: "imap-username",
            password: "imap-password",
            host: "imap-host",
            port: 993, // imap port 
            tls: true,
            tlsOptions: { rejectUnauthorized: false },
            mailbox: "INBOX", // mailbox to monitor 
            searchFilter: ["UNSEEN", "FLAGGED"], // the search filter being used after an IDLE notification has been retrieved 
            markSeen: true, // all fetched email willbe marked as seen and not fetched next time 
            fetchUnreadOnStart: true, // use it only if you want to get all unread email on lib start. Default is `false`, 
            mailParserOptions: {streamAttachments: true}, // options to be passed to mailParser lib. 
            attachments: true, // download attachments as they are encountered to the project directory 
            attachmentOptions: { directory: "attachments/" } // specify a download directory for attachments 
        });
    
        mailListener.start();
    
        mailListener.on("server:connected", function(){
            console.log("Mail listener initialized");
        });
    
        global.mailListener = mailListener;
    }),
    
    onCleanUp: function () {
        mailListener.stop();
    }, 
    
  3. Create a helper getLastEmail() function which would wait for an email to be retrieved:

    function getLastEmail() {
        var deferred = protractor.promise.defer();
        console.log("Waiting for an email...");
    
        mailListener.on("mail", function(mail){
            deferred.fulfill(mail);
        });
        return deferred.promise;
    };
    
  4. Example test case:

    describe("Sample test case", function () {
    
        beforeEach(function () {
            browser.get("/#login");
            browser.waitForAngular();
        });
    
        it("should login with a registration code sent to an email", function () {
            element(by.id("username")).sendKeys("MyUserName");
            element(by.id("password")).sendKeys("MyPassword");
            element(by.id("loginButton")).click();
    
            browser.controlFlow().await(getLastEmail()).then(function (email) {
                expect(email.subject).toEqual("New Registration Code");
                expect(email.headers.to).toEqual("myemail@email.com");
    
                // extract registration code from the email message
                var pattern = /Registration code is: (\w+)/g;
                var regCode = pattern.exec(email.text)[1];
    
                console.log(regCode);
    
             });
        });
    });
    
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • Thanks for the answer, I'll take a look at it and will accept your answer. – Ashish Gaur Apr 06 '15 at 06:58
  • @ashish thanks, I've been using the provided approach for a while and it works for me perfectly. Hope you can say the same. – alecxe Apr 20 '15 at 12:12
  • Hey @alecxe, my requirement is to receive the mails directly while your solution can be used to read from a mail server. We are using mail catcher to fetch the mail but mail-listener2 cannot read it because its an SMTP server. As a solution I am using mailcatcher api directly to parse the messages and fetch the token. – Ashish Gaur Apr 21 '15 at 14:20
  • @ashish could you please post your solution here as an answer? I think this might be useful to the topic visitors. Thanks! – alecxe Dec 26 '15 at 01:49
  • Really sorry for being this late, I've posted my solution hope it helps you and the community. :) – Ashish Gaur Jan 22 '16 at 13:54
  • @alecxe does this code still work for you? Implementing an email validation test suite today and I'm considering both options (the mail-listener 2 and mailcatcher api) – Gunderson Jun 16 '16 at 11:56
  • @Gunderson yup, we've just switched to a different email provider and `mail-listener2` still works for us. – alecxe Jun 16 '16 at 15:20
  • @alecxe awesome just tried it, incredibly easy to setup. Pretty much just copied and pasted your code :) thanks – Gunderson Jun 16 '16 at 16:42
  • @alecxe Have you had any sporadic connection issues? This works great 95% of the time, but every once in awhile I get an instant failure onPrepare from imap `Timed out while authenticating with server` -- I see some config options for `connTimeout` and `authTimeout` but it doesnt seem to wait for the time specified in those options – Gunderson Jun 20 '16 at 11:00
  • @Gunderson yeah, after switching to the new mail server we do have these kind of errors from time to time very rarely..do you have `tlsOptions: { rejectUnauthorized: false }` setting set? – alecxe Jun 20 '16 at 12:10
  • Yep I have that set to false – Gunderson Jun 20 '16 at 12:17
  • @alecxe: nice solution... trying to use it for an E2E test for an Angular app, but I face the following error regarding the `email.subject`: `Property 'subject' does not exist on type '{}'` and the same for: `email.headers` `email.text`. Any idea pls.? – k.vincent Jan 31 '18 at 15:58
  • @alecxe: I fixed the issue regarding: `email.subject` and `email.text` by just changing it to: `email['subject']` and `email['text']`. But now facing the error: `- Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.` when trying to call/invoke the function: `getLastEmail()`. Any idea pls.? – k.vincent Feb 05 '18 at 13:46
  • @alecxe In my case I get a link for email that I need to click in this link to complete the registration of my user. Any ideia to manipulate this steps after access the email with mail-listener2? Thanks! – Rafael C. Jun 14 '18 at 18:41
  • I am getting an error `(node:49611) UnhandledPromiseRejectionWarning: TypeError: deferred.fulfill is not a function` – umutesen Jul 31 '19 at 07:07
7

The solution I implemented was using mailcatcher API, if you scroll down a bit you'll find the following about the API:

A fairly RESTful URL schema means you can download a list of messages in JSON from /messages, each message's metadata with /messages/:id.json, and then the pertinent parts with /messages/:id.html and /messages/:id.plain for the default HTML and plain text version, /messages/:id/:cid for individual attachments by CID, or the whole message with /messages/:id.source.

So we first fetched the whole json response, parse it and fetch the latest email id:

// Returns the last email id
function(emails, user) {
    var email, recipient;
    for(var i = emails.length - 1; i >= 0; i--) {
        email = emails[i];
        for(var j = 0; j < email.recipients.length ; j++) {
            recipient = email.recipients[j];
            if(recipient == "<"+user+">") {
                return email.id;
            }
        }
    }
};

using that email id we can get the body of the email by hitting /messages/:id.plain(of course there are more variants like fetching the email source code or email rendered html, we only needed the message) then we can just parse the body to fetch what we want, following is the code:

browser.driver.get(mailcatcherUrl+"/messages");
browser.driver.findElement(by.tagName('body')).getText().then(function(response) {
    var emails, lastEmailId, partialTokens ;
    emails = JSON.parse(response);
    lastEmailId = getLastEmailId(emails, user);
    browser.driver.get(mailcatcherUrl+"/messages/"+lastEmailId+".plain");
    browser.driver.findElement(by.tagName('body')).getText().then(function(lastEmail) {
        // use latestEmail to get what you want.
    });
});

And Cheers!

Ashish Gaur
  • 2,030
  • 2
  • 18
  • 32
0

I had to do the same thing but the mail testing server we were using did not have imap support. So in case anyone runs into the same issue, I achieved a similar solution as alecxe using mailpop3 npm library.

The thing with the pop3 client, however, was that it doesn't act as a listener so we had to define a helper function that would connect, login and fetch the latest email when we needed to test the latest email.

Something like this:

function getLastEmail() {
  var deferred = protractor.promise.defer();
  var POP3Client = require("mailpop3");
  var client = new POP3Client(port, host, {
          tlserrs: false,
          enabletls: true,
          debug: false
      });

  client.on("connect", function() {
  
          console.log("CONNECT success");
          client.login(username, password);
  
  });

  client.on("login", function(status, rawdata) {
  
      if (status) {
  
          console.log("LOGIN/PASS success");
          client.retr(1);
  
      } else {
  
          console.log("LOGIN/PASS failed");
          client.quit();
  
      }
  });

  client.on("retr", function(status, msgnumber, data, rawdata) {
  
      if (status === true) {
  
          console.log("RETR success for msgnumber " + msgnumber);
          deferred.fulfill(data);
  
      } else {
  
          console.log("RETR failed for msgnumber " + msgnumber);
    
      }
      client.quit();
  });
  return deferred.promise;
}

Kaustubh
  • 11
  • 2