diff --git a/app/views/layouts/mailer.html.slim b/app/views/layouts/mailer.html.slim
new file mode 100644
index 00000000..7cb1224a
--- /dev/null
+++ b/app/views/layouts/mailer.html.slim
@@ -0,0 +1,5 @@
+html xmlns="http://www.w3.org/1999/xhtml"
+ head
+ meta http-equiv="Content-Type" content="text/html; charset=utf-8"
+ meta name="viewport" content="width=device-width, initial-scale=1.0"
+ body lang=I18n.locale = yield
diff --git a/app/views/user_mailer/login_code.html.slim b/app/views/user_mailer/login_code.html.slim
new file mode 100644
index 00000000..58629e0b
--- /dev/null
+++ b/app/views/user_mailer/login_code.html.slim
@@ -0,0 +1 @@
+p Your login code is #{@code}
diff --git a/spec/support/mail_helper.rb b/spec/support/mail_helper.rb
new file mode 100644
index 00000000..2a7dc7c3
--- /dev/null
+++ b/spec/support/mail_helper.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class TestMail
+ attr_reader :to, :from, :subject, :body
+
+ def initialize(mail)
+ @to = mail["to"]
+ @from = mail["from"]
+ @subject = mail.subject
+ @original_body = mail.body.to_s
+ @body = Nokogiri::HTML(mail.body.to_s)
+ end
+
+ def link_urls
+ body.css("a").pluck("href")
+ end
+end
+
+module MailHelper
+ class NoMailSentError < StandardError; end
+
+ def last_mail
+ return nil unless ActionMailer::Base.deliveries.count > 0
+
+ TestMail.new(ActionMailer::Base.deliveries.last)
+ end
+
+ def last_mail!
+ last_mail || raise(NoMailSentError)
+ end
+
+ def reset_mails
+ ActionMailer::Base.deliveries = []
+ end
+
+ def run_jobs
+ # recursivly run the job queue until there are no jobs remaining
+ count = perform_enqueued_jobs
+ run_jobs if count.to_i > 0
+ end
+end
+
+RSpec.configure do |config|
+ config.before do
+ ActionMailer::Base.deliveries = []
+ end
+ config.include MailHelper, type: :system
+ config.include MailHelper, type: :model
+end
diff --git a/spec/system/session_spec.rb b/spec/system/session_spec.rb
new file mode 100644
index 00000000..b6e910cf
--- /dev/null
+++ b/spec/system/session_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "system_helper"
+
+RSpec.describe "Sessions" do
+ fixtures :all
+
+ it "logs a user in via a code" do
+ visit root_path
+ expect(page).to have_content "Login"
+ fill_in "Email", with: "admin@example.com"
+ click_on "Login"
+ expect(page).to have_content "Code"
+
+ run_jobs
+ expect(last_mail!.body).to have_content "Your login code is"
+ code = last_mail.body.to_s.scan(/\d{6}/).first
+ expect(code).to be_present
+ fill_in "Code", with: code
+ click_on "Login"
+ expect(page).to have_content "Hello"
+ end
+
+ it "logs the user out" do
+ login :john
+ visit logout_path
+ expect(page).to have_content "Login"
+ end
+end