Merge pull request #220 from travis-ci/cd-v3

v3: adds star, unstar endpoints, adds starred filter, adds specs
This commit is contained in:
carlad 2015-12-01 17:30:26 +01:00
commit f7cd49f194
22 changed files with 463 additions and 27 deletions

View File

@ -369,6 +369,3 @@ DEPENDENCIES
travis-yaml!
unicorn
yard-sinatra!
BUNDLED WITH
1.10.6

View File

@ -6,6 +6,7 @@ module Travis::API::V3
has_many :builds, dependent: :delete_all, order: 'builds.id DESC'.freeze
has_many :permissions, dependent: :delete_all
has_many :users, through: :permissions
has_many :stars
belongs_to :owner, polymorphic: true
belongs_to :last_build, class_name: 'Travis::API::V3::Models::Build'.freeze

View File

@ -0,0 +1,6 @@
module Travis::API::V3
class Models::Star < Model
belongs_to :user
belongs_to :repository
end
end

View File

@ -6,6 +6,7 @@ module Travis::API::V3
has_many :tokens, dependent: :destroy
has_many :organizations, through: :memberships
has_many :repositories, as: :owner
has_many :stars
has_one :subscription, as: :owner
serialize :github_oauth_token, Extensions::EncryptedColumn.new(disable: true)
@ -17,5 +18,9 @@ module Travis::API::V3
def subscription
super if Features.use_subscriptions?
end
def starred_repository_ids
@starred_repository_ids ||= stars.map(&:repository_id)
end
end
end

View File

@ -10,6 +10,14 @@ module Travis::API::V3
write?
end
def star?
write?
end
def unstar?
write?
end
def create_request?
write?
end

View File

@ -1,26 +1,34 @@
module Travis::API::V3
class Queries::Repositories < Query
params :active, :private, prefix: :repository
params :active, :private, :starred, prefix: :repository
sortable_by :id, :github_id, :owner_name, :name, active: sort_condition(:active)
def for_member(user)
all.joins(:users).where(users: user_condition(user), invalidated_at: nil)
def for_member(user, **options)
all(user: user, **options).joins(:users).where(users: user_condition(user), invalidated_at: nil)
end
def for_owner(owner)
filter(owner.repositories)
def for_owner(owner, **options)
filter(owner.repositories, **options)
end
def all
@all ||= filter(Models::Repository)
def all(**options)
filter(Models::Repository, **options)
end
def filter(list)
def filter(list, user: nil)
list = list.where(invalidated_at: nil)
list = list.where(active: bool(active)) unless active.nil?
list = list.where(private: bool(private)) unless private.nil?
list = list.includes(:owner) if includes? 'repository.owner'.freeze
if user and not starred.nil?
if bool(starred)
list = list.joins(:stars).where(stars: { user_id: user.id })
elsif user.starred_repository_ids.any?
list = list.where("repositories.id NOT IN (?)", user.starred_repository_ids)
end
end
if includes? 'repository.last_build'.freeze or includes? 'build'.freeze
list = list.includes(:last_build)
list = list.includes(last_build: :commit) if includes? 'build.commit'.freeze

View File

@ -3,13 +3,31 @@ module Travis::API::V3
params :id, :slug
def find
@find ||= find!
end
def star(current_user)
repository = find
starred = Models::Star.where(repository_id: repository.id, user_id: current_user.id).first
Models::Star.create(repository_id: repository.id, user_id: current_user.id) unless starred
repository
end
def unstar(current_user)
repository = find
starred = Models::Star.where(repository_id: repository.id, user_id: current_user.id).first
starred.delete if starred
repository
end
private
def find!
return by_slug if slug
return Models::Repository.find_by_id(id) if id
raise WrongParams, 'missing repository.id'.freeze
end
private
def by_slug
owner_name, name = slug.split('/')
Models::Repository.where(owner_name: owner_name, name: name, invalidated_at: nil).first

View File

@ -3,7 +3,7 @@ require 'travis/api/v3/renderer/model_renderer'
module Travis::API::V3
class Renderer::Repository < Renderer::ModelRenderer
representation(:minimal, :id, :name, :slug)
representation(:standard, :id, :name, :slug, :description, :github_language, :active, :private, :owner, :default_branch)
representation(:standard, :id, :name, :slug, :description, :github_language, :active, :private, :owner, :default_branch, :starred)
def active
!!model.active
@ -19,6 +19,11 @@ module Travis::API::V3
}
end
def starred
return false unless user = access_control.user
user.starred_repository_ids.include? id
end
def include_default_branch?
return true if include? 'repository.default_branch'.freeze
return true if include.any? { |i| i.start_with? 'branch'.freeze }

View File

@ -69,6 +69,8 @@ module Travis::API::V3
post :enable, '/enable'
post :disable, '/disable'
post :star, '/star'
post :unstar, '/unstar'
resource :branch do
route '/branch/{branch.name}'

View File

@ -1,6 +1,6 @@
module Travis::API::V3
class Services::Repositories::ForCurrentUser < Service
params :active, :private, prefix: :repository
params :active, :private, :starred, prefix: :repository
paginate(default_limit: 100)
def run!

View File

@ -1,10 +1,10 @@
module Travis::API::V3
class Services::Repositories::ForOwner < Service
params :active, :private, prefix: :repository
params :active, :private, :starred, prefix: :repository
paginate(default_limit: 100)
def run!
unfiltered = query.for_owner(find(:owner))
unfiltered = query.for_owner(find(:owner), user: access_control.user)
access_control.visible_repositories(unfiltered)
end
end

View File

@ -0,0 +1,15 @@
module Travis::API::V3
class Services::Repository::Star < Service
def run!
raise LoginRequired unless access_control.logged_in? or access_control.full_access?
raise NotFound unless repository = find(:repository)
check_access(repository)
current_user = access_control.user
query.star(current_user)
end
def check_access(repository)
access_control.permissions(repository).star!
end
end
end

View File

@ -0,0 +1,15 @@
module Travis::API::V3
class Services::Repository::Unstar < Service
def run!
raise LoginRequired unless access_control.logged_in? or access_control.full_access?
raise NotFound unless repository = find(:repository)
check_access(repository)
current_user = access_control.user
query.unstar(current_user)
end
def check_access(repository)
access_control.permissions(repository).unstar!
end
end
end

View File

@ -64,7 +64,7 @@ describe Travis::API::V3::ServiceIndex do
describe "for_current_user action" do
let(:action) { resource.fetch("actions").fetch("for_current_user") }
specify { expect(action).to include("@type"=>"template", "request_method"=>"GET", "uri_template"=>"#{path}repos{?active,include,limit,offset,private,repository.active,repository.private,sort_by}") }
specify { expect(action).to include("@type"=>"template", "request_method"=>"GET", "uri_template"=>"#{path}repos{?active,include,limit,offset,private,repository.active,repository.private,repository.starred,sort_by,starred}") }
end
end

View File

@ -64,6 +64,8 @@ describe Travis::API::V3::Services::Owner::Find do
"read" => true,
"enable" => false,
"disable" => false,
"star" => false,
"unstar" => false,
"create_request" => false},
"id" => repo.id,
"name" => "example-repo",
@ -77,7 +79,8 @@ describe Travis::API::V3::Services::Owner::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}]
}}
end
@ -108,6 +111,8 @@ describe Travis::API::V3::Services::Owner::Find do
"read" => true,
"enable" => false,
"disable" => false,
"star" => false,
"unstar" => false,
"create_request"=> false},
"id" => repo.id,
"name" => "example-repo",
@ -121,7 +126,8 @@ describe Travis::API::V3::Services::Owner::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation"=> "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}]
}}
end

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe Travis::API::V3::Services::Repositories::ForCurrentUser do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
let(:build) { repo.builds.first }
let(:jobs) { Travis::API::V3::Models::Build.find(build.id).jobs }
@ -42,6 +42,8 @@ describe Travis::API::V3::Services::Repositories::ForCurrentUser do
"read" => true,
"enable" => true,
"disable" => true,
"star" => true,
"unstar" => true,
"create_request" => true},
"id" => repo.id,
"name" => "minimal",
@ -59,7 +61,9 @@ describe Travis::API::V3::Services::Repositories::ForCurrentUser do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}}]
"name" => "master"},
"starred" => false
}]
}}
end
@ -81,4 +85,29 @@ describe Travis::API::V3::Services::Repositories::ForCurrentUser do
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['repositories']) .to be == [] }
end
describe "filter: starred=true" do
before { Travis::API::V3::Models::Star.create(user: repo.owner, repository: repo) }
before { get("/v3/repos", {"starred" => "true"}, headers) }
after { repo.owner.stars.each(&:destroy) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=true" }
example { expect(JSON.load(body)['repositories']) .not_to be_empty }
end
describe "filter: starred=false" do
before { get("/v3/repos", {"starred" => "false"}, headers) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=false" }
example { expect(JSON.load(body)['repositories']) .not_to be_empty }
end
describe "filter: starred=false but no unstarred repos" do
before { Travis::API::V3::Models::Star.create(user: repo.owner, repository: repo) }
after { repo.owner.stars.each(&:destroy) }
before { get("/v3/repos", {"starred" => "false"}, headers) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=false" }
example { expect(JSON.load(body)['repositories']) .to be_empty }
end
end

View File

@ -42,6 +42,8 @@ describe Travis::API::V3::Services::Repositories::ForOwner do
"read" => true,
"enable" => false,
"disable" => false,
"star" => false,
"unstar" => false,
"create_request" => false},
"id" => repo.id,
"name" => "minimal",
@ -59,7 +61,9 @@ describe Travis::API::V3::Services::Repositories::ForOwner do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}}]}}
"name" => "master"},
"starred" => false
}]}}
end
describe "filter: private=false" do
@ -74,4 +78,29 @@ describe Travis::API::V3::Services::Repositories::ForOwner do
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['repositories']) .to be == [] }
end
describe "filter: starred=true" do
before { Travis::API::V3::Models::Star.create(user: repo.owner, repository: repo) }
before { get("/v3/repos", {"starred" => "true"}, headers) }
after { repo.owner.stars.each(&:destroy) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=true" }
example { expect(JSON.load(body)['repositories']) .not_to be_empty }
end
describe "filter: starred=false" do
before { get("/v3/repos", {"starred" => "false"}, headers) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=false" }
example { expect(JSON.load(body)['repositories']) .not_to be_empty }
end
describe "filter: starred=false but no unstarred repos" do
before { Travis::API::V3::Models::Star.create(user: repo.owner, repository: repo) }
after { repo.owner.stars.each(&:destroy) }
before { get("/v3/repos", {"starred" => "false"}, headers) }
example { expect(last_response) .to be_ok }
example { expect(JSON.load(body)['@href']) .to be == "/v3/repos?starred=false" }
example { expect(JSON.load(body)['repositories']) .to be_empty }
end
end

View File

@ -0,0 +1,72 @@
require 'spec_helper'
describe Travis::API::V3::Services::Repository::Disable do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
before do
repo.update_attributes!(active: true)
end
describe "not authenticated" do
before { post("/v3/repo/#{repo.id}/disable") }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "login_required",
"error_message" => "login required"
}}
end
describe "missing repo, authenticated" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/9999999999/disable", {}, headers) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, no push access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/#{repo.id}/disable", {}, headers) }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body).to_s).to include(
"@type",
"error_type",
"insufficient_access",
"error_message",
"operation requires disable access to repository",
"resource_type",
"repository",
"permission",
"disable")
}
end
describe "private repository, no access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { repo.update_attribute(:private, true) }
before { post("/v3/repo/#{repo.id}/disable", {}, headers) }
after { repo.update_attribute(:private, false) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, push access"
# as this reqires a call to github, and stubbing this request has proven difficult,
# this test has been omitted for now
end

View File

@ -0,0 +1,72 @@
require 'spec_helper'
describe Travis::API::V3::Services::Repository::Enable do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
before do
repo.update_attributes!(active: false)
end
describe "not authenticated" do
before { post("/v3/repo/#{repo.id}/enable") }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "login_required",
"error_message" => "login required"
}}
end
describe "missing repo, authenticated" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/9999999999/enable", {}, headers) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, no push access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/#{repo.id}/enable", {}, headers) }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body).to_s).to include(
"@type",
"error_type",
"insufficient_access",
"error_message",
"operation requires enable access to repository",
"resource_type",
"repository",
"permission",
"enable")
}
end
describe "private repository, no access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { repo.update_attribute(:private, true) }
before { post("/v3/repo/#{repo.id}/enable", {}, headers) }
after { repo.update_attribute(:private, false) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, push access"
# as this reqires a call to github, and stubbing this request has proven difficult,
# this test has been omitted for now
end

View File

@ -34,6 +34,8 @@ describe Travis::API::V3::Services::Repository::Find do
"read" => true,
"enable" => false,
"disable" => false,
"star" => false,
"unstar" => false,
"create_request" => false},
"id" => repo.id,
"name" => "minimal",
@ -51,7 +53,8 @@ describe Travis::API::V3::Services::Repository::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}}
end
@ -108,6 +111,8 @@ describe Travis::API::V3::Services::Repository::Find do
"read" => true,
"enable" => false,
"disable" => false,
"star" => false,
"unstar" => false,
"create_request" => false},
"id" => repo.id,
"name" => "minimal",
@ -125,7 +130,8 @@ describe Travis::API::V3::Services::Repository::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}}
end
@ -167,6 +173,8 @@ describe Travis::API::V3::Services::Repository::Find do
"read" => true,
"enable" => true,
"disable" => true,
"star" => true,
"unstar" => true,
"create_request" => true},
"id" => repo.id,
"name" => "minimal",
@ -184,7 +192,8 @@ describe Travis::API::V3::Services::Repository::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}}
end
@ -232,6 +241,8 @@ describe Travis::API::V3::Services::Repository::Find do
"read" => true,
"enable" => true,
"disable" => true,
"star" => true,
"unstar" => true,
"create_request" => true},
"id" => repo.id,
"name" => "minimal",
@ -249,7 +260,8 @@ describe Travis::API::V3::Services::Repository::Find do
"@type" => "branch",
"@href" => "/v3/repo/#{repo.id}/branch/master",
"@representation" => "minimal",
"name" => "master"}
"name" => "master"},
"starred" => false
}}
end

View File

@ -0,0 +1,68 @@
require 'spec_helper'
describe Travis::API::V3::Services::Repository::Star do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
describe "not authenticated" do
before { post("/v3/repo/#{repo.id}/star") }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "login_required",
"error_message" => "login required"
}}
end
describe "missing repo, authenticated" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/9999999999/star", {}, headers) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, no push access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/#{repo.id}/star", {}, headers) }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body).to_s).to include(
"@type",
"error_type",
"insufficient_access",
"error_message",
"operation requires star access to repository",
"resource_type",
"repository",
"permission",
"star")
}
end
describe "private repository, no access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { repo.update_attribute(:private, true) }
before { post("/v3/repo/#{repo.id}/star", {}, headers) }
after { repo.update_attribute(:private, false) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, push access"
# this requires stubing a github request, which is difficult, so has been omitted for now
end

View File

@ -0,0 +1,68 @@
require 'spec_helper'
describe Travis::API::V3::Services::Repository::Unstar do
let(:repo) { Travis::API::V3::Models::Repository.where(owner_name: 'svenfuchs', name: 'minimal').first }
describe "not authenticated" do
before { post("/v3/repo/#{repo.id}/unstar") }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "login_required",
"error_message" => "login required"
}}
end
describe "missing repo, authenticated" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/9999999999/unstar", {}, headers) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, no push access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { post("/v3/repo/#{repo.id}/unstar", {}, headers) }
example { expect(last_response.status).to be == 403 }
example { expect(JSON.load(body).to_s).to include(
"@type",
"error_type",
"insufficient_access",
"error_message",
"operation requires unstar access to repository",
"resource_type",
"repository",
"permission",
"unstar")
}
end
describe "private repository, no access" do
let(:token) { Travis::Api::App::AccessToken.create(user: repo.owner, app_id: 1) }
let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }}
before { repo.update_attribute(:private, true) }
before { post("/v3/repo/#{repo.id}/unstar", {}, headers) }
after { repo.update_attribute(:private, false) }
example { expect(last_response.status).to be == 404 }
example { expect(JSON.load(body)).to be == {
"@type" => "error",
"error_type" => "not_found",
"error_message" => "repository not found (or insufficient access)",
"resource_type" => "repository"
}}
end
describe "existing repository, push access"
# this requires stubing a github request, which is difficult, so has been omitted for now
end