diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/flows/.keep | 0 | ||||
-rw-r--r-- | spec/flows/authentication_spec.cr | 31 | ||||
-rw-r--r-- | spec/flows/reset_password_spec.cr | 18 | ||||
-rw-r--r-- | spec/requests/api/me/show_spec.cr | 17 | ||||
-rw-r--r-- | spec/requests/api/sign_ins/create_spec.cr | 33 | ||||
-rw-r--r-- | spec/requests/api/sign_ups/create_spec.cr | 34 | ||||
-rw-r--r-- | spec/setup/.keep | 0 | ||||
-rw-r--r-- | spec/setup/clean_database.cr | 3 | ||||
-rw-r--r-- | spec/setup/configure_lucky_flow.cr | 37 | ||||
-rw-r--r-- | spec/setup/reset_emails.cr | 3 | ||||
-rw-r--r-- | spec/setup/setup_database.cr | 2 | ||||
-rw-r--r-- | spec/setup/start_app_server.cr | 10 | ||||
-rw-r--r-- | spec/spec_helper.cr | 26 | ||||
-rw-r--r-- | spec/support/.keep | 0 | ||||
-rw-r--r-- | spec/support/api_client.cr | 12 | ||||
-rw-r--r-- | spec/support/factories/.keep | 0 | ||||
-rw-r--r-- | spec/support/factories/user_factory.cr | 6 | ||||
-rw-r--r-- | spec/support/flows/authentication_flow.cr | 45 | ||||
-rw-r--r-- | spec/support/flows/base_flow.cr | 3 | ||||
-rw-r--r-- | spec/support/flows/reset_password_flow.cr | 42 |
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 |