diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index dd85045997b905cba1840ba4d61f83b1ba19ce68..f055fc9b25172184954da5f9a3d779285570e77b 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -47,13 +47,18 @@ class TagsController < ApplicationController
     end
   end
 
-  def edit; end
+  def edit
+    check_your_privilege('EditTag', nil, true)
+  end
 
   def update
-    if @tag.update(tag_params.merge(wiki: helpers.render_markdown(params[:tag][:wiki_markdown])))
+    return unless check_your_privilege('EditTag', nil, true)
+
+    wiki_md = params[:tag][:wiki_markdown]
+    if @tag.update(tag_params.merge(wiki: wiki_md.present? ? helpers.render_markdown(wiki_md) : nil))
       redirect_to tag_path(id: @category.id, tag_id: @tag.id)
     else
-      render :edit
+      render :edit, status: 400
     end
   end
 
diff --git a/app/models/tag.rb b/app/models/tag.rb
index d9daa475dc5697dac6abafe3cc9daae105ffd7ab..5ee4ccc71fd9772e91b54dfd92bb04d036c13d9a 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -13,7 +13,7 @@ class Tag < ApplicationRecord
 
   def self.search(term)
     where('name LIKE ?', "%#{sanitize_sql_like(term)}%")
-      .order(sanitize_sql_array(['name LIKE ? DESC, name', "#{sanitize_sql_like(term)}%"]))
+      .order(Arel.sql(sanitize_sql_array(['name LIKE ? DESC, name', "#{sanitize_sql_like(term)}%"])))
   end
 
   def all_children
diff --git a/test/controllers/tags_controller_test.rb b/test/controllers/tags_controller_test.rb
index c2760733773b67fef121a7dc214086dfae8c5ea8..171dce5a02cc160d7c74d0d429a996574596de1a 100644
--- a/test/controllers/tags_controller_test.rb
+++ b/test/controllers/tags_controller_test.rb
@@ -23,4 +23,123 @@ class TagsControllerTest < ActionController::TestCase
       assert_equal true, tag['name'].start_with?('dis')
     end
   end
+
+  test 'should get category tags list' do
+    get :category, params: { id: categories(:main).id }
+    assert_response 200
+    assert_not_nil assigns(:tags)
+    assert_not_nil assigns(:category)
+
+    sign_in users(:standard_user)
+    get :category, params: { id: categories(:main).id }
+    assert_response 200
+    assert_not_nil assigns(:tags)
+    assert_not_nil assigns(:category)
+  end
+
+  test 'should get children list' do
+    get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 200
+    assert_not_nil assigns(:tags)
+    assert_not_nil assigns(:category)
+
+    sign_in users(:standard_user)
+    get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 200
+    assert_not_nil assigns(:tags)
+    assert_not_nil assigns(:category)
+  end
+
+  test 'should get tag page and RSS' do
+    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 200
+    assert_not_nil assigns(:tag)
+    assert_not_nil assigns(:category)
+    assert_not_nil assigns(:posts)
+
+    sign_in users(:standard_user)
+    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 200
+    assert_not_nil assigns(:tag)
+    assert_not_nil assigns(:category)
+    assert_not_nil assigns(:posts)
+  end
+
+  test 'should get tag RSS feed' do
+    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }
+    assert_response 200
+    assert_not_nil assigns(:tag)
+    assert_not_nil assigns(:category)
+    assert_not_nil assigns(:posts)
+
+    sign_in users(:standard_user)
+    get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }
+    assert_response 200
+    assert_not_nil assigns(:tag)
+    assert_not_nil assigns(:category)
+    assert_not_nil assigns(:posts)
+  end
+
+  test 'should deny edit to anonymous user' do
+    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 302
+    assert_redirected_to new_user_session_path
+  end
+
+  test 'should deny edit to unprivileged user' do
+    sign_in users(:standard_user)
+    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 401
+  end
+
+  test 'should get edit' do
+    sign_in users(:deleter)
+    get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
+    assert_response 200
+    assert_not_nil assigns(:tag)
+    assert_not_nil assigns(:category)
+  end
+
+  test 'should deny update to anonymous user' do
+    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
+                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
+    assert_response 302
+    assert_redirected_to new_user_session_path
+  end
+
+  test 'should deny update to unprivileged user' do
+    sign_in users(:standard_user)
+    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
+                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
+    assert_response 401
+  end
+
+  test 'should update tag' do
+    sign_in users(:deleter)
+    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
+                             tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
+    assert_response 302
+    assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:topic).id)
+    assert_not_nil assigns(:tag)
+    assert_equal tags(:discussion).id, assigns(:tag).parent_id
+    assert_equal 'things', assigns(:tag).excerpt
+  end
+
+  test 'should prevent a tag being its own parent' do
+    sign_in users(:deleter)
+    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
+                             tag: { parent_id: tags(:topic).id, excerpt: 'things' } }
+    assert_response 400
+    assert_not_nil assigns(:tag)
+    assert_equal ['A tag cannot be its own parent.'], assigns(:tag).errors.full_messages
+  end
+
+  test 'should prevent hierarchical loops' do
+    sign_in users(:deleter)
+    patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
+                             tag: { parent_id: tags(:child).id, excerpt: 'things' } }
+    assert_response 400
+    assert_not_nil assigns(:tag)
+    assert_equal ["The #{tags(:child).name} tag is already a child of this tag."], assigns(:tag).errors.full_messages
+  end
 end
diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml
index d35e8f1c03e2499496f4668a33f426d6933f31c5..de28397edc0b3dff9cb435ab40e5e420264e5770 100644
--- a/test/fixtures/tags.yml
+++ b/test/fixtures/tags.yml
@@ -27,3 +27,14 @@ status-completed:
   name: status-completed
   community: sample
   tag_set: meta
+
+topic:
+  name: topic-tag
+  community: sample
+  tag_set: main
+
+child:
+  name: child-tag
+  community: sample
+  tag_set: main
+  parent: topic