From 69639edbabf8ec1f83960e2d293df6b2c9f830e3 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sun, 13 Sep 2026 16:51:12 +0200 Subject: [PATCH 1/5] Add admin member activity strip service --- app/services/admin/members/activity_strip.rb | 57 +++++++++++++++++++ .../admin/members/activity_strip_spec.rb | 52 +++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 app/services/admin/members/activity_strip.rb create mode 100644 spec/services/admin/members/activity_strip_spec.rb diff --git a/app/services/admin/members/activity_strip.rb b/app/services/admin/members/activity_strip.rb new file mode 100644 index 000000000..9feacca6b --- /dev/null +++ b/app/services/admin/members/activity_strip.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Admin + module Members + # Buckets a member's activity log into the 52 ISO weeks ending the current week. + # Sole owner of strip state classification; the component renders, never classifies. + class ActivityStrip + WEEK_COUNT = 52 + LOGIN_ONLY_KEYS = %w[member.login member.logout].freeze + + Row = Struct.new(:week_start, :state, :counts, keyword_init: true) + + def initialize(member, now: Time.zone.now) + @member = member + @now = now + end + + def rows # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + activities = PublicActivity::Activity + .where(owner: @member) + .where(created_at: window_start..@now) + .order(:created_at) + + grouped = activities.group_by { |a| a.created_at.to_date.beginning_of_week.beginning_of_day } + + weeks.map do |week_start| + week_activities = grouped[week_start] || [] + counts = week_activities.map(&:key).tally + state = classify(counts) + + Row.new(week_start:, state:, counts:) + end + end + + private + + def classify(counts) + return :empty if counts.empty? + return :login_only if counts.keys.all? { |key| LOGIN_ONLY_KEYS.include?(key) } + + :active + end + + def weeks + @weeks ||= Array.new(WEEK_COUNT) { |i| current_week_start - (WEEK_COUNT - 1 - i).weeks } + end + + def window_start + @window_start ||= current_week_start - (WEEK_COUNT - 1).weeks + end + + def current_week_start + @current_week_start ||= @now.to_date.beginning_of_week.beginning_of_day + end + end + end +end diff --git a/spec/services/admin/members/activity_strip_spec.rb b/spec/services/admin/members/activity_strip_spec.rb new file mode 100644 index 000000000..e6af6df0f --- /dev/null +++ b/spec/services/admin/members/activity_strip_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::Members::ActivityStrip do + let(:member) { Fabricate(:member) } + let(:now) { Time.zone.local(2026, 9, 2, 12, 0, 0) } # Wednesday, current week starts Mon 31 Aug + let(:strip) { described_class.new(member, now:) } + + def activity_at(time, key: 'member.login') + PublicActivity::Activity.create!(owner: member, key:, trackable: member, + created_at: time, updated_at: time) + end + + it 'returns 52 rows oldest first' do + rows = strip.rows + + expect(rows.size).to eq(52) + expect(rows.first.week_start).to eq(Time.zone.local(2025, 9, 8)) + expect(rows.last.week_start).to eq(Time.zone.local(2026, 8, 31)) + end + + it 'marks weeks with no rows as empty' do + expect(strip.rows.map(&:state)).to all(eq(:empty)) + end + + it 'marks login-only weeks as login_only' do + activity_at(now - 2.weeks, key: 'member.login') + + expect(strip.rows[-3].state).to eq(:login_only) + end + + it 'marks weeks with any non-login key as active' do + activity_at(now - 2.weeks, key: 'event_invitation.rsvp') + + expect(strip.rows[-3].state).to eq(:active) + end + + it 'counts keys for tooltips' do + activity_at(now - 1.week, key: 'member.login') + activity_at(now - 1.week, key: 'event_invitation.rsvp') + + expect(strip.rows[-2].counts).to eq('member.login' => 1, 'event_invitation.rsvp' => 1) + end + + it 'buckets by ISO week with the boundary at window start' do + activity_at(strip.rows.first.week_start) # exactly at the window edge + activity_at(strip.rows.first.week_start - 1.second) # one second before: outside + + expect(strip.rows.first.state).to eq(:login_only) + end +end From 93675110aeeff8e72e0bdbb54df37bd1649ff729 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sun, 13 Sep 2026 16:51:22 +0200 Subject: [PATCH 2/5] Add activity strip view component --- app/assets/stylesheets/application.scss | 1 + .../stylesheets/partials/_activity_strip.scss | 8 ++++ .../members/activity_strip_component.html.erb | 15 ++++++++ .../admin/members/activity_strip_component.rb | 32 ++++++++++++++++ .../members/activity_strip_component_spec.rb | 38 +++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 app/assets/stylesheets/partials/_activity_strip.scss create mode 100644 app/components/admin/members/activity_strip_component.html.erb create mode 100644 app/components/admin/members/activity_strip_component.rb create mode 100644 spec/components/admin/members/activity_strip_component_spec.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 71cbed7d0..3af1249cd 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -23,6 +23,7 @@ $primary: $dark-codebar-blue !default; @import "bootstrap-custom"; +@import "partials/activity_strip"; /* Bootstrap's Reboot sets legends to float: left, which puts the first check box in a fieldset off to the right instead of underneath the legend. This overrides that. */ diff --git a/app/assets/stylesheets/partials/_activity_strip.scss b/app/assets/stylesheets/partials/_activity_strip.scss new file mode 100644 index 000000000..5fac96ec5 --- /dev/null +++ b/app/assets/stylesheets/partials/_activity_strip.scss @@ -0,0 +1,8 @@ +// app/assets/stylesheets/partials/_activity_strip.scss +.activity-strip { + .activity-cell { + &-empty { fill: $gray-200; } + &-login_only { fill: $gray-400; } + &-active { fill: $codebar-blue; } + } +} diff --git a/app/components/admin/members/activity_strip_component.html.erb b/app/components/admin/members/activity_strip_component.html.erb new file mode 100644 index 000000000..a014b5e7c --- /dev/null +++ b/app/components/admin/members/activity_strip_component.html.erb @@ -0,0 +1,15 @@ +<%# app/components/admin/members/activity_strip_component.html.erb %> +
+
Activity — last 12 months
+ + <% weeks.each_with_index do |week, i| %> + + <%= title_for(week) %> + + <% end %> + +
diff --git a/app/components/admin/members/activity_strip_component.rb b/app/components/admin/members/activity_strip_component.rb new file mode 100644 index 000000000..f2a0f4e6d --- /dev/null +++ b/app/components/admin/members/activity_strip_component.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Admin + module Members + class ActivityStripComponent < ViewComponent::Base + CELL_WIDTH = 8 + CELL_GAP = 4 + + def initialize(weeks:) + super() + @weeks = weeks + end + + private + + attr_reader :weeks + + def title_for(week) + summary = week.counts.map { |key, count| "#{count} #{key.tr('.', ' ').humanize}" }.join(', ') + "#{iso_week(week)} (week of #{week.week_start.strftime('%-d %b %Y')}): #{summary.presence || 'no activity'}" + end + + def iso_week(week) + week.week_start.strftime('%G-W%V') + end + + def svg_width + weeks.size * (CELL_WIDTH + CELL_GAP) + end + end + end +end diff --git a/spec/components/admin/members/activity_strip_component_spec.rb b/spec/components/admin/members/activity_strip_component_spec.rb new file mode 100644 index 000000000..8618bed76 --- /dev/null +++ b/spec/components/admin/members/activity_strip_component_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::Members::ActivityStripComponent do + let(:member) { Fabricate(:member) } + let(:now) { Time.zone.local(2026, 9, 2, 12, 0, 0) } + let(:rows) do + Admin::Members::ActivityStrip.new(member, now:).tap do |_strip| + PublicActivity::Activity.create!(owner: member, trackable: member, key: 'member.login', + created_at: now - 1.week, updated_at: now - 1.week) + PublicActivity::Activity.create!(owner: member, trackable: member, key: 'event_invitation.rsvp', + created_at: now - 2.weeks, updated_at: now - 2.weeks) + end.rows + end + + before { render_inline(described_class.new(weeks: rows)) } + + it 'renders 52 cells' do + expect(page).to have_css('rect', count: 52) + end + + it 'renders all three state classes' do + expect(page).to have_css('.activity-cell-empty') + expect(page).to have_css('.activity-cell-login_only') + expect(page).to have_css('.activity-cell-active') + end + + it 'renders a tooltip with the week and counts' do + expect(page).to have_css('rect title', text: 'Event invitation rsvp') + end + + it 'labels each cell with its ISO week' do + expect(page).to have_css('rect[aria-label]', count: 52) + expect(page).to have_css("rect[aria-label='#{(now - 2.weeks).to_date.beginning_of_week.strftime('%G-W%V')}']") + expect(page).to have_css('rect title', text: /^2026-W/) + end +end From d8085439c2ada94a2f1875217039d9a28952dd5a Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Sun, 13 Sep 2026 16:51:31 +0200 Subject: [PATCH 3/5] Render activity strip and activity log on admin member profile --- app/controllers/admin/members_controller.rb | 6 +- app/helpers/application_helper.rb | 19 +++ app/views/admin/members/show.html.haml | 22 ++++ config/locales/en.yml | 4 + .../admin/members_controller_spec.rb | 114 ++++++++++++++---- 5 files changed, 137 insertions(+), 28 deletions(-) diff --git a/app/controllers/admin/members_controller.rb b/app/controllers/admin/members_controller.rb index a649f6815..16d511508 100644 --- a/app/controllers/admin/members_controller.rb +++ b/app/controllers/admin/members_controller.rb @@ -24,9 +24,11 @@ def search end def show - @member = MemberPresenter.new(Member.find(params[:id])) + member = Member.find(params[:id]) + @member = MemberPresenter.new(member) load_attendance_data(@member) - + @activity_weeks = Admin::Members::ActivityStrip.new(member).rows + @activities = PublicActivity::Activity.where(owner: member).order(created_at: :desc).limit(30) @actions = admin_actions(@member).sort_by(&:created_at).reverse end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 72c890099..34cb23bc0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,6 +60,25 @@ def sponsorship_level_title(level) "#{level.humanize} sponsors" end + # Admin-page subject of a member activity: the entity the action was about. + # Returns nil when the subject is the member themselves (e.g. logins, bans). + def activity_subject(activity) + case (trackable = activity.trackable) + when WorkshopInvitation then trackable.workshop + when Invitation then trackable.event + when MeetingInvitation then trackable.meeting + when Member then nil + else trackable + end + end + + def activity_subject_label(subject) + return "#{subject.chapter.name} #{subject.name}" if subject.is_a?(Group) + return subject.full_name if subject.respond_to?(:full_name) + + subject.try(:name) || subject.to_s + end + private def humanize_date_with_time(datetime, end_time) diff --git a/app/views/admin/members/show.html.haml b/app/views/admin/members/show.html.haml index d9c25b0cb..e98021b17 100644 --- a/app/views/admin/members/show.html.haml +++ b/app/views/admin/members/show.html.haml @@ -29,3 +29,25 @@ .row.mt-4 .col = link_to 'View all RSVPS', admin_member_events_path(@member), class: 'btn btn-primary btn-block' + + .row.mt-4 + .col + %h3.mt-4 Activity + = render Admin::Members::ActivityStripComponent.new(weeks: @activity_weeks) + + %ul.list-unstyled.activity-log.mt-3 + - @activities.group_by { |a| a.created_at.beginning_of_week.to_date }.each do |week_start, activities| + %li + %h5.mt-3 + #{week_start.strftime('%G-W%V')} + %span.small.text-muted (week of #{week_start.strftime('%-d %b %Y')}) + %ul.list-unstyled + - activities.each do |activity| + %li.pb-2.mb-2 + = activity.key.tr('.', ' ').humanize + - if (subject = activity_subject(activity)) + %span.text-muted.ms-1 + = t('admin.members.activity.for', subject: link_to(activity_subject_label(subject), [:admin, subject])).html_safe + %span.text-muted.ms-2= l(activity.created_at, format: :activity_log) + - if @activities.empty? + %li.text-muted No recorded activity yet diff --git a/config/locales/en.yml b/config/locales/en.yml index 9cd7385de..729d2b661 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -59,6 +59,7 @@ en: full_date: "%d %B %Y" default_date: "%d/%m/%Y" website_format: "%a, %d %B %Y at %H:%M" + activity_log: "%A, %Y-%m-%d %H:%M" day_in_words: "%a" day: "%d" month: "%B" @@ -658,6 +659,9 @@ en: admin: dashboard: title: "Admin" + members: + activity: + for: "for %{subject}" workshop: manage_rsvps: text: "RSVP Students and Coaches to the workshop. The attendance confirmation email will be triggered and they will be removed from the waiting list." diff --git a/spec/controllers/admin/members_controller_spec.rb b/spec/controllers/admin/members_controller_spec.rb index a3f2182bc..6ab248a8a 100644 --- a/spec/controllers/admin/members_controller_spec.rb +++ b/spec/controllers/admin/members_controller_spec.rb @@ -112,47 +112,109 @@ end end - describe 'GET #send_eligibility_email' do + describe 'GET #show' do + let(:admin) { Fabricate(:member) } + let(:chapter) { Fabricate(:chapter) } + + before do + admin.add_role(:admin) + login_as_admin(admin) + end + + describe 'activity strip' do + render_views + + it 'renders the strip for any member' do + member = Fabricate(:member) + + get :show, params: { id: member.id } + + expect(response.body).to include('activity-strip') + end + end + + describe 'activity log' do + render_views + let(:member) { Fabricate(:member) } - let(:admin) { Fabricate(:member) } - before do - admin.add_role(:admin) - login_as_admin(admin) + it 'shows the latest 30 activities for any member' do + 30.times do |i| + MemberActivityRecorder.record(actor: member, key: 'member.login', + trackable: member) + PublicActivity::Activity.last.update!(created_at: i.days.ago) + end + MemberActivityRecorder.record(actor: member, key: 'member.logout', trackable: member) + PublicActivity::Activity.last.update!(created_at: 31.days.ago) + + get :show, params: { id: member.id } + + within('.activity-log') do + expect(page).to have_text('Member login') + expect(page).to have_no_text('Member logout') + expect(page).to have_no_text('No recorded activity') + end end - it 'creates an eligibility inquiry' do - expect do - get :send_eligibility_email, params: { member_id: member.id } - end.to change(EligibilityInquiry, :count).by(1) + it 'shows an empty state for members without activity' do + get :show, params: { id: member.id } + + expect(response.body).to include('No recorded activity yet') end - it 'sends an eligibility check email' do - mailer = double(deliver_now: true) - allow(MemberMailer).to receive(:eligibility_check) - .with(member, member.email) - .and_return(mailer) + it 'links to the related entity of an activity' do + invitation = Fabricate(:invitation) + MemberActivityRecorder.record(actor: invitation.member, key: 'event_invitation.rsvp', + trackable: invitation) - get :send_eligibility_email, params: { member_id: member.id } + get :show, params: { id: invitation.member.id } - expect(MemberMailer).to have_received(:eligibility_check) - .with(member, member.email) + expect(response.body).to include("href=\"#{admin_event_path(invitation.event)}\"") end + end + end - it 'redirects to the member page' do - get :send_eligibility_email, params: { member_id: member.id } + describe 'GET #send_eligibility_email' do + let(:member) { Fabricate(:member) } + let(:admin) { Fabricate(:member) } - expect(response).to redirect_to([:admin, member]) - end + before do + admin.add_role(:admin) + login_as_admin(admin) + end - context 'when not authenticated' do - before { login(Fabricate(:member)) } + it 'creates an eligibility inquiry' do + expect do + get :send_eligibility_email, params: { member_id: member.id } + end.to change(EligibilityInquiry, :count).by(1) + end + + it 'sends an eligibility check email' do + mailer = double(deliver_now: true) + allow(MemberMailer).to receive(:eligibility_check) + .with(member, member.email) + .and_return(mailer) - it 'redirects to login' do get :send_eligibility_email, params: { member_id: member.id } - expect(response).to have_http_status(:found) + expect(MemberMailer).to have_received(:eligibility_check) + .with(member, member.email) + end + + it 'redirects to the member page' do + get :send_eligibility_email, params: { member_id: member.id } + + expect(response).to redirect_to([:admin, member]) + end + + context 'when not authenticated' do + before { login(Fabricate(:member)) } + + it 'redirects to login' do + get :send_eligibility_email, params: { member_id: member.id } + + expect(response).to have_http_status(:found) + end end - end end end From 066fb1860357a8c73426f3c97a603fab2bd28505 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Tue, 15 Sep 2026 11:18:08 +0200 Subject: [PATCH 4/5] Use Data.define for ActivityStrip::Row Requested by Olle in PR review; Data is immutable and more precise than Struct for value objects. --- app/services/admin/members/activity_strip.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/admin/members/activity_strip.rb b/app/services/admin/members/activity_strip.rb index 9feacca6b..6adfbe5e8 100644 --- a/app/services/admin/members/activity_strip.rb +++ b/app/services/admin/members/activity_strip.rb @@ -8,7 +8,7 @@ class ActivityStrip WEEK_COUNT = 52 LOGIN_ONLY_KEYS = %w[member.login member.logout].freeze - Row = Struct.new(:week_start, :state, :counts, keyword_init: true) + Row = Data.define(:week_start, :state, :counts) def initialize(member, now: Time.zone.now) @member = member From 3da3ea0d5878f9e48ec5fb87a71da56d757f54dd Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Tue, 15 Sep 2026 11:31:52 +0200 Subject: [PATCH 5/5] Fix spec indentation per review feedback Reindent the GET #show and GET #send_eligibility_email blocks in the admin members controller spec and wrap activity-strip component spec create! arguments for legibility in diff mode. --- .../members/activity_strip_component_spec.rb | 12 +- .../admin/members_controller_spec.rb | 142 +++++++++--------- 2 files changed, 79 insertions(+), 75 deletions(-) diff --git a/spec/components/admin/members/activity_strip_component_spec.rb b/spec/components/admin/members/activity_strip_component_spec.rb index 8618bed76..37891aaec 100644 --- a/spec/components/admin/members/activity_strip_component_spec.rb +++ b/spec/components/admin/members/activity_strip_component_spec.rb @@ -7,10 +7,14 @@ let(:now) { Time.zone.local(2026, 9, 2, 12, 0, 0) } let(:rows) do Admin::Members::ActivityStrip.new(member, now:).tap do |_strip| - PublicActivity::Activity.create!(owner: member, trackable: member, key: 'member.login', - created_at: now - 1.week, updated_at: now - 1.week) - PublicActivity::Activity.create!(owner: member, trackable: member, key: 'event_invitation.rsvp', - created_at: now - 2.weeks, updated_at: now - 2.weeks) + PublicActivity::Activity.create!( + owner: member, trackable: member, key: 'member.login', + created_at: now - 1.week, updated_at: now - 1.week + ) + PublicActivity::Activity.create!( + owner: member, trackable: member, key: 'event_invitation.rsvp', + created_at: now - 2.weeks, updated_at: now - 2.weeks + ) end.rows end diff --git a/spec/controllers/admin/members_controller_spec.rb b/spec/controllers/admin/members_controller_spec.rb index 6ab248a8a..3b3267b8a 100644 --- a/spec/controllers/admin/members_controller_spec.rb +++ b/spec/controllers/admin/members_controller_spec.rb @@ -113,108 +113,108 @@ end describe 'GET #show' do - let(:admin) { Fabricate(:member) } - let(:chapter) { Fabricate(:chapter) } + let(:admin) { Fabricate(:member) } + let(:chapter) { Fabricate(:chapter) } - before do - admin.add_role(:admin) - login_as_admin(admin) - end + before do + admin.add_role(:admin) + login_as_admin(admin) + end - describe 'activity strip' do - render_views + describe 'activity strip' do + render_views - it 'renders the strip for any member' do - member = Fabricate(:member) + it 'renders the strip for any member' do + member = Fabricate(:member) - get :show, params: { id: member.id } + get :show, params: { id: member.id } - expect(response.body).to include('activity-strip') + expect(response.body).to include('activity-strip') + end end - end - describe 'activity log' do - render_views + describe 'activity log' do + render_views - let(:member) { Fabricate(:member) } + let(:member) { Fabricate(:member) } - it 'shows the latest 30 activities for any member' do - 30.times do |i| - MemberActivityRecorder.record(actor: member, key: 'member.login', - trackable: member) - PublicActivity::Activity.last.update!(created_at: i.days.ago) - end - MemberActivityRecorder.record(actor: member, key: 'member.logout', trackable: member) - PublicActivity::Activity.last.update!(created_at: 31.days.ago) + it 'shows the latest 30 activities for any member' do + 30.times do |i| + MemberActivityRecorder.record(actor: member, key: 'member.login', + trackable: member) + PublicActivity::Activity.last.update!(created_at: i.days.ago) + end + MemberActivityRecorder.record(actor: member, key: 'member.logout', trackable: member) + PublicActivity::Activity.last.update!(created_at: 31.days.ago) - get :show, params: { id: member.id } + get :show, params: { id: member.id } - within('.activity-log') do - expect(page).to have_text('Member login') - expect(page).to have_no_text('Member logout') - expect(page).to have_no_text('No recorded activity') + within('.activity-log') do + expect(page).to have_text('Member login') + expect(page).to have_no_text('Member logout') + expect(page).to have_no_text('No recorded activity') + end end - end - it 'shows an empty state for members without activity' do - get :show, params: { id: member.id } + it 'shows an empty state for members without activity' do + get :show, params: { id: member.id } - expect(response.body).to include('No recorded activity yet') - end + expect(response.body).to include('No recorded activity yet') + end - it 'links to the related entity of an activity' do - invitation = Fabricate(:invitation) - MemberActivityRecorder.record(actor: invitation.member, key: 'event_invitation.rsvp', - trackable: invitation) + it 'links to the related entity of an activity' do + invitation = Fabricate(:invitation) + MemberActivityRecorder.record(actor: invitation.member, key: 'event_invitation.rsvp', + trackable: invitation) - get :show, params: { id: invitation.member.id } + get :show, params: { id: invitation.member.id } - expect(response.body).to include("href=\"#{admin_event_path(invitation.event)}\"") + expect(response.body).to include("href=\"#{admin_event_path(invitation.event)}\"") + end end end - end describe 'GET #send_eligibility_email' do - let(:member) { Fabricate(:member) } - let(:admin) { Fabricate(:member) } + let(:member) { Fabricate(:member) } + let(:admin) { Fabricate(:member) } - before do - admin.add_role(:admin) - login_as_admin(admin) - end + before do + admin.add_role(:admin) + login_as_admin(admin) + end - it 'creates an eligibility inquiry' do - expect do - get :send_eligibility_email, params: { member_id: member.id } - end.to change(EligibilityInquiry, :count).by(1) - end + it 'creates an eligibility inquiry' do + expect do + get :send_eligibility_email, params: { member_id: member.id } + end.to change(EligibilityInquiry, :count).by(1) + end - it 'sends an eligibility check email' do - mailer = double(deliver_now: true) - allow(MemberMailer).to receive(:eligibility_check) - .with(member, member.email) - .and_return(mailer) + it 'sends an eligibility check email' do + mailer = double(deliver_now: true) + allow(MemberMailer).to receive(:eligibility_check) + .with(member, member.email) + .and_return(mailer) - get :send_eligibility_email, params: { member_id: member.id } + get :send_eligibility_email, params: { member_id: member.id } - expect(MemberMailer).to have_received(:eligibility_check) - .with(member, member.email) - end + expect(MemberMailer).to have_received(:eligibility_check) + .with(member, member.email) + end - it 'redirects to the member page' do - get :send_eligibility_email, params: { member_id: member.id } + it 'redirects to the member page' do + get :send_eligibility_email, params: { member_id: member.id } - expect(response).to redirect_to([:admin, member]) - end + expect(response).to redirect_to([:admin, member]) + end - context 'when not authenticated' do - before { login(Fabricate(:member)) } + context 'when not authenticated' do + before { login(Fabricate(:member)) } - it 'redirects to login' do - get :send_eligibility_email, params: { member_id: member.id } + it 'redirects to login' do + get :send_eligibility_email, params: { member_id: member.id } - expect(response).to have_http_status(:found) - end + expect(response).to have_http_status(:found) end + end end end