aboutsummaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/flows/.keep0
-rw-r--r--spec/flows/authentication_spec.cr31
-rw-r--r--spec/flows/reset_password_spec.cr18
-rw-r--r--spec/requests/api/me/show_spec.cr17
-rw-r--r--spec/requests/api/sign_ins/create_spec.cr33
-rw-r--r--spec/requests/api/sign_ups/create_spec.cr34
-rw-r--r--spec/setup/.keep0
-rw-r--r--spec/setup/clean_database.cr3
-rw-r--r--spec/setup/configure_lucky_flow.cr37
-rw-r--r--spec/setup/reset_emails.cr3
-rw-r--r--spec/setup/setup_database.cr2
-rw-r--r--spec/setup/start_app_server.cr10
-rw-r--r--spec/spec_helper.cr26
-rw-r--r--spec/support/.keep0
-rw-r--r--spec/support/api_client.cr12
-rw-r--r--spec/support/factories/.keep0
-rw-r--r--spec/support/factories/user_factory.cr6
-rw-r--r--spec/support/flows/authentication_flow.cr45
-rw-r--r--spec/support/flows/base_flow.cr3
-rw-r--r--spec/support/flows/reset_password_flow.cr42
20 files changed, 322 insertions, 0 deletions
diff --git a/spec/flows/.keep b/spec/flows/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/spec/flows/.keep
diff --git a/spec/flows/authentication_spec.cr b/spec/flows/authentication_spec.cr
new file mode 100644
index 0000000..b22f93c
--- /dev/null
+++ b/spec/flows/authentication_spec.cr
@@ -0,0 +1,31 @@
+require "../spec_helper"
+
+describe "Authentication flow", tags: "flow" do
+ it "works" do
+ flow = AuthenticationFlow.new("test@example.com")
+
+ flow.sign_up "password"
+ flow.should_be_signed_in
+ flow.sign_out
+ flow.sign_in "wrong-password"
+ flow.should_have_password_error
+ flow.sign_in "password"
+ flow.should_be_signed_in
+ end
+
+ # This is to show you how to sign in as a user during tests.
+ # Use the `visit` method's `as` option in your tests to sign in as that user.
+ #
+ # Feel free to delete this once you have other tests using the 'as' option.
+ it "allows sign in through backdoor when testing" do
+ user = UserFactory.create
+ flow = BaseFlow.new
+
+ flow.visit Me::Show, as: user
+ should_be_signed_in(flow)
+ end
+end
+
+private def should_be_signed_in(flow)
+ flow.should have_element("@sign-out-button")
+end
diff --git a/spec/flows/reset_password_spec.cr b/spec/flows/reset_password_spec.cr
new file mode 100644
index 0000000..b28af34
--- /dev/null
+++ b/spec/flows/reset_password_spec.cr
@@ -0,0 +1,18 @@
+require "../spec_helper"
+
+describe "Reset password flow", tags: "flow" do
+ it "works" do
+ user = UserFactory.create
+ flow = ResetPasswordFlow.new(user)
+
+ flow.request_password_reset
+ flow.should_have_sent_reset_email
+ flow.reset_password "new-password"
+ flow.should_be_signed_in
+ flow.sign_out
+ flow.sign_in "wrong-password"
+ flow.should_have_password_error
+ flow.sign_in "new-password"
+ flow.should_be_signed_in
+ end
+end
diff --git a/spec/requests/api/me/show_spec.cr b/spec/requests/api/me/show_spec.cr
new file mode 100644
index 0000000..0e1f91f
--- /dev/null
+++ b/spec/requests/api/me/show_spec.cr
@@ -0,0 +1,17 @@
+require "../../../spec_helper"
+
+describe Api::Me::Show do
+ it "returns the signed in user" do
+ user = UserFactory.create
+
+ response = ApiClient.auth(user).exec(Api::Me::Show)
+
+ response.should send_json(200, email: user.email)
+ end
+
+ it "fails if not authenticated" do
+ response = ApiClient.exec(Api::Me::Show)
+
+ response.status_code.should eq(401)
+ end
+end
diff --git a/spec/requests/api/sign_ins/create_spec.cr b/spec/requests/api/sign_ins/create_spec.cr
new file mode 100644
index 0000000..520c2df
--- /dev/null
+++ b/spec/requests/api/sign_ins/create_spec.cr
@@ -0,0 +1,33 @@
+require "../../../spec_helper"
+
+describe Api::SignIns::Create do
+ it "returns a token" do
+ UserToken.stub_token("fake-token") do
+ user = UserFactory.create
+
+ response = ApiClient.exec(Api::SignIns::Create, user: valid_params(user))
+
+ response.should send_json(200, token: "fake-token")
+ end
+ end
+
+ it "returns an error if credentials are invalid" do
+ user = UserFactory.create
+ invalid_params = valid_params(user).merge(password: "incorrect")
+
+ response = ApiClient.exec(Api::SignIns::Create, user: invalid_params)
+
+ response.should send_json(
+ 400,
+ param: "password",
+ details: "password is wrong"
+ )
+ end
+end
+
+private def valid_params(user : User)
+ {
+ email: user.email,
+ password: "password",
+ }
+end
diff --git a/spec/requests/api/sign_ups/create_spec.cr b/spec/requests/api/sign_ups/create_spec.cr
new file mode 100644
index 0000000..2a23542
--- /dev/null
+++ b/spec/requests/api/sign_ups/create_spec.cr
@@ -0,0 +1,34 @@
+require "../../../spec_helper"
+
+describe Api::SignUps::Create do
+ it "creates user on sign up" do
+ UserToken.stub_token("fake-token") do
+ response = ApiClient.exec(Api::SignUps::Create, user: valid_params)
+
+ response.should send_json(200, token: "fake-token")
+ new_user = UserQuery.first
+ new_user.email.should eq(valid_params[:email])
+ end
+ end
+
+ it "returns error for invalid params" do
+ invalid_params = valid_params.merge(password_confirmation: "wrong")
+
+ response = ApiClient.exec(Api::SignUps::Create, user: invalid_params)
+
+ UserQuery.new.select_count.should eq(0)
+ response.should send_json(
+ 400,
+ param: "password_confirmation",
+ details: "password_confirmation must match"
+ )
+ end
+end
+
+private def valid_params
+ {
+ email: "test@email.com",
+ password: "password",
+ password_confirmation: "password",
+ }
+end
diff --git a/spec/setup/.keep b/spec/setup/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/spec/setup/.keep
diff --git a/spec/setup/clean_database.cr b/spec/setup/clean_database.cr
new file mode 100644
index 0000000..a1bc631
--- /dev/null
+++ b/spec/setup/clean_database.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ AppDatabase.truncate
+end
diff --git a/spec/setup/configure_lucky_flow.cr b/spec/setup/configure_lucky_flow.cr
new file mode 100644
index 0000000..504a3d3
--- /dev/null
+++ b/spec/setup/configure_lucky_flow.cr
@@ -0,0 +1,37 @@
+# For more detailed documentation, visit
+# https://luckyframework.org/guides/testing/html-and-interactivity
+
+LuckyFlow.configure do |settings|
+ settings.stop_retrying_after = 200.milliseconds
+ settings.base_uri = Lucky::RouteHelper.settings.base_uri
+
+ # LuckyFlow will install the chromedriver for you located in
+ # ~./webdrivers/. Uncomment this to point to a specific driver
+ # settings.driver_path = "/path/to/specific/chromedriver"
+end
+
+# By default, LuckyFlow is set in "headless" mode (no browser window shown).
+# Uncomment this to enable running `LuckyFlow` in a Google Chrome window instead.
+# Be sure to disable for CI.
+#
+# LuckyFlow.default_driver = "chrome"
+
+# LuckyFlow uses a registry for each driver. By default, chrome, and headless_chrome
+# are available. If you'd like to register your own custom driver, you can register
+# it here.
+#
+# LuckyFlow::Registry.register :firefox do
+# # add your custom driver here
+# end
+
+# Setup specs to allow you to change the driver on the fly
+# per spec by setting a tag on specific specs. Requires the
+# driver to be registered through `LuckyFlow::Registry` first.
+#
+# ```
+# it "uses headless_chrome" do
+# end
+# it "uses webless", tags: "webless" do
+# end
+# ```
+LuckyFlow::Spec.setup
diff --git a/spec/setup/reset_emails.cr b/spec/setup/reset_emails.cr
new file mode 100644
index 0000000..140ab41
--- /dev/null
+++ b/spec/setup/reset_emails.cr
@@ -0,0 +1,3 @@
+Spec.before_each do
+ Carbon::DevAdapter.reset
+end
diff --git a/spec/setup/setup_database.cr b/spec/setup/setup_database.cr
new file mode 100644
index 0000000..393c6da
--- /dev/null
+++ b/spec/setup/setup_database.cr
@@ -0,0 +1,2 @@
+Db::Create.new(quiet: true).call
+Db::Migrate.new(quiet: true).call
diff --git a/spec/setup/start_app_server.cr b/spec/setup/start_app_server.cr
new file mode 100644
index 0000000..ff0bfee
--- /dev/null
+++ b/spec/setup/start_app_server.cr
@@ -0,0 +1,10 @@
+app_server = AppServer.new
+
+spawn do
+ app_server.listen
+end
+
+Spec.after_suite do
+ LuckyFlow.shutdown
+ app_server.close
+end
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
new file mode 100644
index 0000000..9391464
--- /dev/null
+++ b/spec/spec_helper.cr
@@ -0,0 +1,26 @@
+ENV["LUCKY_ENV"] = "test"
+ENV["DEV_PORT"] = "5001"
+require "spec"
+require "lucky_flow"
+require "lucky_flow/ext/lucky"
+require "lucky_flow/ext/avram"
+
+require "lucky_flow/ext/authentic"
+require "../src/app"
+require "./support/flows/base_flow"
+require "./support/**"
+require "../db/migrations/**"
+
+# Add/modify files in spec/setup to start/configure programs or run hooks
+#
+# By default there are scripts for setting up and cleaning the database,
+# configuring LuckyFlow, starting the app server, etc.
+require "./setup/**"
+
+include Carbon::Expectations
+include Lucky::RequestExpectations
+include LuckyFlow::Expectations
+
+Avram::Migrator::Runner.new.ensure_migrated!
+Avram::SchemaEnforcer.ensure_correct_column_mappings!
+Habitat.raise_if_missing_settings!
diff --git a/spec/support/.keep b/spec/support/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/spec/support/.keep
diff --git a/spec/support/api_client.cr b/spec/support/api_client.cr
new file mode 100644
index 0000000..46d449a
--- /dev/null
+++ b/spec/support/api_client.cr
@@ -0,0 +1,12 @@
+class ApiClient < Lucky::BaseHTTPClient
+ app AppServer.new
+
+ def initialize
+ super
+ headers("Content-Type": "application/json")
+ end
+
+ def self.auth(user : User)
+ new.headers("Authorization": UserToken.generate(user))
+ end
+end
diff --git a/spec/support/factories/.keep b/spec/support/factories/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/spec/support/factories/.keep
diff --git a/spec/support/factories/user_factory.cr b/spec/support/factories/user_factory.cr
new file mode 100644
index 0000000..bb837ee
--- /dev/null
+++ b/spec/support/factories/user_factory.cr
@@ -0,0 +1,6 @@
+class UserFactory < Avram::Factory
+ def initialize
+ email "#{sequence("test-email")}@example.com"
+ encrypted_password Authentic.generate_encrypted_password("password")
+ end
+end
diff --git a/spec/support/flows/authentication_flow.cr b/spec/support/flows/authentication_flow.cr
new file mode 100644
index 0000000..183697f
--- /dev/null
+++ b/spec/support/flows/authentication_flow.cr
@@ -0,0 +1,45 @@
+class AuthenticationFlow < BaseFlow
+ private getter email
+
+ def initialize(@email : String)
+ end
+
+ def sign_up(password)
+ visit SignUps::New
+ fill_form SignUpUser,
+ email: email,
+ password: password,
+ password_confirmation: password
+ click "@sign-up-button"
+ end
+
+ def sign_out
+ visit Me::Show
+ sign_out_button.click
+ end
+
+ def sign_in(password)
+ visit SignIns::New
+ fill_form SignInUser,
+ email: email,
+ password: password
+ click "@sign-in-button"
+ end
+
+ def should_be_signed_in
+ current_page.should have_element("@sign-out-button")
+ end
+
+ def should_have_password_error
+ current_page.should have_element("body", text: "Password is wrong")
+ end
+
+ private def sign_out_button
+ el("@sign-out-button")
+ end
+
+ # NOTE: this is a shim for readability
+ private def current_page
+ self
+ end
+end
diff --git a/spec/support/flows/base_flow.cr b/spec/support/flows/base_flow.cr
new file mode 100644
index 0000000..93709b2
--- /dev/null
+++ b/spec/support/flows/base_flow.cr
@@ -0,0 +1,3 @@
+# Add methods that all or most Flows need to share
+class BaseFlow < LuckyFlow
+end
diff --git a/spec/support/flows/reset_password_flow.cr b/spec/support/flows/reset_password_flow.cr
new file mode 100644
index 0000000..b1df710
--- /dev/null
+++ b/spec/support/flows/reset_password_flow.cr
@@ -0,0 +1,42 @@
+class ResetPasswordFlow < BaseFlow
+ private getter user, authentication_flow
+ delegate sign_in, sign_out, should_have_password_error, should_be_signed_in,
+ to: authentication_flow
+ delegate email, to: user
+
+ def initialize(@user : User)
+ @authentication_flow = AuthenticationFlow.new(user.email)
+ end
+
+ def request_password_reset
+ with_fake_token do
+ visit PasswordResetRequests::New
+ fill_form RequestPasswordReset,
+ email: email
+ click "@request-password-reset-button"
+ end
+ end
+
+ def should_have_sent_reset_email
+ with_fake_token do
+ user = UserQuery.new.email(email).first
+ PasswordResetRequestEmail.new(user).should be_delivered
+ end
+ end
+
+ def reset_password(password)
+ user = UserQuery.new.email(email).first
+ token = Authentic.generate_password_reset_token(user)
+ visit PasswordResets::New.with(user.id, token)
+ fill_form ResetPassword,
+ password: password,
+ password_confirmation: password
+ click "@update-password-button"
+ end
+
+ private def with_fake_token(&)
+ PasswordResetRequestEmail.temp_config(stubbed_token: "fake") do
+ yield
+ end
+ end
+end