12032: Controller support for group trash.
authorPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 29 Aug 2017 13:08:19 +0000 (09:08 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Thu, 14 Sep 2017 23:39:06 +0000 (19:39 -0400)
Conttroller support and testing for contents, index, show, trash, untrash,
containers.

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

12 files changed:
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/collections_controller.rb
services/api/app/controllers/arvados/v1/groups_controller.rb
services/api/app/models/arvados_model.rb
services/api/app/models/container.rb
services/api/app/models/log.rb
services/api/config/routes.rb
services/api/lib/create_permission_view.sql
services/api/lib/trashable.rb
services/api/test/fixtures/groups.yml
services/api/test/functional/arvados/v1/groups_controller_test.rb
services/api/test/unit/group_test.rb

index a3d8f082adf82c3330b034ba0dde581d0ca493b0..46b96a9571f7a9bd63fa8d55b4ee215ccb611fd9 100644 (file)
@@ -180,7 +180,7 @@ class ApplicationController < ActionController::Base
   end
 
   def find_objects_for_index
-    @objects ||= model_class.readable_by(*@read_users)
+    @objects ||= model_class.readable_by(*@read_users, {:include_trash => (params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name))})
     apply_where_limit_order_params
   end
 
index 87d88fe4f584092e06315ad9eb9f93f0593b850c..0312d956a6168d957f50136d4c03df691f88279c 100644 (file)
@@ -3,9 +3,11 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require "arvados/keep"
+require "trashable"
 
 class Arvados::V1::CollectionsController < ApplicationController
   include DbCurrentTime
+  include TrashableController
 
   def self._index_requires_parameters
     (super rescue {}).
@@ -16,7 +18,6 @@ class Arvados::V1::CollectionsController < ApplicationController
       })
   end
 
-
   def create
     if resource_attrs[:uuid] and (loc = Keep::Locator.parse(resource_attrs[:uuid]))
       resource_attrs[:portable_data_hash] = loc.to_s
@@ -58,39 +59,6 @@ class Arvados::V1::CollectionsController < ApplicationController
     end
   end
 
-  def destroy
-    if !@object.is_trashed
-      @object.update_attributes!(trash_at: db_current_time)
-    end
-    earliest_delete = (@object.trash_at +
-                       Rails.configuration.blob_signature_ttl.seconds)
-    if @object.delete_at > earliest_delete
-      @object.update_attributes!(delete_at: earliest_delete)
-    end
-    show
-  end
-
-  def trash
-    if !@object.is_trashed
-      @object.update_attributes!(trash_at: db_current_time)
-    end
-    show
-  end
-
-  def untrash
-    if @object.is_trashed
-      @object.trash_at = nil
-
-      if params[:ensure_unique_name]
-        @object.save_with_unique_name!
-      else
-        @object.save!
-      end
-    else
-      raise InvalidStateTransitionError
-    end
-    show
-  end
 
   def find_collections(visited, sp, &b)
     case sp
index 75ef095b71ce249d18fb23075782bd32deaf6663..9e0cdf899ef9dcbb8cb15d1e9aa1567f7e3c3f03 100644 (file)
@@ -2,7 +2,19 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
+require "trashable"
+
 class Arvados::V1::GroupsController < ApplicationController
+  include TrashableController
+
+  def self._index_requires_parameters
+    (super rescue {}).
+      merge({
+        include_trash: {
+          type: 'boolean', required: false, description: "Include items whose is_trashed attribute is true."
+        },
+      })
+  end
 
   def self._contents_requires_parameters
     params = _index_requires_parameters.
@@ -152,10 +164,10 @@ class Arvados::V1::GroupsController < ApplicationController
       end.compact
 
       if klass == Collection and params[:include_trash]
-        @objects = klass.unscoped.readable_by(*@read_users).
+        @objects = klass.unscoped.readable_by(*@read_users, {:include_trash => params[:include_trash]}).
           order(request_order).where(where_conds)
       else
-        @objects = klass.readable_by(*@read_users).
+        @objects = klass.readable_by(*@read_users, {:include_trash => params[:include_trash]}).
           order(request_order).where(where_conds)
       end
       klass_limit = limit_all - all_objects.count
index ccd8f8276056528862cbaea8cc81d549ebd350a5..518d4c8ff34ace4256d5664a3c8beb6e2b8a6572 100644 (file)
@@ -254,7 +254,8 @@ class ArvadosModel < ActiveRecord::Base
 
     # Collect the UUIDs of the authorized users.
     sql_table = kwargs.fetch(:table_name, table_name)
-    include_trashed = kwargs.fetch(:include_trashed, false)
+    include_trash = kwargs.fetch(:include_trash, false)
+    query_on = kwargs.fetch(:query_on, self)
 
     sql_conds = []
     user_uuids = users_list.map { |u| u.uuid }
@@ -263,49 +264,53 @@ class ArvadosModel < ActiveRecord::Base
 
     # Check if any of the users are admin.
     if users_list.select { |u| u.is_admin }.any?
-      if !include_trashed
+      if !include_trash
         # exclude rows that are trashed.
         if self.column_names.include? "owner_uuid"
           sql_conds += ["NOT EXISTS(SELECT target_uuid
-                  FROM permission_view pv
+                  FROM permission_view
                   WHERE trashed = 1 AND
-                  (#{sql_table}.uuid = pv.target_uuid OR #{sql_table}.owner_uuid = pv.target_uuid))"]
+                  (#{sql_table}.uuid = target_uuid OR #{sql_table}.owner_uuid = target_uuid))"]
         else
           sql_conds += ["NOT EXISTS(SELECT target_uuid
-                  FROM permission_view pv
+                  FROM permission_view
                   WHERE trashed = 1 AND
-                  (#{sql_table}.uuid = pv.target_uuid))"]
+                  (#{sql_table}.uuid = target_uuid))"]
         end
       end
     else
-      # Match objects which appear in the permission view
-      trash_clause = if !include_trashed then "trashed = 0 AND" else "" end
+      trash_clause = if !include_trash then "trashed = 0 AND" else "" end
 
-      # Match any object (evidently a group or user) whose UUID is
-      # listed explicitly in user_uuids.
+      # Can read object (evidently a group or user) whose UUID is listed
+      # explicitly in user_uuids.
       sql_conds += ["#{sql_table}.uuid IN (:user_uuids)"]
 
+      direct_permission_check = "EXISTS(SELECT 1 FROM permission_view
+                  WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trash_clause}
+                  (#{sql_table}.uuid = target_uuid))"
+
       if self.column_names.include? "owner_uuid"
-        sql_conds += ["EXISTS(SELECT target_uuid
-                  FROM permission_view pv
+        # if an explicit permission row exists for the uuid in question, apply
+        # the "direct_permission_check"
+        # if not, check for permission to read the owner instead
+        sql_conds += ["CASE
+                  WHEN EXISTS(select 1 FROM permission_view where target_uuid = #{sql_table}.uuid)
+                  THEN #{direct_permission_check}
+                  ELSE EXISTS(SELECT 1 FROM permission_view
                   WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trash_clause}
-                  (#{sql_table}.uuid = pv.target_uuid OR
-                  (#{sql_table}.owner_uuid = pv.target_uuid AND pv.target_owner_uuid is NOT NULL)))"]
-        # Match any object whose owner is listed explicitly in
-        # user_uuids.
-        trash_clause = if !include_trashed
+                  (#{sql_table}.owner_uuid = target_uuid AND target_owner_uuid is NOT NULL))
+                  END"]
+        # Can also read if one of the users is the owner of the object.
+        trash_clause = if !include_trash
                          "1 NOT IN (SELECT trashed
-                             FROM permission_view pv
-                             WHERE #{sql_table}.uuid = pv.target_uuid) AND"
+                             FROM permission_view
+                             WHERE #{sql_table}.uuid = target_uuid) AND"
                        else
                          ""
                        end
         sql_conds += ["(#{trash_clause} #{sql_table}.owner_uuid IN (:user_uuids))"]
       else
-        sql_conds += ["EXISTS(SELECT target_uuid
-                  FROM permission_view pv
-                  WHERE user_uuid IN (:user_uuids) AND perm_level >= 1 AND #{trash_clause}
-                  (#{sql_table}.uuid = pv.target_uuid))"]
+        sql_conds += [direct_permission_check]
       end
 
       if sql_table == "links"
@@ -317,9 +322,9 @@ class ArvadosModel < ActiveRecord::Base
       end
     end
 
-    where(sql_conds.join(' OR '),
-          user_uuids: user_uuids,
-          permission_link_classes: ['permission', 'resources'])
+    query_on.where(sql_conds.join(' OR '),
+                    user_uuids: user_uuids,
+                    permission_link_classes: ['permission', 'resources'])
   end
 
   def save_with_unique_name!
index 36c87af2ec154269e0d8f52492684d71b64f8dab..d61ba75e568bc53b578e302909c831dd922578d4 100644 (file)
@@ -297,17 +297,15 @@ class Container < ArvadosModel
   end
 
   def self.readable_by(*users_list)
-    if users_list.select { |u| u.is_admin }.any?
-      return self
+    # Load optional keyword arguments, if they exist.
+    if users_list.last.is_a? Hash
+      kwargs = users_list.pop
+    else
+      kwargs = {}
     end
-    user_uuids = users_list.map { |u| u.uuid }
-    uuid_list = user_uuids + users_list.flat_map { |u| u.groups_i_can(:read) }
-    uuid_list.uniq!
-    permitted = "(SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (:uuids))"
-    joins(:container_requests).
-      where("container_requests.uuid IN #{permitted} OR "+
-            "container_requests.owner_uuid IN (:uuids)",
-            uuids: uuid_list)
+    kwargs[:query_on] = joins(:container_requests)
+    users_list.push kwargs
+    ContainerRequest.readable_by(*users_list)
   end
 
   def final?
index 73f143e8c3c2e98f5164db5f108ce9004930460e..7f2d3ef13e49e088ef09c2af66fc194365697db0 100644 (file)
@@ -60,6 +60,9 @@ class Log < ArvadosModel
   end
 
   def self.readable_by(*users_list)
+    if users_list.last.is_a? Hash
+      users_list.pop
+    end
     if users_list.select { |u| u.is_admin }.any?
       return self
     end
index 2e9d618986825d1871c28ca38695c4ef7c046440..5b6fe80975faf0d5a56be3e17150d600ce525bc6 100644 (file)
@@ -30,6 +30,8 @@ Server::Application.routes.draw do
       resources :groups do
         get 'contents', on: :collection
         get 'contents', on: :member
+        post 'trash', on: :member
+        post 'untrash', on: :member
       end
       resources :humans
       resources :job_tasks
index 639610ac2a6efd53a7be2cbaf9b29a0608e805e6..71ac8c499cd7b7aaefce2a0e4a1f7091c7485df6 100644 (file)
@@ -37,9 +37,8 @@ perm_edges (tail_uuid, head_uuid, val, follow, trashed) AS (
               LEFT JOIN groups ON pv.val<3 AND groups.uuid = links.head_uuid
               WHERE links.link_class = 'permission'
        UNION ALL
-       SELECT owner_uuid, uuid, 3, true, CASE
-              WHEN trash_at IS NOT NULL and trash_at < clock_timestamp() THEN 1
-              ELSE 0 END
+       SELECT owner_uuid, uuid, 3, true,
+              CASE WHEN trash_at IS NOT NULL and trash_at < clock_timestamp() THEN 1 ELSE 0 END
               FROM groups
        ),
 perm (val, follow, user_uuid, target_uuid, trashed, startnode) AS (
index 1e2f466fb7cbd928a8ddeef3dfa7685f0fd623f5..38ebaf7a8bfb5ca483c4b5b4df7d89585b5ff60c 100644 (file)
@@ -89,3 +89,40 @@ module Trashable
     true
   end
 end
+
+module TrashableController
+  def destroy
+    if !@object.is_trashed
+      @object.update_attributes!(trash_at: db_current_time)
+    end
+    earliest_delete = (@object.trash_at +
+                       Rails.configuration.blob_signature_ttl.seconds)
+    if @object.delete_at > earliest_delete
+      @object.update_attributes!(delete_at: earliest_delete)
+    end
+    show
+  end
+
+  def trash
+    if !@object.is_trashed
+      @object.update_attributes!(trash_at: db_current_time)
+    end
+    show
+  end
+
+  def untrash
+    if @object.is_trashed
+      @object.trash_at = nil
+
+      if params[:ensure_unique_name]
+        @object.save_with_unique_name!
+      else
+        @object.save!
+      end
+    else
+      raise InvalidStateTransitionError
+    end
+    show
+  end
+
+end
index 1cb5817b0acabc0605af08f8185697ff84b59902..2411831520a9a9365ef55d67686938afbfa26c9f 100644 (file)
@@ -313,6 +313,13 @@ trashed_subproject:
   owner_uuid: zzzzz-j7d0g-trashedproject1
   name: trashed subproject
   group_class: project
+  is_trashed: false
+
+trashed_subproject3:
+  uuid: zzzzz-j7d0g-trashedproject3
+  owner_uuid: zzzzz-j7d0g-trashedproject1
+  name: trashed subproject 3
+  group_class: project
   trash_at: 2001-01-01T00:00:00Z
   delete_at: 2038-03-01T00:00:00Z
   is_trashed: true
index 043d66506d2c60a899ae486b6a4b5bdca346ca13..b80fcb1c50a046112933743eda5b23fed13bcdc7 100644 (file)
@@ -533,111 +533,145 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
   ### trashed project tests ###
 
   [:active, :admin].each do |auth|
-    test "trashed project is hidden in contents (#{auth})" do
-      authorize_with auth
-      get :contents, {
-            id: users(:active).uuid,
-            format: :json,
-          }
-      item_uuids = json_response['items'].map do |item|
-        item['uuid']
+    # project: to query,    to untrash,    is visible, parent contents listing success
+    [[:trashed_project,     [],                 false, true],
+     [:trashed_project,     [:trashed_project], true,  true],
+     [:trashed_subproject,  [],                 false, false],
+     [:trashed_subproject,  [:trashed_project], true,  true],
+     [:trashed_subproject3, [:trashed_project], false, true],
+     [:trashed_subproject3, [:trashed_subproject3], false, false],
+     [:trashed_subproject3, [:trashed_project, :trashed_subproject3], true, true],
+    ].each do |project, untrash, visible, success|
+
+      test "contents listing #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :contents, {
+              id: groups(project).owner_uuid,
+              format: :json
+            }
+        if success
+          assert_response :success
+          item_uuids = json_response['items'].map do |item|
+            item['uuid']
+          end
+          if visible
+            assert_includes(item_uuids, groups(project).uuid)
+          else
+            assert_not_includes(item_uuids, groups(project).uuid)
+          end
+        else
+          assert_response 404
+        end
       end
-      assert_not_includes(item_uuids, groups(:trashed_project).uuid)
-    end
 
-    test "untrashed project is visible in contents (#{auth})" do
-      authorize_with auth
-      Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
-      get :contents, {
-            id: users(:active).uuid,
-            format: :json,
-          }
-      item_uuids = json_response['items'].map do |item|
-        item['uuid']
+      test "contents of #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :contents, {
+              id: groups(project).uuid,
+              format: :json
+            }
+        if visible
+          assert_response :success
+        else
+          assert_response 404
+        end
       end
-      assert_includes(item_uuids, groups(:trashed_project).uuid)
-    end
 
-    test "trashed project is hidden in index (#{auth})" do
-      authorize_with :active
-      get :index, {
-            format: :json,
-          }
-      item_uuids = json_response['items'].map do |item|
-        item['uuid']
+      test "index #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :index, {
+              format: :json,
+            }
+        assert_response :success
+        item_uuids = json_response['items'].map do |item|
+          item['uuid']
+        end
+        if visible
+          assert_includes(item_uuids, groups(project).uuid)
+        else
+          assert_not_includes(item_uuids, groups(project).uuid)
+        end
       end
-      assert_not_includes(item_uuids, groups(:trashed_project).uuid)
-      assert_not_includes(item_uuids, groups(:trashed_subproject).uuid)
-    end
 
-    test "untrashed project is visible in index (#{auth})" do
-      authorize_with :active
-      Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
-      get :index, {
-            format: :json,
-          }
-      item_uuids = json_response['items'].map do |item|
-        item['uuid']
+      test "show #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :show, {
+              id: groups(project).uuid,
+              format: :json
+            }
+        if visible
+          assert_response :success
+        else
+          assert_response 404
+        end
       end
-      assert_includes(item_uuids, groups(:trashed_project).uuid)
-      assert_includes(item_uuids, groups(:trashed_subproject).uuid)
-    end
 
-    test "cannot get contents of trashed project (#{auth})" do
-      authorize_with :active
-      get :contents, {
-            id: groups(:trashed_project).uuid,
-            format: :json,
-          }
-      assert_response 404
-    end
+      test "show include_trash #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :show, {
+              id: groups(project).uuid,
+              format: :json,
+              include_trash: true
+            }
+        assert_response :success
+      end
 
-    test "cannot get contents of trashed subproject (#{auth})" do
-      authorize_with :active
-      get :contents, {
-            id: groups(:trashed_subproject).uuid,
-            format: :json,
-          }
-      assert_response 404
+      test "index include_trash #{project} #{untrash} as #{auth}" do
+        authorize_with auth
+        untrash.each do |pr|
+          Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+        end
+        get :index, {
+              format: :json,
+              include_trash: true
+            }
+        assert_response :success
+        item_uuids = json_response['items'].map do |item|
+          item['uuid']
+        end
+        assert_includes(item_uuids, groups(project).uuid)
+      end
     end
 
-    test "can get contents of untrashed project (#{auth})" do
-      authorize_with :active
-      Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
-      get :contents, {
+    test "delete project #{auth}" do
+      authorize_with auth
+      [:trashed_project].each do |pr|
+        Group.find_by_uuid(groups(pr).uuid).update! is_trashed: false
+      end
+      assert !Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
+      post :destroy, {
             id: groups(:trashed_project).uuid,
             format: :json,
           }
-      assert_response 200
-    end
-
-    test "can get contents of untrashed subproject (#{auth})" do
-      authorize_with :active
-      Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
-      get :contents, {
-            id: groups(:trashed_subproject).uuid,
-            format: :json,
-          }
-      assert_response 200
+      assert_response :success
+      assert Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
     end
 
-    test "cannot show trashed project (#{auth})" do
-      authorize_with :active
-      get :show, {
+    test "untrash project #{auth}" do
+      authorize_with auth
+      assert Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
+      post :untrash, {
             id: groups(:trashed_project).uuid,
             format: :json,
           }
-      assert_response 404
+      assert_response :success
+      assert !Group.find_by_uuid(groups(:trashed_project).uuid).is_trashed
     end
 
-    test "cannot show trashed subproject (#{auth})" do
-      authorize_with :active
-      get :show, {
-            id: groups(:trashed_subproject).uuid,
-            format: :json,
-          }
-      assert_response 404
-    end
   end
-
 end
index e2c88293c3d82933d95d14d5005e4e1ff1f2f2ad..4672acd0979972be13cbbe639177b81517d40fb6 100644 (file)
@@ -69,30 +69,87 @@ class GroupTest < ActiveSupport::TestCase
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).any?
     g_foo.update! is_trashed: true
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).empty?
+    assert Collection.readable_by(users(:active), {:include_trash => true}).where(uuid: col.uuid).any?
     g_foo.update! is_trashed: false
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).any?
   end
 
+  test "delete group" do
+    set_user_from_auth :active_trustedclient
 
-  test "delete group propagates to subgroups" do
+    g_foo = Group.create!(name: "foo")
+    g_bar = Group.create!(name: "bar", owner_uuid: g_foo.uuid)
+    g_baz = Group.create!(name: "baz", owner_uuid: g_bar.uuid)
+
+    assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).any?
+    g_foo.update! is_trashed: true
+    assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).empty?
+    assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).empty?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
+
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_foo.uuid).any?
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_bar.uuid).any?
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_baz.uuid).any?
+  end
+
+
+  test "delete subgroup" do
     set_user_from_auth :active_trustedclient
 
     g_foo = Group.create!(name: "foo")
     g_bar = Group.create!(name: "bar", owner_uuid: g_foo.uuid)
-    col = Collection.create!(owner_uuid: g_bar.uuid)
+    g_baz = Group.create!(name: "baz", owner_uuid: g_bar.uuid)
 
     assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
     assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).any?
-    assert Collection.readable_by(users(:active)).where(uuid: col.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).any?
+    g_bar.update! is_trashed: true
+
+    assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).empty?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
+
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_bar.uuid).any?
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_baz.uuid).any?
+  end
+
+  test "delete subsubgroup" do
+    set_user_from_auth :active_trustedclient
+
+    g_foo = Group.create!(name: "foo")
+    g_bar = Group.create!(name: "bar", owner_uuid: g_foo.uuid)
+    g_baz = Group.create!(name: "baz", owner_uuid: g_bar.uuid)
+
+    assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).any?
+    g_baz.update! is_trashed: true
+    assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).any?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
+    assert Group.readable_by(users(:active), {:include_trash => true}).where(uuid: g_baz.uuid).any?
+  end
+
+
+  test "delete group propagates to subgroups" do
+    set_user_from_auth :active_trustedclient
+
+    g_foo = groups(:trashed_project)
+    g_bar = groups(:trashed_subproject)
+    g_baz = groups(:trashed_subproject3)
+    col = collections(:collection_in_trashed_subproject)
 
-    g_foo.update! is_trashed: true
     assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).empty?
     assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).empty?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).empty?
 
     set_user_from_auth :admin
     assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).empty?
     assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).empty?
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).empty?
 
     set_user_from_auth :active_trustedclient
@@ -100,6 +157,12 @@ class GroupTest < ActiveSupport::TestCase
     assert Group.readable_by(users(:active)).where(uuid: g_foo.uuid).any?
     assert Group.readable_by(users(:active)).where(uuid: g_bar.uuid).any?
     assert Collection.readable_by(users(:active)).where(uuid: col.uuid).any?
+
+    # this one should still be deleted.
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).empty?
+
+    g_baz.update! is_trashed: false
+    assert Group.readable_by(users(:active)).where(uuid: g_baz.uuid).any?
   end
 
 end