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.