From f2ef4aa4c7ffe47d20bfd216debc9afa5a88a84e Mon Sep 17 00:00:00 2001 From: pangbo13 <51732678+pangbo13@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:51:22 +0800 Subject: [PATCH] dev: add tests --- app/controllers/remake_limit_controller.rb | 47 ++++ app/models/user_deletion_log.rb | 178 ++++++++++------ .../user_deletion_log_serializer.rb | 13 ++ lib/override_jaccount_authenticator.rb | 36 ++++ lib/override_trust_level_3_requirements.rb | 39 ++++ lib/override_users_controller.rb | 59 ++++++ plugin.rb | 200 ++++-------------- spec/create_log_hook_spec.rb | 35 +++ spec/jaccount_spec.rb | 72 +++++++ spec/requests/admin_users_controller_spec.rb | 34 +++ spec/requests/remake_limit_controller_spec.rb | 86 ++++++++ spec/requests/users_controller_spec.rb | 89 ++++++++ spec/serializers/admin_detailed_user_spec.rb | 51 +++++ .../user_deletion_log_serializer_spec.rb | 25 +++ spec/user_deletion_log_spec.rb | 56 +++++ 15 files changed, 796 insertions(+), 224 deletions(-) create mode 100644 app/controllers/remake_limit_controller.rb create mode 100644 app/serializers/user_deletion_log_serializer.rb create mode 100644 lib/override_jaccount_authenticator.rb create mode 100644 lib/override_trust_level_3_requirements.rb create mode 100644 lib/override_users_controller.rb create mode 100644 spec/create_log_hook_spec.rb create mode 100644 spec/jaccount_spec.rb create mode 100644 spec/requests/admin_users_controller_spec.rb create mode 100644 spec/requests/remake_limit_controller_spec.rb create mode 100644 spec/requests/users_controller_spec.rb create mode 100644 spec/serializers/admin_detailed_user_spec.rb create mode 100644 spec/serializers/user_deletion_log_serializer_spec.rb create mode 100644 spec/user_deletion_log_spec.rb diff --git a/app/controllers/remake_limit_controller.rb b/app/controllers/remake_limit_controller.rb new file mode 100644 index 0000000..6b094a0 --- /dev/null +++ b/app/controllers/remake_limit_controller.rb @@ -0,0 +1,47 @@ +module ::RemakeLimit + class RemakeLimitController < ::ApplicationController + def fetch_record_from_params + query_args = params.permit(:user_id, :email, :jaccount_name, :jaccount_id) + if query_args.keys.length == 0 + raise Discourse::InvalidParameters.new("At least one of user_id, email, jaccount_name, jaccount_id should be provided") + end + UserDeletionLog.where(query_args) + end + + def query + record = fetch_record_from_params + raise Discourse::NotFound.new("Record not found") if record.length == 0 + render_serialized(record, UserDeletionLogSerializer) + end + + def ignore + params.require(:id) + record = UserDeletionLog.find_by(id: params[:id]) + raise Discourse::NotFound.new("Record not found") if record.nil? + record.ignore_limit = true + record.save! + render json: { success: "ok" } + end + + def create_for_user + params.require(:user_id) + user = User.find_by(id: params[:user_id]) + raise Discourse::NotFound.new("User not found") if user.nil? + record = UserDeletionLog.create_log(user, refresh_delete_time: true) + if record.nil? + render json: { success: "fail"} , status: :unprocessable_entity + else + render json: { success: "ok" } + end + end + + def ignore_for_user + params.require(:user_id) + record = UserDeletionLog.find_by(user_id: params[:user_id]) + raise Discourse::NotFound.new("Record not found") if record.nil? + record.ignore_limit = true + record.save! + render json: { success: "ok", record: UserDeletionLogSerializer.new(record).as_json } + end + end +end \ No newline at end of file diff --git a/app/models/user_deletion_log.rb b/app/models/user_deletion_log.rb index 3d7e287..1485a0a 100644 --- a/app/models/user_deletion_log.rb +++ b/app/models/user_deletion_log.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Note: 1.`email` and `jaccount_name` are stored in downcase # `jaccount_id` is stored in original # 2. If `ignore_limit` is true, do not count this record when calculate cooldown time @@ -5,81 +7,117 @@ # do not use `created_at` to calculate cooldown time class UserDeletionLog < ActiveRecord::Base - JACCOUNT_PROVIDER_NAME ||= 'jaccount'.freeze - def self.create_log(user, refresh_delete_time = true, ignore_limit = false) - record = UserDeletionLog.find_or_initialize_by(user_id: user.id) - record.username = user.username - record.email = user.email.downcase - jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) - if jaccount_account.nil? - Rails.logger.warn("User #{user.id} has no associated jaccount") - else - record.jaccount_id = jaccount_account.provider_uid - record.jaccount_name = jaccount_account.info&.[]('account').downcase - if record.jaccount_name.nil? - Rails.logger.warn("User #{user.id} has an associated jaccount, but has no jaccount_name \n #{jaccount_account}") - end - end - pc = TrustLevel3Requirements.new(user).penalty_counts_all_time - record.silence_count = pc.silenced - record.suspend_count = pc.suspended - if record.user_deleted_at.nil? && ignore_limit - # A new record and ignore_limit is true - record.ignore_limit = true - end - if record.user_deleted_at.nil? || refresh_delete_time - record.user_deleted_at = Time.now - end - record.save! + JACCOUNT_PROVIDER_NAME ||= 'jaccount'.freeze + def self.create_log(user, refresh_delete_time: true, ignore_limit: false) + record = UserDeletionLog.find_or_initialize_by(user_id: user.id) + record.username = user.username + record.email = user.email.downcase + jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) + if jaccount_account.nil? + Rails.logger.warn("User #{user.id} has no associated jaccount") + else + record.jaccount_id = jaccount_account.provider_uid + record.jaccount_name = jaccount_account.info&.[]('account').downcase + if record.jaccount_name.nil? + Rails.logger.warn("User #{user.id} has an associated jaccount, but has no jaccount_name \n #{jaccount_account}") + end end - - def self.find_latest_time_by_email(email,jaccount_name=nil) - email = email.downcase - if jaccount_name.nil? - jaccount_name = email.split("@").first - end - record = UserDeletionLog.where("email = ? OR jaccount_name = ?",email,jaccount_name).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first - record&.user_deleted_at + pc = TrustLevel3Requirements.new(user).penalty_counts_all_time + record.silence_count = pc.silenced + record.suspend_count = pc.suspended + if record.user_deleted_at.nil? && ignore_limit + # A new record and ignore_limit is true + record.ignore_limit = true end - - def self.find_latest_time_by_jaccount_id(jaccount_id) - record = UserDeletionLog.where("jaccount_id = ?",jaccount_id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first - record&.user_deleted_at + if record.user_deleted_at.nil? || refresh_delete_time + record.user_deleted_at = Time.now end + record.save! + record + end - def self.find_latest_time(user) - jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) - jaccount_id = jaccount_account.provider_uid - jaccount_name = jaccount_account.info&.[]('account')&.downcase - email = user.email.downcase - - record = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first - record&.user_deleted_at + def self.find_latest_time_by_email(email, jaccount_name: nil) + email = email.downcase + if jaccount_name.nil? + jaccount_name = email.split("@").first + elsif email.split("@").first != jaccount_name.downcase \ + && email.split("@").last == "sjtu.edu.cn" + Rails.logger.warn("email and jaccount_name do not match: #{email} #{jaccount_name}") end + record = UserDeletionLog.where("email = ? OR jaccount_name = ?",email,jaccount_name).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first + record&.user_deleted_at + end + + def self.find_latest_time_by_jaccount_id(jaccount_id) + record = UserDeletionLog.where("jaccount_id = ?",jaccount_id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first + record&.user_deleted_at + end - def self.find_user_penalty_history(user, ignore_jaccount_not_found = false) - # ignore `ignore_limit` field as it is only used for cooldown time calculation - # do not count current user - email = user.email - jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) - if jaccount_account.nil? - if !ignore_jaccount_not_found - Rails.logger.warn("User #{user.id} has no jaccount_account") - end - records = UserDeletionLog.where(email: email).where("user_id != ? ",user.id) - else - jaccount_id = jaccount_account.provider_uid - jaccount_name = jaccount_account.info&.[]('account')&.downcase + def self.find_latest_time(user) + jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) + jaccount_id = jaccount_account.provider_uid + jaccount_name = jaccount_account.info&.[]('account')&.downcase + email = user.email.downcase - if !jaccount_name.nil? - records = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id) - else - records = UserDeletionLog.where("email = ? OR jaccount_id = ?",email,jaccount_id).where("user_id != ? ",user.id) - end - end - account_count = records.count - silence_count = records.sum(:silence_count) - suspend_count = records.sum(:suspend_count) - return account_count, silence_count, suspend_count + record = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first + record&.user_deleted_at + end + + def self.find_user_penalty_history(user, ignore_jaccount_not_found: false) + # ignore `ignore_limit` field as it is only used for cooldown time calculation + # do not count current user + + # DEVELOPMENT NOTE: BUG && WON'T FIX + # `user_id != ?` will always return false if user_id is NULL in DB + # this is an unexpected behavior + # However, those affected records came from migration of old data + # and they do not have penalty counts either + # Alough it is a bug, it can still return correct (mostly) result + email = user.email + jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME) + if jaccount_account.nil? + if !ignore_jaccount_not_found + Rails.logger.warn("User #{user.id} has no jaccount_account") + end + records = UserDeletionLog.where(email: email).where("user_id != ?",user.id) + else + jaccount_id = jaccount_account.provider_uid + jaccount_name = jaccount_account.info&.[]('account')&.downcase + + if !jaccount_name.nil? + records = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id) + else + records = UserDeletionLog.where("email = ? OR jaccount_id = ?",email,jaccount_id).where("user_id != ? ",user.id) + end end -end \ No newline at end of file + account_count = records.count + silence_count = records.sum(:silence_count) + suspend_count = records.sum(:suspend_count) + return account_count, silence_count, suspend_count + end +end + +# == Schema Information +# Schema version: 20240327000440 +# +# Table name: user_deletion_logs +# +# id :bigint not null, primary key +# user_id :integer +# username :string +# email :string +# jaccount_name :string +# jaccount_id :string +# silence_count :integer +# suspend_count :integer +# ignore_limit :boolean default(FALSE) +# user_deleted_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_user_deletion_logs_on_email (email) +# index_user_deletion_logs_on_jaccount_id (jaccount_id) +# index_user_deletion_logs_on_jaccount_name (jaccount_name) +# diff --git a/app/serializers/user_deletion_log_serializer.rb b/app/serializers/user_deletion_log_serializer.rb new file mode 100644 index 0000000..afaa4e0 --- /dev/null +++ b/app/serializers/user_deletion_log_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ::RemakeLimit + class UserDeletionLogSerializer < ::ApplicationSerializer + attributes :id + attributes :user_id + attributes :username + attributes :user_deleted_at + attributes :ignore_limit + attributes :silence_count + attributes :suspend_count + end +end \ No newline at end of file diff --git a/lib/override_jaccount_authenticator.rb b/lib/override_jaccount_authenticator.rb new file mode 100644 index 0000000..21c80fc --- /dev/null +++ b/lib/override_jaccount_authenticator.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ::RemakeLimit + module OverrideJAccountAuthenticator + def after_authenticate(auth_token) + result = super(auth_token) + if result.failed || !result.user.nil? || !SiteSetting.remake_limit_enabled + return result + else + # For more detail: + # https://github.com/ShuiyuanSJTU/discourse-omniauth-jaccount/blob/e535f263fbfa71149d14b75b141cbb4827eb5498/plugin.rb#L147-L155 + email = result.email.downcase + jaccount_name = result.username.downcase + jaccount_id = result.extra_data[:jaccount_uid] + old_by_email = UserDeletionLog.find_latest_time_by_email(email, jaccount_name: jaccount_name) + old_by_jaccount_id = UserDeletionLog.find_latest_time_by_jaccount_id(jaccount_id) + # find the latest time, use compact to remove nil + old = [old_by_email, old_by_jaccount_id].compact.max + if !old.nil? + time = old.to_datetime + SiteSetting.remake_limit_period.days + if Time.now < time + result.failed = true + result.failed_reason = "您的账号正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再登录!" + result.name = nil + result.username = nil + result.email = nil + result.email_valid = nil + result.extra_data = nil + result + end + end + return result + end + end + end +end \ No newline at end of file diff --git a/lib/override_trust_level_3_requirements.rb b/lib/override_trust_level_3_requirements.rb new file mode 100644 index 0000000..243aca2 --- /dev/null +++ b/lib/override_trust_level_3_requirements.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module RemakeLimit + module OverrideTrustLevel3Requirements + def penalty_counts_all_time + args = { + user_id: @user.id, + system_user_id: Discourse.system_user.id, + silence_user: UserHistory.actions[:silence_user], + unsilence_user: UserHistory.actions[:unsilence_user], + suspend_user: UserHistory.actions[:suspend_user], + unsuspend_user: UserHistory.actions[:unsuspend_user], + } + + sql = <<~SQL + SELECT + SUM( + CASE + WHEN action = :silence_user THEN 1 + WHEN action = :unsilence_user AND acting_user_id != :system_user_id THEN -1 + ELSE 0 + END + ) AS silence_count, + SUM( + CASE + WHEN action = :suspend_user THEN 1 + WHEN action = :unsuspend_user AND acting_user_id != :system_user_id THEN -1 + ELSE 0 + END + ) AS suspend_count + FROM user_histories AS uh + WHERE uh.target_user_id = :user_id + AND uh.action IN (:silence_user, :suspend_user, :unsilence_user, :unsuspend_user) + SQL + + ::TrustLevel3Requirements::PenaltyCounts.new(@user, DB.query_hash(sql, args).first) + end + end +end \ No newline at end of file diff --git a/lib/override_users_controller.rb b/lib/override_users_controller.rb new file mode 100644 index 0000000..31486ae --- /dev/null +++ b/lib/override_users_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module RemakeLimit + module OverrideUsersController + extend ActiveSupport::Concern + + prepended do + before_action :check_remake_limit, only: [:create] + before_action :add_remake_limit, only: [:destroy] + after_action :add_user_note, only: [:create] + end + + def check_remake_limit + if SiteSetting.remake_limit_enabled + old = UserDeletionLog.find_latest_time_by_email(params[:email]) + if old + time = old.to_datetime + SiteSetting.remake_limit_period.days + if Time.now < time + render json: { success: false, message: "您的邮箱正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再注册!" } + end + end + end + end + + def add_user_note + # add penalty history to user notes + if defined?(::DiscourseUserNotes) + begin + user = fetch_user_from_params(include_inactive:true) + rescue Discourse::NotFound + rails_logger.warn("User not found when adding user note") + return + end + account_count, silence_count, suspend_count = UserDeletionLog.find_user_penalty_history(user) + if account_count > 0 + ::DiscourseUserNotes.add_note( + user, + I18n.t("remake_limit.user_note_text", account_count: account_count, silence_count: silence_count, suspend_count: suspend_count), + Discourse.system_user.id + ) + end + end + end + + def add_remake_limit + if SiteSetting.remake_limit_enabled + user = fetch_user_from_params + guardian.ensure_can_delete_user!(user) + ::UserDeletionLog.create_log(user, refresh_delete_time: true) + if defined?(::DiscourseUserNotes) + ::DiscourseUserNotes.add_note(user, "用户尝试删除账号", Discourse.system_user.id) + end + if user.silenced? && !SiteSetting.remake_silenced_can_delete + render json: { error: "您的账号处于禁言状态,无法自助删除账户,请与管理人员联系!" }, status: :unprocessable_entity + end + end + end + end +end \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index d72a522..c47beb9 100644 --- a/plugin.rb +++ b/plugin.rb @@ -8,178 +8,70 @@ # required_version: 2.7.0 # transpile_js: true -PLUGIN_NAME ||= 'remake-limit'.freeze -PENALTY_HISTORY_STORE_KEY ||= (PLUGIN_NAME + '-penalty-history').freeze - -SJTU_EMAIL = '@sjtu.edu.cn'.freeze -SJTU_ALUMNI_EMAIL = '@alumni.sjtu.edu.cn'.freeze - -enabled_site_setting :remake_limit_enabled - -require_relative 'app/models/user_deletion_log.rb' - module ::RemakeLimit + PLUGIN_NAME ||= 'remake-limit'.freeze end -after_initialize do - class ::UsersController - - before_action :check_remake_limit, only: [:create] +enabled_site_setting :remake_limit_enabled - def check_remake_limit - if SiteSetting.remake_limit_enabled - old = UserDeletionLog.find_latest_time_by_email(params[:email]) - if old - time = old.to_datetime + SiteSetting.remake_limit_period.days - if Time.now < time - render json: { success: false, message: "您的邮箱正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再注册!" } - end - end +after_initialize do + require_relative 'app/models/user_deletion_log.rb' + require_relative 'app/serializers/user_deletion_log_serializer.rb' + require_relative 'app/controllers/remake_limit_controller.rb' + + module ::RemakeLimit + require_relative 'lib/override_users_controller.rb' + require_relative 'lib/override_trust_level_3_requirements.rb' + require_relative 'lib/override_jaccount_authenticator.rb' + + module OverrideUserDestroyer + def destroy(user, opts = {}) + ::UserDeletionLog.create_log(user, refresh_delete_time: true) + super end end - after_action :add_user_note, only: [:create] - - def add_user_note - # add penalty history to user notes - if defined?(::DiscourseUserNotes) - user = fetch_user_from_params - account_count, silence_count, suspend_count = UserDeletionLog.find_user_penalty_history(user) - if account_count > 0 - ::DiscourseUserNotes.add_note(user, - I18n.t("remake_limit.user_note_text", account_count: account_count, silence_count: silence_count, suspend_count: suspend_count), - Discourse.system_user.id) - end + module OverrideUserAnonymizer + def make_anonymous + ::UserDeletionLog.create_log(@user, refresh_delete_time: true) + super end end - before_action :add_remake_limit, only: [:destroy] - - def add_remake_limit - if SiteSetting.remake_limit_enabled - @user = fetch_user_from_params - guardian.ensure_can_delete_user!(@user) - UserDeletionLog.create_log(@user, true) - if defined?(::DiscourseUserNotes) - ::DiscourseUserNotes.add_note(@user, "用户尝试删除账号", Discourse.system_user.id) - end - if @user.silenced? && !SiteSetting.remake_silenced_can_delete - render json: { error: "您的账号处于禁言状态,无法自助删除账户,请与管理人员联系!" }, status: :unprocessable_entity - end - end - end + ::UsersController.prepend OverrideUsersController + ::TrustLevel3Requirements.prepend OverrideTrustLevel3Requirements + ::Auth::JAccountAuthenticator.prepend OverrideJAccountAuthenticator if defined? ::Auth::JAccountAuthenticator + ::UserDestroyer.prepend OverrideUserDestroyer + ::UserAnonymizer.prepend OverrideUserAnonymizer end - class ::TrustLevel3Requirements - def penalty_counts_all_time - args = { - user_id: @user.id, - system_user_id: Discourse.system_user.id, - silence_user: UserHistory.actions[:silence_user], - unsilence_user: UserHistory.actions[:unsilence_user], - suspend_user: UserHistory.actions[:suspend_user], - unsuspend_user: UserHistory.actions[:unsuspend_user], - } - - sql = <<~SQL - SELECT - SUM( - CASE - WHEN action = :silence_user THEN 1 - WHEN action = :unsilence_user AND acting_user_id != :system_user_id THEN -1 - ELSE 0 - END - ) AS silence_count, - SUM( - CASE - WHEN action = :suspend_user THEN 1 - WHEN action = :unsuspend_user AND acting_user_id != :system_user_id THEN -1 - ELSE 0 - END - ) AS suspend_count - FROM user_histories AS uh - WHERE uh.target_user_id = :user_id - AND uh.action IN (:silence_user, :suspend_user, :unsilence_user, :unsuspend_user) - SQL - - PenaltyCounts.new(@user, DB.query_hash(sql, args).first) - end + add_to_serializer(:admin_detailed_user, :penalty_counts) do + pc = TrustLevel3Requirements.new(object).penalty_counts_all_time + penalty_counts = { + "silence_count" => pc.silenced || 0, + "suspend_count" => pc.suspended || 0 + } + account_count, silence_count, suspend_count = UserDeletionLog.find_user_penalty_history(object,ignore_jaccount_not_found: true) + penalty_counts["silence_count"] += silence_count + penalty_counts["suspend_count"] += suspend_count + TrustLevel3Requirements::PenaltyCounts.new(user, penalty_counts) end - # module OverrideUserDestroyer - # def destroy(user, opts = {}) - # UserDeletionLog.create_log(user, false) - # super - # end - # end - - # class ::UserDestroyer - # prepend OverrideUserDestroyer - # end - - # module OverrideUserAnonymizer - # def make_anonymous - # UserDeletionLog.create_log(@user, false) - # super - # end - # end - - # class ::UserAnonymizer - # prepend OverrideUserAnonymizer - # end - - module OverrideAdminDetailedUserSerializer - def penalty_counts - pc = TrustLevel3Requirements.new(object).penalty_counts_all_time - penalty_counts = { - "silence_count" => pc.silenced || 0, - "suspend_count" => pc.suspended || 0 - } - account_count, silence_count, suspend_count = UserDeletionLog.find_user_penalty_history(object,ignore_jaccount_not_found: true) - penalty_counts["silence_count"] += silence_count - penalty_counts["suspend_count"] += suspend_count - TrustLevel3Requirements::PenaltyCounts.new(user, penalty_counts) + module ::RemakeLimit + class Engine < ::Rails::Engine + engine_name PLUGIN_NAME + isolate_namespace ::RemakeLimit end - end - class ::AdminDetailedUserSerializer - prepend OverrideAdminDetailedUserSerializer - end + Discourse::Application.routes.append { mount Engine, at: "/remake_limit" } - module OverrideJAccountAuthenticator - def after_authenticate(auth_token) - result = super(auth_token) - if result.failed || !result.user.nil? || !SiteSetting.remake_limit_enabled - return result - else - # For more detail: - # https://github.com/ShuiyuanSJTU/discourse-omniauth-jaccount/blob/e535f263fbfa71149d14b75b141cbb4827eb5498/plugin.rb#L147-L155 - email = result.email.downcase - jaccount_name = result.username.downcase - jaccount_id = result.extra_data[:jaccount_uid] - old_by_email = UserDeletionLog.find_latest_time_by_email(email,jaccount_name) - old_by_jaccount_id = UserDeletionLog.find_latest_time_by_jaccount_id(jaccount_id) - # find the latest time, use compact to remove nil - old = [old_by_email, old_by_jaccount_id].compact.max - if !old.nil? - time = old.to_datetime + SiteSetting.remake_limit_period.days - if Time.now < time - result.failed = true - result.failed_reason = "您的账号正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再登录!" - result.name = nil - result.username = nil - result.email = nil - result.email_valid = nil - result.extra_data = nil - result - end - end - return result + Engine.routes.draw do + constraints AdminConstraint.new do + get "/query" => "remake_limit#query" + delete "/id/:id" => "remake_limit#ignore" + put "/user/:user_id" => "remake_limit#create_for_user" + delete "/user/:user_id" => "remake_limit#ignore_for_user" end end end - - class ::Auth::JAccountAuthenticator - prepend OverrideJAccountAuthenticator - end end diff --git a/spec/create_log_hook_spec.rb b/spec/create_log_hook_spec.rb new file mode 100644 index 0000000..5d6e33c --- /dev/null +++ b/spec/create_log_hook_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RemakeLimit do + include ActiveSupport::Testing::TimeHelpers + let(:moderator) { Fabricate(:moderator) } + let(:user) { Fabricate(:user) } + + describe "should record user deletion log" do + it "when moderator destroy user" do + delete_time = 1.day.ago.beginning_of_day + travel_to delete_time do + UserDestroyer.new(moderator).destroy(user) + end + log = UserDeletionLog.find_by(email: user.email, user_id: user.id) + expect(log).to be_present + expect(log.user_deleted_at).to eq(delete_time) + expect(log.created_at).to eq(delete_time) + expect(log.ignore_limit).to eq(false) + end + it "when moderator anonymous user" do + prev_email = user.email + delete_time = 1.day.ago.beginning_of_day + travel_to delete_time do + UserAnonymizer.new(user, moderator).make_anonymous + end + log = UserDeletionLog.find_by(email: prev_email, user_id: user.id) + expect(log).to be_present + expect(log.user_deleted_at).to eq(delete_time) + expect(log.created_at).to eq(delete_time) + expect(log.ignore_limit).to eq(false) + end + end +end \ No newline at end of file diff --git a/spec/jaccount_spec.rb b/spec/jaccount_spec.rb new file mode 100644 index 0000000..711efe9 --- /dev/null +++ b/spec/jaccount_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +RSpec.describe RemakeLimit::OverrideJAccountAuthenticator do + skip "jaccount authenticator is not installed" unless defined?(::Auth::JAccountAuthenticator) + + let(:jac_uid) { "AAAAAAAA-1111-BBBB-AACC-AAAAZZZZCCCC" } + let(:email) { "lisi@sjtu.edu.cn" } + let(:hash) do + OmniAuth::AuthHash.new( + provider: "jaccount", + uid: jac_uid, + info: { + account: "lisi", + email: email, + name: "李四", + code: "114514", + type: "student" + }, + extra: { + raw_info: { + randominfo: "some info", + }, + }, + ) + end + let(:authenticator) { ::Auth::JAccountAuthenticator.new } + + before(:example) do + SiteSetting.remake_limit_enabled = true + SiteSetting.remake_limit_period = 100 + end + + describe "#after_authenticate" do + it "ensure prepended successful" do + expect(::Auth::JAccountAuthenticator.ancestors.include?(described_class)).to be_truthy + end + + context "when invoked from authenticator" do + it "should fails if the user is in remake limit period" do + UserDeletionLog.expects(:find_latest_time_by_email)\ + .with(email, jaccount_name:"lisi").returns(1.days.ago).once + UserDeletionLog.expects(:find_latest_time_by_jaccount_id)\ + .with(jac_uid).returns(2.days.ago).once + result = authenticator.after_authenticate(hash) + expect(result.failed).to be_truthy + expect(result.user).to be_nil + expect(result.failed_reason).to be_include("您的账号正处于注册限制期") + end + # it "returns the result without any modifications if current time is after the old time plus remake_limit_period" do + # allow(SiteSetting).to receive(:remake_limit_period).and_return(7) # Assuming remake_limit_period is 7 days + # allow(Time).to receive(:now).and_return(future_time + 8.days) + + # expect(subject.after_authenticate(auth_token)).to eq(result) + # end + + # it "modifies the result and returns it if current time is before the old time plus remake_limit_period" do + # allow(SiteSetting).to receive(:remake_limit_period).and_return(7) # Assuming remake_limit_period is 7 days + # expected_failed_reason = "您的账号正处于注册限制期,请于#{(old_time + 7.days).in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再登录!" + + # modified_result = subject.after_authenticate(auth_token) + + # expect(modified_result.failed).to eq(true) + # expect(modified_result.failed_reason).to eq(expected_failed_reason) + # expect(modified_result.name).to be_nil + # expect(modified_result.username).to be_nil + # expect(modified_result.email).to be_nil + # expect(modified_result.email_valid).to be_nil + # expect(modified_result.extra_data).to be_nil + # end + end + end +end \ No newline at end of file diff --git a/spec/requests/admin_users_controller_spec.rb b/spec/requests/admin_users_controller_spec.rb new file mode 100644 index 0000000..61c9e1e --- /dev/null +++ b/spec/requests/admin_users_controller_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe Admin::UsersController do + let(:delete_me) { Fabricate(:user, refresh_auto_groups: true) } + let(:admin) { Fabricate(:admin) } + describe "#destroy" do + it "should invoke add_remake_limit" do + UserDeletionLog.expects(:create_log).once + sign_in(admin) + delete "/admin/users/#{delete_me.id}.json" + end + it "should create log" do + sign_in(admin) + delete "/admin/users/#{delete_me.id}.json" + log = UserDeletionLog.last + expect(log.user_id).to eq(delete_me.id) + expect(log.ignore_limit).to be_falsey + end + end + describe "#anonymize" do + it "should invoke add_remake_limit" do + UserDeletionLog.expects(:create_log).once + sign_in(admin) + put "/admin/users/#{delete_me.id}/anonymize.json" + end + it "should create log" do + sign_in(admin) + put "/admin/users/#{delete_me.id}/anonymize.json" + log = UserDeletionLog.last + expect(log.user_id).to eq(delete_me.id) + expect(log.ignore_limit).to be_falsey + end + end +end \ No newline at end of file diff --git a/spec/requests/remake_limit_controller_spec.rb b/spec/requests/remake_limit_controller_spec.rb new file mode 100644 index 0000000..a0dc126 --- /dev/null +++ b/spec/requests/remake_limit_controller_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe RemakeLimit do + describe "controller" do + let(:user) { Fabricate(:user) } + let(:moderator) { Fabricate(:moderator) } + let(:admin) { Fabricate(:admin) } + let(:record) { UserDeletionLog.find_by(email: user.email, user_id: user.id) } + + before(:example) do + SiteSetting.remake_limit_enabled = true + UserDestroyer.new(moderator).destroy(user) + sign_in(admin) + end + context "can handle query" do + it "when no params" do + get "/remake_limit/query.json" + expect(response.status).to eq(400) + end + it "when user not found" do + get "/remake_limit/query.json", :params => { user_id: -100 } + expect(response.status).to eq(404) + end + it "when using user_id " do + get "/remake_limit/query.json", :params => { user_id: user.id } + expect(response.status).to eq(200) + end + it "when using email" do + get "/remake_limit/query.json", :params => { email: user.email } + expect(response.status).to eq(200) + end + + context "when find multiple records" do + let!(:email) { user.email } + before(:example) do + UserDestroyer.new(moderator).destroy(user) + another_user = Fabricate(:user, email: email) + UserDestroyer.new(moderator).destroy(another_user) + end + it "should return all records" do + get "/remake_limit/query.json", :params => { email: email } + expect(response.status).to eq(200) + expect(JSON.parse(response.body).length).to eq(2) + end + end + end + context "can handle ignore" do + it "when record not found" do + delete "/remake_limit/id/-100.json" + expect(response.status).to eq(404) + end + it "when record found" do + delete "/remake_limit/id/#{record.id}.json" + expect(response.status).to eq(200) + expect(record.reload.ignore_limit).to eq(true) + end + end + context "can handle create_for_user" do + it "when user not found" do + put "/remake_limit/-100.json" + expect(response.status).to eq(404) + end + it "when user found" do + another_user = Fabricate(:user) + put "/remake_limit/user/#{another_user.id}.json" + expect(response.status).to eq(200) + new_record = UserDeletionLog.find_by(user_id: another_user.id) + expect(new_record).to be_present + expect(new_record.ignore_limit).to eq(false) + end + end + context "can handle ignore_for_user" do + it "when user not found" do + delete "/remake_limit/user/-100.json" + expect(response.status).to eq(404) + end + it "when user found" do + delete "/remake_limit/user/#{user.id}.json" + expect(response.status).to eq(200) + expect(record.reload.ignore_limit).to eq(true) + end + end + end +end \ No newline at end of file diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb new file mode 100644 index 0000000..57d2ab4 --- /dev/null +++ b/spec/requests/users_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RemakeLimit::OverrideUsersController do + describe "POST #create" do + context "when remake limit is enabled" do + before(:example) do + UsersController.any_instance.stubs(:honeypot_value).returns(nil) + UsersController.any_instance.stubs(:challenge_value).returns(nil) + SiteSetting.allow_new_registrations = true + SiteSetting.remake_limit_enabled = true + SiteSetting.remake_limit_period = 100 + @user = Fabricate.build(:user, email: "foobar@example.com", password: "strongpassword") + end + + let(:post_user_params) do + { name: @user.name, username: @user.username, password: "strongpassword", email: @user.email } + end + + def post_user(extra_params = {}) + post "/u.json", params: post_user_params.merge(extra_params) + end + + it "should invoke check_remake_limit & add_user_note" do + UsersController.any_instance.expects(:check_remake_limit).once + UsersController.any_instance.expects(:add_user_note).once + post_user + end + + it "renders an error message if the email is within the remake limit period" do + UserDeletionLog.expects(:find_latest_time_by_email)\ + .with(@user.email).returns(1.days.ago).once + post_user + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["success"]).to eq(false) + expect(json["message"]).to include("您的邮箱正处于注册限制期") + end + + it "does not render an error message if the email is not within the remake limit period" do + UserDeletionLog.expects(:find_latest_time_by_email)\ + .with(@user.email).returns(nil) + # UserDeletionLog.expects(:find_user_penalty_history).returns([1, 2, 3]) + post_user + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["success"]).to be_truthy + end + + it "add a user note if the user has account history" do + skip "Skip if DiscourseUserNotes is not defined" unless defined?(::DiscourseUserNotes) + UserDeletionLog.expects(:find_latest_time_by_email)\ + .with(@user.email).returns(nil) + UserDeletionLog.expects(:find_user_penalty_history)\ + .with(instance_of(User)).returns([1, 2, 3]).once + post_user + expect( + DiscourseUserNotes.notes_for(User.find_by(username:@user.username).id).length + ).to eq(1) + end + end + end + + describe "DELETE #destroy" do + let(:user) { Fabricate(:user) } + + before(:example) do + SiteSetting.delete_user_self_max_post_count = 100 + SiteSetting.remake_limit_enabled = true + end + + context "when remake limit is enabled" do + it "should invoke create_log twice" do + sign_in(user) + UserDeletionLog.expects(:create_log).twice + delete "/u/#{user.username}.json" + expect(response.status).to eq(200) + end + + it 'should not allow user to delete account if silenced' do + sign_in(user) + user.update!(silenced_till: 1.day.from_now) + delete "/u/#{user.username}.json" + expect(response.status).to eq(422) + end + end + end +end \ No newline at end of file diff --git a/spec/serializers/admin_detailed_user_spec.rb b/spec/serializers/admin_detailed_user_spec.rb new file mode 100644 index 0000000..b983f14 --- /dev/null +++ b/spec/serializers/admin_detailed_user_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AdminDetailedUserSerializer do + include ActiveSupport::Testing::TimeHelpers + describe '#penalty_counts' do + let(:user) { Fabricate(:user) } + let(:admin) { Fabricate(:admin) } + before(:example) do + StaffActionLogger.new(admin).log_silence_user(user) + StaffActionLogger.new(Discourse.system_user).log_unsilence_user(user, {}) + StaffActionLogger.new(admin).log_user_suspend(user, "some reason") + StaffActionLogger.new(Discourse.system_user).log_user_unsuspend(user) + end + + it 'should return penalty counts' do + penalty_counts = AdminDetailedUserSerializer.new(user).penalty_counts + expect(penalty_counts.silenced).to eq(1) + expect(penalty_counts.suspended).to eq(1) + end + + it 'should return records long time ago' do + travel_to 10.year.ago do + StaffActionLogger.new(admin).log_silence_user(user) + StaffActionLogger.new(Discourse.system_user).log_unsilence_user(user, {}) + end + travel_to 5.year.ago do + StaffActionLogger.new(admin).log_user_suspend(user, "some reason") + StaffActionLogger.new(Discourse.system_user).log_user_unsuspend(user) + end + penalty_counts = AdminDetailedUserSerializer.new(user).penalty_counts + expect(penalty_counts.silenced).to eq(2) + expect(penalty_counts.suspended).to eq(2) + end + + it 'should return records for previous accounts' do + UserDeletionLog.create!(user_id: -256, email: user.email, silence_count: 3, suspend_count: 4, user_deleted_at: 1.year.ago, ignore_limit: true) + penalty_counts = AdminDetailedUserSerializer.new(user).penalty_counts + expect(penalty_counts.silenced).to eq(4) + expect(penalty_counts.suspended).to eq(5) + end + + it 'should not count current uer twice' do + UserDeletionLog.create!(user_id: user.id, email: user.email, silence_count: 3, suspend_count: 4, user_deleted_at: 1.year.ago) + penalty_counts = AdminDetailedUserSerializer.new(user).penalty_counts + expect(penalty_counts.silenced).to eq(1) + expect(penalty_counts.suspended).to eq(1) + end + end +end \ No newline at end of file diff --git a/spec/serializers/user_deletion_log_serializer_spec.rb b/spec/serializers/user_deletion_log_serializer_spec.rb new file mode 100644 index 0000000..c9f8885 --- /dev/null +++ b/spec/serializers/user_deletion_log_serializer_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe RemakeLimit::UserDeletionLogSerializer do + let(:user) { Fabricate(:user) } + let(:user_deletion_log) { UserDeletionLog.create_log(user) } + let(:serializer) { described_class.new(user_deletion_log) } + + it 'serializes the attributes' do + expect(serializer.id).to eq(user_deletion_log.id) + expect(serializer.user_id).to eq(user_deletion_log.user_id) + expect(serializer.username).to eq(user_deletion_log.username) + expect(serializer.user_deleted_at).to eq(user_deletion_log.user_deleted_at) + expect(serializer.ignore_limit).to eq(user_deletion_log.ignore_limit) + expect(serializer.silence_count).to eq(user_deletion_log.silence_count) + expect(serializer.suspend_count).to eq(user_deletion_log.suspend_count) + end + + it 'should not serialize sensitive attributes' do + serialized = serializer.as_json[:user_deletion_log] + expect(serialized[:id]).to be_present + expect(serialized[:email]).to be_nil + expect(serialized[:jaccount_id]).to be_nil + expect(serialized[:jaccount_name]).to be_nil + end +end \ No newline at end of file diff --git a/spec/user_deletion_log_spec.rb b/spec/user_deletion_log_spec.rb new file mode 100644 index 0000000..34c7d50 --- /dev/null +++ b/spec/user_deletion_log_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe UserDeletionLog, type: :model do + let(:user) { Fabricate(:user) } + + describe ".create_log" do + it "creates a user deletion log record" do + expect { + UserDeletionLog.create_log(user) + }.to change(UserDeletionLog, :count).by(1) + end + + it "creates a user deletion log record many times" do + UserDeletionLog.create_log(user) + UserDeletionLog.create_log(user) + end + + it "sets the correct attributes in the user deletion log record" do + UserDeletionLog.create_log(user) + + log = UserDeletionLog.last + expect(log.user_id).to eq(user.id) + expect(log.username).to eq(user.username) + expect(log.email).to eq(user.email.downcase) + # Add more expectations for other attributes if needed + end + end + + describe ".find_latest_time_by_email" do + it "can handle case-insensitive email" do + log = UserDeletionLog.create(email: user.email.downcase, user_deleted_at: Time.now) + expect(UserDeletionLog.find_latest_time_by_email(user.email.downcase)).to eq(log.user_deleted_at) + expect(UserDeletionLog.find_latest_time_by_email(user.email.upcase)).to eq(log.user_deleted_at) + end + + it "returns nil if no user deletion log record is found" do + expect(UserDeletionLog.find_latest_time_by_email("nonexistent@example.com")).to be_nil + end + + context "when multiple records are found" do + let!(:log1) { UserDeletionLog.create(email: user.email, user_deleted_at: 1.day.ago) } + let!(:log2) { UserDeletionLog.create(email: user.email, user_deleted_at: 2.days.ago) } + + it "can return the last time" do + expect(UserDeletionLog.find_latest_time_by_email(user.email)).to eq(log1.user_deleted_at) + end + + it "can handle ignore_limit" do + log1.update(ignore_limit: true) + expect(UserDeletionLog.find_latest_time_by_email(user.email)).to eq(log2.user_deleted_at) + end + end + end + + # Add more tests for other class methods if needed +end \ No newline at end of file