Commit 6413bfb5 authored by Sytse Sijbrandij's avatar Sytse Sijbrandij

Merge branch 'master' into full-post-to-oss-security

Conflicts:
	doc/release/security.md
parents 250a86d2 6cc3cc51
...@@ -9,6 +9,7 @@ v 6.3.0 ...@@ -9,6 +9,7 @@ v 6.3.0
- Fixed issue with 500 error when group did not exist - Fixed issue with 500 error when group did not exist
- Ability to leave project - Ability to leave project
- You can create file in repo using UI - You can create file in repo using UI
- You can remove file from repo using UI
- API: dropped default_branch attribute from project during creation - API: dropped default_branch attribute from project during creation
- Project default_branch is not stored in db any more. It takes from repo now. - Project default_branch is not stored in db any more. It takes from repo now.
- Admin broadcast messages - Admin broadcast messages
...@@ -16,8 +17,26 @@ v 6.3.0 ...@@ -16,8 +17,26 @@ v 6.3.0
- Dont show last push widget if user removed this branch - Dont show last push widget if user removed this branch
- Fix 500 error for repos with newline in file name - Fix 500 error for repos with newline in file name
- Extended html titles - Extended html titles
- API: create/update repo files - API: create/update/delete repo files
- Admin can transfer project to any namespace - Admin can transfer project to any namespace
- API: projects/all for admin users
- Fix recent branches order
v 6.2.4
- Security: Cast API private_token to string (CVE-2013-4580)
- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
- Fix for Git SSH access for LDAP users
v 6.2.3
- Security: More protection against CVE-2013-4489
- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
- Fix sidekiq rake tasks
v 6.2.2
- Security: Update gitlab_git (CVE-2013-4489)
v 6.2.1
- Security: Fix issue with generated passwords for new users
v 6.2.0 v 6.2.0
- Public project pages are now visible to everyone (files, issues, wik, etc.) - Public project pages are now visible to everyone (files, issues, wik, etc.)
...@@ -104,6 +123,14 @@ v 6.0.0 ...@@ -104,6 +123,14 @@ v 6.0.0
- Improved MR comments logic - Improved MR comments logic
- Render readme file for projects in public area - Render readme file for projects in public area
v 5.4.2
- Security: Cast API private_token to string (CVE-2013-4580)
- Security: Require gitlab-shell 1.7.8 (CVE-2013-4581, CVE-2013-4582, CVE-2013-4583)
v 5.4.1
- Security: Fixes for CVE-2013-4489
- Security: Require gitlab-shell 1.7.4 (CVE-2013-4490, CVE-2013-4546)
v 5.4.0 v 5.4.0
- Ability to edit own comments - Ability to edit own comments
- Documentation improvements - Documentation improvements
......
6.3.0.pre 6.3.0.beta1
...@@ -93,6 +93,12 @@ pre.well-pre { ...@@ -93,6 +93,12 @@ pre.well-pre {
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
&.label-gray {
background-color: #eee;
color: #999;
text-shadow: none;
}
} }
/** Big Labels **/ /** Big Labels **/
......
...@@ -3,6 +3,16 @@ form { ...@@ -3,6 +3,16 @@ form {
label { label {
@extend .control-label; @extend .control-label;
&.radio-label {
text-align: left;
width: 100%;
margin-left: 0;
input[type="radio"] {
margin-top: 1px !important;
}
}
} }
} }
......
module Files
class DeleteContext < BaseContext
def execute
allowed = if project.protected_branch?(ref)
can?(current_user, :push_code_to_protected_branches, project)
else
can?(current_user, :push_code, project)
end
unless allowed
return error("You are not allowed to push into this branch")
end
unless repository.branch_names.include?(ref)
return error("You can only create files if you are on top of a branch")
end
blob = repository.blob_at(ref, path)
unless blob
return error("You can only edit text files")
end
delete_file_action = Gitlab::Satellite::DeleteFileAction.new(current_user, project, ref, path)
deleted_successfully = delete_file_action.commit!(
nil,
params[:commit_message]
)
if deleted_successfully
success
else
error("Your changes could not be commited, because the file has been changed")
end
end
end
end
...@@ -7,9 +7,30 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -7,9 +7,30 @@ class Projects::BlobController < Projects::ApplicationController
before_filter :authorize_code_access! before_filter :authorize_code_access!
before_filter :require_non_empty_project before_filter :require_non_empty_project
before_filter :blob
def show def show
@blob = @repository.blob_at(@commit.id, @path) end
def destroy
result = Files::DeleteContext.new(@project, current_user, params, @ref, @path).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully commited"
redirect_to project_tree_path(@project, @ref)
else
flash[:alert] = result[:error]
render :show
end
end
private
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
return not_found! unless @blob
not_found! unless @blob @blob
end end
end end
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if allowed_tree_edit? - if allowed_tree_edit?
= link_to "edit", project_edit_tree_path(@project, @id), class: "btn btn-small" = link_to "edit", project_edit_tree_path(@project, @id), class: "btn btn-small"
- else - else
%span.btn.btn-small.disabled Edit %span.btn.btn-small.disabled edit
= link_to "raw", project_raw_path(@project, @id), class: "btn btn-small", target: "_blank" = link_to "raw", project_raw_path(@project, @id), class: "btn btn-small", target: "_blank"
-# only show normal/blame view links for text files -# only show normal/blame view links for text files
- if @blob.text? - if @blob.text?
...@@ -13,3 +13,7 @@ ...@@ -13,3 +13,7 @@
- else - else
= link_to "blame", project_blame_path(@project, @id), class: "btn btn-small" unless @blob.empty? = link_to "blame", project_blame_path(@project, @id), class: "btn btn-small" unless @blob.empty?
= link_to "history", project_commits_path(@project, @id), class: "btn btn-small" = link_to "history", project_commits_path(@project, @id), class: "btn btn-small"
- if allowed_tree_edit?
= link_to '#modal-remove-blob', class: "remove-blob btn btn-small btn-remove", "data-toggle" => "modal" do
remove
%div#modal-remove-blob.modal.hide
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Remove #{@blob.name}
%p.light
From branch
%strong= @ref
.modal-body
= form_tag project_blob_path(@project, @id), method: :delete do
.control-group.commit_message-group
= label_tag 'commit_message', class: "control-label" do
Commit message
.controls
= text_area_tag 'commit_message', params[:commit_message], placeholder: "Removed this file because...", required: true, rows: 3
.control-group
.controls
= submit_tag 'Remove file', class: 'btn btn-remove'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
...@@ -2,3 +2,6 @@ ...@@ -2,3 +2,6 @@
= render 'shared/ref_switcher', destination: 'blob', path: @path = render 'shared/ref_switcher', destination: 'blob', path: @path
%div#tree-holder.tree-holder %div#tree-holder.tree-holder
= render 'blob', blob: @blob = render 'blob', blob: @blob
- if allowed_tree_edit?
= render 'projects/blob/remove'
...@@ -13,9 +13,20 @@ ...@@ -13,9 +13,20 @@
= f.label :title = f.label :title
.controls= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true .controls= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true
.control-group .control-group
= f.label "Private?" = f.label "Access"
.controls .controls
= f.check_box :private, {class: ''} = f.label :private_true, class: 'radio-label' do
= f.radio_button :private, true
%span
%strong Private
(only you can see this snippet)
%br
= f.label :private_false, class: 'radio-label' do
= f.radio_button :private, false
%span
%strong Public
(GitLab users can can see this snippet)
.control-group .control-group
.file-editor .file-editor
= f.label :file_name, "File" = f.label :file_name, "File"
...@@ -33,9 +44,10 @@ ...@@ -33,9 +44,10 @@
- else - else
= f.submit 'Save', class: "btn-save btn" = f.submit 'Save', class: "btn-save btn"
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
- unless @snippet.new_record? - unless @snippet.new_record?
.pull-right= link_to 'Destroy', snippet_path(@snippet), confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" .pull-right.prepend-left-20
= link_to 'Remove', snippet_path(@snippet), confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn btn-remove delete-snippet", id: "destroy_snippet_#{@snippet.id}"
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
:javascript :javascript
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= link_to reliable_snippet_path(snippet) do = link_to reliable_snippet_path(snippet) do
= truncate(snippet.title, length: 60) = truncate(snippet.title, length: 60)
- if snippet.private? - if snippet.private?
%span.label.label-success %span.label.label-gray
%i.icon-lock %i.icon-lock
private private
%span.cgray.monospace.tiny.pull-right %span.cgray.monospace.tiny.pull-right
......
...@@ -173,7 +173,7 @@ Gitlab::Application.routes.draw do ...@@ -173,7 +173,7 @@ Gitlab::Application.routes.draw do
end end
scope module: :projects do scope module: :projects do
resources :blob, only: [:show], constraints: {id: /.+/} resources :blob, only: [:show, :destroy], constraints: {id: /.+/}
resources :raw, only: [:show], constraints: {id: /.+/} resources :raw, only: [:show], constraints: {id: /.+/}
resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ }
resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit' resources :edit_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'edit'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
### List projects ### List projects
Get a list of projects owned by the authenticated user. Get a list of projects accessible by the authenticated user.
``` ```
GET /projects GET /projects
...@@ -82,6 +82,22 @@ GET /projects ...@@ -82,6 +82,22 @@ GET /projects
``` ```
#### List owned projects
Get a list of projects owned by the authenticated user.
```
GET /projects/owned
```
#### List ALL projects
Get a list of all GitLab projects (admin only).
```
GET /projects/all
```
### Get single project ### Get single project
Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME , which is owned by the authentication user. Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME , which is owned by the authentication user.
......
...@@ -397,3 +397,15 @@ Parameters: ...@@ -397,3 +397,15 @@ Parameters:
+ `branch_name` (required) - The name of branch + `branch_name` (required) - The name of branch
+ `content` (required) - New file content + `content` (required) - New file content
+ `commit_message` (required) - Commit message + `commit_message` (required) - Commit message
## Delete existing file in repository
```
DELETE /projects/:id/repository/files
```
Parameters:
+ `file_path` (required) - Full path to file. Ex. lib/class.rb
+ `branch_name` (required) - The name of branch
+ `commit_message` (required) - Commit message
...@@ -13,12 +13,14 @@ Please report suspected security vulnerabilities in private to support@gitlab.co ...@@ -13,12 +13,14 @@ Please report suspected security vulnerabilities in private to support@gitlab.co
1. Verify that the issue can be repoduced 1. Verify that the issue can be repoduced
1. Acknowledge the issue to the researcher that disclosed it 1. Acknowledge the issue to the researcher that disclosed it
1. Fix the issue on a feature branch, do this on the private dev.gitlab.org server and update the VERSION and CHANGELOG 1. Fix the issue on a feature branch, do this on the private GitLab development server and update the VERSION and CHANGELOG in this branch
1. Consider creating and testing workarounds 1. Consider creating and testing workarounds
1. Create feature branches for the blog posts on GitLab.org and GitLab.com and link them from the code branch 1. Create feature branches for the blog posts on GitLab.org and GitLab.com and link them from the code branch
1. Merge the code feature branch 1. Merge the code feature branch into master
1. Create a git tag vX.X.X for CE and another one for EE 1. Cherry-pick the code into the latest stable branch
1. Create a git tag vX.X.X for CE and another patch release for EE
1. Push the code and the tags to all the CE and EE repositories 1. Push the code and the tags to all the CE and EE repositories
1. Apply the patch to GitLab Cloud and the private GitLab development server
1. Merge and publish the blog posts 1. Merge and publish the blog posts
1. Send tweets about the release from @gitlabhq and @git_lab 1. Send tweets about the release from @gitlabhq and @git_lab
1. Send out an email to the subscribers mailing list on MailChimp 1. Send out an email to the subscribers mailing list on MailChimp
...@@ -27,13 +29,17 @@ Please report suspected security vulnerabilities in private to support@gitlab.co ...@@ -27,13 +29,17 @@ Please report suspected security vulnerabilities in private to support@gitlab.co
1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number 1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number
1. Add the security researcher to the [Security Researcher Acknowledgments list](http://www.gitlab.com/vulnerability-acknowledgements/) 1. Add the security researcher to the [Security Researcher Acknowledgments list](http://www.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation 1. Thank the security researcher in an email for their cooperation
1. Update the blogposts and the CHANGELOG when we receive a CVE number 1. Update the blogpost and the CHANGELOG when we receive the CVE number
The timing of the code merge into master should be coordinated in advance.
After the merge we strive to publish the announcements within 60 minutes.
## Blog post template ## Blog post template
XXX Security Advisory for GitLab XXX Security Advisory for GitLab
A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately.
We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited.
### Version affected ### Version affected
......
...@@ -19,7 +19,7 @@ class SnippetsFeature < Spinach::FeatureSteps ...@@ -19,7 +19,7 @@ class SnippetsFeature < Spinach::FeatureSteps
end end
And 'I click link "Destroy"' do And 'I click link "Destroy"' do
click_link "Destroy" click_link "Remove"
end end
And 'I submit new snippet "Personal snippet three"' do And 'I submit new snippet "Personal snippet three"' do
...@@ -46,7 +46,7 @@ class SnippetsFeature < Spinach::FeatureSteps ...@@ -46,7 +46,7 @@ class SnippetsFeature < Spinach::FeatureSteps
end end
And 'I uncheck "Private" checkbox' do And 'I uncheck "Private" checkbox' do
find(:xpath, "//input[@id='personal_snippet_private']").set true choose "Public"
click_button "Save" click_button "Save"
end end
......
...@@ -40,8 +40,7 @@ module API ...@@ -40,8 +40,7 @@ module API
# Update existing file in repository # Update existing file in repository
# #
# Parameters: # Parameters:
# file_name (required) - The name of new file. Ex. class.rb # file_path (optional) - The path to file. Ex. lib/class.rb
# file_path (optional) - The path to new file. Ex. lib/
# branch_name (required) - The name of branch # branch_name (required) - The name of branch
# content (required) - File content # content (required) - File content
# commit_message (required) - Commit message # commit_message (required) - Commit message
...@@ -67,7 +66,36 @@ module API ...@@ -67,7 +66,36 @@ module API
render_api_error!(result[:error], 400) render_api_error!(result[:error], 400)
end end
end end
# Delete existing file in repository
#
# Parameters:
# file_path (optional) - The path to file. Ex. lib/class.rb
# branch_name (required) - The name of branch
# content (required) - File content
# commit_message (required) - Commit message
#
# Example Request:
# DELETE /projects/:id/repository/files
#
delete ":id/repository/files" do
required_attributes! [:file_path, :branch_name, :commit_message]
attrs = attributes_for_keys [:file_path, :branch_name, :commit_message]
branch_name = attrs.delete(:branch_name)
file_path = attrs.delete(:file_path)
result = ::Files::DeleteContext.new(user_project, current_user, attrs, branch_name, file_path).execute
if result[:status] == :success
status(200)
{
file_path: file_path,
branch_name: branch_name
}
else
render_api_error!(result[:error], 400)
end
end
end end
end end
end end
...@@ -31,6 +31,16 @@ module API ...@@ -31,6 +31,16 @@ module API
present @projects, with: Entities::Project present @projects, with: Entities::Project
end end
# Get all projects for admin user
#
# Example Request:
# GET /projects/all
get '/all' do
authenticated_as_admin!
@projects = paginate Project
present @projects, with: Entities::Project
end
# Get a single project # Get a single project
# #
# Parameters: # Parameters:
......
require_relative 'file_action'
module Gitlab
module Satellite
class DeleteFileAction < FileAction
# Deletes file and creates a new commit for it
#
# Returns false if committing the change fails
# Returns false if pushing from the satellite to bare repo failed or was rejected
# Returns true otherwise
def commit!(content, commit_message)
in_locked_and_timed_satellite do |repo|
prepare_satellite!(repo)
# create target branch in satellite at the corresponding commit from bare repo
repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
# update the file in the satellite's working dir
file_path_in_satellite = File.join(repo.working_dir, file_path)
File.delete(file_path_in_satellite)
# add removed file
repo.remove(file_path_in_satellite)
# commit the changes
# will raise CommandFailed when commit fails
repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
# push commit back to bare repo
# will raise CommandFailed when push fails
repo.git.push({raise: true, timeout: true}, :origin, ref)
# everything worked
true
end
rescue Grit::Git::CommandFailed => ex
Gitlab::GitLogger.error(ex.message)
false
end
end
end
end
...@@ -8,13 +8,13 @@ module Gitlab ...@@ -8,13 +8,13 @@ module Gitlab
# #
# Returns false if the ref has been updated while editing the file # Returns false if the ref has been updated while editing the file
# Returns false if committing the change fails # Returns false if committing the change fails
# Returns false if pushing from the satellite to Gitolite failed or was rejected # Returns false if pushing from the satellite to bare repo failed or was rejected
# Returns true otherwise # Returns true otherwise
def commit!(content, commit_message) def commit!(content, commit_message)
in_locked_and_timed_satellite do |repo| in_locked_and_timed_satellite do |repo|
prepare_satellite!(repo) prepare_satellite!(repo)
# create target branch in satellite at the corresponding commit from Gitolite # create target branch in satellite at the corresponding commit from bare repo
repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
# update the file in the satellite's working dir # update the file in the satellite's working dir
...@@ -26,7 +26,7 @@ module Gitlab ...@@ -26,7 +26,7 @@ module Gitlab
repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
# push commit back to Gitolite # push commit back to bare repo
# will raise CommandFailed when push fails # will raise CommandFailed when push fails
repo.git.push({raise: true, timeout: true}, :origin, ref) repo.git.push({raise: true, timeout: true}, :origin, ref)
......
...@@ -7,13 +7,13 @@ module Gitlab ...@@ -7,13 +7,13 @@ module Gitlab
# #
# Returns false if the ref has been updated while editing the file # Returns false if the ref has been updated while editing the file
# Returns false if committing the change fails # Returns false if committing the change fails
# Returns false if pushing from the satellite to Gitolite failed or was rejected # Returns false if pushing from the satellite to bare repo failed or was rejected
# Returns true otherwise # Returns true otherwise
def commit!(content, commit_message) def commit!(content, commit_message)
in_locked_and_timed_satellite do |repo| in_locked_and_timed_satellite do |repo|
prepare_satellite!(repo) prepare_satellite!(repo)
# create target branch in satellite at the corresponding commit from Gitolite # create target branch in satellite at the corresponding commit from bare repo
repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}")
# update the file in the satellite's working dir # update the file in the satellite's working dir
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) repo.git.commit(raise: true, timeout: true, a: true, m: commit_message)
# push commit back to Gitolite # push commit back to bare repo
# will raise CommandFailed when push fails # will raise CommandFailed when push fails
repo.git.push({raise: true, timeout: true}, :origin, ref) repo.git.push({raise: true, timeout: true}, :origin, ref)
......
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
in_locked_and_timed_satellite do |merge_repo| in_locked_and_timed_satellite do |merge_repo|
prepare_satellite!(merge_repo) prepare_satellite!(merge_repo)
if merge_in_satellite!(merge_repo) if merge_in_satellite!(merge_repo)
# push merge back to Gitolite # push merge back to bare repo
# will raise CommandFailed when push fails # will raise CommandFailed when push fails
merge_repo.git.push(default_options, :origin, merge_request.target_branch) merge_repo.git.push(default_options, :origin, merge_request.target_branch)
# remove source branch # remove source branch
......
...@@ -123,7 +123,7 @@ module Gitlab ...@@ -123,7 +123,7 @@ module Gitlab
remotes.each { |name| repo.git.remote(default_options,'rm', name)} remotes.each { |name| repo.git.remote(default_options,'rm', name)}
end end
# Updates the satellite from Gitolite # Updates the satellite from bare repo
# #
# Note: this will only update remote branches (i.e. origin/*) # Note: this will only update remote branches (i.e. origin/*)
def update_from_source! def update_from_source!
......
...@@ -78,4 +78,38 @@ describe API::API do ...@@ -78,4 +78,38 @@ describe API::API do
response.status.should == 400 response.status.should == 400
end end
end end
describe "DELETE /projects/:id/repository/files" do
let(:valid_params) {
{
file_path: 'spec/spec_helper.rb',
branch_name: 'master',
commit_message: 'Changed file'
}
}
it "should delete existing file in project repo" do
Gitlab::Satellite::DeleteFileAction.any_instance.stub(
commit!: true,
)
delete api("/projects/#{project.id}/repository/files", user), valid_params
response.status.should == 200
json_response['file_path'].should == 'spec/spec_helper.rb'
end
it "should return a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
response.status.should == 400
end
it "should return a 400 if satellite fails to create file" do
Gitlab::Satellite::DeleteFileAction.any_instance.stub(
commit!: false,
)
delete api("/projects/#{project.id}/repository/files", user), valid_params
response.status.should == 400
end
end
end end
...@@ -36,6 +36,32 @@ describe API::API do ...@@ -36,6 +36,32 @@ describe API::API do
end end
end end
describe "GET /projects/all" do
context "when unauthenticated" do
it "should return authentication error" do
get api("/projects/all")
response.status.should == 401
end
end
context "when authenticated as regular user" do
it "should return authentication error" do
get api("/projects/all", user)
response.status.should == 403
end
end
context "when authenticated as admin" do
it "should return an array of all projects" do
get api("/projects/all", admin)
response.status.should == 200
json_response.should be_an Array
json_response.first['name'].should == project.name
json_response.first['owner']['email'].should == user.email
end
end
end
describe "POST /projects" do describe "POST /projects" do
context "maximum number of projects reached" do context "maximum number of projects reached" do
before do before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment