diff --git a/app/controllers/locomotive/api/base_controller.rb b/app/controllers/locomotive/api/base_controller.rb index 1afc9d41..8d0519b8 100644 --- a/app/controllers/locomotive/api/base_controller.rb +++ b/app/controllers/locomotive/api/base_controller.rb @@ -7,15 +7,13 @@ module Locomotive skip_before_filter :verify_authenticity_token - skip_load_and_authorize_resource - before_filter :require_account before_filter :require_site before_filter :set_locale - # before_filter :validate_site_membership + before_filter :set_current_thread_variables self.responder = Locomotive::ActionController::Responder # custom responder @@ -23,6 +21,11 @@ module Locomotive protected + def set_current_thread_variables + Thread.current[:account] = current_locomotive_account + Thread.current[:site] = current_site + end + def current_ability @current_ability ||= Ability.new(current_locomotive_account, current_site) end @@ -40,4 +43,4 @@ module Locomotive end end -end \ No newline at end of file +end diff --git a/app/controllers/locomotive/api/content_assets_controller.rb b/app/controllers/locomotive/api/content_assets_controller.rb index abde5f8c..2acd8c12 100644 --- a/app/controllers/locomotive/api/content_assets_controller.rb +++ b/app/controllers/locomotive/api/content_assets_controller.rb @@ -2,11 +2,18 @@ module Locomotive module Api class ContentAssetsController < BaseController + load_and_authorize_resource :class => Locomotive::ContentAsset + def index @content_assets = current_site.content_assets respond_with(@content_assets) end + def show + @content_asset = current_site.content_assets.find(params[:id]) + respond_with(@content_asset) + end + def create @content_asset = current_site.content_assets.create(params[:content_asset]) respond_with @content_asset, :location => main_app.locomotive_api_content_assets_url @@ -18,6 +25,12 @@ module Locomotive respond_with @content_asset, :location => main_app.locomotive_api_content_assets_url end + def destroy + @content_asset = current_site.content_assets.find(params[:id]) + @content_asset.destroy + respond_with @content_asset + end + end end end diff --git a/app/controllers/locomotive/api/content_types_controller.rb b/app/controllers/locomotive/api/content_types_controller.rb index afd4ed6a..9b5014d6 100644 --- a/app/controllers/locomotive/api/content_types_controller.rb +++ b/app/controllers/locomotive/api/content_types_controller.rb @@ -2,11 +2,18 @@ module Locomotive module Api class ContentTypesController < BaseController + load_and_authorize_resource :class => Locomotive::ContentType + def index @content_types = current_site.content_types respond_with(@content_types) end + def show + @content_type = current_site.content_types.find(params[:id]) + respond_with @content_type + end + def create @content_type = current_site.content_types.create(params[:content_type]) respond_with @content_type, :location => main_app.locomotive_api_content_types_url @@ -18,6 +25,12 @@ module Locomotive respond_with @content_type, :location => main_app.locomotive_api_content_types_url end + def destroy + @content_type = current_site.content_types.find(params[:id]) + @content_type.destroy + respond_with @content_type + end + end end end diff --git a/app/controllers/locomotive/api/current_site_controller.rb b/app/controllers/locomotive/api/current_site_controller.rb index 71665ea8..408cda14 100644 --- a/app/controllers/locomotive/api/current_site_controller.rb +++ b/app/controllers/locomotive/api/current_site_controller.rb @@ -3,7 +3,9 @@ module Locomotive class CurrentSiteController < BaseController def show - respond_with(current_site) + @site = current_site + authorize! :show, @site + respond_with(@site) end end diff --git a/app/controllers/locomotive/api/memberships_controller.rb b/app/controllers/locomotive/api/memberships_controller.rb new file mode 100644 index 00000000..c889f532 --- /dev/null +++ b/app/controllers/locomotive/api/memberships_controller.rb @@ -0,0 +1,49 @@ +module Locomotive + module Api + class MembershipsController < BaseController + + # It's an embedded document, so we'll just load manually + before_filter :load_membership, :only => [ :show, :update, :destroy ] + before_filter :load_memberships, :only => [ :index ] + + authorize_resource :class => Locomotive::Membership + + def index + respond_with(@memberships) + end + + def show + respond_with(@membership) + end + + def create + build_params = params[:membership].merge({ :role => 'author' }) # force author by default + @membership = current_site.memberships.create(build_params) + respond_with(@membership) + end + + def update + @membership.update_attributes(params[:membership]) + respond_with(@membership) + end + + def destroy + @membership.destroy + respond_with(@membership) + end + + protected + + def load_membership + @membership ||= load_memberships.find(params[:id]) + end + + def load_memberships + @memberships ||= current_site.memberships + end + + end + + end +end + diff --git a/app/controllers/locomotive/api/pages_controller.rb b/app/controllers/locomotive/api/pages_controller.rb index 2a505523..551355a4 100644 --- a/app/controllers/locomotive/api/pages_controller.rb +++ b/app/controllers/locomotive/api/pages_controller.rb @@ -2,11 +2,18 @@ module Locomotive module Api class PagesController < BaseController + load_and_authorize_resource :class => Locomotive::Page + def index @pages = current_site.pages.order_by([[:depth, :asc], [:position, :asc]]) respond_with(@pages) end + def show + @page = current_site.pages.find(params[:id]) + respond_with(@page) + end + def create @page = current_site.pages.create(params[:page]) respond_with @page, :location => main_app.locomotive_api_pages_url @@ -18,6 +25,12 @@ module Locomotive respond_with @page, :location => main_app.locomotive_api_pages_url end + def destroy + @page = current_site.pages.find(params[:id]) + @page.destroy + respond_with @page + end + end end diff --git a/app/controllers/locomotive/api/sites_controller.rb b/app/controllers/locomotive/api/sites_controller.rb new file mode 100644 index 00000000..11469abe --- /dev/null +++ b/app/controllers/locomotive/api/sites_controller.rb @@ -0,0 +1,44 @@ +module Locomotive + module Api + class SitesController < BaseController + + load_and_authorize_resource :class => Locomotive::Site + + # FIXME: the auto-loaded site won't pass authorization for show, update, or destroy + skip_load_and_authorize_resource :only => [ :show, :update, :destroy ] + + def index + @sites = Locomotive::Site.all + respond_with(@sites) + end + + def show + @site = Locomotive::Site.find(params[:id]) + authorize! :show, @site + respond_with(@site) + end + + def create + @site = Locomotive::Site.create(params[:site]) + respond_with(@site) + end + + def update + @site = Locomotive::Site.find(params[:id]) + authorize! :update, @site + @site.update_attributes(params[:site]) + respond_with @site + end + + def destroy + @site = Locomotive::Site.find(params[:id]) + authorize! :destroy, @site + @site.destroy + respond_with @site + end + + end + + end +end + diff --git a/app/controllers/locomotive/api/snippets_controller.rb b/app/controllers/locomotive/api/snippets_controller.rb index b7479745..24067166 100644 --- a/app/controllers/locomotive/api/snippets_controller.rb +++ b/app/controllers/locomotive/api/snippets_controller.rb @@ -2,11 +2,18 @@ module Locomotive module Api class SnippetsController < BaseController + load_and_authorize_resource :class => Locomotive::Snippet + def index @snippets = current_site.snippets.all respond_with(@snippets) end + def show + @snippet = current_site.snippets.find(params[:id]) + respond_with @snippet + end + def create @snippet = current_site.snippets.create(params[:snippet]) respond_with @snippet, :location => main_app.locomotive_api_snippets_url @@ -18,6 +25,12 @@ module Locomotive respond_with @snippet, :location => main_app.locomotive_api_snippets_url end + def destroy + @snippet = current_site.snippets.find(params[:id]) + @snippet.destroy + respond_with @snippet + end + end end end diff --git a/app/controllers/locomotive/api/theme_assets_controller.rb b/app/controllers/locomotive/api/theme_assets_controller.rb index c1cfae8d..c7cd0cd6 100644 --- a/app/controllers/locomotive/api/theme_assets_controller.rb +++ b/app/controllers/locomotive/api/theme_assets_controller.rb @@ -2,11 +2,18 @@ module Locomotive module Api class ThemeAssetsController < BaseController + load_and_authorize_resource :class => Locomotive::ThemeAsset + def index @theme_assets = current_site.theme_assets.all respond_with(@theme_assets) end + def show + @theme_asset = current_site.theme_assets.find(params[:id]) + respond_with @theme_asset + end + def create @theme_asset = current_site.theme_assets.create(params[:theme_asset]) respond_with @theme_asset, :location => main_app.locomotive_api_theme_assets_url @@ -18,6 +25,12 @@ module Locomotive respond_with @theme_asset, :location => main_app.locomotive_api_theme_assets_url end + def destroy + @theme_asset = current_site.theme_assets.find(params[:id]) + @theme_asset.destroy + respond_with @theme_asset + end + end end end diff --git a/app/models/locomotive/ability.rb b/app/models/locomotive/ability.rb index db42da23..f5f841f4 100644 --- a/app/models/locomotive/ability.rb +++ b/app/models/locomotive/ability.rb @@ -37,6 +37,8 @@ module Locomotive can :touch, Site do |site| site == @site end + + can :read, ContentType end def setup_designer_permissions! diff --git a/config/routes.rb b/config/routes.rb index f69a9e31..b6974a20 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,10 @@ Rails.application.routes.draw do resources :content_entries, :path => 'content_types/:slug/entries' + resources :sites + + resources :memberships + resource :current_site, :controller => 'current_site' end diff --git a/features/api/authorization/content_assets.feature b/features/api/authorization/content_assets.feature new file mode 100644 index 00000000..ff3f8aec --- /dev/null +++ b/features/api/authorization/content_assets.feature @@ -0,0 +1,147 @@ +Feature: Content Assets + In order to ensure content assets are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And I have the following content assets: + | id | file | + | 4f832c2cb0d86d3f42fffffe | 5k.png | + | 4f832c2cb0d86d3f42ffffff | 5k_2.png | + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to content_assets.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing content assets + + Scenario: Accessing content assets as an Admin + Given I have an "admin" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing content assets as a Designer + Given I have a "designer" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing content assets as an Author + Given I have an "author" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + # showing content asset + + Scenario: Accessing content asset as an Admin + Given I have an "admin" API token + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "5k.png" + + Scenario: Accessing content asset as a Designer + Given I have a "designer" API token + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "5k.png" + + Scenario: Accessing content asset as an Author + Given I have an "author" API token + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "5k.png" + + # create content asset + + Scenario: Creating new content asset as an Admin + Given I have an "admin" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do a multipart API POST to content_assets.json with base key "content_asset" and: + | source | assets/application.js | + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON at "2/filename" should be "application.js" + + Scenario: Creating new content asset as a Designer + Given I have a "designer" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do a multipart API POST to content_assets.json with base key "content_asset" and: + | source | assets/application.js | + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON at "2/filename" should be "application.js" + + Scenario: Creating new content asset as an Author + Given I have an "author" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do a multipart API POST to content_assets.json with base key "content_asset" and: + | source | assets/application.js | + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON at "2/filename" should be "application.js" + + # update content asset + + Scenario: Updating content asset as an Admin + Given I have an "admin" API token + When I do a multipart API PUT to content_assets/4f832c2cb0d86d3f42fffffe.json with base key "content_asset" and: + | source | assets/main.css | + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "main.css" + + Scenario: Updating content asset as a Designer + Given I have a "designer" API token + When I do a multipart API PUT to content_assets/4f832c2cb0d86d3f42fffffe.json with base key "content_asset" and: + | source | assets/main.css | + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "main.css" + + Scenario: Updating content asset as an Author + Given I have a "author" API token + When I do a multipart API PUT to content_assets/4f832c2cb0d86d3f42fffffe.json with base key "content_asset" and: + | source | assets/main.css | + When I do an API GET request to content_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "filename" should be "main.css" + + # destroy content asset + + Scenario: Destroying content asset as an Admin + Given I have an "admin" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_assets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Destroying content asset as a Designer + Given I have a "designer" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_assets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Deleting content asset as an Author + Given I have a "author" API token + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_assets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_assets.json + Then the JSON response should be an array + And the JSON response should have 1 entry diff --git a/features/api/authorization/content_entries.feature b/features/api/authorization/content_entries.feature new file mode 100644 index 00000000..af305298 --- /dev/null +++ b/features/api/authorization/content_entries.feature @@ -0,0 +1,202 @@ +Feature: Content Entries + In order to ensure content entries are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And I have a custom model named "Projects" with + | label | type | required | + | Name | string | true | + | Description | text | false | + And I have entries for "Projects" with + | id | name | description | + | 4f832c2cb0d86d3f42fffffe | Project 1 | The first project | + | 4f832c2cb0d86d3f42ffffff | Project 2 | The second project | + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to content_types/projects/entries.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing content entries + + Scenario: Accessing content entries as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing content entries as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing content entries as an Author + Given I have an "author" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + # showing content entry + + Scenario: Accessing content entry as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Project 1" + + Scenario: Accessing content entry as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Project 1" + + Scenario: Accessing content entry as an Author + Given I have an "author" API token + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Project 1" + + # create content entry + + Scenario: Creating new content entry as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to content_types/projects/entries.json with: + """ + { + "content_entry": { + "name": "Project 3", + "description": "The third..." + } + } + """ + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON should have the following: + | 2/name | "Project 3" | + | 2/description | "The third..." | + + Scenario: Creating new content entry as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to content_types/projects/entries.json with: + """ + { + "content_entry": { + "name": "Project 3", + "description": "The third..." + } + } + """ + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON should have the following: + | 2/name | "Project 3" | + | 2/description | "The third..." | + + Scenario: Creating new content entry as an Author + Given I have an "author" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to content_types/projects/entries.json with: + """ + { + "content_entry": { + "name": "Project 3", + "description": "The third..." + } + } + """ + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON should have the following: + | 2/name | "Project 3" | + | 2/description | "The third..." | + + # update content entry + + Scenario: Updating content entry as an Admin + Given I have an "admin" API token + When I do an API PUT to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_entry": { + "description": "The awesomest project ever!" + } + } + """ + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Project 1" + And the JSON response at "description" should be "The awesomest project ever!" + + Scenario: Updating content entry as a Designer + Given I have a "designer" API token + When I do an API PUT to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_entry": { + "description": "The awesomest project ever!" + } + } + """ + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Project 1" + And the JSON response at "description" should be "The awesomest project ever!" + + Scenario: Updating content entry as an Author + Given I have a "author" API token + When I do an API PUT to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_entry": { + "description": "The awesomest project ever!" + } + } + """ + When I do an API GET request to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Project 1" + And the JSON response at "description" should be "The awesomest project ever!" + + # destroy content entry + + Scenario: Destroying content entry as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Destroying content entry as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Deleting content entry as an Author + Given I have a "author" API token + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to content_types/projects/entries/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_types/projects/entries.json + Then the JSON response should be an array + And the JSON response should have 1 entry diff --git a/features/api/authorization/content_types.feature b/features/api/authorization/content_types.feature new file mode 100644 index 00000000..0012d91a --- /dev/null +++ b/features/api/authorization/content_types.feature @@ -0,0 +1,237 @@ +Feature: Content Types + In order to ensure content types are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And I have a custom model named "Projects" with id "4f832c2cb0d86d3f42fffffe" and + | label | type | required | + | Name | string | true | + | Description | text | false | + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to content_types.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing content types + + Scenario: Accessing content types as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Accessing content types as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Accessing content types as an Author + Given I have an "author" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + # showing content type + + Scenario: Accessing content type as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Projects" + + Scenario: Accessing content type as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Projects" + + Scenario: Accessing content type as an Author + Given I have an "author" API token + When I do an API GET request to content_types/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Projects" + + # create content type + + Scenario: Creating new content type as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API POST to content_types.json with: + """ + { + "content_type": { + "name": "Employees", + "slug": "employees", + "entries_custom_fields": [ + { + "label": "Name", + "name": "name", + "type": "string" + }, + { + "label": "Position", + "name": "position", + "type": "string" + } + ] + } + } + """ + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 2 entries + And the JSON should have the following: + | 1/name | "Employees" | + | 1/slug | "employees" | + | 1/entries_custom_fields/0/label | "Name" | + | 1/entries_custom_fields/0/name | "name" | + | 1/entries_custom_fields/0/type | "string" | + | 1/entries_custom_fields/1/label | "Position" | + | 1/entries_custom_fields/1/name | "position" | + | 1/entries_custom_fields/1/type | "string" | + + Scenario: Creating new content type as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API POST to content_types.json with: + """ + { + "content_type": { + "name": "Employees", + "slug": "employees", + "entries_custom_fields": [ + { + "label": "Name", + "name": "name", + "type": "string" + }, + { + "label": "Position", + "name": "position", + "type": "string" + } + ] + } + } + """ + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 2 entries + And the JSON should have the following: + | 1/name | "Employees" | + | 1/slug | "employees" | + | 1/entries_custom_fields/0/label | "Name" | + | 1/entries_custom_fields/0/name | "name" | + | 1/entries_custom_fields/0/type | "string" | + | 1/entries_custom_fields/1/label | "Position" | + | 1/entries_custom_fields/1/name | "position" | + | 1/entries_custom_fields/1/type | "string" | + + Scenario: Creating new content type as an Author + Given I have an "author" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API POST to content_types.json with: + """ + { + "content_type": { + "name": "Employees", + "slug": "employees", + "entries_custom_fields": [ + { + "label": "Name", + "name": "name", + "type": "string" + }, + { + "label": "Position", + "name": "position", + "type": "string" + } + ] + } + } + """ + Then an access denied error should occur + + # update content type + + Scenario: Updating content type as an Admin + Given I have an "admin" API token + When I do an API PUT to content_types/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_type": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to content_types/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating content type as a Designer + Given I have a "designer" API token + When I do an API PUT to content_types/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_type": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to content_types/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating content type as an Author + Given I have a "author" API token + When I do an API PUT to content_types/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "content_type": { + "name": "Brand new updated name" + } + } + """ + Then an access denied error should occur + + # destroy content type + + Scenario: Destroying content type as an Admin + Given I have an "admin" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API DELETE to content_types/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 0 entries + + Scenario: Destroying content type as a Designer + Given I have a "designer" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API DELETE to content_types/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 0 entries + + Scenario: Deleting content type as an Author + Given I have a "author" API token + When I do an API GET request to content_types.json + Then the JSON response should be an array + And the JSON response should have 1 entries + When I do an API DELETE to content_types/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur diff --git a/features/api/authorization/current_site.feature b/features/api/authorization/current_site.feature new file mode 100644 index 00000000..8a61a372 --- /dev/null +++ b/features/api/authorization/current_site.feature @@ -0,0 +1,30 @@ +Feature: Current Site + In order to ensure the current site can be viewed by all authenticated users + As an admin, designer or author + I should be able to show the current site + + Background: + Given I have the site: "test site" set up + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to current_site.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # showing current site + + Scenario: Accessing current site as an Admin + Given I have an "admin" API token + When I do an API GET to current_site.json + Then the JSON response at "name" should be "Locomotive test website" + + Scenario: Accessing current site as a Designer + Given I have a "designer" API token + When I do an API GET to current_site.json + Then the JSON response at "name" should be "Locomotive test website" + + Scenario: Accessing current site as an Author + Given I have an "author" API token + When I do an API GET to current_site.json + Then the JSON response at "name" should be "Locomotive test website" diff --git a/features/api/authorization/memberships.feature b/features/api/authorization/memberships.feature new file mode 100644 index 00000000..35663c50 --- /dev/null +++ b/features/api/authorization/memberships.feature @@ -0,0 +1,225 @@ +Feature: Memberships + In order to ensure memberships are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up with id: "4f832c2cb0d86d3f42fffffb" + And I have accounts: + | email | id | + | new-user@a.com | 4f832c2cb0d86d3f42fffffc | + And I have memberships: + | email | role | id | + | admin@a.com | admin | 4f832c2cb0d86d3f42fffffd | + | designer@a.com | designer | 4f832c2cb0d86d3f42fffffe | + | author@a.com | author | 4f832c2cb0d86d3f42ffffff | + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to memberships.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing memberships + + Scenario: Accessing memberships as an Admin + Given I have an "admin" API token + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 4 entries + + Scenario: Accessing memberships as a Designer + Given I have a "designer" API token + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 4 entries + + Scenario: Accessing memberships as an Author + Given I have an "author" API token + When I do an API GET request to memberships.json + Then an access denied error should occur + + # showing membership + + Scenario: Accessing membership as an Admin + Given I have an "admin" API token + When I do an API GET request to memberships/4f832c2cb0d86d3f42fffffd.json + Then the JSON response at "email" should be "admin@a.com" + When I do an API GET request to memberships/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "email" should be "designer@a.com" + When I do an API GET request to memberships/4f832c2cb0d86d3f42ffffff.json + Then the JSON response at "email" should be "author@a.com" + + Scenario: Accessing membership as a Designer + Given I have a "designer" API token + When I do an API GET request to memberships/4f832c2cb0d86d3f42fffffd.json + Then the JSON response at "email" should be "admin@a.com" + When I do an API GET request to memberships/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "email" should be "designer@a.com" + When I do an API GET request to memberships/4f832c2cb0d86d3f42ffffff.json + Then the JSON response at "email" should be "author@a.com" + + Scenario: Accessing membership as an Author + Given I have an "author" API token + When I do an API GET request to memberships/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur + + # create membership + + Scenario: Creating new membership as an Admin + Given I have an "admin" API token + When I do an API POST to memberships.json with: + """ + { + "membership": { + "site_id": "4f832c2cb0d86d3f42fffffb", + "account_id": "4f832c2cb0d86d3f42fffffc" + } + } + """ + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 5 entries + + Scenario: Creating new membership as a Designer + Given I have a "designer" API token + When I do an API POST to memberships.json with: + """ + { + "membership": { + "site_id": "4f832c2cb0d86d3f42fffffb", + "account_id": "4f832c2cb0d86d3f42fffffc" + } + } + """ + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 5 entries + + Scenario: Creating new membership as an Author + Given I have an "author" API token + When I do an API POST to memberships.json with: + """ + { + "membership": { + "site_id": "4f832c2cb0d86d3f42fffffb", + "account_id": "4f832c2cb0d86d3f42fffffc" + } + } + """ + Then an access denied error should occur + + Scenario: Created membership should always be Author + Given I have an "admin" API token + When I do an API POST to memberships.json with: + """ + { + "membership": { + "site_id": "4f832c2cb0d86d3f42fffffb", + "account_id": "4f832c2cb0d86d3f42fffffc", + "role": "admin" + } + } + """ + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 5 entries + And the JSON at "4/role" should be "author" + + # update membership + + Scenario: Updating membership as an Admin + Given I have an "admin" API token + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "admin" + } + } + """ + When I do an API GET request to memberships/4f832c2cb0d86d3f42ffffff.json + Then the JSON response at "role" should be "admin" + + Scenario: Updating membership as a Designer + Given I have a "designer" API token + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "admin" + } + } + """ + When I do an API GET request to memberships/4f832c2cb0d86d3f42ffffff.json + Then the JSON response at "role" should be "author" + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "designer" + } + } + """ + When I do an API GET request to memberships/4f832c2cb0d86d3f42ffffff.json + Then the JSON response at "role" should be "designer" + + Scenario: Updating membership as an Author + Given I have a "author" API token + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "admin" + } + } + """ + Then an access denied error should occur + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "designer" + } + } + """ + Then an access denied error should occur + When I do an API PUT to memberships/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "membership": { + "role": "author" + } + } + """ + Then an access denied error should occur + + # destroy membership + + Scenario: Destroying membership as an Admin + Given I have an "admin" API token + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API DELETE to memberships/4f832c2cb0d86d3f42ffffff.json + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 3 entries + + Scenario: Destroying membership as a Designer + Given I have a "designer" API token + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API DELETE to memberships/4f832c2cb0d86d3f42ffffff.json + When I do an API GET request to memberships.json + Then the JSON response should be an array + And the JSON response should have 3 entries + When I do an API DELETE to memberships/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur + When I do an API DELETE to memberships/4f832c2cb0d86d3f42fffffd.json + Then an access denied error should occur + + Scenario: Deleting membership as an Author + Given I have a "author" API token + When I do an API DELETE to memberships/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur diff --git a/features/api/authorization/pages.feature b/features/api/authorization/pages.feature new file mode 100644 index 00000000..2cc7d855 --- /dev/null +++ b/features/api/authorization/pages.feature @@ -0,0 +1,187 @@ +Feature: Pages + In order to ensure pages are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And I have a custom model named "Projects" with + | label | type | required | + | Name | string | true | + | Description | text | false | + And I have a designer and an author + And a page named "hello-world" with id "4f832c2cb0d86d3f42fffffe" + And a page named "goodbye-world" with id "4f832c2cb0d86d3f42ffffff" + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to pages.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing pages + + Scenario: Accessing pages as an Admin + Given I have an "admin" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + + Scenario: Accessing pages as a Designer + Given I have a "designer" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + + Scenario: Accessing pages as an Author + Given I have an "author" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + + # showing page + + Scenario: Accessing page as an Admin + Given I have an "admin" API token + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "slug" should be "hello-world" + + Scenario: Accessing page as a Designer + Given I have a "designer" API token + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "slug" should be "hello-world" + + Scenario: Accessing page as an Author + Given I have an "author" API token + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "slug" should be "hello-world" + + # create page + + Scenario: Creating new page as an Admin + Given I have an "admin" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API POST to pages.json with: + """ + { + "page": { + "title": "New Page", + "slug": "new-page", + "parent_id": "4f832c2cb0d86d3f42fffffe" + } + } + """ + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 5 entries + + Scenario: Creating new page as a Designer + Given I have a "designer" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API POST to pages.json with: + """ + { + "page": { + "title": "New Page", + "slug": "new-page", + "parent_id": "4f832c2cb0d86d3f42fffffe" + } + } + """ + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 5 entries + + Scenario: Creating new page as an Author + Given I have an "author" API token + When I do an API POST to pages.json with: + """ + { + "page": { + "title": "New Page", + "slug": "new-page", + "parent_id": "4f832c2cb0d86d3f42fffffe" + } + } + """ + Then an access denied error should occur + + # update page + + Scenario: Updating page as an Admin + Given I have an "admin" API token + When I do an API PUT to pages/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "page": { + "title": "Brand new updated title" + } + } + """ + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "title" should be "Brand new updated title" + + Scenario: Updating page as a Designer + Given I have a "designer" API token + When I do an API PUT to pages/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "page": { + "title": "Brand new updated title" + } + } + """ + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "title" should be "Brand new updated title" + + Scenario: Updating page as an Author + Given I have a "author" API token + When I do an API PUT to pages/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "page": { + "title": "Brand new updated title" + } + } + """ + When I do an API GET request to pages/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "title" should be "Brand new updated title" + + # destroy page + + Scenario: Destroying page as an Admin + Given I have an "admin" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API DELETE to pages/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 3 entries + + Scenario: Destroying page as a Designer + Given I have a "designer" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API DELETE to pages/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 3 entries + + Scenario: Deleting page as an Author + Given I have a "author" API token + When I do an API GET request to pages.json + Then the JSON response should be an array + And the JSON response should have 4 entries + When I do an API DELETE to pages/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur diff --git a/features/api/authorization/sites.feature b/features/api/authorization/sites.feature new file mode 100644 index 00000000..bb7b393a --- /dev/null +++ b/features/api/authorization/sites.feature @@ -0,0 +1,206 @@ +Feature: Sites + In order to ensure sites are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up with id: "4f832c2cb0d86d3f42fffffe" + And I have the site: "another site" set up with id: "4f832c2cb0d86d3f42ffffff" + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to sites.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing sites + + Scenario: Accessing sites as an Admin + Given I have an "admin" API token + When I do an API GET request to sites.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing sites as a Designer + Given I have a "designer" API token + When I do an API GET request to sites.json + Then an access denied error should occur + + Scenario: Accessing sites as an Author + Given I have an "author" API token + When I do an API GET request to sites.json + Then an access denied error should occur + + # showing site + + Scenario: Accessing site as an Admin + Given I have an "admin" API token + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Locomotive test website" + + Scenario: Accessing my site as a Designer + Given I have a "designer" API token + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Locomotive test website" + + Scenario: Accessing other site as a Designer + Given I have a "designer" API token + When I do an API GET request to sites/4f832c2cb0d86d3f42ffffff.json + Then an access denied error should occur + + Scenario: Accessing my site as an Author + Given I have an "author" API token + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Locomotive test website" + + Scenario: Accessing other site as an Author + Given I have an "author" API token + When I do an API GET request to sites/4f832c2cb0d86d3f42ffffff.json + Then an access denied error should occur + + # create site + + Scenario: Creating new site as an Admin + Given I have an "admin" API token + When I do an API GET request to sites.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to sites.json with: + """ + { + "site": { + "name": "New site", + "subdomain": "new-site" + } + } + """ + When I do an API GET request to sites.json + Then the JSON response should be an array + And the JSON response should have 3 entries + + Scenario: Creating new site as a Designer + Given I have a "designer" API token + When I do an API POST to sites.json with: + """ + { + "site": { + "name": "New site", + "subdomain": "new-site" + } + } + """ + Then an access denied error should occur + + Scenario: Creating new site as an Author + Given I have an "author" API token + When I do an API POST to sites.json with: + """ + { + "site": { + "name": "New site", + "subdomain": "new-site" + } + } + """ + Then an access denied error should occur + + # update site + + Scenario: Updating site as an Admin + Given I have an "admin" API token + When I do an API PUT to sites/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "site": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating my site as a Designer + Given I have a "designer" API token + When I do an API PUT to sites/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "site": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating other site as a Designer + Given I have a "designer" API token + When I do an API PUT to sites/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "site": { + "name": "Brand new updated name" + } + } + """ + Then an access denied error should occur + + Scenario: Updating my site as an Author + Given I have a "author" API token + When I do an API PUT to sites/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "site": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating other site as an Author + Given I have a "author" API token + When I do an API PUT to sites/4f832c2cb0d86d3f42ffffff.json with: + """ + { + "site": { + "name": "Brand new updated name" + } + } + """ + Then an access denied error should occur + + # destroy site + + Scenario: Destroying site as an Admin + Given I have an "admin" API token + When I do an API GET request to sites.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to sites/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to sites.json + Then the JSON response should be an array + And the JSON response should have 1 entries + + Scenario: Destroying my site as a Designer + Given I have a "designer" API token + When I do an API DELETE to sites/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to sites/4f832c2cb0d86d3f42fffffe.json + Then it should not exist + + Scenario: Deleting other site as a Designer + Given I have a "designer" API token + When I do an API DELETE to sites/4f832c2cb0d86d3f42ffffff.json + Then an access denied error should occur + + Scenario: Deleting my site as an Author + Given I have a "author" API token + When I do an API DELETE to sites/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur + + Scenario: Deleting other site as an Author + Given I have a "author" API token + When I do an API DELETE to sites/4f832c2cb0d86d3f42ffffff.json + Then an access denied error should occur diff --git a/features/api/authorization/snippets.feature b/features/api/authorization/snippets.feature new file mode 100644 index 00000000..058ca7e1 --- /dev/null +++ b/features/api/authorization/snippets.feature @@ -0,0 +1,179 @@ +Feature: Snippets + In order to ensure snippets are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And a snippet named "My Snippet" with id "4f832c2cb0d86d3f42fffffe" and template: + """ + My Snippet + """ + And I have a designer and an author + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to snippets.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing content types + + Scenario: Accessing snippets as an Admin + Given I have an "admin" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Accessing snippets as a Designer + Given I have a "designer" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + + Scenario: Accessing snippets as an Author + Given I have an "author" API token + When I do an API GET request to snippets.json + Then an access denied error should occur + + # showing snippet + + Scenario: Accessing snippet as an Admin + Given I have an "admin" API token + When I do an API GET request to snippets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "My Snippet" + + Scenario: Accessing snippet as a Designer + Given I have a "designer" API token + When I do an API GET request to snippets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "id" should be "4f832c2cb0d86d3f42fffffe" + And the JSON response at "name" should be "My Snippet" + + Scenario: Accessing snippet as an Author + Given I have an "author" API token + When I do an API GET request to snippets/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur + + # create snippet + + Scenario: Creating new snippet as an Admin + Given I have an "admin" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API POST to snippets.json with: + """ + { + "snippet": { + "name": "Another snippet", + "template": "

Another Snippet!

" + } + } + """ + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + And the JSON should have the following: + | 1/name | "Another Snippet" | + | 1/template | "

Another Snippet!

" | + + Scenario: Creating new snippet as a Designer + Given I have a "designer" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API POST to snippets.json with: + """ + { + "snippet": { + "name": "Another snippet", + "template": "

Another Snippet!

" + } + } + """ + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + And the JSON should have the following: + | 1/name | "Another Snippet" | + | 1/template | "

Another Snippet!

" | + + Scenario: Creating new snippet as an Author + Given I have an "author" API token + When I do an API POST to snippets.json with: + """ + { + "snippet": { + "name": "Another snippet", + "template": "

Another Snippet!

" + } + } + """ + Then an access denied error should occur + + # update snippet + + Scenario: Updating snippet as an Admin + Given I have an "admin" API token + When I do an API PUT to snippets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "snippet": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to snippets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating snippet as a Designer + Given I have a "designer" API token + When I do an API PUT to snippets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "snippet": { + "name": "Brand new updated name" + } + } + """ + When I do an API GET request to snippets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "name" should be "Brand new updated name" + + Scenario: Updating snippet as an Author + Given I have a "author" API token + When I do an API PUT to snippets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "snippet": { + "name": "Brand new updated name" + } + } + """ + Then an access denied error should occur + + # destroy snippet + + Scenario: Destroying snippet as an Admin + Given I have an "admin" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API DELETE to snippets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 0 entries + + Scenario: Destroying snippet as a Designer + Given I have a "designer" API token + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 1 entry + When I do an API DELETE to snippets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to snippets.json + Then the JSON response should be an array + And the JSON response should have 0 entries + + Scenario: Deleting snippet as an Author + Given I have a "author" API token + When I do an API DELETE to snippets/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur diff --git a/features/api/authorization/theme_assets.feature b/features/api/authorization/theme_assets.feature new file mode 100644 index 00000000..e366209b --- /dev/null +++ b/features/api/authorization/theme_assets.feature @@ -0,0 +1,185 @@ +Feature: Theme Assets + In order to ensure theme assets are not tampered with + As an admin, designer or author + I will be restricted based on my role + + Background: + Given I have the site: "test site" set up + And a javascript asset named "my_javascript.js" with id "4f832c2cb0d86d3f42fffffe" + And a stylesheet asset named "my_stylesheet.css" with id "4f832c2cb0d86d3f42ffffff" + + Scenario: As an unauthenticated user + Given I am not authenticated + When I do an API GET to theme_assets.json + Then the JSON response at "error" should be "You need to sign in or sign up before continuing." + + # listing theme assets + + Scenario: Accessing theme assets as an Admin + Given I have an "admin" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing theme assets as a Designer + Given I have a "designer" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + Scenario: Accessing theme assets as an Author + Given I have an "author" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + + # showing theme asset + + Scenario: Accessing theme asset as an Admin + Given I have an "admin" API token + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "local_path" should be "my_javascript.js" + + Scenario: Accessing theme asset as a Designer + Given I have a "designer" API token + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "local_path" should be "my_javascript.js" + + Scenario: Accessing theme asset as an Author + Given I have an "author" API token + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response at "local_path" should be "my_javascript.js" + + # create theme asset + + Scenario: Creating new theme asset as an Admin + Given I have an "admin" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to theme_assets.json with: + """ + { + "theme_asset": { + "plain_text_name": "new-javascript.js", + "plain_text": "function doNothing() {}", + "plain_text_type": "javascript", + "performing_plain_text": "true" + } + } + """ + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON should have the following: + | 2/local_path | "new-javascript.js" | + | 2/content_type | "javascript" | + + Scenario: Creating new theme asset as a Designer + Given I have a "designer" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API POST to theme_assets.json with: + """ + { + "theme_asset": { + "plain_text_name": "new-javascript.js", + "plain_text": "function doNothing() {}", + "plain_text_type": "javascript", + "performing_plain_text": "true" + } + } + """ + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 3 entries + And the JSON should have the following: + | 2/local_path | "new-javascript.js" | + | 2/content_type | "javascript" | + + Scenario: Creating new theme asset as an Author + Given I have an "author" API token + When I do an API POST to theme_assets.json with: + """ + { + "theme_asset": { + "plain_text_name": "new-javascript.js", + "plain_text": "function doNothing() {}", + "plain_text_type": "javascript", + "performing_plain_text": "true" + } + } + """ + Then an access denied error should occur + + # update theme asset + + Scenario: Updating theme asset as an Admin + Given I have an "admin" API token + When I do an API PUT to theme_assets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "theme_asset": { + "plain_text_name": "newer-javascript.js" + } + } + """ + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response should have the following: + | local_path | "newer-javascript.js" | + + Scenario: Updating theme asset as a Designer + Given I have a "designer" API token + When I do an API PUT to theme_assets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "theme_asset": { + "plain_text_name": "newer-javascript.js" + } + } + """ + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response should have the following: + | local_path | "newer-javascript.js" | + + Scenario: Updating theme asset as an Author + Given I have a "author" API token + When I do an API PUT to theme_assets/4f832c2cb0d86d3f42fffffe.json with: + """ + { + "theme_asset": { + "plain_text_name": "newer-javascript.js" + } + } + """ + When I do an API GET request to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then the JSON response should have the following: + | local_path | "newer-javascript.js" | + + # destroy theme asset + + Scenario: Destroying theme asset as an Admin + Given I have an "admin" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to theme_assets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 1 entries + + Scenario: Destroying theme asset as a Designer + Given I have a "designer" API token + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 2 entries + When I do an API DELETE to theme_assets/4f832c2cb0d86d3f42fffffe.json + When I do an API GET request to theme_assets.json + Then the JSON response should be an array + And the JSON response should have 1 entries + + Scenario: Deleting theme asset as an Author + Given I have a "author" API token + When I do an API DELETE to theme_assets/4f832c2cb0d86d3f42fffffe.json + Then an access denied error should occur diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb index d46ee3fc..0d3ad2fb 100644 --- a/features/step_definitions/api_steps.rb +++ b/features/step_definitions/api_steps.rb @@ -1,3 +1,26 @@ + +def api_base_url + "http://#{Locomotive::Site.first.domains.first}/locomotive/api/" +end + +def do_api_request(type, url, param_string_or_hash = nil) + begin + if param_string_or_hash + if param_string_or_hash.is_a? Hash + params = param_string_or_hash + else + params = JSON.parse(param_string_or_hash) + end + else + params = {} + end + @json_response = do_request(type, api_base_url, url, + params.merge({ 'CONTENT_TYPE' => 'application/json' })) + rescue CanCan::AccessDenied, Mongoid::Errors::DocumentNotFound + @error = $! + end +end + def last_json @json_response.try(:body) || page.source end @@ -11,13 +34,16 @@ Given /^I have an? "([^"]*)" API token$/ do |role| 'password' => 'easyone' } - response = post("http://#{@site.domains.first}/locomotive/api/tokens.json", login_params.to_json, { 'CONTENT_TYPE' => 'application/json' }) + response = do_request('POST', api_base_url, 'tokens.json', + login_params.merge({ 'CONTENT_TYPE' => 'application/json' })) if response.status == 200 @auth_token = JSON.parse(response.body)['token'] else raise JSON.parse(response.body)['message'] end + + add_default_params(:auth_token => @auth_token) end Given /^I do not have an API token$/ do @@ -35,4 +61,36 @@ end When /^I post to "([^"]*)" with:$/ do |path, json_string| @json_response = post("http://#{@site.domains.first}#{path}", json_string, { 'CONTENT_TYPE' => 'application/json' }) -end \ No newline at end of file +end + +When /^I do an API (\w+) (?:request )?to ([\w.\/]+)$/ do |request_type, url| + do_api_request(request_type, url) +end + +When /^I do an API (\w+) (?:request )?to ([\w.\/]+) with:$/ do |request_type, url, param_string| + do_api_request(request_type, url, param_string) +end + +Then /^an access denied error should occur$/ do + @error.should_not be_nil + @error.is_a?(CanCan::AccessDenied).should be_true +end + +Then /^it should not exist$/ do + @error.should_not be_nil + @error.is_a?(Mongoid::Errors::DocumentNotFound).should be_true +end + +When /^I do a multipart API (\w+) (?:request )?to ([\w.\/]+) with base key "([^"]*)" and:$/ \ + do |request_type, url, base_key, table| + params = {} + params = table.rows_hash + params.each do |key, filename| + params[key] = Rack::Test::UploadedFile.new(Rails.root.join('..', 'fixtures', filename)) + end + do_api_request(request_type, url, { base_key => params }) +end + +Then /^I print the json response$/ do + puts %{JSON: "#{last_json}"} +end diff --git a/features/step_definitions/content_assets_steps.rb b/features/step_definitions/content_assets_steps.rb new file mode 100644 index 00000000..0a53b29f --- /dev/null +++ b/features/step_definitions/content_assets_steps.rb @@ -0,0 +1,12 @@ + +Given /^I have the following content assets:$/ do |table| + site = Locomotive::Site.first + table.hashes.each do |asset_hash| + asset_hash['site'] = site + asset_hash['source'] = FixturedAsset.open(asset_hash['file']) + asset_hash.delete('file') + + asset = FactoryGirl.build(:asset, asset_hash) + asset.save.should be_true + end +end diff --git a/features/step_definitions/content_types_steps.rb b/features/step_definitions/content_types_steps.rb index 8852c0b6..ed45bf9e 100644 --- a/features/step_definitions/content_types_steps.rb +++ b/features/step_definitions/content_types_steps.rb @@ -1,6 +1,9 @@ -Given %r{^I have a custom model named "([^"]*)" with$} do |name, fields| +def build_content_type(name) site = Locomotive::Site.first - content_type = FactoryGirl.build(:content_type, :site => site, :name => name, :order_by => '_position') + FactoryGirl.build(:content_type, :site => site, :name => name, :order_by => '_position') +end + +def set_custom_fields_from_table(content_type, fields) fields.hashes.each do |field| # found a belongs_to association if field['type'] == 'belongs_to' @@ -12,6 +15,19 @@ Given %r{^I have a custom model named "([^"]*)" with$} do |name, fields| content_type.entries_custom_fields.build field end +end + +Given %r{^I have a custom model named "([^"]*)" with id "([^"]*)" and$} do |name, id, fields| + content_type = build_content_type(name) + content_type.id = BSON::ObjectId(id) + set_custom_fields_from_table(content_type, fields) + content_type.valid? + content_type.save.should be_true +end + +Given %r{^I have a custom model named "([^"]*)" with$} do |name, fields| + content_type = build_content_type(name) + set_custom_fields_from_table(content_type, fields) content_type.valid? content_type.save.should be_true end diff --git a/features/step_definitions/membership_steps.rb b/features/step_definitions/membership_steps.rb new file mode 100644 index 00000000..780cac2c --- /dev/null +++ b/features/step_definitions/membership_steps.rb @@ -0,0 +1,19 @@ + +Given /^I have accounts:$/ do |accounts_table| + accounts_table.hashes.each do |account_hash| + FactoryGirl.create(:account, account_hash) + end +end + +Given /^I have memberships:$/ do |members_table| + members_table.hashes.each do |member_hash| + email = member_hash[:email] + account = Locomotive::Account.where(:email => email).first \ + || FactoryGirl.create(:account, :email => email) + + member_hash.delete(:email) + member_hash.merge!({ :account => account, :site => @site }) + + FactoryGirl.create(:membership, member_hash) + end +end diff --git a/features/step_definitions/page_steps.rb b/features/step_definitions/page_steps.rb index fb9d1773..2acc9b93 100644 --- a/features/step_definitions/page_steps.rb +++ b/features/step_definitions/page_steps.rb @@ -2,9 +2,16 @@ # helps create a simple content page (parent: "index") with a slug, contents, and template def create_content_page(page_slug, page_contents, template = nil) - @home = @site.pages.where(:slug => "index").first || FactoryGirl.create(:page) - page = @site.pages.create(:slug => page_slug, :body => page_contents, :parent => @home, :title => "some title", :published => true, :raw_template => template) + page = new_content_page(page_slug, page_contents, template) page.should be_valid + page.save! + page +end + +# build page without saving +def new_content_page(page_slug, page_contents, template = nil) + @home = @site.pages.where(:slug => "index").first || FactoryGirl.create(:page) + page = @site.pages.new(:slug => page_slug, :body => page_contents, :parent => @home, :title => "some title", :published => true, :raw_template => template) page end @@ -17,6 +24,12 @@ Given /^a page named "([^"]*)" with the template:$/ do |page_slug, template| @page = create_content_page(page_slug, '', template) end +Given /^a page named "([^"]*)" with id "([^"]*)"$/ do |page_slug, id| + @page = new_content_page(page_slug, '') + @page.id = BSON::ObjectId(id) + @page.save! +end + # change the title When /^I change the page title to "([^"]*)"$/ do |page_title| page.evaluate_script "window.prompt = function() { return '#{page_title}'; }" diff --git a/features/step_definitions/snippet_steps.rb b/features/step_definitions/snippet_steps.rb index aa2b0ac6..7909931b 100644 --- a/features/step_definitions/snippet_steps.rb +++ b/features/step_definitions/snippet_steps.rb @@ -1,9 +1,13 @@ ### Snippets # helps create a simple snippet with a slug and template +def new_snippet(name, template = nil) + @site.snippets.new(:name => name, :template => template) +end + def create_snippet(name, template = nil) - snippet = @site.snippets.create(:name => name, :template => template) - snippet.should be_valid + snippet = new_snippet(name, template) + snippet.save! snippet end @@ -13,6 +17,12 @@ Given /^a snippet named "([^"]*)" with the template:$/ do |name, template| @snippet = create_snippet(name, template) end +Given /^a snippet named "([^"]*)" with id "([^"]*)" and template:$/ do |name, id, template| + @snippet = new_snippet(name, template) + @snippet.id = BSON::ObjectId(id) + @snippet.save! +end + When /^I change the snippet template to "([^"]*)"$/ do |code| page.evaluate_script "window.application_view.view.editor.setValue('#{code}')" end diff --git a/features/step_definitions/theme_asset_steps.rb b/features/step_definitions/theme_asset_steps.rb index 6cf5b71c..191b2178 100644 --- a/features/step_definitions/theme_asset_steps.rb +++ b/features/step_definitions/theme_asset_steps.rb @@ -1,15 +1,18 @@ ### Theme assets # helps create a theme asset -def create_plain_text_asset(name, type) - asset = FactoryGirl.build(:theme_asset, { +def new_plain_text_asset(name, type) + FactoryGirl.build(:theme_asset, { :site => @site, :plain_text_name => name, :plain_text => 'Lorem ipsum', :plain_text_type => type, :performing_plain_text => true }) +end +def create_plain_text_asset(name, type) + asset = new_plain_text_asset(name, type) asset.save! end @@ -19,10 +22,22 @@ Given /^a javascript asset named "([^"]*)"$/ do |name| @asset = create_plain_text_asset(name, 'javascript') end +Given /^a javascript asset named "([^"]*)" with id "([^"]*)"$/ do |name, id| + @asset = new_plain_text_asset(name, 'javascript') + @asset.id = BSON::ObjectId(id) + @asset.save! +end + Given /^a stylesheet asset named "([^"]*)"$/ do |name| @asset = create_plain_text_asset(name, 'stylesheet') end +Given /^a stylesheet asset named "([^"]*)" with id "([^"]*)"$/ do |name, id| + @asset = new_plain_text_asset(name, 'stylesheet') + @asset.id = BSON::ObjectId(id) + @asset.save! +end + Given /^I have an image theme asset named "([^"]*)"$/ do |name| @asset = FactoryGirl.create(:theme_asset, :site => @site, :source => File.open(Rails.root.join('..', 'fixtures', 'assets', '5k.png'))) @asset.source_filename = name diff --git a/features/support/http.rb b/features/support/http.rb new file mode 100644 index 00000000..5c08e0ee --- /dev/null +++ b/features/support/http.rb @@ -0,0 +1,22 @@ +module HTTPHelpers + + attr_accessor :default_params + + def add_default_params(params) + default_params.merge!(params) + end + + def do_request(type, base_url, url, params) + request_method = type.downcase.to_sym + send(request_method, "#{base_url}/#{url}", default_params.merge(params)) + end + + protected + + def default_params + @default_params ||= {} + end + +end + +World(HTTPHelpers) diff --git a/spec/fixtures/assets/5k_2.png b/spec/fixtures/assets/5k_2.png new file mode 100644 index 00000000..cf85f7cf Binary files /dev/null and b/spec/fixtures/assets/5k_2.png differ