From 71dc1afd6ebfc16f26dae06d3cb629187b32b16e Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 12 Jan 2025 21:52:19 +0300 Subject: [PATCH 01/79] renamed check_posts_permissions to get_accessible_posts to match its function & added unit test for it --- app/helpers/search_helper.rb | 8 ++++---- test/helpers/search_helper_test.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index fe21b7d35..d81c25311 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,12 +1,12 @@ module SearchHelper - def check_posts_permissions - (current_user&.is_moderator || current_user&.is_admin ? Post : Post.undeleted) + # @param user [User] user to check + def get_accessible_posts(user) + (user&.is_moderator || user&.is_admin ? Post : Post.undeleted) .qa_only.list_includes end def search_posts - posts = check_posts_permissions - + posts = get_accessible_posts(current_user) qualifiers = params_to_qualifiers search_string = params[:search] diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index a82e92f6f..f09395e48 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -35,4 +35,22 @@ class SearchHelperTest < ActionView::TestCase assert_equal expect, date_value_sql(input) end end + + test 'get_accessible_posts should correctly check access' do + admin_user = users(:admin) + mod_user = users(:moderator) + standard_user = users(:standard_user) + + admin_posts = get_accessible_posts(admin_user) + mod_posts = get_accessible_posts(mod_user) + user_posts = get_accessible_posts(standard_user) + + can_admin_get_deleted_posts = admin_posts.any?(&:deleted) + can_mod_get_deleted_posts = mod_posts.any?(&:deleted) + can_user_get_deleted_posts = user_posts.any?(&:deleted) + + assert_equal can_admin_get_deleted_posts, true + assert_equal can_mod_get_deleted_posts, true + assert_equal can_user_get_deleted_posts, false + end end From 6e75e6359f9cd5a1df1c464ef9706ed82ef3730a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 12 Jan 2025 23:44:54 +0300 Subject: [PATCH 02/79] renamed get_accessible_posts to accessible_posts_for (per @ArtOfCode-'s note) as 'get' prefix is not idiomatic --- app/helpers/search_helper.rb | 4 ++-- test/helpers/search_helper_test.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index d81c25311..a5a062482 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,12 +1,12 @@ module SearchHelper # @param user [User] user to check - def get_accessible_posts(user) + def accessible_posts_for(user) (user&.is_moderator || user&.is_admin ? Post : Post.undeleted) .qa_only.list_includes end def search_posts - posts = get_accessible_posts(current_user) + posts = accessible_posts_for(current_user) qualifiers = params_to_qualifiers search_string = params[:search] diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index f09395e48..37cd5f4e7 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -36,14 +36,14 @@ class SearchHelperTest < ActionView::TestCase end end - test 'get_accessible_posts should correctly check access' do + test 'accessible_posts_for should correctly check access' do admin_user = users(:admin) mod_user = users(:moderator) standard_user = users(:standard_user) - admin_posts = get_accessible_posts(admin_user) - mod_posts = get_accessible_posts(mod_user) - user_posts = get_accessible_posts(standard_user) + admin_posts = accessible_posts_for(admin_user) + mod_posts = accessible_posts_for(mod_user) + user_posts = accessible_posts_for(standard_user) can_admin_get_deleted_posts = admin_posts.any?(&:deleted) can_mod_get_deleted_posts = mod_posts.any?(&:deleted) From 914375d2fae54ba58b7bdfe8694ef8f684fcd60d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 12 Jan 2025 23:59:06 +0300 Subject: [PATCH 03/79] parametrized user for search_posts & qualifiers_to_sql methods of SearchHelper --- app/controllers/categories_controller.rb | 2 +- app/controllers/search_controller.rb | 2 +- app/helpers/search_helper.rb | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index c319160f3..f32c76386 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -194,7 +194,7 @@ def set_list_posts end end - @posts = helpers.qualifiers_to_sql(filter_qualifiers, @posts) + @posts = helpers.qualifiers_to_sql(filter_qualifiers, @posts, current_user) @filtered = filter_qualifiers.any? @posts = @posts.paginate(page: params[:page], per_page: 50).order(sort_param) end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 202a3636b..cdc3fb139 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,6 +1,6 @@ class SearchController < ApplicationController def search - @posts, @qualifiers = helpers.search_posts + @posts, @qualifiers = helpers.search_posts(current_user) @signed_out_me = @qualifiers.any? { |q| q[:param] == :user && q[:user_id].nil? } diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a5a062482..546001c03 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -5,8 +5,8 @@ def accessible_posts_for(user) .qa_only.list_includes end - def search_posts - posts = accessible_posts_for(current_user) + def search_posts(user) + posts = accessible_posts_for(user) qualifiers = params_to_qualifiers search_string = params[:search] @@ -17,7 +17,7 @@ def search_posts search_string = search_data[:search] end - posts = qualifiers_to_sql(qualifiers, posts) + posts = qualifiers_to_sql(qualifiers, posts, user) posts = posts.paginate(page: params[:page], per_page: 25) posts = if search_string.present? @@ -187,8 +187,8 @@ def parse_qualifier_strings(qualifiers) # Consider partitioning and telling the user which filters were invalid end - def qualifiers_to_sql(qualifiers, query) - trust_level = current_user&.trust_level || 0 + def qualifiers_to_sql(qualifiers, query, user) + trust_level = user&.trust_level || 0 allowed_categories = Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) query = query.where(category_id: allowed_categories) From c06d4e9a50b88dcbda8aaeba63b33719edd28841 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 02:14:03 +0300 Subject: [PATCH 04/79] added unit test for :user search qualifier --- test/helpers/search_helper_test.rb | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 37cd5f4e7..220dc04ad 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -49,8 +49,24 @@ class SearchHelperTest < ActionView::TestCase can_mod_get_deleted_posts = mod_posts.any?(&:deleted) can_user_get_deleted_posts = user_posts.any?(&:deleted) - assert_equal can_admin_get_deleted_posts, true - assert_equal can_mod_get_deleted_posts, true - assert_equal can_user_get_deleted_posts, false + assert can_admin_get_deleted_posts + assert can_mod_get_deleted_posts + assert_not can_user_get_deleted_posts + end + + test 'qualifiers_to_sql should correctly narrow by :user qualifier' do + standard_user = users(:standard_user) + editor_user = users(:editor) + + posts_query = accessible_posts_for(standard_user) + + eq_editor = [{ param: :user, operator: '=', user_id: editor_user.id }] + qualified_query = qualifiers_to_sql(eq_editor, posts_query, standard_user) + + is_owner_editor = qualified_query.to_a.all? { |p| p.user.id == editor_user.id } + + assert_not_equal posts_query.size, qualified_query.size + assert_not_equal qualified_query.size, 0 + assert is_owner_editor end end From f7a048e92b711b73f0009dab2578830caf55f303 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 02:29:51 +0300 Subject: [PATCH 05/79] added unit test for :status search qualifier --- test/helpers/search_helper_test.rb | 33 ++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 220dc04ad..6ee591b56 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -59,14 +59,35 @@ class SearchHelperTest < ActionView::TestCase editor_user = users(:editor) posts_query = accessible_posts_for(standard_user) - eq_editor = [{ param: :user, operator: '=', user_id: editor_user.id }] - qualified_query = qualifiers_to_sql(eq_editor, posts_query, standard_user) + editor_query = qualifiers_to_sql(eq_editor, posts_query, standard_user) + + only_editor_posts = editor_query.to_a.all? { |p| p.user.id == editor_user.id } + + assert_not_equal posts_query.size, editor_query.size + assert_not_equal editor_query.size, 0 + assert only_editor_posts + end + + test 'qualifiers_to_sql should correctly narrow by :status qualifier' do + standard_user = users(:standard_user) + + posts_query = accessible_posts_for(standard_user) + eq_open = [{ param: :status, value: 'open' }] + eq_closed = [{ param: :status, value: 'closed' }] + + open_query = qualifiers_to_sql(eq_open, posts_query, standard_user) + closed_query = qualifiers_to_sql(eq_closed, posts_query, standard_user) + + only_open_posts = open_query.to_a.none?(&:closed) + only_closed_posts = closed_query.to_a.all?(&:closed) - is_owner_editor = qualified_query.to_a.all? { |p| p.user.id == editor_user.id } + assert_not_equal posts_query.size, open_query.size + assert_not_equal open_query.size, 0 + assert only_open_posts - assert_not_equal posts_query.size, qualified_query.size - assert_not_equal qualified_query.size, 0 - assert is_owner_editor + assert_not_equal posts_query.size, closed_query.size + assert_not_equal closed_query.size, 0 + assert only_closed_posts end end From 78a165f76507a763bbe604492ba3a844a6fcab0e Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 02:47:46 +0300 Subject: [PATCH 06/79] added unit test for :score search qualifier --- test/fixtures/posts.yml | 13 ++++++ test/helpers/search_helper_test.rb | 65 +++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml index c3597fb96..a5f564c5e 100644 --- a/test/fixtures/posts.yml +++ b/test/fixtures/posts.yml @@ -306,6 +306,19 @@ deleted_answer: upvote_count: 0 downvote_count: 0 +good_answer: + post_type: answer + body: A3GA - This is the seventh answer to question number 1 (Q1). It has a good score. Posted by standard_user. + body_markdown: A7 - This is the seventh answer to question number 1 (Q1). It has a good score. Posted by standard_user. + score: 0.6 + parent: question_one + user: standard_user + community: sample + category: main + license: cc_by_sa + upvote_count: 1 + downvote_count: 0 + policy_doc: post_type: policy_doc body: PD - This is a policy document called "Terms of Service", or "tos" for short. diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 6ee591b56..dc3de978d 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -37,17 +37,17 @@ class SearchHelperTest < ActionView::TestCase end test 'accessible_posts_for should correctly check access' do - admin_user = users(:admin) + adm_user = users(:admin) mod_user = users(:moderator) - standard_user = users(:standard_user) + std_user = users(:standard_user) - admin_posts = accessible_posts_for(admin_user) + adm_posts = accessible_posts_for(adm_user) mod_posts = accessible_posts_for(mod_user) - user_posts = accessible_posts_for(standard_user) + std_posts = accessible_posts_for(std_user) - can_admin_get_deleted_posts = admin_posts.any?(&:deleted) + can_admin_get_deleted_posts = adm_posts.any?(&:deleted) can_mod_get_deleted_posts = mod_posts.any?(&:deleted) - can_user_get_deleted_posts = user_posts.any?(&:deleted) + can_user_get_deleted_posts = std_posts.any?(&:deleted) assert can_admin_get_deleted_posts assert can_mod_get_deleted_posts @@ -55,29 +55,58 @@ class SearchHelperTest < ActionView::TestCase end test 'qualifiers_to_sql should correctly narrow by :user qualifier' do - standard_user = users(:standard_user) - editor_user = users(:editor) + std_user = users(:standard_user) + edt_user = users(:editor) - posts_query = accessible_posts_for(standard_user) - eq_editor = [{ param: :user, operator: '=', user_id: editor_user.id }] - editor_query = qualifiers_to_sql(eq_editor, posts_query, standard_user) + posts_query = accessible_posts_for(std_user) + eq_editor = [{ param: :user, operator: '=', user_id: edt_user.id }] + edt_query = qualifiers_to_sql(eq_editor, posts_query, std_user) - only_editor_posts = editor_query.to_a.all? { |p| p.user.id == editor_user.id } + only_editor_posts = edt_query.to_a.all? { |p| p.user.id == edt_user.id } - assert_not_equal posts_query.size, editor_query.size - assert_not_equal editor_query.size, 0 + assert_not_equal posts_query.size, edt_query.size + assert_not_equal edt_query.size, 0 assert only_editor_posts end + test 'qualifiers_to_sql should correctly narrow by :score qualifier' do + std_user = users(:standard_user) + + posts_query = accessible_posts_for(std_user) + bad_post = [{ param: :score, operator: '<', value: 0.5 }] + good_post = [{ param: :score, operator: '>', value: 0.5 }] + neut_post = [{ param: :score, operator: '=', value: 0.5 }] + + bad_posts_query = qualifiers_to_sql(bad_post, posts_query, std_user) + good_posts_query = qualifiers_to_sql(good_post, posts_query, std_user) + neut_posts_query = qualifiers_to_sql(neut_post, posts_query, std_user) + + only_bad_posts = bad_posts_query.to_a.all? { |p| p.score < 0.5 } + only_good_posts = good_posts_query.to_a.all? { |p| p.score > 0.5 } + only_neut_posts = neut_posts_query.to_a.all? { |p| p.score.to_d == 0.5.to_d } + + assert_not_equal posts_query.size, bad_posts_query.size + assert_not_equal bad_posts_query.size, 0 + assert only_bad_posts + + assert_not_equal posts_query.size, good_posts_query.size + assert_not_equal good_posts_query.size, 0 + assert only_good_posts + + assert_not_equal posts_query.size, neut_posts_query.size + assert_not_equal neut_posts_query.size, 0 + assert only_neut_posts + end + test 'qualifiers_to_sql should correctly narrow by :status qualifier' do - standard_user = users(:standard_user) + std_user = users(:standard_user) - posts_query = accessible_posts_for(standard_user) + posts_query = accessible_posts_for(std_user) eq_open = [{ param: :status, value: 'open' }] eq_closed = [{ param: :status, value: 'closed' }] - open_query = qualifiers_to_sql(eq_open, posts_query, standard_user) - closed_query = qualifiers_to_sql(eq_closed, posts_query, standard_user) + open_query = qualifiers_to_sql(eq_open, posts_query, std_user) + closed_query = qualifiers_to_sql(eq_closed, posts_query, std_user) only_open_posts = open_query.to_a.none?(&:closed) only_closed_posts = closed_query.to_a.all?(&:closed) From e4f7ffca536e7bb6b11eb44cf8fcd51181b7615c Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 03:01:59 +0300 Subject: [PATCH 07/79] added unit test for :upvotes qualifier --- test/helpers/search_helper_test.rb | 34 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index dc3de978d..4bfd66087 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -59,8 +59,8 @@ class SearchHelperTest < ActionView::TestCase edt_user = users(:editor) posts_query = accessible_posts_for(std_user) - eq_editor = [{ param: :user, operator: '=', user_id: edt_user.id }] - edt_query = qualifiers_to_sql(eq_editor, posts_query, std_user) + edt_post = [{ param: :user, operator: '=', user_id: edt_user.id }] + edt_query = qualifiers_to_sql(edt_post, posts_query, std_user) only_editor_posts = edt_query.to_a.all? { |p| p.user.id == edt_user.id } @@ -102,11 +102,11 @@ class SearchHelperTest < ActionView::TestCase std_user = users(:standard_user) posts_query = accessible_posts_for(std_user) - eq_open = [{ param: :status, value: 'open' }] - eq_closed = [{ param: :status, value: 'closed' }] + open_post = [{ param: :status, value: 'open' }] + closed_post = [{ param: :status, value: 'closed' }] - open_query = qualifiers_to_sql(eq_open, posts_query, std_user) - closed_query = qualifiers_to_sql(eq_closed, posts_query, std_user) + open_query = qualifiers_to_sql(open_post, posts_query, std_user) + closed_query = qualifiers_to_sql(closed_post, posts_query, std_user) only_open_posts = open_query.to_a.none?(&:closed) only_closed_posts = closed_query.to_a.all?(&:closed) @@ -119,4 +119,26 @@ class SearchHelperTest < ActionView::TestCase assert_not_equal closed_query.size, 0 assert only_closed_posts end + + test 'qualifiers_to_sql should correctly narrow by :upvotes qualifier' do + std_user = users(:standard_user) + + posts_query = accessible_posts_for(std_user) + upvoted_post = [{ param: :upvotes, operator: '>', value: 0 }] + neutral_post = [{ param: :upvotes, operator: '=', value: 0 }] + + upvoted_query = qualifiers_to_sql(upvoted_post, posts_query, std_user) + neutral_query = qualifiers_to_sql(neutral_post, posts_query, std_user) + + only_upvoted_posts = upvoted_query.to_a.all? { |p| p[:upvote_count].positive? } + only_neutral_posts = neutral_query.to_a.all? { |p| p[:upvote_count].zero? } + + assert_not_equal posts_query.size, upvoted_query.size + assert_not_equal upvoted_query.size, 0 + assert only_upvoted_posts + + assert_not_equal posts_query.size, neutral_query.size + assert_not_equal neutral_query.size, 0 + assert only_neutral_posts + end end From 46851fd5a434dce056fa10bf1130f06f4ef04810 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 03:04:43 +0300 Subject: [PATCH 08/79] added unit test for :downvotes qualifier --- test/helpers/search_helper_test.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 4bfd66087..ce031bad8 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -141,4 +141,26 @@ class SearchHelperTest < ActionView::TestCase assert_not_equal neutral_query.size, 0 assert only_neutral_posts end + + test 'qualifiers_to_sql should correctly narrow by :downvotes qualifier' do + std_user = users(:standard_user) + + posts_query = accessible_posts_for(std_user) + downvoted_post = [{ param: :downvotes, operator: '>', value: 0 }] + neutral_post = [{ param: :downvotes, operator: '=', value: 0 }] + + downvoted_query = qualifiers_to_sql(downvoted_post, posts_query, std_user) + neutral_query = qualifiers_to_sql(neutral_post, posts_query, std_user) + + only_downvoted_posts = downvoted_query.to_a.all? { |p| p[:downvote_count].positive? } + only_neutral_posts = neutral_query.to_a.all? { |p| p[:downvote_count].zero? } + + assert_not_equal posts_query.size, downvoted_query.size + assert_not_equal downvoted_query.size, 0 + assert only_downvoted_posts + + assert_not_equal posts_query.size, neutral_query.size + assert_not_equal neutral_query.size, 0 + assert only_neutral_posts + end end From 78b965712490fcc52ba6eeb92b3e12d56064cc5e Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 03:20:32 +0300 Subject: [PATCH 09/79] added unit test for :net_votes qualifier --- test/fixtures/posts.yml | 13 +++++++++++++ test/helpers/search_helper_test.rb | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml index a5f564c5e..ec8738541 100644 --- a/test/fixtures/posts.yml +++ b/test/fixtures/posts.yml @@ -319,6 +319,19 @@ good_answer: upvote_count: 1 downvote_count: 0 +divisive_answer: + post_type: answer + body: A3DA - This is the eighth answer to question number 1 (Q1). It has divisive votes. Posted by standard_user. + body_markdown: A8 - This is the eighth answer to question number 1 (Q1). It has divisive votes. Posted by standard_user. + score: 0.6 + parent: question_one + user: standard_user + community: sample + category: main + license: cc_by_sa + upvote_count: 4 + downvote_count: 2 + policy_doc: post_type: policy_doc body: PD - This is a policy document called "Terms of Service", or "tos" for short. diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index ce031bad8..b0b253589 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -163,4 +163,21 @@ class SearchHelperTest < ActionView::TestCase assert_not_equal neutral_query.size, 0 assert only_neutral_posts end + + test 'qualifiers_to_sql should correctly narrow by :net_votes qualifier' do + std_user = users(:standard_user) + + posts_query = accessible_posts_for(std_user) + divisive_post = [{ param: :net_votes, operator: '=', value: 2 }] + + divisive_query = qualifiers_to_sql(divisive_post, posts_query, std_user) + + only_divisive_posts = divisive_query.to_a.all? do |p| + (p[:upvote_count] - p[:downvote_count]) == 2 + end + + assert_not_equal posts_query.size, divisive_query.size + assert_not_equal divisive_query.size, 0 + assert only_divisive_posts + end end From 8f0eb44266d41cae466fb9cf23d20102124b6d58 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 11:32:09 +0300 Subject: [PATCH 10/79] moved category access check into SearchHelper#accessible_categories_for method --- app/helpers/search_helper.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 546001c03..ab092fd74 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,4 +1,10 @@ module SearchHelper + # @param user [User] user to check + def accessible_categories_for(user) + trust_level = user&.trust_level || 0 + Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) + end + # @param user [User] user to check def accessible_posts_for(user) (user&.is_moderator || user&.is_admin ? Post : Post.undeleted) @@ -188,9 +194,8 @@ def parse_qualifier_strings(qualifiers) end def qualifiers_to_sql(qualifiers, query, user) - trust_level = user&.trust_level || 0 - allowed_categories = Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) - query = query.where(category_id: allowed_categories) + categories = accessible_categories_for(user) + query = query.where(category_id: categories) qualifiers.each do |qualifier| # rubocop:disable Metrics/BlockLength case qualifier[:param] From ae8e5f1b14064882cccb88cdfcc281fa0658657f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 11:36:52 +0300 Subject: [PATCH 11/79] added User#mod_or_admin? QoL method for access checks --- app/helpers/search_helper.rb | 3 +-- app/models/user.rb | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ab092fd74..84ff0f471 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -7,8 +7,7 @@ def accessible_categories_for(user) # @param user [User] user to check def accessible_posts_for(user) - (user&.is_moderator || user&.is_admin ? Post : Post.undeleted) - .qa_only.list_includes + (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes end def search_posts(user) diff --git a/app/models/user.rb b/app/models/user.rb index 46a27df1a..4a248beeb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -138,6 +138,10 @@ def is_admin is_global_admin || community_user&.is_admin || false end + def mod_or_admin? + is_admin || is_moderator + end + # Used by network profile: does this user have a profile on that other comm? def has_profile_on(community_id) cu = community_users.where(community_id: community_id).first From 23f841d30ce8d1f581704398f1112867ff39210d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 11:53:30 +0300 Subject: [PATCH 12/79] mods or admins should be able to access all categories when searching --- app/helpers/search_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 84ff0f471..91b512cac 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,6 +1,10 @@ module SearchHelper # @param user [User] user to check def accessible_categories_for(user) + if user&.mod_or_admin? + return Category.all + end + trust_level = user&.trust_level || 0 Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) end From cdc5aea382484c48128824da2d6ef7f023b0d139 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 13 Jan 2025 11:54:29 +0300 Subject: [PATCH 13/79] added unit test for :category qualifier --- test/helpers/search_helper_test.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index b0b253589..5dd8348f7 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -54,6 +54,32 @@ class SearchHelperTest < ActionView::TestCase assert_not can_user_get_deleted_posts end + test 'qualifiers_to_sql should correctly narrow by :category qualifier' do + main = categories(:main) + admin_only = categories(:admin_only) + + std_user = users(:standard_user) + adm_user = users(:admin) + + posts_query_std = accessible_posts_for(std_user) + posts_query_adm = accessible_posts_for(adm_user) + + std_post = [{ param: :category, operator: '=', category_id: main.id }] + adm_post = [{ param: :category, operator: '=', category_id: admin_only.id }] + + std_posts_query_standard = qualifiers_to_sql(std_post, posts_query_std, std_user) + adm_posts_query_standard = qualifiers_to_sql(adm_post, posts_query_std, std_user) + adm_posts_query_admin = qualifiers_to_sql(adm_post, posts_query_adm, adm_user) + + assert_not_equal posts_query_std.size, std_posts_query_standard.size + assert_not_equal std_posts_query_standard.size, 0 + + assert_not_equal posts_query_adm.size, adm_posts_query_admin.size + assert_not_equal adm_posts_query_admin.size, 0 + + assert_equal adm_posts_query_standard.size, 0 + end + test 'qualifiers_to_sql should correctly narrow by :user qualifier' do std_user = users(:standard_user) edt_user = users(:editor) From 08cf18f78dbffcd1a15264b29fbdf965a1dcfdff Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 9 May 2025 00:15:43 +0300 Subject: [PATCH 14/79] added storage directory to gitignore to prevent accidental commits of local files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d665bfb07..f4cf5397e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,10 +38,13 @@ test/reports TODO.md +# configuration files config/storage.yml config/email.yml config/database.yml +storage + deploy # Ignore master key for decrypting credentials and more. From 89c0526d2b8934a363b488eb3bf47dbb0fccfc04 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 22:26:06 +0300 Subject: [PATCH 15/79] added YARD for SearchHelper#search_posts (prevents needing to solve a nasty MC with develop) --- app/helpers/search_helper.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 3ceeede4e..0857ea7c1 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -14,6 +14,16 @@ def accessible_posts_for(user) (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes end + ## + # Search & sort a default posts list based on parameters in the current request. + # + # Generates initial post list using {Post#qa_only}, including deleted posts for mods and admins. Takes search string + # from params[:search], applies any qualifiers, and searches post bodies for the remaining term(s). + # + # Search uses MySQL fulltext search in boolean mode which is what provides advanced search syntax (excluding + # qualifiers) - see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. + # + # @return [ActiveRecord::Relation] def search_posts(user) posts = accessible_posts_for(user) qualifiers = params_to_qualifiers From 57cd00d4e1b5b1c6a6c188dcde6468e5b74ca22f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:12:59 +0300 Subject: [PATCH 16/79] made SearchHelper#accessible_categories_for a class method on Category [accessible_to] --- Gemfile.lock | 4 +++- app/helpers/search_helper.rb | 12 +----------- app/models/category.rb | 12 ++++++++++++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ca34dc696..0f4bdb291 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,6 +203,7 @@ GEM mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.16.3) minitest-ci (3.4.0) minitest (>= 5.0.6) @@ -217,7 +218,8 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.7.3) - nokogiri (1.18.4-x86_64-linux-gnu) + nokogiri (1.18.8) + mini_portile2 (~> 2.8.2) racc (~> 1.4) omniauth (2.1.0) hashie (>= 3.4.6) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 0857ea7c1..4e8b8ff23 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,14 +1,4 @@ module SearchHelper - # @param user [User] user to check - def accessible_categories_for(user) - if user&.mod_or_admin? - return Category.all - end - - trust_level = user&.trust_level || 0 - Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) - end - # @param user [User] user to check def accessible_posts_for(user) (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes @@ -209,7 +199,7 @@ def parse_qualifier_strings(qualifiers) end def qualifiers_to_sql(qualifiers, query, user) - categories = accessible_categories_for(user) + categories = Category.accessible_to(user) query = query.where(category_id: categories) qualifiers.each do |qualifier| # rubocop:disable Metrics/BlockLength diff --git a/app/models/category.rb b/app/models/category.rb index f5951519c..ee9cf73b9 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -31,6 +31,18 @@ def update_activity(last_activity) RequestContext.redis.set("#{community_id}/#{id}/last_activity", last_activity) end + # Gets categories appropriately scoped for the user + # @param user [User] user to check + # @return [ActiveRecord::Relation] + def self.accessible_to(user) + if user&.mod_or_admin? + return Category.all + end + + trust_level = user&.trust_level || 0 + Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) + end + def self.by_lowercase_name(name) categories = Rails.cache.fetch 'categories/by_lowercase_name' do Category.all.to_h { |c| [c.name.downcase, c] } From be1887cacc10eb0c10ceebf6bb2a7c37e0a400f7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:24:01 +0300 Subject: [PATCH 17/79] made SearchHelper#accessible_posts_for a class method on Post [accessible_to] --- app/helpers/search_helper.rb | 7 +----- app/models/category.rb | 2 +- app/models/post.rb | 7 ++++++ test/helpers/search_helper_test.rb | 34 +++++++----------------------- test/models/post_test.rb | 18 ++++++++++++++++ 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 4e8b8ff23..b821e4db5 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,9 +1,4 @@ module SearchHelper - # @param user [User] user to check - def accessible_posts_for(user) - (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes - end - ## # Search & sort a default posts list based on parameters in the current request. # @@ -15,7 +10,7 @@ def accessible_posts_for(user) # # @return [ActiveRecord::Relation] def search_posts(user) - posts = accessible_posts_for(user) + posts = Post.accessible_to(user) qualifiers = params_to_qualifiers search_string = params[:search] diff --git a/app/models/category.rb b/app/models/category.rb index ee9cf73b9..863cd19b3 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -31,7 +31,7 @@ def update_activity(last_activity) RequestContext.redis.set("#{community_id}/#{id}/last_activity", last_activity) end - # Gets categories appropriately scoped for the user + # Gets categories appropriately scoped for a given user # @param user [User] user to check # @return [ActiveRecord::Relation] def self.accessible_to(user) diff --git a/app/models/post.rb b/app/models/post.rb index 9545bda5e..0c741bd09 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -57,6 +57,13 @@ class Post < ApplicationRecord after_save :update_category_activity, if: -> { post_type.has_category && !destroyed? } after_save :recalc_score + # Gets posts appropriately scoped for a given user + # @param user [User] user to check + # @return [ActiveRecord::Relation] + def self.accessible_to(user) + (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes + end + # @param term [String] the search term # @return [ActiveRecord::Relation] def self.search(term) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 5dd8348f7..8afa2d25a 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -36,24 +36,6 @@ class SearchHelperTest < ActionView::TestCase end end - test 'accessible_posts_for should correctly check access' do - adm_user = users(:admin) - mod_user = users(:moderator) - std_user = users(:standard_user) - - adm_posts = accessible_posts_for(adm_user) - mod_posts = accessible_posts_for(mod_user) - std_posts = accessible_posts_for(std_user) - - can_admin_get_deleted_posts = adm_posts.any?(&:deleted) - can_mod_get_deleted_posts = mod_posts.any?(&:deleted) - can_user_get_deleted_posts = std_posts.any?(&:deleted) - - assert can_admin_get_deleted_posts - assert can_mod_get_deleted_posts - assert_not can_user_get_deleted_posts - end - test 'qualifiers_to_sql should correctly narrow by :category qualifier' do main = categories(:main) admin_only = categories(:admin_only) @@ -61,8 +43,8 @@ class SearchHelperTest < ActionView::TestCase std_user = users(:standard_user) adm_user = users(:admin) - posts_query_std = accessible_posts_for(std_user) - posts_query_adm = accessible_posts_for(adm_user) + posts_query_std = Post.accessible_to(std_user) + posts_query_adm = Post.accessible_to(adm_user) std_post = [{ param: :category, operator: '=', category_id: main.id }] adm_post = [{ param: :category, operator: '=', category_id: admin_only.id }] @@ -84,7 +66,7 @@ class SearchHelperTest < ActionView::TestCase std_user = users(:standard_user) edt_user = users(:editor) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) edt_post = [{ param: :user, operator: '=', user_id: edt_user.id }] edt_query = qualifiers_to_sql(edt_post, posts_query, std_user) @@ -98,7 +80,7 @@ class SearchHelperTest < ActionView::TestCase test 'qualifiers_to_sql should correctly narrow by :score qualifier' do std_user = users(:standard_user) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) bad_post = [{ param: :score, operator: '<', value: 0.5 }] good_post = [{ param: :score, operator: '>', value: 0.5 }] neut_post = [{ param: :score, operator: '=', value: 0.5 }] @@ -127,7 +109,7 @@ class SearchHelperTest < ActionView::TestCase test 'qualifiers_to_sql should correctly narrow by :status qualifier' do std_user = users(:standard_user) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) open_post = [{ param: :status, value: 'open' }] closed_post = [{ param: :status, value: 'closed' }] @@ -149,7 +131,7 @@ class SearchHelperTest < ActionView::TestCase test 'qualifiers_to_sql should correctly narrow by :upvotes qualifier' do std_user = users(:standard_user) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) upvoted_post = [{ param: :upvotes, operator: '>', value: 0 }] neutral_post = [{ param: :upvotes, operator: '=', value: 0 }] @@ -171,7 +153,7 @@ class SearchHelperTest < ActionView::TestCase test 'qualifiers_to_sql should correctly narrow by :downvotes qualifier' do std_user = users(:standard_user) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) downvoted_post = [{ param: :downvotes, operator: '>', value: 0 }] neutral_post = [{ param: :downvotes, operator: '=', value: 0 }] @@ -193,7 +175,7 @@ class SearchHelperTest < ActionView::TestCase test 'qualifiers_to_sql should correctly narrow by :net_votes qualifier' do std_user = users(:standard_user) - posts_query = accessible_posts_for(std_user) + posts_query = Post.accessible_to(std_user) divisive_post = [{ param: :net_votes, operator: '=', value: 2 }] divisive_query = qualifiers_to_sql(divisive_post, posts_query, std_user) diff --git a/test/models/post_test.rb b/test/models/post_test.rb index e4b0105bc..06f95259e 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -47,6 +47,24 @@ class PostTest < ActiveSupport::TestCase end end + test 'accessible_to should correctly check user access' do + adm_user = users(:admin) + mod_user = users(:moderator) + std_user = users(:standard_user) + + adm_posts = Post.accessible_to(adm_user) + mod_posts = Post.accessible_to(mod_user) + std_posts = Post.accessible_to(std_user) + + can_admin_get_deleted_posts = adm_posts.any?(&:deleted) + can_mod_get_deleted_posts = mod_posts.any?(&:deleted) + can_user_get_deleted_posts = std_posts.any?(&:deleted) + + assert can_admin_get_deleted_posts + assert can_mod_get_deleted_posts + assert_not can_user_get_deleted_posts + end + test 'should allow specified post types in a category' do category = categories(:main) post_type = post_types(:question) From 8e21b7341da374653fc4185adf58c78717d24d8d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:40:01 +0300 Subject: [PATCH 18/79] moved mod_or_admin? around to avoid having to solve yet another nasty merge conflict --- app/models/user.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index f5dfbd15d..9418ecd23 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -142,6 +142,10 @@ def valid_websites_for user_websites.where.not(url: [nil, '']).order(position: :asc) end + def mod_or_admin? + is_admin || is_moderator + end + def ensure_websites pos = user_websites.size while pos < UserWebsite::MAX_ROWS @@ -158,10 +162,6 @@ def is_admin is_global_admin || community_user&.is_admin || false end - def mod_or_admin? - is_admin || is_moderator - end - # Used by network profile: does this user have a profile on that other comm? def has_profile_on(community_id) cu = community_users.where(community_id: community_id).first From 24b41b33571df8ab23f391d5009cf7ede0b64bd7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:42:26 +0300 Subject: [PATCH 19/79] accidentally committed Gemfile.lock update (one less merge conflict to go) --- Gemfile.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0f4bdb291..ca34dc696 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,7 +203,6 @@ GEM mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (5.16.3) minitest-ci (3.4.0) minitest (>= 5.0.6) @@ -218,8 +217,7 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.7.3) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) + nokogiri (1.18.4-x86_64-linux-gnu) racc (~> 1.4) omniauth (2.1.0) hashie (>= 3.4.6) From aa673e49b39b875d2deaee18c7771cc9aeff0aeb Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:46:00 +0300 Subject: [PATCH 20/79] moving YARD SearchHelper#qualifiers_to_sql from develop (avoiding the last nasty merge conflict) --- app/helpers/search_helper.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index b821e4db5..6a9c85848 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -193,6 +193,11 @@ def parse_qualifier_strings(qualifiers) # Consider partitioning and telling the user which filters were invalid end + ## + # Parses a qualifiers hash and applies it to an ActiveRecord query. + # @param qualifiers [Array Object}>] A qualifiers hash, as returned by other methods in this module. + # @param query [ActiveRecord::Relation] An ActiveRecord query to which to add conditions based on the qualifiers. + # @return [ActiveRecord::Relation] def qualifiers_to_sql(qualifiers, query, user) categories = Category.accessible_to(user) query = query.where(category_id: categories) From 3b07e00b77522e82297f686771be5f37b3f1eb71 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Fri, 11 Jul 2025 23:59:42 +0300 Subject: [PATCH 21/79] syncing with develop: User#mod_or_admin? is now User#at_least_moderator? --- app/models/category.rb | 2 +- app/models/post.rb | 2 +- app/models/user.rb | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/category.rb b/app/models/category.rb index 863cd19b3..f33ceb305 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -35,7 +35,7 @@ def update_activity(last_activity) # @param user [User] user to check # @return [ActiveRecord::Relation] def self.accessible_to(user) - if user&.mod_or_admin? + if user&.at_least_moderator? return Category.all end diff --git a/app/models/post.rb b/app/models/post.rb index 63d10149c..83cb277c5 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -67,7 +67,7 @@ class Post < ApplicationRecord # @param user [User] user to check # @return [ActiveRecord::Relation] def self.accessible_to(user) - (user&.mod_or_admin? ? Post : Post.undeleted).qa_only.list_includes + (user&.at_least_moderator? ? Post : Post.undeleted).qa_only.list_includes end # @param term [String] the search term diff --git a/app/models/user.rb b/app/models/user.rb index 5c7e94724..899f0d2b4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -217,10 +217,6 @@ def valid_websites_for user_websites.where.not(url: [nil, '']).order(position: :asc) end - def mod_or_admin? - is_admin || is_moderator - end - def ensure_websites pos = user_websites.size while pos < UserWebsite::MAX_ROWS From 12705257aa031b00cec634f68223065f4c836a30 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 00:27:00 +0300 Subject: [PATCH 22/79] let's setup Rubcop CI while we are at it --- .github/workflows/rubocop.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/rubocop.yml diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 000000000..ea21f0688 --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,28 @@ +name: Rubocop check + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + rubocop: + name: Rubocop checking + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup dependencies + run: | + sudo apt-get update -q + sudo apt-get install -yq libmagickwand-dev + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1.2 + bundler-cache: true + - run: bundle exec rubocop From ad961f8b0c3e70c946d784711c3264a6add9c503 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 00:45:49 +0300 Subject: [PATCH 23/79] uhm, do we really need rubocop-rake in GH Actions? --- .github/workflows/rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index ea21f0688..5ca8bad1c 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -25,4 +25,5 @@ jobs: with: ruby-version: 3.1.2 bundler-cache: true + - run: gem install rubocop-rake - run: bundle exec rubocop From 212ac9d00ca6f7593793e0245b0fdfb695db411a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 00:48:09 +0300 Subject: [PATCH 24/79] let's not shout, shall we (Rubocop GH Actions setup)? --- .github/workflows/rubocop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 5ca8bad1c..17bce048a 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -18,8 +18,8 @@ jobs: uses: actions/checkout@v3 - name: Setup dependencies run: | - sudo apt-get update -q - sudo apt-get install -yq libmagickwand-dev + sudo apt-get update -qq + sudo apt-get install -yqq libmagickwand-dev - name: Setup Ruby uses: ruby/setup-ruby@v1 with: From d9b78362337b81d9a5fea56e38de2f0e04809a6d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 00:53:55 +0300 Subject: [PATCH 25/79] installing rubocop-rake directly won't hurt --- .github/workflows/rubocop.yml | 1 - Gemfile | 1 + Gemfile.lock | 3 +++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 17bce048a..0e7ed3023 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -25,5 +25,4 @@ jobs: with: ruby-version: 3.1.2 bundler-cache: true - - run: gem install rubocop-rake - run: bundle exec rubocop diff --git a/Gemfile b/Gemfile index f68128ef3..22c6a0d94 100644 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,7 @@ gem 'whenever', '~> 1.0', require: false gem 'awesome_print', '~> 1.9' gem 'rubocop', '~> 1' gem 'rubocop-rails', '~> 2.15' +gem 'rubocop-rake', '~> 0.6.0' # MiniProfiler support, including stack traces & memory dumps, plus flamegraphs. gem 'flamegraph', '~> 0.9' diff --git a/Gemfile.lock b/Gemfile.lock index 6e2dc6b30..caa3996ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,6 +316,8 @@ GEM activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) ruby-progressbar (1.11.0) ruby-saml (1.18.0) nokogiri (>= 1.13.10) @@ -442,6 +444,7 @@ DEPENDENCIES rqrcode (~> 2.1) rubocop (~> 1) rubocop-rails (~> 2.15) + rubocop-rake (~> 0.6.0) ruby-progressbar (~> 1.11) sass-rails (~> 6.0) selenium-webdriver (~> 4.7) From 299bc09106a0f10aaf196620a7523a811fe524f7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 00:58:10 +0300 Subject: [PATCH 26/79] apparently, excluding vendor directory is a must for GitHub Actions --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index ae30f0306..8711cef12 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ AllCops: - 'scripts/**/*' - 'bin/**/*' - 'lib/namespaced_env_cache.rb' + - 'vendor/bundle/**/*' NewCops: enable SuggestExtensions: false From 6bbfe2b459afd443eca63291e8c5df5b53fe7ad4 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 01:17:12 +0300 Subject: [PATCH 27/79] let's see if Ruby 3.1 testing works --- .github/workflows/test.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..85b75e270 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Rails testing + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + ruby-3_1: + name: Ruby 3_1 testing + runs-on: ubuntu-latest + env: + RAILS_ENV: test + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup dependencies + run: | + sudo apt-get -qq update + sudo apt-get -yqq install libmariadb-dev libmagickwand-dev + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1.2 + bundler-cache: true + - name: Prepare for testing + run: | + cp config/database.sample.yml config/database.yml + cp config/storage.sample.yml config/storage.yml + bundle exec rails db:create + bundle exec rails db:schema:load + bundle exec rails db:migrate + bundle exec rails test:prepare + - run: bundle exec rails test From 11d998e8fcb9a6b61ed08fbf97b150bf547f32a8 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 01:22:27 +0300 Subject: [PATCH 28/79] let's make RAILS_ENV a workflow-level var --- .github/workflows/test.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85b75e270..298d290b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,13 +8,14 @@ on: branches: - develop +env: + RAILS_ENV: test + jobs: ruby-3_1: name: Ruby 3_1 testing runs-on: ubuntu-latest - env: - RAILS_ENV: test - + steps: - name: Checkout repo uses: actions/checkout@v3 From bb17e1cd250ee60fe6ac1dc804743981c8f83986 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 01:37:57 +0300 Subject: [PATCH 29/79] now let's add MYSQL to the testing action --- .github/workflows/test.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 298d290b6..528827cff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,9 +13,20 @@ env: jobs: ruby-3_1: - name: Ruby 3_1 testing + name: Ruby 3.1 testing runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: 'root' + MYSQL_DATABASE: 'qpixel_test' + ports: + 3306:3306 + options: --default-authentication-plugin=mysql_native_password + steps: - name: Checkout repo uses: actions/checkout@v3 @@ -26,7 +37,7 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.1.2 + ruby-version: 3.1 bundler-cache: true - name: Prepare for testing run: | From f7ee0205a053a91ec2817b766797619687494042 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 01:38:58 +0300 Subject: [PATCH 30/79] ports is an array, duh --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 528827cff..e243c9f66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: MYSQL_ROOT_PASSWORD: 'root' MYSQL_DATABASE: 'qpixel_test' ports: - 3306:3306 + - 3306:3306 options: --default-authentication-plugin=mysql_native_password steps: From 92a7fe25792881f5677702cd00a29fb455ef7dcd Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 02:00:04 +0300 Subject: [PATCH 31/79] welp, there's no way to set service container entrypoint options, let's remove for now --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e243c9f66..d509dfd01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,6 @@ jobs: MYSQL_DATABASE: 'qpixel_test' ports: - 3306:3306 - options: --default-authentication-plugin=mysql_native_password steps: - name: Checkout repo From 4e3f19ba137e00f1f1d44d5baeb83af518b15916 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 02:06:27 +0300 Subject: [PATCH 32/79] now add Rails for our testing workflow, won't work without it --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d509dfd01..4a47da8f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,8 @@ jobs: MYSQL_DATABASE: 'qpixel_test' ports: - 3306:3306 + redis: + image: redis:8.0 steps: - name: Checkout repo From bb453513fc317926a4a13b40dae7e0299e4dc7a1 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 02:10:56 +0300 Subject: [PATCH 33/79] expose Redis service container port 6379 --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a47da8f5..2d64a6b01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,8 @@ jobs: - 3306:3306 redis: image: redis:8.0 + ports: + - 6379:6379 steps: - name: Checkout repo From f95e58bd940208aff259dff3528fee387cd1ce10 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 02:22:03 +0300 Subject: [PATCH 34/79] add Ruby 3.2 testing via GH Actions matrix --- .github/workflows/test.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d64a6b01..de1efb3d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,10 +12,14 @@ env: RAILS_ENV: test jobs: - ruby-3_1: - name: Ruby 3.1 testing + tests: + name: General Ruby on Rails tests runs-on: ubuntu-latest + strategy: + matrix: + ruby_version: [3.1, 3.2] + services: mysql: image: mysql:8.0 @@ -40,7 +44,7 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 3.1 + ruby-version: ${{ matrix.ruby_version }} bundler-cache: true - name: Prepare for testing run: | From 02b634dd7dcbe7f75c73d3891a81a83d1482a709 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 02:49:46 +0300 Subject: [PATCH 35/79] testing system tests & screenshots --- .github/workflows/test.yml | 14 +++++++++++--- test/system/post_system_test.rb | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de1efb3d6..43f5d98e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,12 +13,13 @@ env: jobs: tests: - name: General Ruby on Rails tests + name: Ruby on Rails tests runs-on: ubuntu-latest strategy: matrix: ruby_version: [3.1, 3.2] + test_type: [test, test:system] services: mysql: @@ -40,7 +41,7 @@ jobs: - name: Setup dependencies run: | sudo apt-get -qq update - sudo apt-get -yqq install libmariadb-dev libmagickwand-dev + sudo apt-get -yqq install libmariadb-dev libmagickwand-dev firefox-esr - name: Setup Ruby uses: ruby/setup-ruby@v1 with: @@ -54,4 +55,11 @@ jobs: bundle exec rails db:schema:load bundle exec rails db:migrate bundle exec rails test:prepare - - run: bundle exec rails test + - run: bundle exec rails ${{ matrix.test_type }} + - name: Upload screenshots + if: ${{ matrix.test_type == 'test:system' }} + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.ruby_version }} + path: ~/tmp/screenshots + if-no-files-found: ignore diff --git a/test/system/post_system_test.rb b/test/system/post_system_test.rb index ddc6f31ed..7af9d062a 100644 --- a/test/system/post_system_test.rb +++ b/test/system/post_system_test.rb @@ -7,6 +7,11 @@ class PostSystemTest < ApplicationSystemTestCase # Create # ------------------------------------------------------- + test 'dummy test for screenshot uploading' do + visit root_url + take_screenshot + end + test 'Not-signed in user cannot create a post' do visit root_url click_on 'Create Post' From c8cf46425055fcdb8cc29785b62238814d750041 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 03:35:03 +0300 Subject: [PATCH 36/79] let's see if setup-firefox is any good --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43f5d98e5..a813dccd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,11 @@ jobs: - name: Setup dependencies run: | sudo apt-get -qq update - sudo apt-get -yqq install libmariadb-dev libmagickwand-dev firefox-esr + sudo apt-get -yqq install libmariadb-dev libmagickwand-dev + - name: Setup Firefox + if: ${{ matrix.test_type == 'test:system' }} + uses: browser-actions/setup-firefox@v1.5.4 + - run: firefox --version - name: Setup Ruby uses: ruby/setup-ruby@v1 with: From 772d2684baf3955652e9b09c84c53d580b04b5a9 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 03:39:53 +0300 Subject: [PATCH 37/79] minor testing workflow updates & temp check for where the heck are screenshots stored --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a813dccd8..7fbbe370a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,9 @@ jobs: - name: Setup Firefox if: ${{ matrix.test_type == 'test:system' }} uses: browser-actions/setup-firefox@v1.5.4 - - run: firefox --version + - name: Check Firefox setup + if: ${{ matrix.test_type == 'test:system' }} + run: firefox --version - name: Setup Ruby uses: ruby/setup-ruby@v1 with: @@ -60,6 +62,7 @@ jobs: bundle exec rails db:migrate bundle exec rails test:prepare - run: bundle exec rails ${{ matrix.test_type }} + - run: ls -la ~ - name: Upload screenshots if: ${{ matrix.test_type == 'test:system' }} uses: actions/upload-artifact@v4 From 2cae369fae318f70eb913875219ebf3dc54ed5a2 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 03:50:05 +0300 Subject: [PATCH 38/79] I hope this is the correct path for screenshot artifacts --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fbbe370a..afce2b1fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,11 +62,10 @@ jobs: bundle exec rails db:migrate bundle exec rails test:prepare - run: bundle exec rails ${{ matrix.test_type }} - - run: ls -la ~ - name: Upload screenshots if: ${{ matrix.test_type == 'test:system' }} uses: actions/upload-artifact@v4 with: name: screenshots-${{ matrix.ruby_version }} - path: ~/tmp/screenshots + path: tmp/screenshots if-no-files-found: ignore From 3bc884c41cc75f86c3689e4131892b65653f3bfb Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 03:55:46 +0300 Subject: [PATCH 39/79] final touch for system tests: remove the dummy screenshot test case --- test/system/post_system_test.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/system/post_system_test.rb b/test/system/post_system_test.rb index 7af9d062a..ddc6f31ed 100644 --- a/test/system/post_system_test.rb +++ b/test/system/post_system_test.rb @@ -7,11 +7,6 @@ class PostSystemTest < ApplicationSystemTestCase # Create # ------------------------------------------------------- - test 'dummy test for screenshot uploading' do - visit root_url - take_screenshot - end - test 'Not-signed in user cannot create a post' do visit root_url click_on 'Create Post' From dce725f6f2fe25884c209f4698b360f37cc1f692 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:03:32 +0300 Subject: [PATCH 40/79] renamed admin_only category fixture to disambiguate from high_trust --- test/fixtures/categories.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml index 4f103923a..44744686b 100644 --- a/test/fixtures/categories.yml +++ b/test/fixtures/categories.yml @@ -29,8 +29,8 @@ high_trust: admin_only: community: sample - name: High Trust - short_wiki: High Trust + name: Admin Only + short_wiki: Admin Only display_post_types: - <%= Question.post_type_id %> tag_set: main From 666702b1d51056dbda81fa4716fc2a97c426c371 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:09:18 +0300 Subject: [PATCH 41/79] no, search_posts does not return an ActiveRecord::Relation, but a 2-tuple now --- app/helpers/search_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a3f8d49fa..7a27810f6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -8,7 +8,7 @@ module SearchHelper # Search uses MySQL fulltext search in boolean mode which is what provides advanced search syntax (excluding # qualifiers) - see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. # - # @return [ActiveRecord::Relation] + # @return [[ActiveRecord::Relation, Array Object}>]] def search_posts(user) posts = Post.accessible_to(user) qualifiers = params_to_qualifiers From c2613d7961cacd742d395194adb339732373412c Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:16:37 +0300 Subject: [PATCH 42/79] simplified search_posts helper YARD --- app/helpers/search_helper.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 7a27810f6..4529ec2e3 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -2,12 +2,10 @@ module SearchHelper ## # Search & sort a default posts list based on parameters in the current request. # - # Generates initial post list using {Post#qa_only}, including deleted posts for mods and admins. Takes search string - # from params[:search], applies any qualifiers, and searches post bodies for the remaining term(s). - # - # Search uses MySQL fulltext search in boolean mode which is what provides advanced search syntax (excluding - # qualifiers) - see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. + # Search uses MySQL FTS in boolean mode which is what provides advanced search syntax (excluding qualifiers) + # see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. # + # @param user [User] user for search context # @return [[ActiveRecord::Relation, Array Object}>]] def search_posts(user) posts = Post.accessible_to(user) From 1fbd9be3698c6063502ab7ed9f23badb68bb858b Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:20:03 +0300 Subject: [PATCH 43/79] made search_string an explicit parameter of search_posts (so as it can be used outside param-less contexts) --- app/controllers/search_controller.rb | 2 +- app/helpers/search_helper.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index cdc3fb139..167506a66 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,6 +1,6 @@ class SearchController < ApplicationController def search - @posts, @qualifiers = helpers.search_posts(current_user) + @posts, @qualifiers = helpers.search_posts(current_user, params[:search]) @signed_out_me = @qualifiers.any? { |q| q[:param] == :user && q[:user_id].nil? } diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 4529ec2e3..0bf0e7d0b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -6,11 +6,11 @@ module SearchHelper # see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. # # @param user [User] user for search context + # @param search_string [String, nil] search query string, if any # @return [[ActiveRecord::Relation, Array Object}>]] - def search_posts(user) + def search_posts(user, search_string) posts = Post.accessible_to(user) qualifiers = params_to_qualifiers - search_string = params[:search] # Filter based on search string qualifiers if search_string.present? From 03d2865c4e8523e881125f6885b1dc1ddfa00c9e Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:40:40 +0300 Subject: [PATCH 44/79] switched search_posts & params_to_qualifiers to accept params directly --- app/controllers/categories_controller.rb | 2 +- app/controllers/search_controller.rb | 2 +- app/helpers/search_helper.rb | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index f32c76386..09661928a 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -163,7 +163,7 @@ def set_list_posts sort_param = sort_params[params[:sort]&.to_sym] || { last_activity: :desc } @posts = @category.posts.undeleted.where(post_type_id: @category.display_post_types) .includes(:post_type, :tags).list_includes - filter_qualifiers = helpers.params_to_qualifiers + filter_qualifiers = helpers.params_to_qualifiers(params) @active_filter = helpers.active_filter if filter_qualifiers.blank? && @active_filter[:name].blank? diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 167506a66..eb47d38c4 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,6 +1,6 @@ class SearchController < ApplicationController def search - @posts, @qualifiers = helpers.search_posts(current_user, params[:search]) + @posts, @qualifiers = helpers.search_posts(current_user, params) @signed_out_me = @qualifiers.any? { |q| q[:param] == :user && q[:user_id].nil? } diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 0bf0e7d0b..fde6e0c8c 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -6,16 +6,17 @@ module SearchHelper # see {MySQL manual 14.9.2}[https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html]. # # @param user [User] user for search context - # @param search_string [String, nil] search query string, if any + # @param params [ActionController::Parameters] search parameters # @return [[ActiveRecord::Relation, Array Object}>]] - def search_posts(user, search_string) + def search_posts(user, params) posts = Post.accessible_to(user) - qualifiers = params_to_qualifiers + qualifiers = params_to_qualifiers(params) + search_string = params[:search] # Filter based on search string qualifiers if search_string.present? search_data = parse_search(search_string) - qualifiers += parse_qualifier_strings search_data[:qualifiers] + qualifiers += parse_qualifier_strings(search_data[:qualifiers]) search_string = search_data[:search] end @@ -71,8 +72,9 @@ def active_filter ## # Retrieves parameters from +params+, validates their values, and adds them to a qualifiers hash. + # @param params [ActionController::Parameters] params to convert to qualifiers # @return [Array Object}>] - def params_to_qualifiers + def params_to_qualifiers(params) valid_value = { date: /^[\d.]+(?:s|m|h|d|w|mo|y)?$/, status: /any|open|closed/, From 6c15022d29bd03dc5e0744990adeebe9cb90673a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 18:44:50 +0300 Subject: [PATCH 45/79] added test for checking that search_posts never returns posts from categories not accessible to users --- test/helpers/search_helper_test.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb index 8afa2d25a..11f30c8f1 100644 --- a/test/helpers/search_helper_test.rb +++ b/test/helpers/search_helper_test.rb @@ -188,4 +188,15 @@ class SearchHelperTest < ActionView::TestCase assert_not_equal divisive_query.size, 0 assert only_divisive_posts end + + test 'search_posts should not show posts in categories that a user cannot view' do + std_user = users(:standard_user) + + params = ActionController::Parameters.new({ search: 'high trust' }) + posts, _qualifiers = search_posts(std_user, params) + + admin_category = categories(:admin_only) + + assert_not(posts.any? { |p| p.category.id == admin_category.id }) + end end From e4a6fc6066422df14e240a5a9d55221d095aa31a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:07:19 +0300 Subject: [PATCH 46/79] added missing staff user community user fixture --- test/fixtures/community_users.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fixtures/community_users.yml b/test/fixtures/community_users.yml index 4d7ac6963..f6ce2bd61 100644 --- a/test/fixtures/community_users.yml +++ b/test/fixtures/community_users.yml @@ -61,6 +61,13 @@ sample_global_admin: is_moderator: false reputation: 1 +sample_staff: + user: staff + community: sample + is_admin: false + is_moderator: false + reputation: 1 + sample_deleted_account: user: deleted_account community: sample From 5eb01f3b31f6574a2a0121ffb26df7fbc9341d7a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:15:27 +0300 Subject: [PATCH 47/79] added User#can_see_category? access control method --- app/models/user.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 899f0d2b4..1c1ce475a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -148,6 +148,13 @@ def can_reply_to?(thread) can_comment_on?(thread.post) && !thread.read_only? end + # Can the user see a given category at all? + # @param category [Category] category to check + # @return [Boolean] check result + def can_see_category?(category) + community_user&.trust_level&.>= category.min_view_trust_level + end + # Is the user allowed to see deleted posts? # @return [Boolean] check result def can_see_deleted_posts? From a10f0dacbe867494bb05335650c0652ec0f92ba7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:21:15 +0300 Subject: [PATCH 48/79] restored User#trust_level to prevent errors on users with no community_user --- app/models/user.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1c1ce475a..22e699496 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord validate :is_not_blocklisted validate :email_not_bad_pattern - delegate :trust_level, :reputation, :reputation=, :privilege?, :privilege, to: :community_user + delegate :reputation, :reputation=, :privilege?, :privilege, to: :community_user def self.list_includes includes(:posts, :avatar_attachment) @@ -51,6 +51,12 @@ def self.search(term) where('username LIKE ?', "%#{sanitize_sql_like(term)}%") end + # Safely gets the user's trust level even if they don't have a community user + # @return [Integer] user's trust level + def trust_level + community_user&.trust_level || 0 + end + # Is the user a new user? # @return [Boolean] check result def new? From 0f5f17f576a57ff2f5a49498e7f66ca59dcf44a7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:24:12 +0300 Subject: [PATCH 49/79] made User#can_see_category? more robust --- app/models/user.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 22e699496..5bc2ee21d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -158,7 +158,8 @@ def can_reply_to?(thread) # @param category [Category] category to check # @return [Boolean] check result def can_see_category?(category) - community_user&.trust_level&.>= category.min_view_trust_level + category_trust_level = category.min_view_trust_level || -1 + trust_level >= category_trust_level end # Is the user allowed to see deleted posts? From 47b9431067e3367497d043680ac898c35be0e06d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:33:41 +0300 Subject: [PATCH 50/79] added Category#public? access control method --- app/models/category.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/category.rb b/app/models/category.rb index f33ceb305..94298bdf0 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -15,6 +15,13 @@ class Category < ApplicationRecord validates :name, uniqueness: { scope: [:community_id], case_sensitive: false } + # Can anyone view the category (even if not logged in)? + # @return [Boolean] check result + def public? + trust_level = min_view_trust_level || -1 + trust_level <= 0 + end + def new_posts_for?(user) key = "#{community_id}/#{user.id}/#{id}/last_visit" Rails.cache.fetch key, expires_in: 5.minutes do From 0d5a104af14eb1a87f2a14c5c3d3f069969fa8ff Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:34:33 +0300 Subject: [PATCH 51/79] made CategoriesController#verify_view_access action callback more robust --- app/controllers/categories_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 09661928a..b026c3fbe 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -150,9 +150,7 @@ def category_params end def verify_view_access - unless (current_user&.trust_level || 0) >= (@category.min_view_trust_level || -1) - not_found - end + not_found unless @category.public? || current_user&.can_see_category?(@category) end def set_list_posts From da9fae5ab15946863890c27082c199cb50c79873 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:40:10 +0300 Subject: [PATCH 52/79] we don't even have trust level 6 --- test/fixtures/categories.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml index 44744686b..f44432b70 100644 --- a/test/fixtures/categories.yml +++ b/test/fixtures/categories.yml @@ -34,8 +34,8 @@ admin_only: display_post_types: - <%= Question.post_type_id %> tag_set: main - min_trust_level: 6 - min_view_trust_level: 6 + min_trust_level: 5 + min_view_trust_level: 5 license: cc_by_sa articles_only: From 12ccb1eddf78cc5b41f46393bb89b26848b209bc Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:43:41 +0300 Subject: [PATCH 53/79] added test for checking that categories with high trust level to view are not accessible to users with lower level --- .../controllers/categories_controller_test.rb | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index 66b194f10..cc81bfada 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -75,9 +75,25 @@ class CategoriesControllerTest < ActionController::TestCase end test 'should prevent users under min_view_trust_level viewing category that requires higher' do - get :show, params: { id: categories(:admin_only).id } + staff_only = categories(:admin_only) - assert_response(:not_found) - assert_not_nil assigns(:category) + users.reject { |u| u.can_see_category?(staff_only) && !u.staff? }.each do |user| + sign_in user + try_show_category(staff_only) + + if user.staff? + assert_response(:success) + else + assert_response(:not_found) + end + + assert_not_nil assigns(:category) + end + end + + private + + def try_show_category(category) + get :show, params: { id: category.id } end end From 72d6bdc18674fd15cc5e1bd90610bc42d0e276f7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 19:57:42 +0300 Subject: [PATCH 54/79] made categories controller :show test cover all cases --- .../controllers/categories_controller_test.rb | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index cc81bfada..181375e0b 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -74,20 +74,21 @@ class CategoriesControllerTest < ActionController::TestCase assert_redirected_to category_path(assigns(:category)) end - test 'should prevent users under min_view_trust_level viewing category that requires higher' do - staff_only = categories(:admin_only) - - users.reject { |u| u.can_see_category?(staff_only) && !u.staff? }.each do |user| + test ':show should only succeed for users who can see the category in the first place' do + users.each do |user| sign_in user - try_show_category(staff_only) - if user.staff? - assert_response(:success) - else - assert_response(:not_found) - end + categories.each do |category| + try_show_category(category) - assert_not_nil assigns(:category) + if category.public? || user.can_see_category?(category) + assert_response(:success) + else + assert_response(:not_found) + end + + assert_not_nil assigns(:category) + end end end From baa38f8ed0d9c1eb05fa87f8e9a13cbcde14048d Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 20:05:50 +0300 Subject: [PATCH 55/79] added try_create_category test helper method --- .../controllers/categories_controller_test.rb | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index 181375e0b..4e147fca5 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -43,29 +43,19 @@ class CategoriesControllerTest < ActionController::TestCase end test 'should require authentication to create category' do - post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id], - post_type_ids: [Question.post_type_id, Answer.post_type_id], - tag_set: tag_sets(:main).id, color_code: 'blue', - license_id: licenses(:cc_by_sa).id } } - + try_create_category assert_redirected_to_sign_in end test 'should require admin to create category' do sign_in users(:standard_user) - post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id], - post_type_ids: [Question.post_type_id, Answer.post_type_id], - tag_set: tag_sets(:main).id, color_code: 'blue', - license_id: licenses(:cc_by_sa).id } } + try_create_category assert_response(:not_found) end test 'should allow admins to create category' do sign_in users(:admin) - post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id], - post_type_ids: [Question.post_type_id, Answer.post_type_id], - tag_set_id: tag_sets(:main).id, color_code: 'blue', - license_id: licenses(:cc_by_sa).id } } + try_create_category assert_response(:found) assert_not_nil assigns(:category) @@ -94,6 +84,24 @@ class CategoriesControllerTest < ActionController::TestCase private + def try_create_category(**opts) + name = opts[:name] || 'test' + short_wiki = opts[:short_wiki] || 'test' + license = opts[:license] || licenses(:cc_by_sa) + color_code = opts[:color_code] || 'blue' + display_post_types = opts[:display_post_types] || [Question.post_type_id] + post_types = opts[:post_types] || [Question, Answer] + tag_set = opts[:tag_set] || tag_sets(:main) + + post :create, params: { category: { name: name, + short_wiki: short_wiki, + display_post_types: display_post_types, + post_type_ids: post_types.map(&:post_type_id), + tag_set_id: tag_set.id, + color_code: color_code, + license_id: license.id } } + end + def try_show_category(category) get :show, params: { id: category.id } end From 1270732e7366c5ebc2b0c221601fe9dfe1d922a9 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 20:19:55 +0300 Subject: [PATCH 56/79] cleaned up & expanded categories controller tests --- .../controllers/categories_controller_test.rb | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index 4e147fca5..cd06ea4b7 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -9,37 +9,62 @@ class CategoriesControllerTest < ActionController::TestCase assert_not_nil assigns(:categories) end - test 'should get show' do - get :show, params: { id: categories(:main).id } - assert_response(:success) - assert_not_nil assigns(:category) - assert_not_nil assigns(:posts) + test 'should correctly show public categories' do + public_categories = categories.select(&:public?) + + assert_not public_categories.size.zero? + + public_categories.each do |category| + try_show_category(category) + + assert_response(:success) + assert_not_nil assigns(:category) + assert_not_nil assigns(:posts) + end end - test 'fake community should not get show' do + test 'fake community should never be shown' do RequestContext.community = communities(:fake) request.env['HTTP_HOST'] = 'fake.qpixel.com' - get :show, params: { id: categories(:main).id } + try_show_category(categories(:main)) + assert_response(:not_found) end - test 'should require authentication to get new' do - get :new - assert_redirected_to_sign_in - end + test 'categories should only be shown to those who can see them' do + users.each do |user| + sign_in user - test 'should require admin to get new' do - sign_in users(:standard_user) - get :new - assert_response(:not_found) + categories.each do |category| + try_show_category(category) + + if category.public? || user.can_see_category?(category) + assert_response(:success) + else + assert_response(:not_found) + end + + assert_not_nil assigns(:category) + end + end end - test 'should allow admins to get new' do - sign_in users(:admin) - get :new - assert_response(:success) - assert_not_nil assigns(:category) + test ':new should require the user to be an admin' do + users.each do |user| + sign_in user + + get :new + + if user.admin? + assert_response(:success) + assert_not_nil assigns(:category) + elsif @controller.helpers.user_signed_in? + assert_response(:not_found) + else + assert_redirected_to_sign_in + end + end end test 'should require authentication to create category' do @@ -64,24 +89,6 @@ class CategoriesControllerTest < ActionController::TestCase assert_redirected_to category_path(assigns(:category)) end - test ':show should only succeed for users who can see the category in the first place' do - users.each do |user| - sign_in user - - categories.each do |category| - try_show_category(category) - - if category.public? || user.can_see_category?(category) - assert_response(:success) - else - assert_response(:not_found) - end - - assert_not_nil assigns(:category) - end - end - end - private def try_create_category(**opts) From 99802f5a4bb655c68b98acf4c26b4d53b4e73f0b Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 22:23:13 +0300 Subject: [PATCH 57/79] let's dry-run CodeCov --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afce2b1fa..3a66d7cd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,6 +62,14 @@ jobs: bundle exec rails db:migrate bundle exec rails test:prepare - run: bundle exec rails ${{ matrix.test_type }} + + - uses: codecov/codecov-action@v5 + with: + directory: test/reports + dry_run: true # temporary + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload screenshots if: ${{ matrix.test_type == 'test:system' }} uses: actions/upload-artifact@v4 From c86bd0707113e01754095f7eea13da0d8422bbc7 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 22:41:36 +0300 Subject: [PATCH 58/79] working on CodeCov upload actions step --- .github/workflows/test.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a66d7cd8..c542631c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,14 +62,6 @@ jobs: bundle exec rails db:migrate bundle exec rails test:prepare - run: bundle exec rails ${{ matrix.test_type }} - - - uses: codecov/codecov-action@v5 - with: - directory: test/reports - dry_run: true # temporary - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload screenshots if: ${{ matrix.test_type == 'test:system' }} uses: actions/upload-artifact@v4 @@ -77,3 +69,14 @@ jobs: name: screenshots-${{ matrix.ruby_version }} path: tmp/screenshots if-no-files-found: ignore + + - run: ls -la test/reports + + - uses: codecov/codecov-action@v5 + if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} + with: + directory: test/reports + dry_run: true # temporary + # fail_ci_if_error: true + report_type: test_results + token: ${{ secrets.CODECOV_TOKEN }} From 201067b97325373e2828b506bf7c433a193a1666 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 22:48:24 +0300 Subject: [PATCH 59/79] enabled verbose mode & CI failure on error if CodeCov fails to get coverage --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c542631c7..dbfa60f45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,6 +77,7 @@ jobs: with: directory: test/reports dry_run: true # temporary - # fail_ci_if_error: true + fail_ci_if_error: true report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} + verbose: true From b82407fa6573f31e66630a8c64c93a33d1611761 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 22:52:31 +0300 Subject: [PATCH 60/79] let's run CodeCov for real this time --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbfa60f45..ad049e4fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,15 +69,10 @@ jobs: name: screenshots-${{ matrix.ruby_version }} path: tmp/screenshots if-no-files-found: ignore - - - run: ls -la test/reports - - uses: codecov/codecov-action@v5 if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} with: directory: test/reports - dry_run: true # temporary fail_ci_if_error: true report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} - verbose: true From d38408095da6b9d58d0daa014155b450e4aee918 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 23:19:10 +0300 Subject: [PATCH 61/79] eh, apparently, CodeCov needs coverage output --- .github/workflows/test.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad049e4fd..dc4a4f0ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,7 +72,8 @@ jobs: - uses: codecov/codecov-action@v5 if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} with: - directory: test/reports - fail_ci_if_error: true - report_type: test_results + directory: coverage + dry_run: true + fail_ci_if_error: false + report_type: coverage token: ${{ secrets.CODECOV_TOKEN }} From 2f2b6a6d60a52a6b507aa87a25f9deaad4b4594f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 13 Jul 2025 23:31:56 +0300 Subject: [PATCH 62/79] coverage option works too, let's actually upload --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc4a4f0ab..a36b79508 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,7 +73,6 @@ jobs: if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} with: directory: coverage - dry_run: true - fail_ci_if_error: false + fail_ci_if_error: true report_type: coverage token: ${{ secrets.CODECOV_TOKEN }} From 247200a148883dd25baedeff28e1f1161c80f783 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Mon, 14 Jul 2025 16:34:32 +0300 Subject: [PATCH 63/79] cleaned up posts controller create tests --- test/controllers/posts/create_test.rb | 99 +++++++++++++-------------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/test/controllers/posts/create_test.rb b/test/controllers/posts/create_test.rb index 7d31b0e49..255f50742 100644 --- a/test/controllers/posts/create_test.rb +++ b/test/controllers/posts/create_test.rb @@ -3,25 +3,10 @@ class PostsControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers - test 'can create help post' do - sign_in users(:moderator) - - post :create, params: { post_type: post_types(:help_doc).id, - post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic', - body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } } - - assert_response(:found) - assert_not_nil assigns(:post).id - assert_redirected_to help_path(assigns(:post).doc_slug) - end - test 'can create category post' do sign_in users(:standard_user) - post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:main).id, - tags_cache: sample.tags_cache, license_id: licenses(:cc_by_sa).id } } + try_create_post assert_response(:found) assert_not_nil assigns(:post).id @@ -32,10 +17,7 @@ class PostsControllerTest < ActionController::TestCase sign_in users(:closer) before_notifs = posts(:question_one).user.notifications.count - post :create, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id, - post: { post_type_id: post_types(:answer).id, title: sample.title, - body_markdown: sample.body_markdown, parent_id: posts(:question_one).id, - license_id: licenses(:cc_by_sa).id } } + try_create_post(post_type: post_types(:answer), parent: posts(:question_one)) after_notifs = posts(:question_one).user.notifications.count assert_response(:found) @@ -45,14 +27,22 @@ class PostsControllerTest < ActionController::TestCase end test 'create requires authentication' do - post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:main).id, - tags_cache: sample.tags_cache } } - + try_create_post assert_redirected_to_sign_in end + test 'can create help post' do + sign_in users(:moderator) + + post :create, params: { post_type: post_types(:help_doc).id, + post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic', + body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } } + + assert_response(:found) + assert_not_nil assigns(:post).id + assert_redirected_to help_path(assigns(:post).doc_slug) + end + test 'standard users cannot create help posts' do sign_in users(:standard_user) @@ -76,9 +66,7 @@ class PostsControllerTest < ActionController::TestCase test 'category post type rejects without category' do sign_in users(:standard_user) - post :create, params: { post_type: post_types(:question).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, tags_cache: sample.tags_cache } } + try_create_post(category: nil) assert_response(:found) assert_redirected_to root_path @@ -89,10 +77,7 @@ class PostsControllerTest < ActionController::TestCase test 'category post type checks required trust level' do sign_in users(:standard_user) - post :create, params: { post_type: post_types(:question).id, category: categories(:high_trust).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:high_trust).id, - tags_cache: sample.tags_cache } } + try_create_post(category: categories(:high_trust)) assert_response(:forbidden) assert_nil assigns(:post).id @@ -102,9 +87,7 @@ class PostsControllerTest < ActionController::TestCase test 'parented post type rejects without parent' do sign_in users(:standard_user) - post :create, params: { post_type: post_types(:answer).id, - post: { post_type_id: post_types(:answer).id, title: sample.title, - body_markdown: sample.body_markdown } } + try_create_post(post_type: post_types(:answer)) assert_response(:found) assert_redirected_to root_path @@ -117,34 +100,44 @@ class PostsControllerTest < ActionController::TestCase before = CommunityUser.where(user: user, community: communities(:sample)).count sign_in user - post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:main).id, - tags_cache: sample.tags_cache } } + try_create_post after = CommunityUser.where(user: user, community: communities(:sample)).count assert_equal before + 1, after, 'No CommunityUser record was created' end - test 'should prevent deleted account creating post' do + test 'should prevent deleted accounts from creating posts' do sign_in users(:deleted_account) - - post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:main).id, - tags_cache: sample.tags_cache } } - + try_create_post assert_redirected_to_sign_in end - test 'should prevent deleted profile creating post' do + test 'should prevent deleted profiles from creating posts' do sign_in users(:deleted_profile) - - post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, - post: { post_type_id: post_types(:question).id, title: sample.title, - body_markdown: sample.body_markdown, category_id: categories(:main).id, - tags_cache: sample.tags_cache } } - + try_create_post assert_redirected_to_sign_in end + + private + + # Attempts to create a post + # @param post_type [PostType] + # @param category [Category, nil] + # @param parent [Post, nil] + # @param license [String] + def try_create_post(post_type: post_types(:question), + category: categories(:main), + parent: nil, + license: licenses(:cc_by_sa)) + post :create, params: { post_type: post_type.id, + parent: parent&.id, + category: category&.id, + post: { post_type_id: post_type.id, + title: sample.title, + body_markdown: sample.body_markdown, + category_id: category&.id, + parent_id: parent&.id, + tags_cache: sample.tags_cache, + license_id: license.id } } + end end From a7fc02eeecfc89713aa85f20ffee7f154de3182a Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 00:23:37 +0300 Subject: [PATCH 64/79] bumped rubocop-rake to 0.7.1 --- Gemfile | 2 +- Gemfile.lock | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index c52fd33be..47f6f9fc2 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'whenever', '~> 1.0', require: false gem 'awesome_print', '~> 1.9' gem 'rubocop', '~> 1' gem 'rubocop-rails', '~> 2.15' -gem 'rubocop-rake', '~> 0.6.0' +gem 'rubocop-rake', '~> 0.7.1' # MiniProfiler support, including stack traces & memory dumps, plus flamegraphs. gem 'flamegraph', '~> 0.9' diff --git a/Gemfile.lock b/Gemfile.lock index ef870885c..c13cc91ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -373,6 +373,9 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) ruby-progressbar (1.13.0) ruby-saml (1.18.0) nokogiri (>= 1.13.10) @@ -509,7 +512,7 @@ DEPENDENCIES rqrcode (~> 2.1) rubocop (~> 1) rubocop-rails (~> 2.15) - rubocop-rake (~> 0.6.0) + rubocop-rake (~> 0.7.1) ruby-progressbar (~> 1.11) sass-rails (~> 6.0) selenium-webdriver (~> 4.7) From b9441e45b5ea62cf48bd4f2b17021da44a4e1ce3 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 00:31:53 +0300 Subject: [PATCH 65/79] size.zero? <=> empty? (Rubocop) --- test/controllers/categories_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index cd06ea4b7..82bdbfdc5 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -12,7 +12,7 @@ class CategoriesControllerTest < ActionController::TestCase test 'should correctly show public categories' do public_categories = categories.select(&:public?) - assert_not public_categories.size.zero? + assert_not public_categories.empty? public_categories.each do |category| try_show_category(category) From 6b587343ba28ce57d02be0d97d8039afffd0bdaf Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 20:32:36 +0300 Subject: [PATCH 66/79] let's see how a merged workflow goes --- .github/workflows/{test.yml => ci.yml} | 38 +++++++++++++++++++++++++- .github/workflows/rubocop.yml | 28 ------------------- .github/workflows/typescript.yml | 26 ------------------ 3 files changed, 37 insertions(+), 55 deletions(-) rename .github/workflows/{test.yml => ci.yml} (72%) delete mode 100644 .github/workflows/rubocop.yml delete mode 100644 .github/workflows/typescript.yml diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 72% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index a36b79508..e6019e0ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Rails testing +name: ci on: push: @@ -12,6 +12,42 @@ env: RAILS_ENV: test jobs: + rubocop: + name: Rubocop checking + runs-on: ubuntu-latest + + strategy: + matrix: + ruby_version: [3.1, 3.2] + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -yqq libmagickwand-dev + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + bundler-cache: true + - run: bundle exec rubocop + + typescript: + name: TypeScript type checking + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 22 + - run: npm install + - run: tsc + tests: name: Ruby on Rails tests runs-on: ubuntu-latest diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml deleted file mode 100644 index 0e7ed3023..000000000 --- a/.github/workflows/rubocop.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Rubocop check - -on: - push: - branches: - - develop - pull_request: - branches: - - develop - -jobs: - rubocop: - name: Rubocop checking - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Setup dependencies - run: | - sudo apt-get update -qq - sudo apt-get install -yqq libmagickwand-dev - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.1.2 - bundler-cache: true - - run: bundle exec rubocop diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml deleted file mode 100644 index 4b19bf686..000000000 --- a/.github/workflows/typescript.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: TypeScript check - -on: - push: - branches: - - develop - pull_request: - branches: - - develop - -jobs: - typescript: - name: TypeScript type checking - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Install Node - uses: actions/setup-node@v1 - with: - node-version: 22 - - name: npm install - run: npm install - - name: tsc - run: tsc From c8e9a70c5b8dd03adb3a45f53eb0eeae91ca51ce Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 21:31:16 +0300 Subject: [PATCH 67/79] here goes nothing - testing deployments --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6019e0ef..49090dcb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,3 +112,31 @@ jobs: fail_ci_if_error: true report_type: coverage token: ${{ secrets.CODECOV_TOKEN }} + + deploy: + name: Dev server deployment + runs-on: ubuntu-latest + if: ${{ github.event_name != 'push' }} # this is INTENTIONAL + needs: + - rubocop + - typescript + - tests + + env: + SSH_IP: ${{ secrets.DEV_SSH_IP }} + SSH_KEY: ${{ secrets.DEV_SSH_KEY }} + SSH_PORT: ${{ secrets.DEV_SSH_PORT }} + SSH_USER: ${{ secrets.DEV_SSH_USER }} + + steps: + - name: Import key + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" | base64 --decode > ~/.ssh/deploy.key + chmod 0700 ~/.ssh/deploy.key + - name: Deploy + - run: | + ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ + -p "$SSH_PORT" \ + -i ~/.ssh/deploy.key \ + "sudo su -l qpixel /var/apps/deploy-dev" From dde6596c91da132fbc8c0f132934b9506294875f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 21:32:31 +0300 Subject: [PATCH 68/79] whoops, yaml... --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49090dcb0..58abe0523 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: echo "$SSH_KEY" | base64 --decode > ~/.ssh/deploy.key chmod 0700 ~/.ssh/deploy.key - name: Deploy - - run: | + run: | ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ -p "$SSH_PORT" \ -i ~/.ssh/deploy.key \ From 69046cf5d0f19ff94b0eeea0592fdf56e75f1bdd Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 21:45:34 +0300 Subject: [PATCH 69/79] quick test - do we have extra keys in? --- .github/workflows/ci.yml | 205 ++++++++++++++++++++------------------- 1 file changed, 103 insertions(+), 102 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58abe0523..e543f521d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,115 +12,115 @@ env: RAILS_ENV: test jobs: - rubocop: - name: Rubocop checking - runs-on: ubuntu-latest + # rubocop: + # name: Rubocop checking + # runs-on: ubuntu-latest - strategy: - matrix: - ruby_version: [3.1, 3.2] + # strategy: + # matrix: + # ruby_version: [3.1, 3.2] - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Setup dependencies - run: | - sudo apt-get update -qq - sudo apt-get install -yqq libmagickwand-dev - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby_version }} - bundler-cache: true - - run: bundle exec rubocop + # steps: + # - name: Checkout repo + # uses: actions/checkout@v3 + # - name: Setup dependencies + # run: | + # sudo apt-get update -qq + # sudo apt-get install -yqq libmagickwand-dev + # - name: Setup Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: ${{ matrix.ruby_version }} + # bundler-cache: true + # - run: bundle exec rubocop - typescript: - name: TypeScript type checking - runs-on: ubuntu-latest + # typescript: + # name: TypeScript type checking + # runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Install Node - uses: actions/setup-node@v1 - with: - node-version: 22 - - run: npm install - - run: tsc + # steps: + # - name: Checkout repo + # uses: actions/checkout@v3 + # - name: Install Node + # uses: actions/setup-node@v1 + # with: + # node-version: 22 + # - run: npm install + # - run: tsc - tests: - name: Ruby on Rails tests - runs-on: ubuntu-latest + # tests: + # name: Ruby on Rails tests + # runs-on: ubuntu-latest - strategy: - matrix: - ruby_version: [3.1, 3.2] - test_type: [test, test:system] + # strategy: + # matrix: + # ruby_version: [3.1, 3.2] + # test_type: [test, test:system] - services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_HOST: '%' - MYSQL_ROOT_PASSWORD: 'root' - MYSQL_DATABASE: 'qpixel_test' - ports: - - 3306:3306 - redis: - image: redis:8.0 - ports: - - 6379:6379 + # services: + # mysql: + # image: mysql:8.0 + # env: + # MYSQL_ROOT_HOST: '%' + # MYSQL_ROOT_PASSWORD: 'root' + # MYSQL_DATABASE: 'qpixel_test' + # ports: + # - 3306:3306 + # redis: + # image: redis:8.0 + # ports: + # - 6379:6379 - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Setup dependencies - run: | - sudo apt-get -qq update - sudo apt-get -yqq install libmariadb-dev libmagickwand-dev - - name: Setup Firefox - if: ${{ matrix.test_type == 'test:system' }} - uses: browser-actions/setup-firefox@v1.5.4 - - name: Check Firefox setup - if: ${{ matrix.test_type == 'test:system' }} - run: firefox --version - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby_version }} - bundler-cache: true - - name: Prepare for testing - run: | - cp config/database.sample.yml config/database.yml - cp config/storage.sample.yml config/storage.yml - bundle exec rails db:create - bundle exec rails db:schema:load - bundle exec rails db:migrate - bundle exec rails test:prepare - - run: bundle exec rails ${{ matrix.test_type }} - - name: Upload screenshots - if: ${{ matrix.test_type == 'test:system' }} - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ matrix.ruby_version }} - path: tmp/screenshots - if-no-files-found: ignore - - uses: codecov/codecov-action@v5 - if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} - with: - directory: coverage - fail_ci_if_error: true - report_type: coverage - token: ${{ secrets.CODECOV_TOKEN }} + # steps: + # - name: Checkout repo + # uses: actions/checkout@v3 + # - name: Setup dependencies + # run: | + # sudo apt-get -qq update + # sudo apt-get -yqq install libmariadb-dev libmagickwand-dev + # - name: Setup Firefox + # if: ${{ matrix.test_type == 'test:system' }} + # uses: browser-actions/setup-firefox@v1.5.4 + # - name: Check Firefox setup + # if: ${{ matrix.test_type == 'test:system' }} + # run: firefox --version + # - name: Setup Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: ${{ matrix.ruby_version }} + # bundler-cache: true + # - name: Prepare for testing + # run: | + # cp config/database.sample.yml config/database.yml + # cp config/storage.sample.yml config/storage.yml + # bundle exec rails db:create + # bundle exec rails db:schema:load + # bundle exec rails db:migrate + # bundle exec rails test:prepare + # - run: bundle exec rails ${{ matrix.test_type }} + # - name: Upload screenshots + # if: ${{ matrix.test_type == 'test:system' }} + # uses: actions/upload-artifact@v4 + # with: + # name: screenshots-${{ matrix.ruby_version }} + # path: tmp/screenshots + # if-no-files-found: ignore + # - uses: codecov/codecov-action@v5 + # if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} + # with: + # directory: coverage + # fail_ci_if_error: true + # report_type: coverage + # token: ${{ secrets.CODECOV_TOKEN }} deploy: name: Dev server deployment runs-on: ubuntu-latest if: ${{ github.event_name != 'push' }} # this is INTENTIONAL - needs: - - rubocop - - typescript - - tests + # needs: + # - rubocop + # - typescript + # - tests env: SSH_IP: ${{ secrets.DEV_SSH_IP }} @@ -134,9 +134,10 @@ jobs: mkdir -p ~/.ssh echo "$SSH_KEY" | base64 --decode > ~/.ssh/deploy.key chmod 0700 ~/.ssh/deploy.key - - name: Deploy - run: | - ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ - -p "$SSH_PORT" \ - -i ~/.ssh/deploy.key \ - "sudo su -l qpixel /var/apps/deploy-dev" + ls -la ~/.ssh + # - name: Deploy + # run: | + # ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ + # -p "$SSH_PORT" \ + # -i ~/.ssh/deploy.key \ + # "sudo su -l qpixel /var/apps/deploy-dev" From 14af66988cb483ae4919d82ddd08ff7208cc5c2b Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 21:58:46 +0300 Subject: [PATCH 70/79] let's see if verbose mode yields anything of use --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e543f521d..8bc6ad46d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,10 +134,10 @@ jobs: mkdir -p ~/.ssh echo "$SSH_KEY" | base64 --decode > ~/.ssh/deploy.key chmod 0700 ~/.ssh/deploy.key - ls -la ~/.ssh - # - name: Deploy - # run: | - # ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ - # -p "$SSH_PORT" \ - # -i ~/.ssh/deploy.key \ - # "sudo su -l qpixel /var/apps/deploy-dev" + - name: Deploy + run: | + ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ + -p "$SSH_PORT" \ + -i ~/.ssh/deploy.key \ + -v \ + "sudo su -l qpixel /var/apps/deploy-dev" From e57813e60658954a7d2f2fd969bc45977c39300f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 22:13:14 +0300 Subject: [PATCH 71/79] drop base64 decoding & check SSH version --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc6ad46d..8cbdd0c3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,10 +129,12 @@ jobs: SSH_USER: ${{ secrets.DEV_SSH_USER }} steps: + - name: Check SSH version + run: ssh -V - name: Import key run: | mkdir -p ~/.ssh - echo "$SSH_KEY" | base64 --decode > ~/.ssh/deploy.key + echo "$SSH_KEY" > ~/.ssh/deploy.key chmod 0700 ~/.ssh/deploy.key - name: Deploy run: | From 4c7c92d19fbd390fb1a0c1a702c9c140fd850f43 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 22:19:28 +0300 Subject: [PATCH 72/79] this ssh command order makes more sense --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cbdd0c3e..826fe5925 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,8 +138,9 @@ jobs: chmod 0700 ~/.ssh/deploy.key - name: Deploy run: | - ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" \ + ssh -o 'StrictHostKeyChecking no' \ -p "$SSH_PORT" \ -i ~/.ssh/deploy.key \ -v \ + "$SSH_USER"@"$SSH_IP" \ "sudo su -l qpixel /var/apps/deploy-dev" From 441e81ff4a30493aa760cc3d29c2e37aa5aa4ced Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 22:22:20 +0300 Subject: [PATCH 73/79] don't even try keyboard & pwd auth --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 826fe5925..7530f41ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,6 +139,8 @@ jobs: - name: Deploy run: | ssh -o 'StrictHostKeyChecking no' \ + -o 'KbdInteractiveAuthentication no' \ + -o 'PasswordAuthentication no' \ -p "$SSH_PORT" \ -i ~/.ssh/deploy.key \ -v \ From 8f4f3d3a0d32033bc310ffdbea0c909a0ae501d8 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Tue, 22 Jul 2025 22:31:58 +0300 Subject: [PATCH 74/79] restore full pipeline and test --- .github/workflows/ci.yml | 193 +++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7530f41ee..4fcd94d21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,115 +12,115 @@ env: RAILS_ENV: test jobs: - # rubocop: - # name: Rubocop checking - # runs-on: ubuntu-latest + rubocop: + name: Rubocop checking + runs-on: ubuntu-latest - # strategy: - # matrix: - # ruby_version: [3.1, 3.2] + strategy: + matrix: + ruby_version: [3.1, 3.2] - # steps: - # - name: Checkout repo - # uses: actions/checkout@v3 - # - name: Setup dependencies - # run: | - # sudo apt-get update -qq - # sudo apt-get install -yqq libmagickwand-dev - # - name: Setup Ruby - # uses: ruby/setup-ruby@v1 - # with: - # ruby-version: ${{ matrix.ruby_version }} - # bundler-cache: true - # - run: bundle exec rubocop + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -yqq libmagickwand-dev + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + bundler-cache: true + - run: bundle exec rubocop - # typescript: - # name: TypeScript type checking - # runs-on: ubuntu-latest + typescript: + name: TypeScript type checking + runs-on: ubuntu-latest - # steps: - # - name: Checkout repo - # uses: actions/checkout@v3 - # - name: Install Node - # uses: actions/setup-node@v1 - # with: - # node-version: 22 - # - run: npm install - # - run: tsc + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 22 + - run: npm install + - run: tsc - # tests: - # name: Ruby on Rails tests - # runs-on: ubuntu-latest + tests: + name: Ruby on Rails tests + runs-on: ubuntu-latest - # strategy: - # matrix: - # ruby_version: [3.1, 3.2] - # test_type: [test, test:system] + strategy: + matrix: + ruby_version: [3.1, 3.2] + test_type: [test, test:system] - # services: - # mysql: - # image: mysql:8.0 - # env: - # MYSQL_ROOT_HOST: '%' - # MYSQL_ROOT_PASSWORD: 'root' - # MYSQL_DATABASE: 'qpixel_test' - # ports: - # - 3306:3306 - # redis: - # image: redis:8.0 - # ports: - # - 6379:6379 + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_HOST: '%' + MYSQL_ROOT_PASSWORD: 'root' + MYSQL_DATABASE: 'qpixel_test' + ports: + - 3306:3306 + redis: + image: redis:8.0 + ports: + - 6379:6379 - # steps: - # - name: Checkout repo - # uses: actions/checkout@v3 - # - name: Setup dependencies - # run: | - # sudo apt-get -qq update - # sudo apt-get -yqq install libmariadb-dev libmagickwand-dev - # - name: Setup Firefox - # if: ${{ matrix.test_type == 'test:system' }} - # uses: browser-actions/setup-firefox@v1.5.4 - # - name: Check Firefox setup - # if: ${{ matrix.test_type == 'test:system' }} - # run: firefox --version - # - name: Setup Ruby - # uses: ruby/setup-ruby@v1 - # with: - # ruby-version: ${{ matrix.ruby_version }} - # bundler-cache: true - # - name: Prepare for testing - # run: | - # cp config/database.sample.yml config/database.yml - # cp config/storage.sample.yml config/storage.yml - # bundle exec rails db:create - # bundle exec rails db:schema:load - # bundle exec rails db:migrate - # bundle exec rails test:prepare - # - run: bundle exec rails ${{ matrix.test_type }} - # - name: Upload screenshots - # if: ${{ matrix.test_type == 'test:system' }} - # uses: actions/upload-artifact@v4 - # with: - # name: screenshots-${{ matrix.ruby_version }} - # path: tmp/screenshots - # if-no-files-found: ignore - # - uses: codecov/codecov-action@v5 - # if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} - # with: - # directory: coverage - # fail_ci_if_error: true - # report_type: coverage - # token: ${{ secrets.CODECOV_TOKEN }} + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup dependencies + run: | + sudo apt-get -qq update + sudo apt-get -yqq install libmariadb-dev libmagickwand-dev + - name: Setup Firefox + if: ${{ matrix.test_type == 'test:system' }} + uses: browser-actions/setup-firefox@v1.5.4 + - name: Check Firefox setup + if: ${{ matrix.test_type == 'test:system' }} + run: firefox --version + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby_version }} + bundler-cache: true + - name: Prepare for testing + run: | + cp config/database.sample.yml config/database.yml + cp config/storage.sample.yml config/storage.yml + bundle exec rails db:create + bundle exec rails db:schema:load + bundle exec rails db:migrate + bundle exec rails test:prepare + - run: bundle exec rails ${{ matrix.test_type }} + - name: Upload screenshots + if: ${{ matrix.test_type == 'test:system' }} + uses: actions/upload-artifact@v4 + with: + name: screenshots-${{ matrix.ruby_version }} + path: tmp/screenshots + if-no-files-found: ignore + - uses: codecov/codecov-action@v5 + if: ${{ matrix.ruby_version == 3.2 && matrix.test_type == 'test' }} + with: + directory: coverage + fail_ci_if_error: true + report_type: coverage + token: ${{ secrets.CODECOV_TOKEN }} deploy: name: Dev server deployment runs-on: ubuntu-latest if: ${{ github.event_name != 'push' }} # this is INTENTIONAL - # needs: - # - rubocop - # - typescript - # - tests + needs: + - rubocop + - typescript + - tests env: SSH_IP: ${{ secrets.DEV_SSH_IP }} @@ -143,6 +143,5 @@ jobs: -o 'PasswordAuthentication no' \ -p "$SSH_PORT" \ -i ~/.ssh/deploy.key \ - -v \ "$SSH_USER"@"$SSH_IP" \ "sudo su -l qpixel /var/apps/deploy-dev" From 14ce76c30a491350cd71f1b0519d247800795d4f Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Wed, 23 Jul 2025 00:08:09 +0300 Subject: [PATCH 75/79] run deploy only on push to develop --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fcd94d21..661e8ca39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,7 @@ jobs: deploy: name: Dev server deployment runs-on: ubuntu-latest - if: ${{ github.event_name != 'push' }} # this is INTENTIONAL + if: ${{ github.event_name == 'push' }} needs: - rubocop - typescript From 9554b26f2a7a551f7c17dfca808d17d20b962752 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Wed, 23 Jul 2025 00:08:57 +0300 Subject: [PATCH 76/79] go away, CircleCI --- .circleci/config.yml | 337 ------------------------------------------- 1 file changed, 337 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index cb7f4fbcf..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,337 +0,0 @@ -version: 2.1 -orbs: - codecov: codecov/codecov@5 -jobs: - test-ruby31: - docker: - - image: cimg/ruby:3.1-node - - image: cimg/mysql:8.0 - command: [ --default-authentication-plugin=mysql_native_password ] - environment: - MYSQL_ROOT_HOST: '%' - MYSQL_ROOT_PASSWORD: 'root' - MYSQL_DATABASE: 'qpixel_test' - - image: cimg/redis:7.0 - - working_directory: ~/qpixel - - steps: - - run: - name: Install packages - command: | - sudo apt-get --allow-releaseinfo-change -qq update - sudo apt-get -y install git libmariadb-dev libmagickwand-dev - - checkout - - restore_cache: - keys: - - qpixel-ruby31-{{ checksum "Gemfile.lock" }} - - qpixel-ruby31- - - run: - name: Install Bundler & gems - command: | - gem install bundler - bundle install --path=~/gems - - run: - name: Clean unnecessary gems - command: | - bundle clean --force - - save_cache: - key: qpixel-ruby31-{{ checksum "Gemfile.lock" }} - paths: - - ~/gems - - run: - name: Copy key - command: | - if [ -z "$MASTER_KEY" ]; then rm config/credentials.yml.enc; else echo "$MASTER_KEY" > config/master.key; fi - - run: - name: Prepare config & database - environment: - RAILS_ENV: test - command: | - cp config/database.sample.yml config/database.yml - cp config/storage.sample.yml config/storage.yml - bundle exec rails db:create - bundle exec rails db:schema:load - bundle exec rails db:migrate - bundle exec rails test:prepare - - run: - name: Current revision - command: | - git rev-parse $(git rev-parse --abbrev-ref HEAD) - - run: - name: Test - environment: - RAILS_ENV: test - command: | - bundle exec rails test - - store_test_results: - path: "~/qpixel/test/reports" - system-test-ruby31: - docker: - - image: cimg/ruby:3.1-browsers - - image: cimg/mysql:8.0 - command: [ --default-authentication-plugin=mysql_native_password ] - environment: - MYSQL_ROOT_HOST: '%' - MYSQL_ROOT_PASSWORD: 'root' - MYSQL_DATABASE: 'qpixel_test' - - image: cimg/redis:7.0 - - working_directory: ~/qpixel - - steps: - - run: - name: Install packages - command: | - sudo apt-get --allow-releaseinfo-change -qq update - sudo apt-get -y install git libmariadb-dev libmagickwand-dev - - checkout - - restore_cache: - keys: - - qpixel-ruby31-{{ checksum "Gemfile.lock" }} - - qpixel-ruby31- - - run: - name: Install Bundler & gems - command: | - gem install bundler - bundle install --path=~/gems - - run: - name: Clean unnecessary gems - command: | - bundle clean --force - - save_cache: - key: qpixel-ruby31-{{ checksum "Gemfile.lock" }} - paths: - - ~/gems - - run: - name: Copy key - command: | - if [ -z "$MASTER_KEY" ]; then rm config/credentials.yml.enc; else echo "$MASTER_KEY" > config/master.key; fi - - run: - name: Prepare config & database - environment: - RAILS_ENV: test - command: | - cp config/database.sample.yml config/database.yml - cp config/storage.sample.yml config/storage.yml - bundle exec rails db:create - bundle exec rails db:schema:load - bundle exec rails db:migrate - bundle exec rails test:prepare - - run: - name: Current revision - command: | - git rev-parse $(git rev-parse --abbrev-ref HEAD) - - run: - name: Test - environment: - RAILS_ENV: test - command: | - bundle exec rails test:system - - store_test_results: - path: "~/qpixel/test/reports" - - store_artifacts: - path: "~/qpixel/tmp/screenshots" - when: on_fail - - test-ruby32: - docker: - - image: cimg/ruby:3.2-node - - image: cimg/mysql:8.0 - command: [ --default-authentication-plugin=mysql_native_password ] - environment: - MYSQL_ROOT_HOST: '%' - MYSQL_ROOT_PASSWORD: 'root' - MYSQL_DATABASE: 'qpixel_test' - - image: cimg/redis:7.0 - - working_directory: ~/qpixel - - steps: - - run: - name: Install packages - command: | - sudo apt-get --allow-releaseinfo-change -qq update - sudo apt-get -y install git libmariadb-dev libmagickwand-dev - - checkout - - restore_cache: - keys: - - qpixel-ruby32-{{ checksum "Gemfile.lock" }} - - qpixel-ruby32- - - run: - name: Install Bundler & gems - command: | - gem install bundler - bundle install --path=~/gems - - run: - name: Clean unnecessary gems - command: | - bundle clean --force - - save_cache: - key: qpixel-ruby32-{{ checksum "Gemfile.lock" }} - paths: - - ~/gems - - run: - name: Copy key - command: | - if [ -z "$MASTER_KEY" ]; then rm config/credentials.yml.enc; else echo "$MASTER_KEY" > config/master.key; fi - - run: - name: Prepare config & database - environment: - RAILS_ENV: test - command: | - cp config/database.sample.yml config/database.yml - cp config/storage.sample.yml config/storage.yml - bundle exec rails db:create - bundle exec rails db:schema:load - bundle exec rails db:migrate - bundle exec rails test:prepare - - run: - name: Current revision - command: | - git rev-parse $(git rev-parse --abbrev-ref HEAD) - - run: - name: Test - environment: - RAILS_ENV: test - command: | - bundle exec rails test - - store_test_results: - path: "~/qpixel/test/reports" - - codecov/upload - system-test-ruby32: - docker: - - image: cimg/ruby:3.2-browsers - - image: cimg/mysql:8.0 - command: [ --default-authentication-plugin=mysql_native_password ] - environment: - MYSQL_ROOT_HOST: '%' - MYSQL_ROOT_PASSWORD: 'root' - MYSQL_DATABASE: 'qpixel_test' - - image: cimg/redis:7.0 - - working_directory: ~/qpixel - - steps: - - run: - name: Install packages - command: | - sudo apt-get --allow-releaseinfo-change -qq update - sudo apt-get -y install git libmariadb-dev libmagickwand-dev - - checkout - - restore_cache: - keys: - - qpixel-ruby32-{{ checksum "Gemfile.lock" }} - - qpixel-ruby32- - - run: - name: Install Bundler & gems - command: | - gem install bundler - bundle install --path=~/gems - - run: - name: Clean unnecessary gems - command: | - bundle clean --force - - save_cache: - key: qpixel-ruby32-{{ checksum "Gemfile.lock" }} - paths: - - ~/gems - - run: - name: Copy key - command: | - if [ -z "$MASTER_KEY" ]; then rm config/credentials.yml.enc; else echo "$MASTER_KEY" > config/master.key; fi - - run: - name: Prepare config & database - environment: - RAILS_ENV: test - command: | - cp config/database.sample.yml config/database.yml - cp config/storage.sample.yml config/storage.yml - bundle exec rails db:create - bundle exec rails db:schema:load - bundle exec rails db:migrate - bundle exec rails test:prepare - - run: - name: Current revision - command: | - git rev-parse $(git rev-parse --abbrev-ref HEAD) - - run: - name: Test - environment: - RAILS_ENV: test - command: | - bundle exec rails test:system - - store_test_results: - path: "~/qpixel/test/reports" - - store_artifacts: - path: "~/qpixel/tmp/screenshots" - when: on_fail - - rubocop: - docker: - - image: cimg/ruby:3.2-node - - working_directory: ~/qpixel - - steps: - - run: - name: Install packages - command: | - sudo apt-get --allow-releaseinfo-change -qq update - sudo apt-get -y install git libmariadb-dev libmagickwand-dev - - checkout - - restore_cache: - keys: - - qpixel-ruby32-{{ checksum "Gemfile.lock" }} - - qpixel-ruby32- - - run: - name: Install Bundler & gems - command: | - gem install bundler - bundle install --path=~/gems - - run: - name: Clean unnecessary gems - command: | - bundle clean --force - - save_cache: - key: qpixel-ruby32-{{ checksum "Gemfile.lock" }} - paths: - - ~/gems - - run: - name: Rubocop - command: | - bundle exec rubocop - - deploy: - docker: - - image: cimg/ruby:3.2-node - - working_directory: ~/qpixel - - steps: - - run: - name: Import SSH key - command: | - echo "$DEV_SSH_KEY" | base64 --decode > ~/deploy.key - chmod 0700 ~/deploy.key - - run: - name: Run deploy - command: | - ssh -o 'StrictHostKeyChecking no' "$SSH_USER"@"$SSH_IP" -p "$SSH_PORT" -i ~/deploy.key "sudo su -l qpixel /var/apps/deploy-dev" - -workflows: - test_lint: - jobs: - - test-ruby31 - - system-test-ruby31 - - test-ruby32 - - system-test-ruby32 - - rubocop - - deploy: - requires: - - test-ruby32 - - system-test-ruby32 - - rubocop - filters: - branches: - only: develop From 84638afeecc914ed1061fe936de7f208098c31e1 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Wed, 23 Jul 2025 00:09:42 +0300 Subject: [PATCH 77/79] renamed main workflow to ci-cd --- .github/workflows/{ci.yml => ci-cd.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{ci.yml => ci-cd.yml} (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-cd.yml similarity index 99% rename from .github/workflows/ci.yml rename to .github/workflows/ci-cd.yml index 661e8ca39..86570f1e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-cd.yml @@ -1,4 +1,4 @@ -name: ci +name: ci-cd on: push: From bd3904ef2db2fdbd7f5d1051d5e128f7742aab41 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Wed, 23 Jul 2025 00:46:54 +0300 Subject: [PATCH 78/79] CircleCI badge is not needed anymore --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ab41bbe44..5fdc63278 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@

- CircleCI Build Status Coverage Status DOI

From 527c09b8954c973739f22b4b2ed3df7e5e637c88 Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Wed, 23 Jul 2025 01:14:16 +0300 Subject: [PATCH 79/79] proudly disply the new status badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5fdc63278..774efdd62 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@

+ Pipeline status Coverage Status DOI