From c4884143c0cbe14c7d9700e9f6876160b6c50ec0 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 8 Nov 2012 21:35:47 -0800 Subject: [PATCH 1/9] got question and solution json apis versioned --- app/assets/javascripts/application.js | 24 ++++++++++ app/controllers/api/v1/api_controller.rb | 45 +++++++++++++++++++ .../api/v1/questions_controller.rb | 12 +++++ .../api/v1/solutions_controller.rb | 18 ++++++++ app/controllers/api_keys_controller.rb | 22 +++++++++ app/helpers/jquery_helper.rb | 12 +++++ app/models/api_key.rb | 19 ++++++++ app/models/user.rb | 2 + .../api/v1/multipart_questions/_show.jsonify | 34 ++++++++++++++ .../api/v1/multipart_questions/show.jsonify | 5 +++ .../api/v1/questions/_attribution.jsonify | 24 ++++++++++ app/views/api/v1/questions/_common.jsonify | 5 +++ app/views/api/v1/questions/show.jsonify | 10 +++++ .../_content_and_answers.jsonify | 15 +++++++ .../api/v1/simple_questions/_show.jsonify | 25 +++++++++++ .../api/v1/simple_questions/show.jsonify | 5 +++ app/views/api/v1/solutions/_show.jsonify | 30 +++++++++++++ app/views/api/v1/solutions/index.jsonify | 17 +++++++ app/views/api/v1/solutions/show.jsonify | 6 +++ app/views/api_keys/_terms.html.erb | 16 +++++++ app/views/api_keys/create.js.erb | 2 + app/views/api_keys/new.js.erb | 3 ++ app/views/devise/registrations/edit.html.erb | 13 ++++++ app/views/help/topics/_api_keys.html.erb | 3 ++ app/views/layouts/application.html.erb | 5 +++ app/views/shared/specified_dialog.js.erb | 15 +++++++ config/environment.rb | 1 + config/environments/production.rb | 2 +- config/routes.rb | 11 +++++ db/migrate/20121108231643_create_api_keys.rb | 10 +++++ db/schema.rb | 9 +++- lib/api_constraints.rb | 10 +++++ test/factories/api_keys.rb | 8 ++++ test/functional/api_keys_controller_test.rb | 7 +++ test/unit/api_key_test.rb | 7 +++ 35 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/api_controller.rb create mode 100644 app/controllers/api/v1/questions_controller.rb create mode 100644 app/controllers/api/v1/solutions_controller.rb create mode 100644 app/controllers/api_keys_controller.rb create mode 100644 app/models/api_key.rb create mode 100644 app/views/api/v1/multipart_questions/_show.jsonify create mode 100644 app/views/api/v1/multipart_questions/show.jsonify create mode 100644 app/views/api/v1/questions/_attribution.jsonify create mode 100644 app/views/api/v1/questions/_common.jsonify create mode 100644 app/views/api/v1/questions/show.jsonify create mode 100644 app/views/api/v1/simple_questions/_content_and_answers.jsonify create mode 100644 app/views/api/v1/simple_questions/_show.jsonify create mode 100644 app/views/api/v1/simple_questions/show.jsonify create mode 100644 app/views/api/v1/solutions/_show.jsonify create mode 100644 app/views/api/v1/solutions/index.jsonify create mode 100644 app/views/api/v1/solutions/show.jsonify create mode 100644 app/views/api_keys/_terms.html.erb create mode 100644 app/views/api_keys/create.js.erb create mode 100644 app/views/api_keys/new.js.erb create mode 100644 app/views/help/topics/_api_keys.html.erb create mode 100644 app/views/shared/specified_dialog.js.erb create mode 100644 db/migrate/20121108231643_create_api_keys.rb create mode 100644 lib/api_constraints.rb create mode 100644 test/factories/api_keys.rb create mode 100644 test/functional/api_keys_controller_test.rb create mode 100644 test/unit/api_key_test.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7f0b9a8..2209cc6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -133,3 +133,27 @@ function hide_question_until_loaded() { $(".question-wrapper").fadeIn('500'); }); } + +function open_specified_dialog(name, is_modal, height, width, title, body) { + $('#' + name + '_dialog_errors').html(''); + + $("#" + name + "_dialog_body").html(body); + + $("#" + name + "_dialog").dialog({ + autoOpen: false, + modal: is_modal, + height: height, + width: width, + title: title, + position: 'center' + }); + + refresh_buttons(); + $("#" + name + "_dialog").dialog('open'); + $("#" + name + "_dialog").scrollTop(0); + $("#" + name + "_dialog").dialog('open').closeOnClickOutside(); +} + +function open_message_dialog(is_modal, height, width, title, body) { + open_specified_dialog('message', is_modal, height, width, title, body); +} diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb new file mode 100644 index 0000000..4395f19 --- /dev/null +++ b/app/controllers/api/v1/api_controller.rb @@ -0,0 +1,45 @@ +module Api + module V1 + class ApiController < ApplicationController + skip_before_filter :authenticate_user! + before_filter :check_token_and_get_user + + respond_to :json + + rescue_from Exception, :with => :rescue_from_exception + + private + + def check_token_and_get_user + authenticate_or_request_with_http_token do |token, options| + api_key = ApiKey.find_by_access_token(token) + @api_user = api_key.try(:user) + !api_key.nil? + end + end + + def rescue_from_exception(exception) + # See https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L453 for error names/symbols + + error = :internal_server_error + send_email = true + + case exception + when SecurityTransgression + error = :forbidden + send_email = false + when ActiveRecord::RecordNotFound, + ActionController::RoutingError, + ActionController::UnknownController, + AbstractController::ActionNotFound + error = :not_found + send_email = false + end + + DeveloperErrorNotifier.exception_email(exception, request, present_user) if send_email + head error + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb new file mode 100644 index 0000000..2abc111 --- /dev/null +++ b/app/controllers/api/v1/questions_controller.rb @@ -0,0 +1,12 @@ +module Api + module V1 + class QuestionsController < ApiController + + def show + @question = Question.from_param(params[:id]) + raise SecurityTransgression unless @api_user.can_read?(@question) + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/solutions_controller.rb b/app/controllers/api/v1/solutions_controller.rb new file mode 100644 index 0000000..1f2813e --- /dev/null +++ b/app/controllers/api/v1/solutions_controller.rb @@ -0,0 +1,18 @@ +module Api + module V1 + class SolutionsController < ApiController + + def index + @question = Question.from_param(params[:question_id]) + raise SecurityTransgression unless @api_user.can_read?(@question) + @solutions = Vote.order_by_votes(@question.valid_solutions_visible_for(@api_user)) + end + + def show + @solution = Solution.find(params[:id]) + raise SecurityTransgression unless @api_user.can_read?(@solution) + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb new file mode 100644 index 0000000..c0955b2 --- /dev/null +++ b/app/controllers/api_keys_controller.rb @@ -0,0 +1,22 @@ +class ApiKeysController < ApplicationController + + before_filter :get_user + + def new + @api_key = ApiKey.new + end + + def create + raise SecurityTransgression unless current_user == @user + @api_key = ApiKey.new + @api_key.user = @user + @api_key.save + end + +protected + + def get_user + @user = User.find(params[:user_id]) + end + +end diff --git a/app/helpers/jquery_helper.rb b/app/helpers/jquery_helper.rb index 0f6542b..5bd602a 100644 --- a/app/helpers/jquery_helper.rb +++ b/app/helpers/jquery_helper.rb @@ -44,4 +44,16 @@ def reload_mathjax(element_id="") element_id + '")]);').html_safe end + def message_dialog(title=nil, options={}, &block) + specified_dialog("message", title, options, &block) + end + + def specified_dialog(name=nil, title=nil, options={}, &block) + @name ||= name + @title ||= title + @options = options + @body = capture(&block) + render :template => 'shared/specified_dialog' + end + end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..2725554 --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,19 @@ +class ApiKey < ActiveRecord::Base + belongs_to :user + + before_create :generate_access_token + before_create :destroy_previous + +private + + def generate_access_token + begin + self.access_token = SecureRandom.hex + end while self.class.exists?(access_token: access_token) + end + + def destroy_previous + user.try(:api_key).try(:destroy) + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index 4e64e1c..4498029 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,8 @@ class User < ActiveRecord::Base has_many :deputizers, :through => :received_deputizations + has_one :api_key, :dependent => :destroy + validates_presence_of :first_name, :last_name, :username, :user_profile validates_uniqueness_of :username, :case_sensitive => false validates_length_of :username, :in => 3..40 diff --git a/app/views/api/v1/multipart_questions/_show.jsonify b/app/views/api/v1/multipart_questions/_show.jsonify new file mode 100644 index 0000000..915623b --- /dev/null +++ b/app/views/api/v1/multipart_questions/_show.jsonify @@ -0,0 +1,34 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.multipart_question do + json.ingest! (render :partial => 'questions/common', + :locals => {:question => @question}) + + qs = @question.question_setup + unless qs.nil? + json.introduction do + json.markup qs.content + json.html qs.content_html + end + end + + json.parts(@question.child_question_parts) do |part| + child_question = part.child_question + + json.prerequisites(child_question.prerequisite_questions) do |pq| + json.id pq.to_param + end + + json.supported_by(child_question.supporting_questions) do |sq| + json.id sq.to_param + end + + json.ingest! (render :partial => 'simple_questions/show', + :locals => {:question => child_question, + :skip_question_setup => true, + :skip_attribution => true}) + end + + json.ingest! (render :partial => 'questions/attribution') +end diff --git a/app/views/api/v1/multipart_questions/show.jsonify b/app/views/api/v1/multipart_questions/show.jsonify new file mode 100644 index 0000000..e50a725 --- /dev/null +++ b/app/views/api/v1/multipart_questions/show.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.ingest! (render :partial => 'multipart_questions/show', + :locals => {:question => question}) diff --git a/app/views/api/v1/questions/_attribution.jsonify b/app/views/api/v1/questions/_attribution.jsonify new file mode 100644 index 0000000..c31eb51 --- /dev/null +++ b/app/views/api/v1/questions/_attribution.jsonify @@ -0,0 +1,24 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +collaborators = @question.question_collaborators + +authors = collaborators.select{|c| c.is_author} +copyright_holders = collaborators.select{|c| c.is_copyright_holder} + +json.attribution do + json.authors(authors) do |author| + json.id author.user.id + json.name author.user.full_name + end + + json.copyright_holders(copyright_holders) do |copyright_holder| + json.id copyright_holder.user.id + json.name copyright_holder.user.full_name + end + + json.license do + json.name @question.license.long_name + json.url @question.license.url + end +end \ No newline at end of file diff --git a/app/views/api/v1/questions/_common.jsonify b/app/views/api/v1/questions/_common.jsonify new file mode 100644 index 0000000..b952a13 --- /dev/null +++ b/app/views/api/v1/questions/_common.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.id question.to_param +json.url question_url(question) \ No newline at end of file diff --git a/app/views/api/v1/questions/show.jsonify b/app/views/api/v1/questions/show.jsonify new file mode 100644 index 0000000..dfdf875 --- /dev/null +++ b/app/views/api/v1/questions/show.jsonify @@ -0,0 +1,10 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +if @question.errors.none? + json.ingest! (render :partial => "#{view_dir(@question)}/show", + :locals => {:question => @question}) +else + json.ingest! (render :partial => 'shared/errors', + :locals => {:errors => @question.errors}) +end \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/_content_and_answers.jsonify b/app/views/api/v1/simple_questions/_content_and_answers.jsonify new file mode 100644 index 0000000..b21b74f --- /dev/null +++ b/app/views/api/v1/simple_questions/_content_and_answers.jsonify @@ -0,0 +1,15 @@ + +json.content do + json.markup question.content + json.html question.variated_content_html +end + +if !question.answer_choices.empty? + json.answer_choices(question.answer_choices) do |answer_choice| + json.markup answer_choice.content + json.html answer_choice.variated_content_html + json.credit answer_choice.credit + end +end + +json.answer_can_be_sketched question.answer_can_be_sketched \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/_show.jsonify b/app/views/api/v1/simple_questions/_show.jsonify new file mode 100644 index 0000000..2d0b6bb --- /dev/null +++ b/app/views/api/v1/simple_questions/_show.jsonify @@ -0,0 +1,25 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +skip_question_setup ||= false +skip_attribution ||= false + +json.simple_question do + json.ingest! (render :partial => 'questions/common', + :locals => {:question => question}) + + if !question.question_setup.blank? && !skip_question_setup + json.introduction do + json.markup question.question_setup.content + json.html question.question_setup.variated_content_html + end + end + + json.ingest! (render :partial => 'simple_questions/content_and_answers', + :locals => {:question => question}) + + if !skip_attribution + json.ingest! (render :partial => 'questions/attribution', + :locals => {:question => question}) + end +end \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/show.jsonify b/app/views/api/v1/simple_questions/show.jsonify new file mode 100644 index 0000000..6b58acf --- /dev/null +++ b/app/views/api/v1/simple_questions/show.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.ingest! (render :partial => 'simple_questions/show', + :locals => {:question => question}) diff --git a/app/views/api/v1/solutions/_show.jsonify b/app/views/api/v1/solutions/_show.jsonify new file mode 100644 index 0000000..2acefa0 --- /dev/null +++ b/app/views/api/v1/solutions/_show.jsonify @@ -0,0 +1,30 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.id solution.id +json.url solution_url(solution) +json.question_id solution.question.to_param + +json.explanation solution.explanation + +json.details do + json.markup solution.content + json.html solution.content_html +end + +json.attachments(solution.attachable_assets) do |aa| + json.name aa.local_name + json.mime_type aa.asset.attachment_content_type + json.url attachable_asset_download_url(aa) +end + +json.creator do + json.name solution.creator.full_name + json.gravatar_hash gravatar_hash(solution.creator) +end + +json.votes do + json.up solution.up_votes.count + json.down solution.down_votes.count + json.total solution.combined_vote_count +end diff --git a/app/views/api/v1/solutions/index.jsonify b/app/views/api/v1/solutions/index.jsonify new file mode 100644 index 0000000..ea8f27c --- /dev/null +++ b/app/views/api/v1/solutions/index.jsonify @@ -0,0 +1,17 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +if @question.is_multipart? + json.parts(@question.child_question_parts) do |part| + child_question = part.child_question + + json.solutions(child_question.solutions) do |solution| + json.ingest! (render :partial=>'show', :locals => {:solution => solution}) + end + end + +else + json.solutions(@solutions) do |solution| + json.ingest! (render :partial=>'show', :locals => {:solution => solution}) + end +end \ No newline at end of file diff --git a/app/views/api/v1/solutions/show.jsonify b/app/views/api/v1/solutions/show.jsonify new file mode 100644 index 0000000..744da6f --- /dev/null +++ b/app/views/api/v1/solutions/show.jsonify @@ -0,0 +1,6 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.solution do + json.ingest! render(:partial => 'show', :locals => {:solution => @solution}) +end \ No newline at end of file diff --git a/app/views/api_keys/_terms.html.erb b/app/views/api_keys/_terms.html.erb new file mode 100644 index 0000000..c4e2688 --- /dev/null +++ b/app/views/api_keys/_terms.html.erb @@ -0,0 +1,16 @@ +

You must agree to the following terms of use to generate an API key.

+ +

Note that agreeing to these terms will permanently delete any existing API key you already have.

+ +

By clicking "I agree" below, you agree that:

+ +
    +
  1. Your usage of the Quadbase API can be revoked at any time for any reason. Typical reasons for revokation include abuse or spamming the system.
  2. +
  3. These terms can change at any time.
  4. +
+ +

If you do not agree, just close this popup.

+ +<%= form_for [current_user, @api_key], :method => :post, :remote => true do |f| %> + <%= f.submit "I agree", :class => submit_classes, :onclick => please_wait_js %> +<% end %> diff --git a/app/views/api_keys/create.js.erb b/app/views/api_keys/create.js.erb new file mode 100644 index 0000000..15ee216 --- /dev/null +++ b/app/views/api_keys/create.js.erb @@ -0,0 +1,2 @@ + $("#api_key").html("<%= @api_key.access_token %>"); + $("#message_dialog").dialog('close'); \ No newline at end of file diff --git a/app/views/api_keys/new.js.erb b/app/views/api_keys/new.js.erb new file mode 100644 index 0000000..931f081 --- /dev/null +++ b/app/views/api_keys/new.js.erb @@ -0,0 +1,3 @@ +<%= message_dialog "API Terms of Use", {:width => 450, :height => 500} do %> + <%= render :partial => 'api_keys/terms' %> +<% end %> \ No newline at end of file diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 0cefcac..cc662a4 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -120,6 +120,19 @@
<%= gravatar_image current_user %>
+ +
+
API Key
+ +
+ <%= current_user.api_key.nil? ? 'None'.html_safe : current_user.api_key.access_token %> + <%= link_to "Regenerate", new_user_api_key_path(current_user), :remote => true, :style => 'float:right' %> +
+ +

<%= link_to_help 'api_keys', "Learn more about Quadbase API keys" %>

+ +
+ diff --git a/app/views/help/topics/_api_keys.html.erb b/app/views/help/topics/_api_keys.html.erb new file mode 100644 index 0000000..c206234 --- /dev/null +++ b/app/views/help/topics/_api_keys.html.erb @@ -0,0 +1,3 @@ +

Your API key is used to access Quadbase from other systems. Protect your API key like a password. +You can get a new API key at any time by clicking the "regenerate" button. Note that this will permanently +delete any existing API key you have.

\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b3994ea..1de9ef8 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -205,6 +205,11 @@ <% if @include_help_dialog %> <% end %> + + <%= yield :javascript %> diff --git a/app/views/shared/specified_dialog.js.erb b/app/views/shared/specified_dialog.js.erb new file mode 100644 index 0000000..c01a6a3 --- /dev/null +++ b/app/views/shared/specified_dialog.js.erb @@ -0,0 +1,15 @@ +<%# Copyright 2011-2012 Rice University. Licensed under the Affero General Public + License version 3 or later. See the COPYRIGHT file for details. %> + +<% + @options ||= {} + @height = @options[:height] || 300 + @width = @options[:width] || 400 + @body ||= yield #'Specify a body argument!' + @title ||= '' +%> + +open_specified_dialog('<%= @name %>', true, <%= @height %>, <%= @width %>, "<%= @title %>", "<%= escape_javascript(@body) %>"); + +refresh_buttons(); +<%= reload_mathjax("#{@name}_dialog") %> diff --git a/config/environment.rb b/config/environment.rb index 8a8718f..590fb1b 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -15,6 +15,7 @@ require 'acts_as_numberable' require 'variated_content_html' require 'will_paginate/array' +require 'api_constraints' ActionMailer::Base.delivery_method = :sendmail diff --git a/config/environments/production.rb b/config/environments/production.rb index 8ce8cf5..1e821f1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -70,7 +70,7 @@ # Took ":string => true" out b/c session cookies weren't making it across the break # from https to http - config.middleware.use Rack::SslEnforcer, :only => /^\/users/ #, :strict => true + config.middleware.use Rack::SslEnforcer, :only => /^\/users|^\/api/ #, :strict => true #config.middleware.insert_before ActionDispatch::Cookies, Rack::SslEnforcer # Log the query plan for queries taking more than this (works diff --git a/config/routes.rb b/config/routes.rb index 22d3a03..75bdbe4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,16 @@ Quadbase::Application.routes.draw do + namespace :api, defaults: {format: 'json'} do + scope module: :v1, constraints: ApiConstraints.new(version: 1) do + resources :questions, :only => [:show] do + resources :solutions, :only => [:index] + end + resources :solutions, :only => [:show] + end + end + + namespace :admin do resources :logic_libraries do resources :logic_library_versions, :shallow => true @@ -73,6 +83,7 @@ def votable resources :users, :only => [:index, :show, :edit, :update] do post 'become' post 'confirm' + resources :api_keys, :shallow => true, :only => [:new, :create] end resources :deputizations, :only => [:create, :destroy, :new] do diff --git a/db/migrate/20121108231643_create_api_keys.rb b/db/migrate/20121108231643_create_api_keys.rb new file mode 100644 index 0000000..b5766f4 --- /dev/null +++ b/db/migrate/20121108231643_create_api_keys.rb @@ -0,0 +1,10 @@ +class CreateApiKeys < ActiveRecord::Migration + def change + create_table :api_keys do |t| + t.string :access_token + t.integer :user_id + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7bf1275..efa0b59 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120726211039) do +ActiveRecord::Schema.define(:version => 20121108231643) do create_table "announcements", :force => true do |t| t.integer "user_id" @@ -35,6 +35,13 @@ add_index "answer_choices", ["question_id"], :name => "index_answer_choices_on_question_id" + create_table "api_keys", :force => true do |t| + t.string "access_token" + t.integer "user_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "assets", :force => true do |t| t.string "attachment_file_name" t.string "attachment_content_type" diff --git a/lib/api_constraints.rb b/lib/api_constraints.rb new file mode 100644 index 0000000..8693c3c --- /dev/null +++ b/lib/api_constraints.rb @@ -0,0 +1,10 @@ +class ApiConstraints + def initialize(options) + @version = options[:version] + @default = options[:default] + end + + def matches?(req) + @default || req.headers['Accept'].include?("application/vnd.quadbase.v#{@version}") + end +end \ No newline at end of file diff --git a/test/factories/api_keys.rb b/test/factories/api_keys.rb new file mode 100644 index 0000000..7e88779 --- /dev/null +++ b/test/factories/api_keys.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :api_key do + access_token "MyString" + user_id 1 + end +end diff --git a/test/functional/api_keys_controller_test.rb b/test/functional/api_keys_controller_test.rb new file mode 100644 index 0000000..fb2f782 --- /dev/null +++ b/test/functional/api_keys_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ApiKeysControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/unit/api_key_test.rb b/test/unit/api_key_test.rb new file mode 100644 index 0000000..2b10127 --- /dev/null +++ b/test/unit/api_key_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ApiKeyTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From c72507cd9499f258cef26a76362eadaecf42ce35 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Wed, 14 Nov 2012 14:16:32 -0800 Subject: [PATCH 2/9] doorkeeper implementation --- Gemfile | 2 + Gemfile.lock | 3 + app/controllers/api/v1/api_controller.rb | 18 +++-- .../api/v1/questions_controller.rb | 5 +- app/controllers/application_controller.rb | 10 +-- app/helpers/application_helper.rb | 3 + .../doorkeeper/applications/_form.html.erb | 34 ++++++++++ .../doorkeeper/applications/edit.html.erb | 13 ++++ .../doorkeeper/applications/index.html.erb | 25 +++++++ .../doorkeeper/applications/new.html.erb | 13 ++++ .../doorkeeper/applications/show.html.erb | 26 +++++++ .../doorkeeper/authorizations/error.html.erb | 6 ++ .../doorkeeper/authorizations/new.html.erb | 37 ++++++++++ .../doorkeeper/authorizations/show.html.erb | 4 ++ .../authorized_applications/index.html.erb | 25 +++++++ app/views/layouts/application.html.erb | 4 +- config/application.rb | 4 ++ config/environment.rb | 1 + config/initializers/doorkeeper.rb | 59 ++++++++++++++++ config/locales/doorkeeper.en.yml | 68 +++++++++++++++++++ config/routes.rb | 2 + ...20121109221727_create_doorkeeper_tables.rb | 45 ++++++++++++ db/schema.rb | 44 +++++++++++- lib/shared_application_methods.rb | 13 ++++ 24 files changed, 450 insertions(+), 14 deletions(-) create mode 100644 app/views/doorkeeper/applications/_form.html.erb create mode 100644 app/views/doorkeeper/applications/edit.html.erb create mode 100644 app/views/doorkeeper/applications/index.html.erb create mode 100644 app/views/doorkeeper/applications/new.html.erb create mode 100644 app/views/doorkeeper/applications/show.html.erb create mode 100644 app/views/doorkeeper/authorizations/error.html.erb create mode 100644 app/views/doorkeeper/authorizations/new.html.erb create mode 100644 app/views/doorkeeper/authorizations/show.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/index.html.erb create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 db/migrate/20121109221727_create_doorkeeper_tables.rb create mode 100644 lib/shared_application_methods.rb diff --git a/Gemfile b/Gemfile index 928da38..bb5ae33 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,8 @@ gem 'therubyracer', '~> 0.10.1' gem 'bullring', '~> 0.8.3' +gem 'doorkeeper', '~> 0.6.0' + group :development, :test do gem 'sqlite3', '~> 1.3.6' gem 'debugger', '~> 1.1.4' diff --git a/Gemfile.lock b/Gemfile.lock index 30d7a2f..4bf8519 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,8 @@ GEM orm_adapter (~> 0.1) railties (~> 3.1) warden (~> 1.2.1) + doorkeeper (0.6.1) + railties (~> 3.1) erubis (2.7.0) eventmachine (0.12.10) execjs (1.4.0) @@ -204,6 +206,7 @@ DEPENDENCIES coffee-rails (~> 3.2.2) debugger (~> 1.1.4) devise (~> 2.1.0) + doorkeeper (~> 0.6.0) execjs (~> 1.4.0) factory_girl_rails (~> 3.4.0) faker (~> 1.0.1) diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 4395f19..f3dd168 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -2,7 +2,7 @@ module Api module V1 class ApiController < ApplicationController skip_before_filter :authenticate_user! - before_filter :check_token_and_get_user + # before_filter :check_token_and_get_user respond_to :json @@ -10,11 +10,17 @@ class ApiController < ApplicationController private - def check_token_and_get_user - authenticate_or_request_with_http_token do |token, options| - api_key = ApiKey.find_by_access_token(token) - @api_user = api_key.try(:user) - !api_key.nil? + # def check_token_and_get_user + # authenticate_or_request_with_http_token do |token, options| + # api_key = ApiKey.find_by_access_token(token) + # @api_user = api_key.try(:user) + # !api_key.nil? + # end + # end + + def current_user + if doorkeeper_token + @current_user ||= User.find(doorkeeper_token.resource_owner_id) end end diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb index 2abc111..f7e83d6 100644 --- a/app/controllers/api/v1/questions_controller.rb +++ b/app/controllers/api/v1/questions_controller.rb @@ -1,10 +1,11 @@ module Api module V1 - class QuestionsController < ApiController + class QuestionsController < ApiController + doorkeeper_for :all # don't really need this if do SecurityTransgression stuff def show @question = Question.from_param(params[:id]) - raise SecurityTransgression unless @api_user.can_read?(@question) + raise SecurityTransgression unless current_user.can_read?(@question) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 81f55dd..743ff62 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,7 +16,7 @@ class ApplicationController < ActionController::Base helper_method :user_is_disabled?, :site_in_maintenance?, - :user_is_admin?, + # :user_is_admin?, :present_user, :get_error_messages, :protect_form, @@ -24,6 +24,8 @@ class ApplicationController < ActionController::Base respond_to :html, :js + include SharedApplicationMethods + unless Quadbase::Application.config.consider_all_requests_local rescue_from Exception, :with => :rescue_from_exception end @@ -97,9 +99,9 @@ def site_not_in_maintenance! redirect_maintenance end - def user_is_admin? - user_signed_in? && current_user.is_administrator? - end + # def user_is_admin? + # user_signed_in? && current_user.is_administrator? + # end def authenticate_admin! user_is_admin? || redirect_not_admin diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 29d1826..9005d0c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,6 +4,9 @@ require 'digest/md5' module ApplicationHelper + + include SharedApplicationMethods + def trash_icon image_tag("trash.gif", {:border => 0, :alt => "Delete", :title => "Delete"}) #{}"".html_safe diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 0000000..0ab352d --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,34 @@ +<%= form_for([:oauth, application]) do |f| %> +
+ <% if application.errors.any? %> +
×

Whoops! Check your form for possible errors

+ <% end %> + +
+ <%= f.label :name %> +
+ <%= f.text_field :name %> + <%= errors_for application, :name %> +
+
+ +
+ <%= f.label :redirect_uri %> +
+ <%= f.text_field :redirect_uri %> + <%= errors_for application, :redirect_uri %> + <% if Doorkeeper.configuration.test_redirect_uri %> + Use <%= Doorkeeper.configuration.test_redirect_uri %> for local tests + <% end %> +
+
+ +
+ +
+ <%= f.submit :Submit, :class => "btn primary" %> + <%= link_to "Cancel", oauth_applications_path, :class => "btn" %> +
+
+<% end %> + diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 0000000..7df339c --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,13 @@ +
+ +
+ +
+ <%= render 'form', :application => @application %> +
+ +
+

Actions

+

<%= link_to 'Back to application list', oauth_applications_path %>

+
+ diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 0000000..2488a6b --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,25 @@ +<%= pageHeading "Oauth Applications" %> + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + + <% end %> + +
NameCallback url
<%= link_to application.name, [:oauth, application] %><%= application.redirect_uri %><%= link_to 'Edit', edit_oauth_application_path(application) %><%= link_to 'Destroy', [:oauth, application], :data => { :confirm => 'Are you sure?' }, :method => :delete %>
+ + +

<%= link_to 'New Application', new_oauth_application_path %>

diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 0000000..0f82126 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,13 @@ +
+ +
+ +
+ <%= render 'form', :application => @application %> +
+ +
+

Actions

+

<%= link_to 'Back to application list', oauth_applications_path %>

+
+ diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 0000000..56bef81 --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,26 @@ +
+ +
+ +
+

Callback url:

+

<%= @application.redirect_uri %>

+ +

Application Id:

+

<%= @application.uid %>

+ +

Secret:

+

<%= @application.secret %>

+ +

Link to authorization code:

+

<%= link_to 'Authorize', oauth_authorization_path(:client_id => @application.uid, :redirect_uri => @application.redirect_uri, :response_type => 'code' ) %>

+
+ +
+

Actions

+

<%= link_to 'List all', oauth_applications_path %>

+

<%= link_to 'Edit', edit_oauth_application_path(@application) %>

+

<%= link_to 'Remove', [:oauth, @application], :method => :delete, :data => { :confirm => "Are you sure?" } %>

+
diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 0000000..c3dd1f6 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,6 @@ +
+

An error has occurred

+

+

<%= @pre_auth.error_response.body[:error_description] %>
+

+
diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 0000000..30a2000 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,37 @@ +
+

Authorize <%= @pre_auth.client.name %> to use your account?

+
+ +
+ <% if @pre_auth.scopes %> +

+ This application will be able to: +

+
    + <% @pre_auth.scopes.each do |scope| %> +
  • <%= t scope, :scope => [:doorkeeper, :scopes] %>
  • + <% end %> +
+ <% end %> + +
+ <%= form_tag oauth_authorization_path, :method => :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag "Authorize", :class => "btn success" %> or + <% end %> +
+
+ <%= form_tag oauth_authorization_path, :method => :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag "Deny", :class => "btn" %> + <% end %> +
+
diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 0000000..df03b44 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,4 @@ +
+

Authorization code:

+ <%= params[:code] %> +
diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 0000000..9016dea --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,25 @@ +
+ + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
ApplicationAuthorized at
<%= application.name %><%= application.created_at %><%= link_to 'Revoke', oauth_authorized_application_path(application), :data => { :confirm => 'Are you sure?' }, :method => :delete, :class => 'btn danger' %>
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1de9ef8..e1586a0 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,6 +1,8 @@ <%# Copyright 2011-2012 Rice University. Licensed under the Affero General Public License version 3 or later. See the COPYRIGHT file for details. %> + + @@ -142,7 +144,7 @@ <% end %> - <% if user_is_admin? %> + <% if user_is_admin? %>
  • > <%= link_to "Admin", admin_path %>
  • diff --git a/config/application.rb b/config/application.rb index a6dc708..29645f5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -83,6 +83,10 @@ class Application < Rails::Application # Example: # config.question_lock_timeout = 5.minutes config.question_lock_timeout = 0 + + config.to_prepare do + Doorkeeper::ApplicationController.layout "application" + end end end diff --git a/config/environment.rb b/config/environment.rb index 590fb1b..ad28621 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -4,6 +4,7 @@ # Load the rails application require File.expand_path('../application', __FILE__) +require 'shared_application_methods' require 'extensions' require 'uri_validator' require 'quadbase_markup' diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 0000000..b933270 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,59 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use. + # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" + # Put your resource owner authentication logic here. + current_user || warden.authenticate!(:scope => :user) + # Example implementation: + # User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url) + end + + # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + admin_authenticator do + current_user && current_user.is_administrator? || warden.authenticate!(:scope => :user) + end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + # access_token_expires_in 2.hours + + # Issue access tokens with refresh token (disabled by default) + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter :confirmation => true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # enable_application_owner :confirmation => false + + # Define access token scopes for your provider + # For more information go to https://github.com/applicake/doorkeeper/wiki/Using-Scopes + # default_scopes :public + # optional_scopes :write, :update + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out the wiki for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out the wiki for mor information on customization + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Change the test redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL + # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # + # test_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 0000000..8a51ce2 --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,68 @@ +en: + activerecord: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + has_query_parameter: 'cannot contain a query parameter.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + mongoid: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + has_query_parameter: 'cannot contain a query parameter.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + mongo_mapper: + errors: + models: + application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + has_query_parameter: 'cannot contain a query parameter.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + doorkeeper: + errors: + messages: + # Common error messages + invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + invalid_redirect_uri: 'The redirect uri included is not valid.' + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + #configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + # Password Access token errors + invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found' + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' diff --git a/config/routes.rb b/config/routes.rb index 75bdbe4..12012e3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,8 @@ Quadbase::Application.routes.draw do + use_doorkeeper + namespace :api, defaults: {format: 'json'} do scope module: :v1, constraints: ApiConstraints.new(version: 1) do resources :questions, :only => [:show] do diff --git a/db/migrate/20121109221727_create_doorkeeper_tables.rb b/db/migrate/20121109221727_create_doorkeeper_tables.rb new file mode 100644 index 0000000..26c2616 --- /dev/null +++ b/db/migrate/20121109221727_create_doorkeeper_tables.rb @@ -0,0 +1,45 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration + def change + create_table :oauth_applications do |t| + t.string :name, :null => false + t.string :uid, :null => false + t.string :secret, :null => false + t.string :redirect_uri, :null => false + t.integer :owner_id, :null => true + t.string :owner_type, :null => true + t.timestamps + end + + add_index :oauth_applications, :uid, :unique => true + add_index :oauth_applications, [:owner_id, :owner_type] + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, :null => false + t.integer :application_id, :null => false + t.string :token, :null => false + t.integer :expires_in, :null => false + t.string :redirect_uri, :null => false + t.datetime :created_at, :null => false + t.datetime :revoked_at + t.string :scopes + end + + add_index :oauth_access_grants, :token, :unique => true + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.integer :application_id, :null => false + t.string :token, :null => false + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, :null => false + t.string :scopes + end + + add_index :oauth_access_tokens, :token, :unique => true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, :unique => true + + end +end diff --git a/db/schema.rb b/db/schema.rb index efa0b59..c69ba6e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20121108231643) do +ActiveRecord::Schema.define(:version => 20121109221727) do create_table "announcements", :force => true do |t| t.integer "user_id" @@ -193,6 +193,48 @@ t.datetime "updated_at", :null => false end + create_table "oauth_access_grants", :force => true do |t| + t.integer "resource_owner_id", :null => false + t.integer "application_id", :null => false + t.string "token", :null => false + t.integer "expires_in", :null => false + t.string "redirect_uri", :null => false + t.datetime "created_at", :null => false + t.datetime "revoked_at" + t.string "scopes" + end + + add_index "oauth_access_grants", ["token"], :name => "index_oauth_access_grants_on_token", :unique => true + + create_table "oauth_access_tokens", :force => true do |t| + t.integer "resource_owner_id" + t.integer "application_id", :null => false + t.string "token", :null => false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", :null => false + t.string "scopes" + end + + add_index "oauth_access_tokens", ["refresh_token"], :name => "index_oauth_access_tokens_on_refresh_token", :unique => true + add_index "oauth_access_tokens", ["resource_owner_id"], :name => "index_oauth_access_tokens_on_resource_owner_id" + add_index "oauth_access_tokens", ["token"], :name => "index_oauth_access_tokens_on_token", :unique => true + + create_table "oauth_applications", :force => true do |t| + t.string "name", :null => false + t.string "uid", :null => false + t.string "secret", :null => false + t.string "redirect_uri", :null => false + t.integer "owner_id" + t.string "owner_type" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "oauth_applications", ["owner_id", "owner_type"], :name => "index_oauth_applications_on_owner_id_and_owner_type" + add_index "oauth_applications", ["uid"], :name => "index_oauth_applications_on_uid", :unique => true + create_table "question_collaborators", :force => true do |t| t.integer "user_id" t.integer "question_id" diff --git a/lib/shared_application_methods.rb b/lib/shared_application_methods.rb new file mode 100644 index 0000000..49eac7c --- /dev/null +++ b/lib/shared_application_methods.rb @@ -0,0 +1,13 @@ +# We originally had some methods in the ApplicationController that were shared +# with views using helper_method. However, when we added some isolate engines +# (e.g. Doorkeeper), they were not able to access these methods from the views +# (from the layouts). So we put them here and explicitly include then in both +# the ApplicationController and ApplicationHelper + +module SharedApplicationMethods + + def user_is_admin? + user_signed_in? && current_user.is_administrator? + end + +end \ No newline at end of file From e4dc786e6f7ae7865ca1d45770bb8a8d3615ac6b Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Wed, 14 Nov 2012 14:27:03 -0800 Subject: [PATCH 3/9] got rid of api key code in favor of oauth --- app/controllers/api_keys_controller.rb | 22 -------------------- app/models/api_key.rb | 19 ----------------- app/views/api_keys/_terms.html.erb | 16 -------------- app/views/api_keys/create.js.erb | 2 -- app/views/api_keys/new.js.erb | 3 --- config/initializers/doorkeeper.rb | 2 +- db/migrate/20121108231643_create_api_keys.rb | 10 --------- test/factories/api_keys.rb | 8 ------- test/functional/api_keys_controller_test.rb | 7 ------- test/unit/api_key_test.rb | 7 ------- 10 files changed, 1 insertion(+), 95 deletions(-) delete mode 100644 app/controllers/api_keys_controller.rb delete mode 100644 app/models/api_key.rb delete mode 100644 app/views/api_keys/_terms.html.erb delete mode 100644 app/views/api_keys/create.js.erb delete mode 100644 app/views/api_keys/new.js.erb delete mode 100644 db/migrate/20121108231643_create_api_keys.rb delete mode 100644 test/factories/api_keys.rb delete mode 100644 test/functional/api_keys_controller_test.rb delete mode 100644 test/unit/api_key_test.rb diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb deleted file mode 100644 index c0955b2..0000000 --- a/app/controllers/api_keys_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -class ApiKeysController < ApplicationController - - before_filter :get_user - - def new - @api_key = ApiKey.new - end - - def create - raise SecurityTransgression unless current_user == @user - @api_key = ApiKey.new - @api_key.user = @user - @api_key.save - end - -protected - - def get_user - @user = User.find(params[:user_id]) - end - -end diff --git a/app/models/api_key.rb b/app/models/api_key.rb deleted file mode 100644 index 2725554..0000000 --- a/app/models/api_key.rb +++ /dev/null @@ -1,19 +0,0 @@ -class ApiKey < ActiveRecord::Base - belongs_to :user - - before_create :generate_access_token - before_create :destroy_previous - -private - - def generate_access_token - begin - self.access_token = SecureRandom.hex - end while self.class.exists?(access_token: access_token) - end - - def destroy_previous - user.try(:api_key).try(:destroy) - end - -end diff --git a/app/views/api_keys/_terms.html.erb b/app/views/api_keys/_terms.html.erb deleted file mode 100644 index c4e2688..0000000 --- a/app/views/api_keys/_terms.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -

    You must agree to the following terms of use to generate an API key.

    - -

    Note that agreeing to these terms will permanently delete any existing API key you already have.

    - -

    By clicking "I agree" below, you agree that:

    - -
      -
    1. Your usage of the Quadbase API can be revoked at any time for any reason. Typical reasons for revokation include abuse or spamming the system.
    2. -
    3. These terms can change at any time.
    4. -
    - -

    If you do not agree, just close this popup.

    - -<%= form_for [current_user, @api_key], :method => :post, :remote => true do |f| %> - <%= f.submit "I agree", :class => submit_classes, :onclick => please_wait_js %> -<% end %> diff --git a/app/views/api_keys/create.js.erb b/app/views/api_keys/create.js.erb deleted file mode 100644 index 15ee216..0000000 --- a/app/views/api_keys/create.js.erb +++ /dev/null @@ -1,2 +0,0 @@ - $("#api_key").html("<%= @api_key.access_token %>"); - $("#message_dialog").dialog('close'); \ No newline at end of file diff --git a/app/views/api_keys/new.js.erb b/app/views/api_keys/new.js.erb deleted file mode 100644 index 931f081..0000000 --- a/app/views/api_keys/new.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= message_dialog "API Terms of Use", {:width => 450, :height => 500} do %> - <%= render :partial => 'api_keys/terms' %> -<% end %> \ No newline at end of file diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index b933270..b0402f7 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -22,7 +22,7 @@ # Access token expiration time (default 2 hours). # If you want to disable expiration, set this to nil. - # access_token_expires_in 2.hours + access_token_expires_in nil # Issue access tokens with refresh token (disabled by default) # use_refresh_token diff --git a/db/migrate/20121108231643_create_api_keys.rb b/db/migrate/20121108231643_create_api_keys.rb deleted file mode 100644 index b5766f4..0000000 --- a/db/migrate/20121108231643_create_api_keys.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreateApiKeys < ActiveRecord::Migration - def change - create_table :api_keys do |t| - t.string :access_token - t.integer :user_id - - t.timestamps - end - end -end diff --git a/test/factories/api_keys.rb b/test/factories/api_keys.rb deleted file mode 100644 index 7e88779..0000000 --- a/test/factories/api_keys.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :api_key do - access_token "MyString" - user_id 1 - end -end diff --git a/test/functional/api_keys_controller_test.rb b/test/functional/api_keys_controller_test.rb deleted file mode 100644 index fb2f782..0000000 --- a/test/functional/api_keys_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ApiKeysControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/api_key_test.rb b/test/unit/api_key_test.rb deleted file mode 100644 index 2b10127..0000000 --- a/test/unit/api_key_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ApiKeyTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end From 27a1b4c8fe8d52e7846c4e30c53d8acb9dcb79e1 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 15 Nov 2012 09:59:26 -0800 Subject: [PATCH 4/9] added oauth-client example --- examples/oauth-client/.gitignore | 1 + examples/oauth-client/.rvmrc | 1 + examples/oauth-client/Gemfile | 6 ++ examples/oauth-client/Gemfile.lock | 43 +++++++++ examples/oauth-client/README.md | 17 ++++ examples/oauth-client/config.ru | 8 ++ examples/oauth-client/public/application.css | 19 ++++ examples/oauth-client/public/application.js | 32 +++++++ examples/oauth-client/quadbase_client.rb | 96 ++++++++++++++++++++ examples/oauth-client/views/error.erb | 2 + examples/oauth-client/views/explore.erb | 19 ++++ examples/oauth-client/views/home.erb | 30 ++++++ examples/oauth-client/views/layout.erb | 41 +++++++++ 13 files changed, 315 insertions(+) create mode 100644 examples/oauth-client/.gitignore create mode 100644 examples/oauth-client/.rvmrc create mode 100644 examples/oauth-client/Gemfile create mode 100644 examples/oauth-client/Gemfile.lock create mode 100644 examples/oauth-client/README.md create mode 100644 examples/oauth-client/config.ru create mode 100644 examples/oauth-client/public/application.css create mode 100644 examples/oauth-client/public/application.js create mode 100644 examples/oauth-client/quadbase_client.rb create mode 100644 examples/oauth-client/views/error.erb create mode 100644 examples/oauth-client/views/explore.erb create mode 100644 examples/oauth-client/views/home.erb create mode 100644 examples/oauth-client/views/layout.erb diff --git a/examples/oauth-client/.gitignore b/examples/oauth-client/.gitignore new file mode 100644 index 0000000..1327e51 --- /dev/null +++ b/examples/oauth-client/.gitignore @@ -0,0 +1 @@ +env.rb diff --git a/examples/oauth-client/.rvmrc b/examples/oauth-client/.rvmrc new file mode 100644 index 0000000..21edc04 --- /dev/null +++ b/examples/oauth-client/.rvmrc @@ -0,0 +1 @@ +rvm --create ruby-1.9.3-p194@quadbase-ouath-client diff --git a/examples/oauth-client/Gemfile b/examples/oauth-client/Gemfile new file mode 100644 index 0000000..cf64e19 --- /dev/null +++ b/examples/oauth-client/Gemfile @@ -0,0 +1,6 @@ +source "http://rubygems.org" + +gem 'sinatra' +gem 'oauth2' +gem 'pry' +gem 'redcarpet' diff --git a/examples/oauth-client/Gemfile.lock b/examples/oauth-client/Gemfile.lock new file mode 100644 index 0000000..91a9d71 --- /dev/null +++ b/examples/oauth-client/Gemfile.lock @@ -0,0 +1,43 @@ +GEM + remote: http://rubygems.org/ + specs: + addressable (2.2.6) + coderay (0.9.8) + faraday (0.7.5) + addressable (~> 2.2.6) + multipart-post (~> 1.1.3) + rack (>= 1.1.0, < 2) + method_source (0.6.7) + ruby_parser (>= 2.3.1) + multi_json (1.0.4) + multipart-post (1.1.4) + oauth2 (0.5.1) + faraday (~> 0.7.4) + multi_json (~> 1.0.3) + pry (0.9.7.4) + coderay (~> 0.9.8) + method_source (~> 0.6.7) + ruby_parser (>= 2.3.1) + slop (~> 2.1.0) + rack (1.3.5) + rack-protection (1.1.4) + rack + redcarpet (2.0.0b5) + ruby_parser (2.3.1) + sexp_processor (~> 3.0) + sexp_processor (3.0.8) + sinatra (1.3.1) + rack (~> 1.3, >= 1.3.4) + rack-protection (~> 1.1, >= 1.1.2) + tilt (~> 1.3, >= 1.3.3) + slop (2.1.0) + tilt (1.3.3) + +PLATFORMS + ruby + +DEPENDENCIES + oauth2 + pry + redcarpet + sinatra diff --git a/examples/oauth-client/README.md b/examples/oauth-client/README.md new file mode 100644 index 0000000..5fc23b5 --- /dev/null +++ b/examples/oauth-client/README.md @@ -0,0 +1,17 @@ +# Example Quadbase OAuth 2 Client + +This app is an example of OAuth 2 client, based on the DoorKeeper [example client](https://github.com/applicake/doorkeeper-sinatra-client). + +## Installation + +Here are the steps for firing up this client app. + +1. Run ````bundle install````. This example directory has its own ````.rvmrc```` file for setting up an RVM gem dir, so running ````bundle```` will not interfere with your Quadbase gem dir. +2. Create a [new oauth app](http://localhost:3000/oauth/applications/new) in your development instance of Quadbase. +3. Create an ````env.rb```` file in the top-level ````oauth-client```` directory that has the following contents, where the ````OAUTH2_CLIENT_ID```` and ````OAUTH2_CLIENT_SECRET```` have the appropriate values from the result of the prior step. + + # Change these hashes to match what your local version of Quadbase gives you + ENV['OAUTH2_CLIENT_ID'] = "40348dc38..." + ENV['OAUTH2_CLIENT_SECRET'] = "69d7e8493..." + ENV['OAUTH2_CLIENT_REDIRECT_URI'] = "http://localhost:9292/callback" +4. Run ````rackup config.ru```` to start the server on port 9292. diff --git a/examples/oauth-client/config.ru b/examples/oauth-client/config.ru new file mode 100644 index 0000000..338c6ac --- /dev/null +++ b/examples/oauth-client/config.ru @@ -0,0 +1,8 @@ +require 'rubygems' +require 'bundler' + +Bundler.require + +require './quadbase_client' + +run QuadbaseClient diff --git a/examples/oauth-client/public/application.css b/examples/oauth-client/public/application.css new file mode 100644 index 0000000..6021ba3 --- /dev/null +++ b/examples/oauth-client/public/application.css @@ -0,0 +1,19 @@ +body { + padding-top: 60px; +} + +section > .row { + margin-bottom: 10px; +} + +.row h2 { + margin-bottom: 10px; +} + +.row h3 { + margin-bottom: 7px; +} + +pre > code { + background-color: transparent; +} diff --git a/examples/oauth-client/public/application.js b/examples/oauth-client/public/application.js new file mode 100644 index 0000000..6e8892d --- /dev/null +++ b/examples/oauth-client/public/application.js @@ -0,0 +1,32 @@ +$(function() { + $('[data-explore]').click(function() { + var link = $(this); + link.button('loading'); + $('#display-json').load(link.attr('href'), function() { + link.button('reset'); + }); + return false; + }); +}); + +function syntaxHighlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} diff --git a/examples/oauth-client/quadbase_client.rb b/examples/oauth-client/quadbase_client.rb new file mode 100644 index 0000000..2aea696 --- /dev/null +++ b/examples/oauth-client/quadbase_client.rb @@ -0,0 +1,96 @@ +require "sinatra/base" +require 'logger' + +# Load custom environment variables +load 'env.rb' if File.exists?('env.rb') + +class QuadbaseClient < Sinatra::Base + enable :sessions + + helpers do + include Rack::Utils + alias_method :h, :escape_html + + def pretty_json(json) + JSON.pretty_generate(json) + end + + def signed_in? + !session[:access_token].nil? + end + end + + logger = Logger.new(STDOUT) + + def client(token_method = :post) + OAuth2::Client.new( + ENV['OAUTH2_CLIENT_ID'], + ENV['OAUTH2_CLIENT_SECRET'], + :site => ENV['SITE'] || "http://localhost:3000", + :token_method => token_method, + ) + end + + def access_token + OAuth2::AccessToken.new(client, session[:access_token], :refresh_token => session[:refresh_token]) + end + + def redirect_uri + ENV['OAUTH2_CLIENT_REDIRECT_URI'] + end + + get '/' do + erb :home + end + + get '/sign_in' do + # scope = params[:scope] || "public" + # redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri, :scope => scope) + redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri) + end + + get '/sign_out' do + session[:access_token] = nil + redirect '/' + end + + get '/callback' do + new_token = client.auth_code.get_token(params[:code], :redirect_uri => redirect_uri) + session[:access_token] = new_token.token + session[:refresh_token] = new_token.refresh_token + redirect '/' + end + + get '/refresh' do + new_token = access_token.refresh! + session[:access_token] = new_token.token + session[:refresh_token] = new_token.refresh_token + redirect '/' + end + + # get '/explore/:api' do + # raise "Please call a valid endpoint" unless params[:api] + # begin + # response = access_token.get("/api/#{params[:api]}/d1", {:headers => {'Accept' => 'application/vnd.quadbase.v1'}}) + # @json = JSON.parse(response.body) + # erb :explore, :layout => !request.xhr? + # rescue OAuth2::Error => @error + # erb :error, :layout => !request.xhr? + # end + # end + + get '/explore/*' do + + @endpoint = params[:splat].first + + raise "Please call a valid endpoint" unless @endpoint + begin + response = access_token.get("/api/#{@endpoint}", {:headers => {'Accept' => 'application/vnd.quadbase.v1'}}) + @json = JSON.parse(response.body) + erb :explore, :layout => !request.xhr? + rescue OAuth2::Error => @error + erb :error, :layout => !request.xhr? + end + end + +end diff --git a/examples/oauth-client/views/error.erb b/examples/oauth-client/views/error.erb new file mode 100644 index 0000000..cc23bdc --- /dev/null +++ b/examples/oauth-client/views/error.erb @@ -0,0 +1,2 @@ +

    OAuth2::Error

    +<%= h @error.response.inspect %> diff --git a/examples/oauth-client/views/explore.erb b/examples/oauth-client/views/explore.erb new file mode 100644 index 0000000..58df5da --- /dev/null +++ b/examples/oauth-client/views/explore.erb @@ -0,0 +1,19 @@ + + +

    API endpoint /api/<%= @endpoint %> returns...

    + +
    + + \ No newline at end of file diff --git a/examples/oauth-client/views/home.erb b/examples/oauth-client/views/home.erb new file mode 100644 index 0000000..d3ab894 --- /dev/null +++ b/examples/oauth-client/views/home.erb @@ -0,0 +1,30 @@ +
    +
    +
    + <% unless signed_in? %> +

    Sign in first to explore the provider's API

    +

    Sign in with OAuth 2 provider »

    + <% end %> +
    +
    +
    + +
    +
    +
    + <% if signed_in? %> +

    Explore the API

    + + + +

    Your access token: <%= session[:access_token] %> (refresh)

    + +

    Select one of the api methods above

    + <% end %> +
    +
    +
    diff --git a/examples/oauth-client/views/layout.erb b/examples/oauth-client/views/layout.erb new file mode 100644 index 0000000..6cfa8d2 --- /dev/null +++ b/examples/oauth-client/views/layout.erb @@ -0,0 +1,41 @@ + + + + Example Quadbase OAuth 2 Client + + + + + +
    +
    +
    + Example Quadbase OAuth 2 Client + +
    +
    + + + +
    + <%= yield %> +
    + + + + + + From 1d5535c52cce20b56e4bc9afe08cb6791ce5eee3 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 15 Nov 2012 14:32:28 -0800 Subject: [PATCH 5/9] api updates and start at tests --- Gemfile | 1 + Gemfile.lock | 13 ++ app/controllers/api/v1/api_controller.rb | 15 +- .../api/v1/questions_controller.rb | 2 - config/initializers/doorkeeper.rb | 8 + test/factories.rb | 5 + .../api/v1/questions_controller_test.rb | 217 ++++++++++++++++++ test/integration/api/integration_test.rb | 56 +++++ .../api/v1/questions_controller_test.rb | 50 ++++ 9 files changed, 353 insertions(+), 14 deletions(-) create mode 100644 test/functional/api/v1/questions_controller_test.rb create mode 100644 test/integration/api/integration_test.rb create mode 100644 test/integration/api/v1/questions_controller_test.rb diff --git a/Gemfile b/Gemfile index bb5ae33..7cf4455 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,7 @@ group :development, :test do gem 'thin', '~> 1.3.1' gem 'quiet_assets', '~> 1.0.1' gem 'rvm-capistrano' + gem 'oauth2' end gem 'single_test', '~> 0.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 4bf8519..c36a722 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,8 +81,11 @@ GEM railties (>= 3.0.0) faker (1.0.1) i18n (~> 0.4) + faraday (0.8.4) + multipart-post (~> 1.1) highline (1.6.13) hike (1.2.1) + httpauth (0.2.0) i18n (0.6.0) journey (1.0.4) jquery-rails (2.0.2) @@ -94,6 +97,8 @@ GEM jsonify-rails (0.3.2) actionpack jsonify (< 0.4.0) + jwt (0.1.5) + multi_json (>= 1.0) libv8 (3.3.10.4) mail (2.4.4) i18n (>= 0.4.0) @@ -101,6 +106,7 @@ GEM treetop (~> 1.4.8) mime-types (1.19) multi_json (1.3.6) + multipart-post (1.1.5) mysql2 (0.3.11) mysql2 (0.3.11-x86-mingw32) net-scp (1.0.4) @@ -110,6 +116,12 @@ GEM net-ssh (2.5.2) net-ssh-gateway (1.1.0) net-ssh (>= 1.99.1) + oauth2 (0.8.0) + faraday (~> 0.8) + httpauth (~> 0.1) + jwt (~> 0.1.4) + multi_json (~> 1.0) + rack (~> 1.2) orm_adapter (0.4.0) paperclip (3.0.4) activemodel (>= 3.0.0) @@ -215,6 +227,7 @@ DEPENDENCIES jsonify-rails (~> 0.3.2) mime-types (~> 1.18) mysql2 (~> 0.3.11) + oauth2 paperclip (~> 3.0.4) parslet (~> 1.4.0) quiet_assets (~> 1.0.1) diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index f3dd168..c0a0e77 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -2,7 +2,6 @@ module Api module V1 class ApiController < ApplicationController skip_before_filter :authenticate_user! - # before_filter :check_token_and_get_user respond_to :json @@ -10,18 +9,10 @@ class ApiController < ApplicationController private - # def check_token_and_get_user - # authenticate_or_request_with_http_token do |token, options| - # api_key = ApiKey.find_by_access_token(token) - # @api_user = api_key.try(:user) - # !api_key.nil? - # end - # end - def current_user - if doorkeeper_token - @current_user ||= User.find(doorkeeper_token.resource_owner_id) - end + @current_user ||= doorkeeper_token ? + User.find(doorkeeper_token.resource_owner_id) : + AnonymousUser.instance end def rescue_from_exception(exception) diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb index f7e83d6..08be2cb 100644 --- a/app/controllers/api/v1/questions_controller.rb +++ b/app/controllers/api/v1/questions_controller.rb @@ -1,8 +1,6 @@ module Api module V1 class QuestionsController < ApiController - doorkeeper_for :all # don't really need this if do SecurityTransgression stuff - def show @question = Question.from_param(params[:id]) raise SecurityTransgression unless current_user.can_read?(@question) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index b0402f7..e8da945 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -56,4 +56,12 @@ # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) # # test_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' + + # Enable password authentication in the test environment + if Rails.env.test? + resource_owner_from_credentials do |routes| + u = User.find_for_database_authentication(:email => params[:username]) + u if u && u.valid_password?(params[:password]) + end + end end diff --git a/test/factories.rb b/test/factories.rb index 21c611b..c842b04 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -281,4 +281,9 @@ def make_list(options = {}) f.variables "x" end + factory :oauth_application, class: Doorkeeper::Application do |f| + f.name { FactoryGirl.generate(:couple_of_words) } + f.redirect_uri 'http://localhost:3000' + end + end diff --git a/test/functional/api/v1/questions_controller_test.rb b/test/functional/api/v1/questions_controller_test.rb new file mode 100644 index 0000000..9a01490 --- /dev/null +++ b/test/functional/api/v1/questions_controller_test.rb @@ -0,0 +1,217 @@ +require 'test_helper' + +class Api::V1::QuestionsControllerTest < ActionController::TestCase + + setup do + @published_question = make_simple_question(:method => :create, :published => true) + @unpublished_question = make_simple_question(:method => :create) + @oauth_application = FactoryGirl.create(:oauth_application) + end + + def oauth_token(email, password) + client = OAuth2::Client.new(app.uid, app.secret) do |b| + b.request :url_encoded + b.adapter :rack, Rails.application + end + client.password.get_token(email, password) + end + + + def http_get(url, params={}, header_fields={}) + Rails.logger.debug("http post url: #{url}, params: #{params.inspect}") + uri = URI(url) + req = Net::HTTP::Get.new(uri.path) + req.set_form_data(params) + header_fields.each do |key, value| + req.add_field key, value + end + + res = Net::HTTP.start(uri.hostname, uri.port) do |http| + http.request(req) + end + + # case res + # when Net::HTTPSuccess, Net::HTTPRedirection + # res + # else + # res.value + # end + + end + + test "should be able to access published question without credentials" do + @request.env["Accept"] = 'application/vnd.quadbase.v1' + get("/api/questions/#{@published_question.to_param}") + asssert_response :success + + # oauth_token() + # Rails.logger.debug(http_get("http://localhost:3000/api/questions/#{@published_question.to_param}", {}, {'Accept' => 'application/vnd.quadbase.v1'})) + end + + # test "should not get index not logged in" do + # get :index, :question_id => @question.to_param + # assert_redirected_to login_path + # end + + # test "should not get index not authorized" do + # user_login + # get :index, :question_id => @question.to_param + # assert_response(403) + # end + + # test "should get index" do + # sign_in @user + # get :index, :question_id => @question.to_param + # assert_response :success + # end + + # test "should get index published question" do + # user_login + # get :index, :question_id => @published_question.to_param + # assert_response :success + # end + + # test "should not get new not logged in" do + # get :new, :question_id => @question.to_param + # assert_redirected_to login_path + # end + + # test "should not get new not authorized" do + # user_login + # get :new, :question_id => @question.to_param + # assert_response(403) + # end + + # test "should get new" do + # sign_in @user + # get :new, :question_id => @question.to_param + # assert_response :success + # assert_not_nil assigns(:comments) + # end + + # test "should get new published question" do + # user_login + # get :new, :question_id => @published_question.to_param + # assert_response :success + # assert_not_nil assigns(:comments) + # end + + # test "should not create comment not logged in" do + # assert_difference('Comment.count', 0) do + # post :create, :question_id => @question.to_param, :comment => @comment.attributes + # end + + # assert_redirected_to login_path + # end + + # test "should not create comment not authorized" do + # user_login + # assert_difference('Comment.count', 0) do + # post :create, :question_id => @question.to_param, :comment => @comment.attributes + # end + + # assert_response(403) + # end + + # test "should create comment" do + # sign_in @user + # assert_difference('Comment.count') do + # post :create, :question_id => @question.to_param, :comment => @comment.attributes + # end + + # assert_redirected_to question_comments_path(@question.to_param) + # end + + # test "should create comment published question" do + # user_login + # assert_difference('Comment.count') do + # post :create, :question_id => @published_question.to_param, :comment => @comment.attributes + # end + + # assert_redirected_to question_comments_path(@published_question.to_param) + # end + + # test "should not show comment not logged in" do + # get :show, :id => @comment.to_param + # assert_redirected_to login_path + # end + + # test "should not show comment not authorized" do + # user_login + # get :show, :id => @comment.to_param + # assert_response(403) + # end + + # test "should show comment" do + # sign_in @user + # get :show, :id => @comment.to_param + # assert_response :success + # end + + # test "should show comment published question" do + # user_login + # get :show, :id => @published_comment.to_param + # assert_response :success + # end + + # test "should not get edit not logged in" do + # get :edit, :id => @comment.to_param + # assert_redirected_to login_path + # end + + # test "should not get edit not authorized" do + # user_login + # get :edit, :id => @comment.to_param + # assert_response(403) + # end + + # test "should get edit" do + # sign_in @user + # get :edit, :id => @comment.to_param + # assert_response :success + # end + + # test "should not update comment not logged in" do + # put :update, :id => @comment.to_param + # assert_redirected_to login_path + # end + + # test "should not update comment not authorized" do + # user_login + # put :update, :id => @comment.to_param + # assert_response(403) + # end + + # test "should update comment" do + # sign_in @user + # put :update, :id => @comment.to_param + # assert_redirected_to question_comments_path(@question.to_param) + # end + + # test "should not destroy comment not logged in" do + # assert_difference('Comment.count', 0) do + # delete :destroy, :id => @comment.to_param + # end + + # assert_redirected_to login_path + # end + + # test "should not destroy comment not authorized" do + # user_login + # assert_difference('Comment.count', 0) do + # delete :destroy, :id => @comment.to_param + # end + + # assert_response(403) + # end + + # test "should destroy comment" do + # sign_in @user + # assert_difference('Comment.count', -1) do + # delete :destroy, :id => @comment.to_param + # end + + # assert_redirected_to question_comments_path(@question.to_param) + # end + +end diff --git a/test/integration/api/integration_test.rb b/test/integration/api/integration_test.rb new file mode 100644 index 0000000..5baaf6d --- /dev/null +++ b/test/integration/api/integration_test.rb @@ -0,0 +1,56 @@ +require 'test_helper' +require 'json' + +# References: +# http://twobitlabs.com/2010/09/setting-request-headers-in-rails-functional-tests/ + +class Api::IntegrationTest < ActionDispatch::IntegrationTest + + class TokenWrapper + attr_reader :oauth_token + + def initialize(oauth_token) + @oauth_token = oauth_token + end + + def get(url, api_version, params={}) + @oauth_token.get(url, {:headers => {'Accept' => "application/vnd.quadbase.#{api_version}"}}) + end + end + + def oauth_token_wrapper(email, password) + client = OAuth2::Client.new(@oauth_application.uid, @oauth_application.secret) do |b| + b.request :url_encoded + b.adapter :rack, Rails.application + end + TokenWrapper.new(client.password.get_token(email, password)) + end + + def api_call(method, url, api_version, params={}) + case method + when :get + get(url, params, {'Accept' => "application/vnd.quadbase.#{api_version}"}) + end + end + + def assert_oauth_error(expected_status, msg=nil) + got_oauth_error = false + oauth_error_status = expected_status + + begin + yield + rescue OAuth2::Error => e + got_oauth_error = true + oauth_error_status = e.response.status + end + + if !got_oauth_error + flunk(build_message(msg, "Expression did not produce an oauth error as expected")) + elsif oauth_error_status != expected_status + flunk(build_message(msg, "Expression expected to produce an oauth error with status ? but had status ?", expected_status, oauth_error_status)) + end + end + +end + + diff --git a/test/integration/api/v1/questions_controller_test.rb b/test/integration/api/v1/questions_controller_test.rb new file mode 100644 index 0000000..3db47bf --- /dev/null +++ b/test/integration/api/v1/questions_controller_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' +require 'json' +require 'integration/api/integration_test' + +class Api::V1::QuestionsControllerTest < Api::IntegrationTest + + setup do + @published_question = make_simple_question(:method => :create, :published => true) + @unpublished_question = make_simple_question(:method => :create) + @oauth_application = FactoryGirl.create(:oauth_application) + + @unpublished_question_user = @unpublished_question.question_collaborators.first.user + @unpublished_question_user.update_attribute(:password, "password") + + @published_question_user = @published_question.question_collaborators.first.user + @published_question_user.update_attribute(:password, "password") + end + + test "should be able to access published question without credentials" do + api_call :get, "/api/questions/#{@published_question.to_param}", "v1" + assert_response :success + end + + test "should get published question using non author oauth credentials" do + token = oauth_token_wrapper(@unpublished_question_user.email, "password") + response = token.get("/api/questions/#{@published_question.to_param}", "v1") + assert_equal 200, response.status + end + + test "should not be able to access unpublished question without credentials" do + api_call :get, "/api/questions/#{@unpublished_question.to_param}", "v1" + assert_response :forbidden + end + + test "should get unpublished question using oauth credentials" do + token = oauth_token_wrapper(@unpublished_question_user.email, "password") + response = token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + assert_equal 200, response.status + end + + test "should not get unpublished question using wrong oauth credentials" do + token = oauth_token_wrapper(@published_question_user.email, "password") + assert_oauth_error (403) { + token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + } + end + +end + + From 9e02550391b149785bee45c40afd1f840fc22e13 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 15 Nov 2012 14:42:57 -0800 Subject: [PATCH 6/9] test tokens never expire, slight refactoring of helper test methods --- test/integration/api/common_test.rb | 20 +++++++++++++++++++ test/integration/api/integration_test.rb | 4 ++-- .../api/v1/questions_controller_test.rb | 8 +++----- 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 test/integration/api/common_test.rb diff --git a/test/integration/api/common_test.rb b/test/integration/api/common_test.rb new file mode 100644 index 0000000..04b6dec --- /dev/null +++ b/test/integration/api/common_test.rb @@ -0,0 +1,20 @@ +require 'integration/api/integration_test' + +class Api::CommonTest < Api::IntegrationTest + + setup do + @oauth_application = FactoryGirl.create(:oauth_application) + end + + test "tokens never expire" do + user = FactoryGirl.create(:user, :password => 'password') + token = oauth_token_wrapper(@oauth_application, user.email, "password") + assert_nil token.oauth_token.expires_at + Timecop.travel(Time.local(2999,1,1,1,0,0)) + assert !token.oauth_token.expired? + Timecop.return + end + +end + + diff --git a/test/integration/api/integration_test.rb b/test/integration/api/integration_test.rb index 5baaf6d..1d726e1 100644 --- a/test/integration/api/integration_test.rb +++ b/test/integration/api/integration_test.rb @@ -18,8 +18,8 @@ def get(url, api_version, params={}) end end - def oauth_token_wrapper(email, password) - client = OAuth2::Client.new(@oauth_application.uid, @oauth_application.secret) do |b| + def oauth_token_wrapper(application, email, password) + client = OAuth2::Client.new(application.uid, application.secret) do |b| b.request :url_encoded b.adapter :rack, Rails.application end diff --git a/test/integration/api/v1/questions_controller_test.rb b/test/integration/api/v1/questions_controller_test.rb index 3db47bf..6c0ce2e 100644 --- a/test/integration/api/v1/questions_controller_test.rb +++ b/test/integration/api/v1/questions_controller_test.rb @@ -1,5 +1,3 @@ -require 'test_helper' -require 'json' require 'integration/api/integration_test' class Api::V1::QuestionsControllerTest < Api::IntegrationTest @@ -22,7 +20,7 @@ class Api::V1::QuestionsControllerTest < Api::IntegrationTest end test "should get published question using non author oauth credentials" do - token = oauth_token_wrapper(@unpublished_question_user.email, "password") + token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") response = token.get("/api/questions/#{@published_question.to_param}", "v1") assert_equal 200, response.status end @@ -33,13 +31,13 @@ class Api::V1::QuestionsControllerTest < Api::IntegrationTest end test "should get unpublished question using oauth credentials" do - token = oauth_token_wrapper(@unpublished_question_user.email, "password") + token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") response = token.get("/api/questions/#{@unpublished_question.to_param}", "v1") assert_equal 200, response.status end test "should not get unpublished question using wrong oauth credentials" do - token = oauth_token_wrapper(@published_question_user.email, "password") + token = oauth_token_wrapper(@oauth_application, @published_question_user.email, "password") assert_oauth_error (403) { token.get("/api/questions/#{@unpublished_question.to_param}", "v1") } From d12b19da5358160902280bae6b7499f2b38dfb5b Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 15 Nov 2012 14:56:47 -0800 Subject: [PATCH 7/9] got rid of old functional api test --- README.md | 2 +- .../api/v1/questions_controller_test.rb | 217 ------------------ test/integration/api/integration_test.rb | 4 +- 3 files changed, 4 insertions(+), 219 deletions(-) delete mode 100644 test/functional/api/v1/questions_controller_test.rb diff --git a/README.md b/README.md index 54da78e..8334f18 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Quadbase ======== -[![Build Status](https://secure.travis-ci.org/lml/quadbase.png)](http://travis-ci.org/lml/quadbase) +[![Build Status](https://secure.travis-ci.org/lml/quadbase.png?branch=master)](http://travis-ci.org/lml/quadbase) Quadbase is an open homework and test question bank, where questions are written by the community and access is free. diff --git a/test/functional/api/v1/questions_controller_test.rb b/test/functional/api/v1/questions_controller_test.rb deleted file mode 100644 index 9a01490..0000000 --- a/test/functional/api/v1/questions_controller_test.rb +++ /dev/null @@ -1,217 +0,0 @@ -require 'test_helper' - -class Api::V1::QuestionsControllerTest < ActionController::TestCase - - setup do - @published_question = make_simple_question(:method => :create, :published => true) - @unpublished_question = make_simple_question(:method => :create) - @oauth_application = FactoryGirl.create(:oauth_application) - end - - def oauth_token(email, password) - client = OAuth2::Client.new(app.uid, app.secret) do |b| - b.request :url_encoded - b.adapter :rack, Rails.application - end - client.password.get_token(email, password) - end - - - def http_get(url, params={}, header_fields={}) - Rails.logger.debug("http post url: #{url}, params: #{params.inspect}") - uri = URI(url) - req = Net::HTTP::Get.new(uri.path) - req.set_form_data(params) - header_fields.each do |key, value| - req.add_field key, value - end - - res = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request(req) - end - - # case res - # when Net::HTTPSuccess, Net::HTTPRedirection - # res - # else - # res.value - # end - - end - - test "should be able to access published question without credentials" do - @request.env["Accept"] = 'application/vnd.quadbase.v1' - get("/api/questions/#{@published_question.to_param}") - asssert_response :success - - # oauth_token() - # Rails.logger.debug(http_get("http://localhost:3000/api/questions/#{@published_question.to_param}", {}, {'Accept' => 'application/vnd.quadbase.v1'})) - end - - # test "should not get index not logged in" do - # get :index, :question_id => @question.to_param - # assert_redirected_to login_path - # end - - # test "should not get index not authorized" do - # user_login - # get :index, :question_id => @question.to_param - # assert_response(403) - # end - - # test "should get index" do - # sign_in @user - # get :index, :question_id => @question.to_param - # assert_response :success - # end - - # test "should get index published question" do - # user_login - # get :index, :question_id => @published_question.to_param - # assert_response :success - # end - - # test "should not get new not logged in" do - # get :new, :question_id => @question.to_param - # assert_redirected_to login_path - # end - - # test "should not get new not authorized" do - # user_login - # get :new, :question_id => @question.to_param - # assert_response(403) - # end - - # test "should get new" do - # sign_in @user - # get :new, :question_id => @question.to_param - # assert_response :success - # assert_not_nil assigns(:comments) - # end - - # test "should get new published question" do - # user_login - # get :new, :question_id => @published_question.to_param - # assert_response :success - # assert_not_nil assigns(:comments) - # end - - # test "should not create comment not logged in" do - # assert_difference('Comment.count', 0) do - # post :create, :question_id => @question.to_param, :comment => @comment.attributes - # end - - # assert_redirected_to login_path - # end - - # test "should not create comment not authorized" do - # user_login - # assert_difference('Comment.count', 0) do - # post :create, :question_id => @question.to_param, :comment => @comment.attributes - # end - - # assert_response(403) - # end - - # test "should create comment" do - # sign_in @user - # assert_difference('Comment.count') do - # post :create, :question_id => @question.to_param, :comment => @comment.attributes - # end - - # assert_redirected_to question_comments_path(@question.to_param) - # end - - # test "should create comment published question" do - # user_login - # assert_difference('Comment.count') do - # post :create, :question_id => @published_question.to_param, :comment => @comment.attributes - # end - - # assert_redirected_to question_comments_path(@published_question.to_param) - # end - - # test "should not show comment not logged in" do - # get :show, :id => @comment.to_param - # assert_redirected_to login_path - # end - - # test "should not show comment not authorized" do - # user_login - # get :show, :id => @comment.to_param - # assert_response(403) - # end - - # test "should show comment" do - # sign_in @user - # get :show, :id => @comment.to_param - # assert_response :success - # end - - # test "should show comment published question" do - # user_login - # get :show, :id => @published_comment.to_param - # assert_response :success - # end - - # test "should not get edit not logged in" do - # get :edit, :id => @comment.to_param - # assert_redirected_to login_path - # end - - # test "should not get edit not authorized" do - # user_login - # get :edit, :id => @comment.to_param - # assert_response(403) - # end - - # test "should get edit" do - # sign_in @user - # get :edit, :id => @comment.to_param - # assert_response :success - # end - - # test "should not update comment not logged in" do - # put :update, :id => @comment.to_param - # assert_redirected_to login_path - # end - - # test "should not update comment not authorized" do - # user_login - # put :update, :id => @comment.to_param - # assert_response(403) - # end - - # test "should update comment" do - # sign_in @user - # put :update, :id => @comment.to_param - # assert_redirected_to question_comments_path(@question.to_param) - # end - - # test "should not destroy comment not logged in" do - # assert_difference('Comment.count', 0) do - # delete :destroy, :id => @comment.to_param - # end - - # assert_redirected_to login_path - # end - - # test "should not destroy comment not authorized" do - # user_login - # assert_difference('Comment.count', 0) do - # delete :destroy, :id => @comment.to_param - # end - - # assert_response(403) - # end - - # test "should destroy comment" do - # sign_in @user - # assert_difference('Comment.count', -1) do - # delete :destroy, :id => @comment.to_param - # end - - # assert_redirected_to question_comments_path(@question.to_param) - # end - -end diff --git a/test/integration/api/integration_test.rb b/test/integration/api/integration_test.rb index 1d726e1..a41f7a9 100644 --- a/test/integration/api/integration_test.rb +++ b/test/integration/api/integration_test.rb @@ -47,7 +47,9 @@ def assert_oauth_error(expected_status, msg=nil) if !got_oauth_error flunk(build_message(msg, "Expression did not produce an oauth error as expected")) elsif oauth_error_status != expected_status - flunk(build_message(msg, "Expression expected to produce an oauth error with status ? but had status ?", expected_status, oauth_error_status)) + flunk(build_message(msg, + "Expression expected to produce an oauth error with status ? but had status ?", + expected_status, oauth_error_status)) end end From 2723193cdfb0fdf7c4da69611486ce6a1aed2f3e Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 15 Nov 2012 16:28:48 -0800 Subject: [PATCH 8/9] minor check of json content --- .travis.yml | 5 ++++- test/integration/api/v1/questions_controller_test.rb | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c801c73..cb98bd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,7 @@ script: - RAILS_ENV=test bundle exec rake --trace db:migrate test before_script: - gem install mysql2 - - mysql -e 'create database quadbase_test' \ No newline at end of file + - mysql -e 'create database quadbase_test' +notifications: + email: + - dev@quadbase.org \ No newline at end of file diff --git a/test/integration/api/v1/questions_controller_test.rb b/test/integration/api/v1/questions_controller_test.rb index 6c0ce2e..99d944a 100644 --- a/test/integration/api/v1/questions_controller_test.rb +++ b/test/integration/api/v1/questions_controller_test.rb @@ -15,8 +15,11 @@ class Api::V1::QuestionsControllerTest < Api::IntegrationTest end test "should be able to access published question without credentials" do - api_call :get, "/api/questions/#{@published_question.to_param}", "v1" + response = api_call :get, "/api/questions/#{@published_question.to_param}", "v1" assert_response :success + json = JSON.parse(@response.body) + # lame test that the api is working; ponder switch to rspec and https://github.com/collectiveidea/json_spec + assert json.has_key?("simple_question") end test "should get published question using non author oauth credentials" do From 851950197b79afe5b27a8ea9d722443ff574db86 Mon Sep 17 00:00:00 2001 From: JP Slavinsky Date: Thu, 13 Dec 2012 16:09:48 -0800 Subject: [PATCH 9/9] more work --- .../api/v1/solutions_controller.rb | 7 +- app/models/solution.rb | 4 +- test/factories.rb | 1 - .../api/v1/solutions_controller_test.rb | 65 +++++++++++++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 test/integration/api/v1/solutions_controller_test.rb diff --git a/app/controllers/api/v1/solutions_controller.rb b/app/controllers/api/v1/solutions_controller.rb index 1f2813e..6f2a87f 100644 --- a/app/controllers/api/v1/solutions_controller.rb +++ b/app/controllers/api/v1/solutions_controller.rb @@ -3,14 +3,15 @@ module V1 class SolutionsController < ApiController def index + # debugger @question = Question.from_param(params[:question_id]) - raise SecurityTransgression unless @api_user.can_read?(@question) - @solutions = Vote.order_by_votes(@question.valid_solutions_visible_for(@api_user)) + raise SecurityTransgression unless current_user.can_read?(@question) + @solutions = Vote.order_by_votes(@question.valid_solutions_visible_for(current_user)) end def show @solution = Solution.find(params[:id]) - raise SecurityTransgression unless @api_user.can_read?(@solution) + raise SecurityTransgression unless current_user.can_read?(@solution) end end diff --git a/app/models/solution.rb b/app/models/solution.rb index 78186bf..4a16fc4 100644 --- a/app/models/solution.rb +++ b/app/models/solution.rb @@ -23,8 +23,8 @@ class Solution < ActiveRecord::Base scope :visible_for, lambda { |user| joins{question}.where{(creator_id == user.id) | (is_visible == true) | - ( question.version == nil && - question.id.in(Question.visible_for(user).select{id}) )} + ( (question.version == nil) & + (question.id.in(Question.visible_for(user).select{id})) )} } before_save :auto_subscribe diff --git a/test/factories.rb b/test/factories.rb index c842b04..b7cb110 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -126,7 +126,6 @@ def make_list(options = {}) factory :simple_question do |f| f.content { FactoryGirl.generate(:couple_of_words) } f.association :question_setup - f.number { FactoryGirl.generate(:unique_number) } f.license_id { common_license.id } f.version nil end diff --git a/test/integration/api/v1/solutions_controller_test.rb b/test/integration/api/v1/solutions_controller_test.rb new file mode 100644 index 0000000..857feb9 --- /dev/null +++ b/test/integration/api/v1/solutions_controller_test.rb @@ -0,0 +1,65 @@ +require 'integration/api/integration_test' + +class Api::V1::SolutionsControllerTest < Api::IntegrationTest + + setup do + @oauth_application = FactoryGirl.create(:oauth_application) + + ContentParseAndCache.enable_test_parser = true + @user = FactoryGirl.create(:user, :password => "password") + @solution = FactoryGirl.create(:solution, :creator => @user) + @question = @solution.question + + @published_question = make_simple_question(:method => :create, :published => true) + @published_question_user = @published_question.question_collaborators.first.user + @published_question_user.update_attribute(:password, "password") + @published_solution = FactoryGirl.create(:solution, :question => @published_question) + @visible_published_solution = FactoryGirl.create(:solution, :question => @published_question, + :is_visible => true) + ContentParseAndCache.enable_test_parser = false + end + + test "should be able to list published question solutions without credentials" do + response = api_call :get, "/api/questions/#{@published_question.to_param}/solutions", "v1" + assert_response :success + json = JSON.parse(@response.body) + # jj json + assert_equal json["solutions"].length, 1 + end + + test "should get single visible solution for published question without credentials" do + api_call :get, "/api/solutions/#{@visible_published_solution.id}", "v1" + assert_response :success + end + + test "should get published question solutions using non author oauth credentials" do + token = oauth_token_wrapper(@oauth_application, @user.email, "password") + response = token.get("/api/questions/#{@published_question.to_param}/solutions", "v1") + # puts JSON.parse(response.body) + assert_equal 200, response.status + end + + # test "should not be able to access unpublished question without credentials" do + # api_call :get, "/api/questions/#{@unpublished_question.to_param}", "v1" + # assert_response :forbidden + # end + + # test "should get unpublished question using oauth credentials" do + # token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") + # response = token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + # assert_equal 200, response.status + # end + + # test "should not get unpublished question using wrong oauth credentials" do + # token = oauth_token_wrapper(@oauth_application, @published_question_user.email, "password") + # assert_oauth_error (403) { + # token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + # } + # end + +end + + + + +