Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ SMEE_TUNNEL=https://smee.io/MLq0n9kvAes2vydX
# BYPASS_OAUTH=true

HOST_URL=http://localhost:3009
EDITOR_PUBLIC_URL=http://localhost:3012
EDITOR_PUBLIC_URL=http://classroom.localhost:3013

PROFILE_API_KEY=test # This has to match the value set in Profile (https://github.com/RaspberryPiFoundation/profile/blob/ca10a4f360b6fe2b04be76264e03283054126b0f/.env.example#L45).

Expand Down
15 changes: 15 additions & 0 deletions app/mailers/school_ownership_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class SchoolOwnershipMailer < ApplicationMailer
default from: email_address_with_name('web@raspberrypi.org', 'Raspberry Pi Foundation')

def request_ownership_transfer
@school = params[:ownership_transfer].school
@token = params[:ownership_transfer].generate_token_for(:ownership_transfer)

mail(to: params[:ownership_transfer].email_address,
subject: "You have been asked to take ownership of #{@school.name}",
track_opens: 'true',
message_stream: 'outbound')
end
end
21 changes: 21 additions & 0 deletions app/models/ownership_transfer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

class OwnershipTransfer < ApplicationRecord
delegate :name, to: :school, prefix: true

belongs_to :school
validates :email_address,
format: { with: EmailValidator.regexp, message: I18n.t('validations.invitation.email_address') }
after_create_commit :send_ownership_transfer_request_email
encrypts :email_address

generates_token_for :ownership_transfer, expires_in: 30.days do
email_address
end

private

def send_ownership_transfer_request_email
SchoolOwnershipMailer.with(ownership_transfer: self).request_ownership_transfer.deliver_later
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
You have been asked to take ownership of:

<%= @school.name %>

Log in to your account to accept this request and become the owner of this school account.

Accept ownership transfer:

<%= "#{ENV.fetch('EDITOR_PUBLIC_URL')}/en/ownership_transfers/#{@token}" %>

--
Raspberry Pi Foundation
Copyright Raspberry Pi Foundation UK registered charity 1129409
All rights reserved
13 changes: 13 additions & 0 deletions db/migrate/20260911104254_ownership_transfers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class OwnershipTransfers < ActiveRecord::Migration[8.1]
def change
create_table :ownership_transfers, id: :uuid do |t|
t.string :email_address
t.datetime :accepted_at
t.references :school, null: false, foreign_key: true, type: :uuid

t.timestamps
end
end
end
12 changes: 11 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions spec/factories/ownership_transfer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

FactoryBot.define do
factory :ownership_transfer do
email_address { 'new-owner@example.com' }
school factory: :verified_school
end
end
2 changes: 1 addition & 1 deletion spec/mailers/previews/invitation_preview.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Preview all emails at http://localhost:3000/rails/mailers/invitation
# Preview all emails at http://localhost:3009/rails/mailers/invitation
class InvitationPreview < ActionMailer::Preview
def invite_teacher
school = School.new(name: 'Elmwood Secondary School')
Expand Down
10 changes: 10 additions & 0 deletions spec/mailers/previews/school_ownership_mailer_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Preview all emails at http://localhost:3009/rails/mailers/school_ownership_mailer
class SchoolOwnershipMailerPreview < ActionMailer::Preview
def request_ownership_transfer
school = School.new(name: 'Elmwood Secondary School')
ownership_transfer = OwnershipTransfer.new(email_address: 'teacher@example.com', school:)
SchoolOwnershipMailer.with(ownership_transfer:).request_ownership_transfer
end
end
29 changes: 29 additions & 0 deletions spec/mailers/school_ownership_mailer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SchoolOwnershipMailer do
describe 'request_ownership_transfer' do
subject(:email) { described_class.with(ownership_transfer:).request_ownership_transfer }

let(:ownership_transfer) { create(:ownership_transfer) }

before do
allow(ENV).to receive(:fetch).with('EDITOR_PUBLIC_URL').and_return('http://example.com')
end

it 'includes the school name in the body' do
expect(email.body.to_s).to include(ownership_transfer.school.name)
end

it 'includes a link to respond to the ownership transfer request in the body' do
allow(ownership_transfer).to receive(:generate_token_for).and_return('token-id')

expect(email.body.to_s).to include('http://example.com/en/ownership_transfers/token-id')
end

it 'includes the school name in the subject' do
expect(email.subject).to include(ownership_transfer.school.name)
end
end
end
67 changes: 67 additions & 0 deletions spec/models/ownership_transfer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe OwnershipTransfer do
include ActionMailer::TestHelper
include ActiveSupport::Testing::TimeHelpers

it 'has a valid factory' do
ownership_transfer = build(:ownership_transfer)

expect(ownership_transfer).to be_valid
end

it 'is invalid with an incorrectly formatted email address' do
ownership_transfer = build(:ownership_transfer, email_address: 'not-an-email-address')

expect(ownership_transfer).not_to be_valid
end

it 'sends an ownership transfer request email after create' do
school = create(:verified_school)

ownership_transfer = described_class.create!(email_address: 'new-owner@example.com', school:)

assert_enqueued_email_with SchoolOwnershipMailer, :request_ownership_transfer, params: { ownership_transfer: }
end

it 'generates a token for ownership transfer' do
ownership_transfer = create(:ownership_transfer)
token = ownership_transfer.generate_token_for(:ownership_transfer)

expect(described_class.find_by_token_for(:ownership_transfer, token)).to eq(ownership_transfer)
end

it 'generates a token valid for 30 days' do
ownership_transfer = create(:ownership_transfer)
token = ownership_transfer.generate_token_for(:ownership_transfer)

travel 31.days do
expect(described_class.find_by_token_for(:ownership_transfer, token)).to be_nil
end
end

it 'invalidates the token if the email address changes' do
ownership_transfer = create(:ownership_transfer)
token = ownership_transfer.generate_token_for(:ownership_transfer)

ownership_transfer.update(email_address: 'different-owner@example.com')

expect(described_class.find_by_token_for(:ownership_transfer, token)).to be_nil
end

it 'delegates #school_name to School#name' do
school = build(:school, name: 'school-name')
ownership_transfer = build(:ownership_transfer, school:)

expect(ownership_transfer.school_name).to eq('school-name')
end

it 'non-deterministically encrypts the email_address' do
school = create(:verified_school)
described_class.create!(email_address: 'new-owner@example.com', school:)

expect(described_class.find_by(email_address: 'new-owner@example.com')).to be_nil
end
end
Loading