connect user authentication to API client authorization
authorTom Clegg <tom@clinicalfuture.com>
Thu, 24 Jan 2013 02:06:44 +0000 (18:06 -0800)
committerTom Clegg <tom@clinicalfuture.com>
Thu, 24 Jan 2013 02:39:34 +0000 (18:39 -0800)
32 files changed:
app/assets/images/logo.png [new file with mode: 0644]
app/assets/javascripts/api_client_authorizations.js.coffee [new file with mode: 0644]
app/assets/javascripts/api_clients.js.coffee [new file with mode: 0644]
app/assets/stylesheets/api_client_authorizations.css.scss [new file with mode: 0644]
app/assets/stylesheets/api_clients.css.scss [new file with mode: 0644]
app/controllers/api_client_authorizations_controller.rb [new file with mode: 0644]
app/controllers/api_clients_controller.rb [new file with mode: 0644]
app/controllers/application_controller.rb
app/controllers/collections_controller.rb
app/controllers/nodes_controller.rb
app/controllers/pipeline_invocations_controller.rb
app/controllers/static_controller.rb
app/controllers/user_sessions_controller.rb
app/helpers/api_client_authorizations_helper.rb [new file with mode: 0644]
app/helpers/api_clients_helper.rb [new file with mode: 0644]
app/models/api_client.rb [new file with mode: 0644]
app/models/api_client_authorization.rb [new file with mode: 0644]
app/models/user.rb
app/views/api_client_authorizations/index.html.erb [new file with mode: 0644]
app/views/static/intro.html.erb
config/routes.rb
db/migrate/20130123180224_create_api_clients.rb [new file with mode: 0644]
db/migrate/20130123180228_create_api_client_authorizations.rb [new file with mode: 0644]
db/schema.rb
test/fixtures/api_client_authorizations.yml [new file with mode: 0644]
test/fixtures/api_clients.yml [new file with mode: 0644]
test/functional/api_client_authorizations_controller_test.rb [new file with mode: 0644]
test/functional/api_clients_controller_test.rb [new file with mode: 0644]
test/unit/api_client_authorization_test.rb [new file with mode: 0644]
test/unit/api_client_test.rb [new file with mode: 0644]
test/unit/helpers/api_client_authorizations_helper_test.rb [new file with mode: 0644]
test/unit/helpers/api_clients_helper_test.rb [new file with mode: 0644]

diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png
new file mode 100644 (file)
index 0000000..4db96ef
Binary files /dev/null and b/app/assets/images/logo.png differ
diff --git a/app/assets/javascripts/api_client_authorizations.js.coffee b/app/assets/javascripts/api_client_authorizations.js.coffee
new file mode 100644 (file)
index 0000000..7615679
--- /dev/null
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/javascripts/api_clients.js.coffee b/app/assets/javascripts/api_clients.js.coffee
new file mode 100644 (file)
index 0000000..7615679
--- /dev/null
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/app/assets/stylesheets/api_client_authorizations.css.scss b/app/assets/stylesheets/api_client_authorizations.css.scss
new file mode 100644 (file)
index 0000000..fd2c9d8
--- /dev/null
@@ -0,0 +1,3 @@
+// Place all the styles related to the ApiClientAuthorizations controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/api_clients.css.scss b/app/assets/stylesheets/api_clients.css.scss
new file mode 100644 (file)
index 0000000..bd62734
--- /dev/null
@@ -0,0 +1,3 @@
+// Place all the styles related to the ApiClients controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/api_client_authorizations_controller.rb b/app/controllers/api_client_authorizations_controller.rb
new file mode 100644 (file)
index 0000000..d1b26fb
--- /dev/null
@@ -0,0 +1,11 @@
+class ApiClientAuthorizationsController < ApplicationController
+  def index
+    if Thread.current[:api_client_trusted]
+      @objects = model_class.
+        joins(:user, :api_client).
+        where('user_id=?', current_user.id)
+    else
+      @objects = model_class.where('1=0')
+    end
+  end
+end
diff --git a/app/controllers/api_clients_controller.rb b/app/controllers/api_clients_controller.rb
new file mode 100644 (file)
index 0000000..684e4e7
--- /dev/null
@@ -0,0 +1,2 @@
+class ApiClientsController < ApplicationController
+end
index 7d1fd13c041279928aa2a609b467c3f4a8f3d7cf..98b31a5f5ae2d92a9627515e5c4d18216a1e17ba 100644 (file)
@@ -1,10 +1,10 @@
 class ApplicationController < ActionController::Base
   protect_from_forgery
   before_filter :uncamelcase_params_hash_keys
+  around_filter :thread_with_auth_info, :except => [:render_error, :render_not_found]
   before_filter :find_object_by_uuid, :except => :index
-  before_filter :authenticate_api_token, :except => :render_not_found
 
-  before_filter :set_remote_ip
+  before_filter :remote_ip
   before_filter :login_required, :except => :render_not_found
 
   before_filter :catch_redirect_hint
@@ -17,26 +17,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  # Authentication
-  def login_required
-    if !current_user
-      respond_to do |format|
-        format.html  {
-          redirect_to '/auth/joshid'
-        }
-        format.json {
-          render :json => { 'error' => 'Not logged in' }.to_json
-        }
-      end
-    end
-  end
-
-  def current_user
-    return nil unless session[:user_id]
-    @current_user ||= User.find(session[:user_id]) rescue nil
-  end
-  # /Authentication
-
   unless Rails.application.config.consider_all_requests_local
     rescue_from Exception,
     :with => :render_error
@@ -67,7 +47,11 @@ class ApplicationController < ActionController::Base
   end
 
   def index
-    @objects ||= if params[:where]
+    @objects ||= model_class.
+      joins("LEFT JOIN metadata permissions ON permissions.tail=#{table_name}.uuid AND permissions.head=#{model_class.sanitize Thread.current[:user_uuid]} AND permissions.metadata_class='permission' AND permissions.name='visible_to'").
+      where("#{table_name}.created_by_user=? OR permissions.head IS NOT NULL",
+            Thread.current[:user_uuid])
+    if params[:where]
       where = params[:where]
       where = JSON.parse(where) if where.is_a?(String)
       conditions = ['1=1']
@@ -76,17 +60,20 @@ class ApplicationController < ActionController::Base
             attr.to_s.match(/^[a-z][_a-z0-9]+$/) and
             model_class.columns.collect(&:name).index(attr))
           if value.is_a? Array
-            conditions[0] << " and #{attr} in (?)"
+            conditions[0] << " and #{table_name}.#{attr} in (?)"
             conditions << value
           else
-            conditions[0] << " and #{attr}=?"
+            conditions[0] << " and #{table_name}.#{attr}=?"
             conditions << value
           end
         end
       end
-      model_class.where(*conditions)
+      if conditions.length > 1
+        conditions[0].sub!(/^1=1 and /, '')
+        @objects = @objects.
+          where(*conditions)
+      end
     end
-    @objects ||= model_class.all
     if params[:eager] and params[:eager] != '0' and params[:eager] != 0 and params[:eager] != ''
       @objects.each(&:eager_load_associations)
     end
@@ -123,8 +110,57 @@ class ApplicationController < ActionController::Base
     show
   end
 
+  def current_user
+    Thread.current[:user]
+  end
+
   protected
 
+  # Authentication
+  def login_required
+    if !current_user
+      respond_to do |format|
+        format.html  {
+          redirect_to '/auth/joshid'
+        }
+        format.json {
+          render :json => { 'error' => 'Not logged in' }.to_json
+        }
+      end
+    end
+  end
+
+  def thread_with_auth_info
+    begin
+      if params[:api_token]
+        @api_client_auth = ApiClientAuthorization.
+          includes(:api_client, :user).
+          where('api_token=?', params[:api_token]).
+          first
+        if @api_client_auth
+          session[:user_id] = @api_client_auth.user.id
+          session[:user_uuid] = @api_client_auth.user.uuid
+          session[:api_client_uuid] = @api_client_auth.api_client.uuid
+        end
+      end
+      Thread.current[:api_client_trusted] = session[:api_client_trusted]
+      Thread.current[:api_client_ip_address] = remote_ip
+      Thread.current[:api_client_uuid] = session[:api_client_uuid]
+      Thread.current[:user_uuid] = session[:user_uuid]
+      Thread.current[:remote_ip] = remote_ip
+      Thread.current[:user] = User.find(session[:user_id]) rescue nil
+      yield
+    ensure
+      Thread.current[:api_client_trusted] = nil
+      Thread.current[:api_client_ip_address] = nil
+      Thread.current[:api_client_uuid] = nil
+      Thread.current[:user_uuid] = nil
+      Thread.current[:remote_ip] = nil
+      Thread.current[:user] = nil
+    end
+  end
+  # /Authentication
+
   def model_class
     controller_name.classify.constantize
   end
@@ -133,6 +169,10 @@ class ApplicationController < ActionController::Base
     controller_name.singularize
   end
 
+  def table_name
+    controller_name
+  end
+
   def find_object_by_uuid
     if params[:id] and params[:id].match /\D/
       params[:uuid] = params.delete :id
@@ -188,17 +228,8 @@ class ApplicationController < ActionController::Base
     render json: @object_list
   end
 
-  def authenticate_api_token
-    unless Rails.configuration.
-      accept_api_token.
-      has_key?(params[:api_token] ||
-               cookies[:api_token])
-      render_error(Exception.new("Invalid API token"))
-    end
-  end
-
 private
-  def set_remote_ip
+  def remote_ip
     # Caveat: this is highly dependent on the proxy setup. YMMV.
     if request.headers.has_key?('HTTP_X_REAL_IP') then
       # We're behind a reverse proxy
@@ -208,5 +239,4 @@ private
       @remote_ip = request.env['REMOTE_ADDR']
     end
   end
-
 end
index 242b8b394dd7d31257979aad7b622dc4e44b640a..3d8f9909eaee4442e0c088c514bced9bb01fccb3 100644 (file)
@@ -1,6 +1,2 @@
 class CollectionsController < ApplicationController
-  skip_before_filter :authenticate_api_token
-  def index
-    @objects = model_class.order("created_at desc")
-  end
 end
index ed5ffc0fab29e4a582cfdfaf30f177f857e361ce..5dce1b13cc5273bc8926d656cf67a48e5f83c5f9 100644 (file)
@@ -1,5 +1,4 @@
 class NodesController < ApplicationController
-  skip_before_filter :authenticate_api_token
   def index
     @objects = model_class.order("created_at desc")
   end
index fa8c68a894d104d3c90d507a81c03c991ca50048..4982ed7d5ea877819bedc6b3057268d06b839d16 100644 (file)
@@ -1,6 +1,2 @@
 class PipelineInvocationsController < ApplicationController
-  skip_before_filter :authenticate_api_token
-  def index
-    @objects = model_class.order("created_at desc")
-  end
 end
index 99cccffabbb1d80b5daa8e16f21cc36bd25a5651..168ec43ce63c64d799a62e4712a34357d983314c 100644 (file)
@@ -2,7 +2,6 @@ class StaticController < ApplicationController
 
   skip_before_filter :uncamelcase_params_hash_keys
   skip_before_filter :find_object_by_uuid
-  skip_before_filter :authenticate_api_token
   skip_before_filter :login_required, :only => :home
 
   def home
index 45ea30c4e5141fd2a3d4d1fef61e148344ae6789..20da46732098dd0b87f699954a62caac98e2ec4c 100644 (file)
@@ -3,7 +3,6 @@ class UserSessionsController < ApplicationController
 
   skip_before_filter :uncamelcase_params_hash_keys
   skip_before_filter :find_object_by_uuid
-  skip_before_filter :authenticate_api_token
 
   respond_to :html
 
@@ -39,12 +38,15 @@ class UserSessionsController < ApplicationController
 
     omniauth.delete('extra')
 
+    # Give the authenticated user a cookie for direct API access
     session[:user_id] = user.id
+    session[:user_uuid] = user.uuid
+    session[:api_client_uuid] = nil
+    session[:api_client_trusted] = true # full permission to see user's secrets
 
     @redirect_to = root_path
-    if session.has_key?('redirect_to') then
-      @redirect_to = session[:redirect_to]
-      session.delete(:redirect_to)
+    if session.has_key? :return_to
+      return send_api_token_to(session.delete :return_to)
     end
     redirect_to @redirect_to
   end
@@ -67,6 +69,46 @@ class UserSessionsController < ApplicationController
   # to save the redirect_to parameter (if it exists; see the application
   # controller). /auth/joshid bypasses the application controller.
   def login
-    redirect_to "/auth/joshid"
+    if current_user and params[:return_to]
+      # Already logged in; just need to send a token to the requesting
+      # API client.
+      #
+      # FIXME: if current_user has never authorized this app before,
+      # ask for confirmation here!
+
+      send_api_token_to(params[:return_to])
+    else
+      # TODO: make joshid propagate return_to as a GET parameter, and
+      # use that GET parameter instead of session[] when redirecting
+      # in create().  Using session[] is inappropriate: completing a
+      # login in browser window A can cause a token to be sent to a
+      # different API client who has requested a token in window B.
+
+      session[:return_to] = params[:return_to]
+      redirect_to "/auth/joshid"
+    end
+  end
+
+  def send_api_token_to(callback_url)
+    # Give the API client a token for making API calls on behalf of
+    # the authenticated user
+
+    # Stub: automatically register all new API clients
+    api_client_url_prefix = callback_url.match(%r{^.*?://[^/]+})[0] + '/'
+    api_client = ApiClient.find_or_create_by_url_prefix(api_client_url_prefix)
+
+    api_client_auth = ApiClientAuthorization.
+      new(user: user,
+          api_client: api_client,
+          created_by_ip_address: Thread.current[:remote_ip])
+    api_client_auth.save!
+
+    if callback_url.index('?')
+      callback_url << '&'
+    else
+      callback_url << '?'
+    end
+    callback_url << 'api_token=' << api_client_auth.api_token
+    redirect_to callback_url
   end
 end
diff --git a/app/helpers/api_client_authorizations_helper.rb b/app/helpers/api_client_authorizations_helper.rb
new file mode 100644 (file)
index 0000000..98ddddc
--- /dev/null
@@ -0,0 +1,2 @@
+module ApiClientAuthorizationsHelper
+end
diff --git a/app/helpers/api_clients_helper.rb b/app/helpers/api_clients_helper.rb
new file mode 100644 (file)
index 0000000..2432bfe
--- /dev/null
@@ -0,0 +1,2 @@
+module ApiClientsHelper
+end
diff --git a/app/models/api_client.rb b/app/models/api_client.rb
new file mode 100644 (file)
index 0000000..b3e1b2e
--- /dev/null
@@ -0,0 +1,11 @@
+class ApiClient < ActiveRecord::Base
+  include AssignUuid
+  include KindAndEtag
+  include CommonApiTemplate
+  has_many :api_client_authorizations
+
+  api_accessible :superuser, :extend => :common do |t|
+    t.add :name
+    t.add :url_prefix
+  end
+end
diff --git a/app/models/api_client_authorization.rb b/app/models/api_client_authorization.rb
new file mode 100644 (file)
index 0000000..0e89869
--- /dev/null
@@ -0,0 +1,9 @@
+class ApiClientAuthorization < ActiveRecord::Base
+  belongs_to :api_client
+  belongs_to :user
+  after_initialize :assign_random_api_token
+
+  def assign_random_api_token
+    self.api_token ||= rand(2**256).to_s(36)
+  end
+end
index 023b1795571fcd7b3dc3d798ffda237bf4268f9b..f374c086a55ab821b86c4b5dc495942b8d2014d1 100644 (file)
@@ -3,6 +3,7 @@ class User < ActiveRecord::Base
   include KindAndEtag
   include CommonApiTemplate
   serialize :prefs, Hash
+  has_many :api_client_authorizations
 
   api_accessible :superuser, :extend => :common do |t|
     t.add :email
diff --git a/app/views/api_client_authorizations/index.html.erb b/app/views/api_client_authorizations/index.html.erb
new file mode 100644 (file)
index 0000000..f80ecef
--- /dev/null
@@ -0,0 +1,36 @@
+<table style="width:100%">
+  <tr class="contain-align-left">
+    <th>
+      API client
+    </th><th>
+      Token
+    </th><th>
+      Created at
+    </th><th>
+      Used at
+    </th><th>
+      Expires
+    </th>
+  </tr>
+
+  <% @objects.each do |o| %>
+
+  <tr>
+    <td>
+      <%= o.api_client.name || o.api_client.url_prefix || o.api_client.uuid %>
+    </td><td>
+      <%= o.api_token %>
+    </td><td>
+      <%= o.created_at %>
+    </td><td>
+      <%= o.last_used_at %>
+      /
+      <%= o.last_used_by_ip_address %>
+    </td><td>
+      <%= o.expires_at %>
+    </td>
+  </tr>
+
+  <% end %>
+
+</table>
index 22eb4d2a4ae2baf7b837ffe22f7a15876e79f482..94187e800a6192af6c5b17fdb1411d015c6bb67d 100644 (file)
@@ -4,7 +4,7 @@ $(function(){
 });
 <% end %>
 <div id="intropage">
-  <img class="clinicalfuture-logo" src="/images/logo.png" style="display:block; margin:2em auto"/>
+  <img class="clinicalfuture-logo" src="<%= asset_path('logo.png') %>" style="display:block; margin:2em auto"/>
   <div style="width:30em; margin:2em auto 0 auto">
     <h1>Welcome</h1>
     <h4>Clinical Future ORVOS</h4>
index ef896f6af6ae4c63eb06393e4f1d5249bb00eec1..b5e09253858c970cc213b61d52ce834e6b98b66c 100644 (file)
@@ -1,4 +1,8 @@
 Server::Application.routes.draw do
+  resources :api_client_authorizations
+
+  resources :api_clients
+
   resources :logs
   resources :projects
   resources :specimens
diff --git a/db/migrate/20130123180224_create_api_clients.rb b/db/migrate/20130123180224_create_api_clients.rb
new file mode 100644 (file)
index 0000000..2bd8234
--- /dev/null
@@ -0,0 +1,17 @@
+class CreateApiClients < ActiveRecord::Migration
+  def change
+    create_table :api_clients do |t|
+      t.string :uuid
+      t.string :created_by_client
+      t.string :created_by_user
+      t.string :modified_by_client
+      t.string :modified_by_user
+      t.datetime :modified_at
+      t.string :name
+      t.string :url_prefix
+
+      t.timestamps
+    end
+    add_index :api_clients, :uuid, :unique => true
+  end
+end
diff --git a/db/migrate/20130123180228_create_api_client_authorizations.rb b/db/migrate/20130123180228_create_api_client_authorizations.rb
new file mode 100644 (file)
index 0000000..c3d99e6
--- /dev/null
@@ -0,0 +1,19 @@
+class CreateApiClientAuthorizations < ActiveRecord::Migration
+  def change
+    create_table :api_client_authorizations do |t|
+      t.string :api_token, :null => false
+      t.references :api_client, :null => false
+      t.references :user, :null => false
+      t.string :created_by_ip_address
+      t.string :last_used_by_ip_address
+      t.datetime :last_used_at
+      t.datetime :expires_at
+
+      t.timestamps
+    end
+    add_index :api_client_authorizations, :api_token, :unique => true
+    add_index :api_client_authorizations, :api_client_id
+    add_index :api_client_authorizations, :user_id
+    add_index :api_client_authorizations, :expires_at
+  end
+end
index 3b9804271c3e81a450140c0ed5fce6ba06a05b51..331ae2767d56aa37e6e52d1c63d38425ad92308f 100644 (file)
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20130123174514) do
+ActiveRecord::Schema.define(:version => 20130123180228) do
+
+  create_table "api_client_authorizations", :force => true do |t|
+    t.string   "api_token",               :null => false
+    t.integer  "api_client_id",           :null => false
+    t.integer  "user_id",                 :null => false
+    t.string   "created_by_ip_address"
+    t.string   "last_used_by_ip_address"
+    t.datetime "last_used_at"
+    t.datetime "expires_at"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  add_index "api_client_authorizations", ["api_client_id"], :name => "index_api_client_authorizations_on_api_client_id"
+  add_index "api_client_authorizations", ["api_token"], :name => "index_api_client_authorizations_on_api_token", :unique => true
+  add_index "api_client_authorizations", ["expires_at"], :name => "index_api_client_authorizations_on_expires_at"
+  add_index "api_client_authorizations", ["user_id"], :name => "index_api_client_authorizations_on_user_id"
+
+  create_table "api_clients", :force => true do |t|
+    t.string   "uuid"
+    t.string   "created_by_client"
+    t.string   "created_by_user"
+    t.string   "modified_by_client"
+    t.string   "modified_by_user"
+    t.datetime "modified_at"
+    t.string   "name"
+    t.string   "url_prefix"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  add_index "api_clients", ["uuid"], :name => "index_api_clients_on_uuid", :unique => true
 
   create_table "collections", :force => true do |t|
     t.string   "locator"
diff --git a/test/fixtures/api_client_authorizations.yml b/test/fixtures/api_client_authorizations.yml
new file mode 100644 (file)
index 0000000..7e743ce
--- /dev/null
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+  api_token: MyString
+  api_client: 
+  user: 
+
+two:
+  api_token: MyString
+  api_client: 
+  user: 
diff --git a/test/fixtures/api_clients.yml b/test/fixtures/api_clients.yml
new file mode 100644 (file)
index 0000000..a0f04df
--- /dev/null
@@ -0,0 +1,21 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+
+one:
+  uuid: MyString
+  created_by_client: MyString
+  created_by_user: MyString
+  modified_by_client: MyString
+  modified_by_user: MyString
+  modified_at: 2013-01-23 10:02:24
+  name: MyString
+  url_prefix: MyString
+
+two:
+  uuid: MyString
+  created_by_client: MyString
+  created_by_user: MyString
+  modified_by_client: MyString
+  modified_by_user: MyString
+  modified_at: 2013-01-23 10:02:24
+  name: MyString
+  url_prefix: MyString
diff --git a/test/functional/api_client_authorizations_controller_test.rb b/test/functional/api_client_authorizations_controller_test.rb
new file mode 100644 (file)
index 0000000..3803b8e
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ApiClientAuthorizationsControllerTest < ActionController::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/functional/api_clients_controller_test.rb b/test/functional/api_clients_controller_test.rb
new file mode 100644 (file)
index 0000000..569836a
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ApiClientsControllerTest < ActionController::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/unit/api_client_authorization_test.rb b/test/unit/api_client_authorization_test.rb
new file mode 100644 (file)
index 0000000..b5b07d1
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ApiClientAuthorizationTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/unit/api_client_test.rb b/test/unit/api_client_test.rb
new file mode 100644 (file)
index 0000000..7f16490
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class ApiClientTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/test/unit/helpers/api_client_authorizations_helper_test.rb b/test/unit/helpers/api_client_authorizations_helper_test.rb
new file mode 100644 (file)
index 0000000..4225e04
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ApiClientAuthorizationsHelperTest < ActionView::TestCase
+end
diff --git a/test/unit/helpers/api_clients_helper_test.rb b/test/unit/helpers/api_clients_helper_test.rb
new file mode 100644 (file)
index 0000000..3e58181
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ApiClientsHelperTest < ActionView::TestCase
+end