Skip to content

Instantly share code, notes, and snippets.

@poiyzy
Last active Dec 25, 2015
Embed
What would you like to do?

The use case:

  • When a new ticket in one project was created, all the users of this project will receive a notify email whose address is "ticket+PROJECT_UID+TICKET_UID@info.pragmatic.ly"

  • When a new iteration in one project was created, all the users of this project will receive a notify email whose address is "iteration+PROJECT_UID+TICKET_UID@info.pragmatic.ly"

  • We use SendCloud mail server, when it received an email replied by user, it will post the json data to the app route "/api/email_replies", we had defined in SendCloud.

  • When the app received the post request, first, it needs to verify that the request is posted from SendCloud. We get the authentication strategy from SendCloud Api.

  • Then the send email address is valid, and the project uid is valid, the ticket or iteration uid is valid, and the user should have the right to access this porject.

  • When the email includes attachment, it will update the attachment to website.

Read this use case, when SendCloud received a email, it will post it to the app callback url. So it needs a controller to handle this pull request.

class EmailRepliesController < ApplicationController
  def create
  end
end

Now, let's write the test. First, I need to write the test case. Follow the test case, I need to write some test examples.

When the first post request posted to this controller, first I need to verify whether it is a valid request. I expect when it is a valid post request, it should return status 200, when it is not valid request, it should return status 422.

And then, I need to ask myself, what means the valid post request? As the use case said, I need to verify the post request is posted from SendCloud. I checked the webhook api autentication strategy api of SendCloud:

Use SendCloud of Webhooks, need to use the application key (appKey) for verification of information. You can log SendCloud site for application key (appKey) settings. Webhooks use SendCloud for your application key information to create a signature in order to verify the source of information whether you are SendCloud. Signature string together with event-related parameters that you configure POST to a URL.

In order to verify the information received did come from SendCloud, you need to make the following certification:

(1) the timestamp and token joint;
(2) use the HMAC algorithm encrypted string (appkey as a parameter and use the SHA256 hash method);
(3) compare signature and the resulting encrypted string.

So I need to generate 4 parameters api_key, token, timestamps and signature, and post them to create action.

describe EmailRepiesController < ApplicationController
  describe "POST create" do
    context "when the post request is a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          api_key,
                          '%s%s' % [timestamp, token])

      it "returns status 200" do
        post :create, timestamp: timestamps, token: token, signature: signature
        response.status.should == 200
      end
    end

    context "when the post request is not a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          "123123",
                          #here I changed the api_key, expect the verification to be failed.
                          '%s%s' % [timestamp, token])

      it "returns status 422" do
        post :create, timestamp: timestaps, token: token, signature: signature
        response.status.should == 422
      end
    end
  end
end

Now, I need to write the implementation code. By the way, because we customized the verification strategy, so here we don't need to use Rails CSRF protection.

class EmailRepliesController < ApplicationController

  skip_before_filter :verify_authenticity_token

  def create
    verify_result = params[:signature] == OpenSSL::HMAC.hexdigest(
                                            OpenSSL::Digest::Digest.new('sha256'),
                                            api_key,
                                            '%s%s' % [params[:timestamp], params[:token]])
    if verify_result
      head(200)
    else
      head(422)
    end
  end
end

After verifying that the post request is posted from SendCloud, I need to check the received email address is whether valid or not. First, I need to test the porject uid is whether valid, and then I need to test the send email address is whether valid. If they are all valid, I need to make sure that the user has the right to access this porject. After that I need to check this email is tend to create a comment for ticket or iteration, and then I need to make sure that the ticket or iteration uid is valid.

Ok, let's write the test first.

describe EmailRepiesController < ApplicationController
  describe "POST create" do
    context "when the post request is a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          api_key,
                          '%s%s' % [timestamp, token])

      it "returns status 200" do
        post :create, timestamp: timestamps, token: token, signature: signature
        response.status.should == 200
      end

      context "when the project uid is valid" do
        context "when the user uid is valid" do
          context "when the user has the right to access this project" do
            context "when the email is tend to create a comment of ticket" do
              context "when the ticket uid is valid" do
                it "creates the comment for iteration"
              end

              context "when the ticket uid is invalid" do
                it "doesn't create the comment"
              end
            end

            context "when the email is tend to create a comment of iteration" do
              context "when the iteration uid is valid" do
                it "creates the comment for iteration"
              end

              context "when the iteration uid is invalid" do
                it "doesn't create the comment"
              end
            end
          end

          context "when the user has no right to access this project" do
            it "doesn't create the comment"
          end
        end

        context "when the user uid is invalid" dp
          it "doesn't create the comment"
        end
      end

      context "when the project uid is invalid" do
        it "doesn't create the comment"
      end
    end

    context "when the post request is not a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          "123123",
                          #here I changed the api_key, expect the verification to be failed.
                          '%s%s' % [timestamp, token])

      it "returns status 422" do
        post :create, timestamp: timestaps, token: token, signature: signature
        response.status.should == 422
      end
    end
  end
end

After I wrote the test, I found that if I want to create a comment, all of the conditions need to be true, or it will not create a comment. So maybe I can call the conditions a name "valid email replies", and rewrite the test case like below:

describe EmailRepiesController < ApplicationController
  describe "POST create" do
    context "when the post request is a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          api_key,
                          '%s%s' % [timestamp, token])

      it "returns status 200" do
        post :create, timestamp: timestamps, token: token, signature: signature
        response.status.should == 200
      end

      context "when the project uid is invalid" do
        it "doesn't create the comment"
      end

      context "when the email replies is valid" do
        it "creates the comment"
      end
    end

    context "when the post request is not a valid request" do
      let(:api_key) { "234234" }

      let(:timestamp) { "201310011225" }
      let(:token) { "123123" }
      let(:signature) { OpenSSL::HMAC.hexdigest(
                          OpenSSL::Digest::Digest.new('sha256'),
                          "123123",
                          #here I changed the api_key, expect the verification to be failed.
                          '%s%s' % [timestamp, token])

      it "returns status 422" do
        post :create, timestamp: timestaps, token: token, signature: signature
        response.status.should == 422
      end
    end
  end
end

After that, I found now maybe I can abstract a new class to handle all of that thing, the controller will not care about the project, user, ticket or iteration, the controller just need to send the parameters to another class and let it to handle that.

So now, I don't care about whether the controller will create a comment for iteration or ticket, I just care about whether the controller will send the right parameters to the next class after the post request verification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment