From f19de9d134ae8ee42554842d3610c1ff6ba740cf Mon Sep 17 00:00:00 2001
From: Piotr Sarnacki <drogus@gmail.com>
Date: Thu, 8 Aug 2013 14:25:20 +0200
Subject: [PATCH] Implement jobs/:id/cancel and builds/:id/cancel endpoints

---
 Gemfile                               |  2 +-
 Gemfile.lock                          |  3 +-
 lib/travis/api/app/endpoint/builds.rb | 21 +++++++++++++
 lib/travis/api/app/endpoint/jobs.rb   | 21 +++++++++++++
 spec/integration/v2/builds_spec.rb    | 43 +++++++++++++++++++++++++++
 spec/integration/v2/jobs_spec.rb      | 42 ++++++++++++++++++++++++++
 6 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/Gemfile b/Gemfile
index 4ebc17f3..aa3d5661 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@ ruby '1.9.3' rescue nil
 source 'https://rubygems.org'
 gemspec
 
-gem 'travis-core',     github: 'travis-ci/travis-core'
+gem 'travis-core',     github: 'travis-ci/travis-core', branch: 'ps-cancel'
 gem 'travis-support',  github: 'travis-ci/travis-support'
 gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs', require: nil, ref: 'cde9741'
 gem 'sinatra'
diff --git a/Gemfile.lock b/Gemfile.lock
index 139f61d9..8de84c79 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -31,7 +31,8 @@ GIT
 
 GIT
   remote: git://github.com/travis-ci/travis-core.git
-  revision: 0a194c97c857a39c03a5a02eec283e751099d402
+  revision: fa4e4051d35da428d9d67e1c2e2230568f3c2653
+  branch: ps-cancel
   specs:
     travis-core (0.0.1)
       actionmailer (~> 3.2.12)
diff --git a/lib/travis/api/app/endpoint/builds.rb b/lib/travis/api/app/endpoint/builds.rb
index f956c7ad..98395afc 100644
--- a/lib/travis/api/app/endpoint/builds.rb
+++ b/lib/travis/api/app/endpoint/builds.rb
@@ -12,6 +12,27 @@ class Travis::Api::App
       get '/:id' do
         respond_with service(:find_build, params)
       end
+
+      post '/:id/cancel' do
+        service = self.service(:cancel_build, params)
+        if !service.authorized?
+          json = { error: {
+            message: "You don't have access to cancel build(#{params[:id]})"
+          } }
+          status 403
+          respond_with json
+        elsif !service.can_cancel?
+          json = { error: {
+            message: "The build(#{params[:id]}) can't be canceled",
+            code: 'cant_cancel'
+          } }
+          status 422
+          respond_with json
+        else
+          service.run
+          status 204
+        end
+      end
     end
   end
 end
diff --git a/lib/travis/api/app/endpoint/jobs.rb b/lib/travis/api/app/endpoint/jobs.rb
index 966ef7cd..2ac3ba94 100644
--- a/lib/travis/api/app/endpoint/jobs.rb
+++ b/lib/travis/api/app/endpoint/jobs.rb
@@ -36,5 +36,26 @@ class Travis::Api::App
         "#{name}#{'-staging' if Travis.env == 'staging'}.#{Travis.config.host.split('.')[-2, 2].join('.')}"
       end
     end
+
+    post '/:id/cancel' do
+      service = self.service(:cancel_job, params)
+      if !service.authorized?
+        json = { error: {
+          message: "You don't have access to cancel job(#{params[:id]})"
+        } }
+        status 403
+        respond_with json
+      elsif !service.can_cancel?
+        json = { error: {
+          message: "The job(#{params[:id]}) can't be canceled",
+          code: 'cant_cancel'
+        } }
+        status 422
+        respond_with json
+      else
+        service.run
+        status 204
+      end
+    end
   end
 end
diff --git a/spec/integration/v2/builds_spec.rb b/spec/integration/v2/builds_spec.rb
index 8410374f..325ece4f 100644
--- a/spec/integration/v2/builds_spec.rb
+++ b/spec/integration/v2/builds_spec.rb
@@ -46,4 +46,47 @@ describe 'Builds' do
     response = get "/builds?repository_id=#{repo.id}&branches=true", {}, headers
     response.should deliver_json_for(repo.last_finished_builds_by_branches, version: 'v2')
   end
+
+  describe 'POST /builds/:id/cancel' do
+    let(:user)    { User.where(login: 'svenfuchs').first }
+    let(:token)   { Travis::Api::App::AccessToken.create(user: user, app_id: -1) }
+
+    before {
+      headers.merge! 'HTTP_AUTHORIZATION' => "token #{token}"
+      user.permissions.create!(repository_id: build.repository.id, :push => true)
+    }
+
+    context 'when user does not have rights to cancel the build' do
+      before { user.permissions.destroy_all }
+
+      it 'responds with 403' do
+        response = post "/builds/#{build.id}/cancel", {}, headers
+        response.status.should == 403
+      end
+    end
+
+    context 'when build is not cancelable' do
+      before { build.matrix.each { |j| j.update_attribute(:state, 'passed') } }
+
+      it 'responds with 422' do
+        response = post "/builds/#{build.id}/cancel", {}, headers
+        response.status.should == 422
+      end
+    end
+
+    context 'when build can be canceled' do
+      it 'cancels the build and responds with 204' do
+        build.matrix.each { |j| j.update_attribute(:state, 'created') }
+        build.update_attribute(:state, 'created')
+
+        response = nil
+        expect {
+          response = post "/builds/#{build.id}/cancel", {}, headers
+        }.to change { build.reload.state }
+        response.status.should == 204
+
+        build.state.should == 'canceled'
+      end
+    end
+  end
 end
diff --git a/spec/integration/v2/jobs_spec.rb b/spec/integration/v2/jobs_spec.rb
index 4c7d202e..8dcc7a0c 100644
--- a/spec/integration/v2/jobs_spec.rb
+++ b/spec/integration/v2/jobs_spec.rb
@@ -75,4 +75,46 @@ describe 'Jobs' do
       end
     end
   end
+
+  describe 'POST /jobs/:id/cancel' do
+    let(:user)    { User.where(login: 'svenfuchs').first }
+    let(:token)   { Travis::Api::App::AccessToken.create(user: user, app_id: -1) }
+
+    before {
+      headers.merge! 'HTTP_AUTHORIZATION' => "token #{token}"
+      user.permissions.create!(repository_id: job.repository.id, :push => true)
+    }
+
+    context 'when user does not have rights to cancel the job' do
+      before { user.permissions.destroy_all }
+
+      it 'responds with 403' do
+        response = post "/jobs/#{job.id}/cancel", {}, headers
+        response.status.should == 403
+      end
+    end
+
+    context 'when job is not cancelable' do
+      before { job.update_attribute(:state, 'passed') }
+
+      it 'responds with 422' do
+        response = post "/jobs/#{job.id}/cancel", {}, headers
+        response.status.should == 422
+      end
+    end
+
+    context 'when job can be canceled' do
+      it 'cancels the job and responds with 204' do
+        job.update_attribute(:state, 'created')
+
+        response = nil
+        expect {
+          response = post "/jobs/#{job.id}/cancel", {}, headers
+        }.to change { job.reload.state }
+        response.status.should == 204
+
+        job.state.should == 'canceled'
+      end
+    end
+  end
 end