diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a41a31f3..f0161d40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,11 @@ jobs: continue-on-error: false steps: - uses: actions/checkout@v4 + - name: Crystal Ameba Linter + id: crystal-ameba + uses: crystal-ameba/github-action@v0.7.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Crystal uses: crystal-lang/install-crystal@v1 with: @@ -22,8 +27,8 @@ jobs: run: shards install - name: Format run: crystal tool format --check - - name: Lint - run: ./bin/ameba + #- name: Lint + #run: ./bin/ameba specs: strategy: diff --git a/.gitignore b/.gitignore index 5c7203d9..2683bbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lucky /test-project/ node_modules /tmp/ +!/fixtures/*/expected/**/* # Libraries don't need dependency lock # Dependencies will be locked in application that uses them diff --git a/fixtures/api_authentication_template/expected/spec/requests/api/me/show_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/me/show_spec.cr new file mode 100644 index 00000000..0e1f91fe --- /dev/null +++ b/fixtures/api_authentication_template/expected/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/fixtures/api_authentication_template/expected/spec/requests/api/sign_ins/create_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ins/create_spec.cr new file mode 100644 index 00000000..520c2dff --- /dev/null +++ b/fixtures/api_authentication_template/expected/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/fixtures/api_authentication_template/expected/spec/requests/api/sign_ups/create_spec.cr b/fixtures/api_authentication_template/expected/spec/requests/api/sign_ups/create_spec.cr new file mode 100644 index 00000000..2a23542e --- /dev/null +++ b/fixtures/api_authentication_template/expected/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/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr b/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr new file mode 100644 index 00000000..00602713 --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/api/me/show.cr @@ -0,0 +1,5 @@ +class Api::Me::Show < ApiAction + get "/api/me" do + json UserSerializer.new(current_user) + end +end diff --git a/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr b/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr new file mode 100644 index 00000000..3670356c --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/api/sign_ins/create.cr @@ -0,0 +1,13 @@ +class Api::SignIns::Create < ApiAction + include Api::Auth::SkipRequireAuthToken + + post "/api/sign_ins" do + SignInUser.run(params) do |operation, user| + if user + json({token: UserToken.generate(user)}) + else + raise Avram::InvalidOperationError.new(operation) + end + end + end +end diff --git a/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr b/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr new file mode 100644 index 00000000..15bbd04b --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/api/sign_ups/create.cr @@ -0,0 +1,8 @@ +class Api::SignUps::Create < ApiAction + include Api::Auth::SkipRequireAuthToken + + post "/api/sign_ups" do + user = SignUpUser.create!(params) + json({token: UserToken.generate(user)}) + end +end diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr new file mode 100644 index 00000000..6b51cb5a --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/helpers.cr @@ -0,0 +1,28 @@ +module Api::Auth::Helpers + # The 'memoize' macro makes sure only one query is issued to find the user + memoize def current_user? : User? + auth_token.try do |value| + user_from_auth_token(value) + end + end + + private def auth_token : String? + bearer_token || token_param + end + + private def bearer_token : String? + context.request.headers["Authorization"]? + .try(&.gsub("Bearer", "")) + .try(&.strip) + end + + private def token_param : String? + params.get?(:auth_token) + end + + private def user_from_auth_token(token : String) : User? + UserToken.decode_user_id(token).try do |user_id| + UserQuery.new.id(user_id).first? + end + end +end diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr new file mode 100644 index 00000000..e018638f --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/require_auth_token.cr @@ -0,0 +1,34 @@ +module Api::Auth::RequireAuthToken + macro included + before require_auth_token + end + + private def require_auth_token + if current_user? + continue + else + json auth_error_json, 401 + end + end + + private def auth_error_json + ErrorSerializer.new( + message: "Not authenticated.", + details: auth_error_details + ) + end + + private def auth_error_details : String + if auth_token + "The provided authentication token was incorrect." + else + "An authentication token is required. Please include a token in an 'auth_token' param or 'Authorization' header." + end + end + + # Tells the compiler that the current_user is not nil since we have checked + # that the user is signed in + private def current_user : User + current_user?.as(User) + end +end diff --git a/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr new file mode 100644 index 00000000..68098cf5 --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/actions/mixins/api/auth/skip_require_auth_token.cr @@ -0,0 +1,10 @@ +module Api::Auth::SkipRequireAuthToken + macro included + skip require_auth_token + end + + # Since sign in is not required, current_user might be nil + def current_user : User? + current_user? + end +end diff --git a/fixtures/api_authentication_template/expected/src/models/user_token.cr b/fixtures/api_authentication_template/expected/src/models/user_token.cr new file mode 100644 index 00000000..65863032 --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/models/user_token.cr @@ -0,0 +1,30 @@ +# Generates and decodes JSON Web Tokens for Authenticating users. +class UserToken + Habitat.create { setting stubbed_token : String? } + ALGORITHM = JWT::Algorithm::HS256 + + def self.generate(user : User) : String + payload = {"user_id" => user.id} + + settings.stubbed_token || create_token(payload) + end + + def self.create_token(payload) + JWT.encode(payload, Lucky::Server.settings.secret_key_base, ALGORITHM) + end + + def self.decode_user_id(token : String) : Int64? + payload, _header = JWT.decode(token, Lucky::Server.settings.secret_key_base, ALGORITHM) + payload["user_id"].to_s.to_i64 + rescue e : JWT::Error + Lucky::Log.dexter.error { {jwt_decode_error: e.message} } + nil + end + + # Used in tests to return a fake token to test against. + def self.stub_token(token : String, &) + temp_config(stubbed_token: token) do + yield + end + end +end diff --git a/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr b/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr new file mode 100644 index 00000000..1a86f14a --- /dev/null +++ b/fixtures/api_authentication_template/expected/src/serializers/user_serializer.cr @@ -0,0 +1,8 @@ +class UserSerializer < BaseSerializer + def initialize(@user : User) + end + + def render + {email: @user.email} + end +end diff --git a/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr new file mode 100644 index 00000000..d0432007 --- /dev/null +++ b/fixtures/app_sec_tester_template/expected/spec/flows/security_spec.cr @@ -0,0 +1,14 @@ +{% skip_file unless flag?(:with_sec_tests) %} +# Run these specs with `crystal spec -Dwith_sec_tests` + +require "../spec_helper" + +describe "SecTester" do +end + +private def scan_with_cleanup(&) : Nil + scanner = LuckySecTester.new + yield scanner +ensure + scanner.try &.cleanup +end diff --git a/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr new file mode 100644 index 00000000..bbf9207a --- /dev/null +++ b/fixtures/app_sec_tester_template/expected/spec/setup/sec_tester.cr @@ -0,0 +1,9 @@ +require "lucky_sec_tester" + +# Signup for a `BRIGHT_TOKEN` at +# [NeuraLegion](https://app.neuralegion.com/signup) +# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester +LuckySecTester.configure do |setting| + setting.bright_token = ENV["BRIGHT_TOKEN"] + setting.project_id = ENV["BRIGHT_PROJECT_ID"] +end diff --git a/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr new file mode 100644 index 00000000..623a9683 --- /dev/null +++ b/fixtures/app_sec_tester_template__browser/expected/spec/flows/security_spec.cr @@ -0,0 +1,44 @@ +{% skip_file unless flag?(:with_sec_tests) %} +# Run these specs with `crystal spec -Dwith_sec_tests` + +require "../spec_helper" + +describe "SecTester" do + it "tests the home page general infra issues" do + scan_with_cleanup do |scanner| + target = scanner.build_target(Home::Index) + scanner.run_check( + scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}", + severity_threshold: SecTester::Severity::Medium, + tests: [ + "header_security", # Testing for header security issues (https://docs.brightsec.com/docs/misconfigured-security-headers) + "cookie_security", # Testing for Cookie Security issues (https://docs.brightsec.com/docs/sensitive-cookie-in-https-session-without-secure-attribute) + "proto_pollution", # Testing for proto pollution based vulnerabilities (https://docs.brightsec.com/docs/prototype-pollution) + "open_buckets", # Testing for open buckets (https://docs.brightsec.com/docs/open-bucket) + ], + target: target + ) + end + end + + it "tests app.js for 3rd party issues" do + scan_with_cleanup do |scanner| + target = SecTester::Target.new(Lucky::RouteHelper.settings.base_uri + Lucky::AssetHelpers.asset("js/app.js")) + scanner.run_check( + scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}", + tests: [ + "retire_js", # Testing for 3rd party issues (https://docs.brightsec.com/docs/javascript-component-with-known-vulnerabilities) + "cve_test", # Testing for known CVEs (https://docs.brightsec.com/docs/cves) + ], + target: target + ) + end + end +end + +private def scan_with_cleanup(&) : Nil + scanner = LuckySecTester.new + yield scanner +ensure + scanner.try &.cleanup +end diff --git a/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr new file mode 100644 index 00000000..bbf9207a --- /dev/null +++ b/fixtures/app_sec_tester_template__browser/expected/spec/setup/sec_tester.cr @@ -0,0 +1,9 @@ +require "lucky_sec_tester" + +# Signup for a `BRIGHT_TOKEN` at +# [NeuraLegion](https://app.neuralegion.com/signup) +# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester +LuckySecTester.configure do |setting| + setting.bright_token = ENV["BRIGHT_TOKEN"] + setting.project_id = ENV["BRIGHT_PROJECT_ID"] +end diff --git a/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr b/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr new file mode 100644 index 00000000..8db5ab7f --- /dev/null +++ b/fixtures/app_sec_tester_template__generate_auth/expected/spec/flows/security_spec.cr @@ -0,0 +1,34 @@ +{% skip_file unless flag?(:with_sec_tests) %} +# Run these specs with `crystal spec -Dwith_sec_tests` + +require "../spec_helper" + +describe "SecTester" do + it "tests the sign_in API for SQLi, and JWT attacks" do + scan_with_cleanup do |scanner| + api_headers = HTTP::Headers{"Content-Type" => "application/json", "Accept" => "application/json"} + target = scanner.build_target(Api::SignIns::Create, headers: api_headers) do |t| + t.body = {"user" => {"email" => "aa@aa.com", "password" => "123456789"}}.to_json + end + scanner.run_check( + scan_name: "ref: #{ENV["GITHUB_REF"]?} commit: #{ENV["GITHUB_SHA"]?} run id: #{ENV["GITHUB_RUN_ID"]?}", + tests: [ + "sqli", # Testing for SQL Injection issues (https://docs.brightsec.com/docs/sql-injection) + "jwt", # Testing JWT usage (https://docs.brightsec.com/docs/broken-jwt-authentication) + "xss", # Testing for Cross Site Scripting attacks (https://docs.brightsec.com/docs/reflective-cross-site-scripting-rxss) + "ssrf", # Testing for SSRF (https://docs.brightsec.com/docs/server-side-request-forgery-ssrf) + "mass_assignment", # Testing for Mass Assignment issues (https://docs.brightsec.com/docs/mass-assignment) + "full_path_disclosure", # Testing for full path disclourse on api error (https://docs.brightsec.com/docs/full-path-disclosure) + ], + target: target + ) + end + end +end + +private def scan_with_cleanup(&) : Nil + scanner = LuckySecTester.new + yield scanner +ensure + scanner.try &.cleanup +end diff --git a/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr b/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr new file mode 100644 index 00000000..bbf9207a --- /dev/null +++ b/fixtures/app_sec_tester_template__generate_auth/expected/spec/setup/sec_tester.cr @@ -0,0 +1,9 @@ +require "lucky_sec_tester" + +# Signup for a `BRIGHT_TOKEN` at +# [NeuraLegion](https://app.neuralegion.com/signup) +# Read more about the SecTester on https://github.com/luckyframework/lucky_sec_tester +LuckySecTester.configure do |setting| + setting.bright_token = ENV["BRIGHT_TOKEN"] + setting.project_id = ENV["BRIGHT_PROJECT_ID"] +end diff --git a/fixtures/base_authentication_src_template/expected/config/authentic.cr b/fixtures/base_authentication_src_template/expected/config/authentic.cr new file mode 100644 index 00000000..b9efc318 --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/config/authentic.cr @@ -0,0 +1,11 @@ +require "./server" + +Authentic.configure do |settings| + settings.secret_key = Lucky::Server.settings.secret_key_base + + unless LuckyEnv.production? + # This value can be between 4 and 31 + fastest_encryption_possible = 4 + settings.encryption_cost = fastest_encryption_possible + end +end diff --git a/fixtures/base_authentication_src_template/expected/db/migrations/.keep b/fixtures/base_authentication_src_template/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr b/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr new file mode 100644 index 00000000..96283bfa --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/db/migrations/00000000000001_create_users.cr @@ -0,0 +1,17 @@ +class CreateUsers::V00000000000001 < Avram::Migrator::Migration::V1 + def migrate + enable_extension "citext" + + create table_for(User) do + primary_key id : Int64 + add_timestamps + add email : String, unique: true, case_sensitive: false + add encrypted_password : String + end + end + + def rollback + drop table_for(User) + disable_extension "citext" + end +end diff --git a/fixtures/base_authentication_src_template/expected/spec/support/.keep b/fixtures/base_authentication_src_template/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/base_authentication_src_template/expected/spec/support/factories/user_factory.cr b/fixtures/base_authentication_src_template/expected/spec/support/factories/user_factory.cr new file mode 100644 index 00000000..bb837ee1 --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/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/fixtures/base_authentication_src_template/expected/src/models/user.cr b/fixtures/base_authentication_src_template/expected/src/models/user.cr new file mode 100644 index 00000000..39729bb2 --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/models/user.cr @@ -0,0 +1,13 @@ +class User < BaseModel + include Carbon::Emailable + include Authentic::PasswordAuthenticatable + + table do + column email : String + column encrypted_password : String + end + + def emailable : Carbon::Address + Carbon::Address.new(email) + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/.keep b/fixtures/base_authentication_src_template/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/.keep b/fixtures/base_authentication_src_template/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr b/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr new file mode 100644 index 00000000..c56b9750 --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/mixins/password_validations.cr @@ -0,0 +1,12 @@ +module PasswordValidations + macro included + before_save run_password_validations + end + + private def run_password_validations + validate_required password, password_confirmation + validate_confirmation_of password, with: password_confirmation + # 72 is a limitation of BCrypt + validate_size_of password, min: 6, max: 72 + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr b/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr new file mode 100644 index 00000000..862fa9ac --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/mixins/user_from_email.cr @@ -0,0 +1,7 @@ +module UserFromEmail + private def user_from_email : User? + email.value.try do |value| + UserQuery.new.email(value).first? + end + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr b/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr new file mode 100644 index 00000000..4941aa7f --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/request_password_reset.cr @@ -0,0 +1,25 @@ +class RequestPasswordReset < Avram::Operation + # You can modify this in src/operations/mixins/user_from_email.cr + include UserFromEmail + + attribute email : String + + # Run validations and yield the operation and the user if valid + def run + user = user_from_email + validate(user) + + if valid? + user + else + nil + end + end + + def validate(user : User?) + validate_required email + if user.nil? + email.add_error "is not in our system" + end + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr b/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr new file mode 100644 index 00000000..3bdd3c89 --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/reset_password.cr @@ -0,0 +1,11 @@ +class ResetPassword < User::SaveOperation + # Change password validations in src/operations/mixins/password_validations.cr + include PasswordValidations + + attribute password : String + attribute password_confirmation : String + + before_save do + Authentic.copy_and_encrypt password, to: encrypted_password + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr b/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr new file mode 100644 index 00000000..de80342e --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/sign_in_user.cr @@ -0,0 +1,40 @@ +class SignInUser < Avram::Operation + param_key :user + # You can modify this in src/operations/mixins/user_from_email.cr + include UserFromEmail + + attribute email : String + attribute password : String + + # Run validations and yields the operation and the user if valid + def run + user = user_from_email + validate_credentials(user) + + if valid? + user + else + nil + end + end + + # `validate_credentials` determines if a user can sign in. + # + # If desired, you can add additional checks in this method, e.g. + # + # if user.locked? + # email.add_error "is locked out" + # end + private def validate_credentials(user) + if user + unless Authentic.correct_password?(user, password.value.to_s) + password.add_error "is wrong" + end + else + # Usually ok to say that an email is not in the system: + # https://kev.inburke.com/kevin/invalid-username-or-password-useless/ + # https://github.com/luckyframework/lucky_cli/issues/192 + email.add_error "is not in our system" + end + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr b/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr new file mode 100644 index 00000000..8c46fadc --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/operations/sign_up_user.cr @@ -0,0 +1,14 @@ +class SignUpUser < User::SaveOperation + param_key :user + # Change password validations in src/operations/mixins/password_validations.cr + include PasswordValidations + + permit_columns email + attribute password : String + attribute password_confirmation : String + + before_save do + validate_uniqueness_of email + Authentic.copy_and_encrypt(password, to: encrypted_password) if password.valid? + end +end diff --git a/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr b/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr new file mode 100644 index 00000000..8a7e9a7f --- /dev/null +++ b/fixtures/base_authentication_src_template/expected/src/queries/user_query.cr @@ -0,0 +1,2 @@ +class UserQuery < User::BaseQuery +end diff --git a/fixtures/browser_authentication_src_template/expected/spec/flows/authentication_spec.cr b/fixtures/browser_authentication_src_template/expected/spec/flows/authentication_spec.cr new file mode 100644 index 00000000..b22f93cd --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/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/fixtures/browser_authentication_src_template/expected/spec/flows/reset_password_spec.cr b/fixtures/browser_authentication_src_template/expected/spec/flows/reset_password_spec.cr new file mode 100644 index 00000000..b28af34a --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/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/fixtures/browser_authentication_src_template/expected/spec/support/.keep b/fixtures/browser_authentication_src_template/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_authentication_src_template/expected/spec/support/flows/authentication_flow.cr b/fixtures/browser_authentication_src_template/expected/spec/support/flows/authentication_flow.cr new file mode 100644 index 00000000..183697f9 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/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/fixtures/browser_authentication_src_template/expected/spec/support/flows/reset_password_flow.cr b/fixtures/browser_authentication_src_template/expected/spec/support/flows/reset_password_flow.cr new file mode 100644 index 00000000..b1df7104 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/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 diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr b/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr new file mode 100644 index 00000000..5e35848e --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/me/show.cr @@ -0,0 +1,5 @@ +class Me::Show < BrowserAction + get "/me" do + html ShowPage + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/.keep b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr new file mode 100644 index 00000000..3961399b --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/allow_guests.cr @@ -0,0 +1,10 @@ +module Auth::AllowGuests + macro included + skip require_sign_in + end + + # Since sign in is not required, current_user might be nil + def current_user : User? + current_user? + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr new file mode 100644 index 00000000..77166a97 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/base.cr @@ -0,0 +1,7 @@ +module Auth::PasswordResets::Base + macro included + include Auth::RedirectSignedInUsers + include Auth::PasswordResets::FindUser + include Auth::PasswordResets::RequireToken + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr new file mode 100644 index 00000000..cab02d5c --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/find_user.cr @@ -0,0 +1,5 @@ +module Auth::PasswordResets::FindUser + private def user : User + UserQuery.find(user_id) + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr new file mode 100644 index 00000000..15da4238 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/require_token.cr @@ -0,0 +1,17 @@ +module Auth::PasswordResets::RequireToken + macro included + before require_valid_password_reset_token + end + + abstract def token : String + abstract def user : User + + private def require_valid_password_reset_token + if Authentic.valid_password_reset_token?(user, token) + continue + else + flash.failure = "The password reset link is incorrect or expired. Please try again." + redirect to: PasswordResetRequests::New + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr new file mode 100644 index 00000000..820b91bc --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/password_resets/token_from_session.cr @@ -0,0 +1,5 @@ +module Auth::PasswordResets::TokenFromSession + private def token : String + session.get?(:password_reset_token) || raise "Password reset token not found in session" + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr new file mode 100644 index 00000000..546bf7bb --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/redirect_signed_in_users.cr @@ -0,0 +1,19 @@ +module Auth::RedirectSignedInUsers + macro included + include Auth::AllowGuests + before redirect_signed_in_users + end + + private def redirect_signed_in_users + if current_user? + flash.success = "You are already signed in" + redirect to: Home::Index + else + continue + end + end + + # current_user returns nil because signed in users are redirected. + def current_user + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr new file mode 100644 index 00000000..27a6f5ea --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/require_sign_in.cr @@ -0,0 +1,21 @@ +module Auth::RequireSignIn + macro included + before require_sign_in + end + + private def require_sign_in + if current_user? + continue + else + Authentic.remember_requested_path(self) + flash.info = "Please sign in first" + redirect to: SignIns::New + end + end + + # Tells the compiler that the current_user is not nil since we have checked + # that the user is signed in + private def current_user : User + current_user?.as(User) + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr new file mode 100644 index 00000000..68c9d91a --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/mixins/auth/test_backdoor.cr @@ -0,0 +1,13 @@ +module Auth::TestBackdoor + macro included + before test_backdoor + end + + private def test_backdoor + if LuckyEnv.test? && (user_id = params.get?(:backdoor_user_id)) + user = UserQuery.find(user_id) + sign_in user + end + continue + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr new file mode 100644 index 00000000..8f3c5130 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/create.cr @@ -0,0 +1,15 @@ +class PasswordResetRequests::Create < BrowserAction + include Auth::RedirectSignedInUsers + + post "/password_reset_requests" do + RequestPasswordReset.run(params) do |operation, user| + if user + PasswordResetRequestEmail.new(user).deliver + flash.success = "You should receive an email on how to reset your password shortly" + redirect SignIns::New + else + html NewPage, operation: operation + end + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr new file mode 100644 index 00000000..7d16a7dd --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_reset_requests/new.cr @@ -0,0 +1,7 @@ +class PasswordResetRequests::New < BrowserAction + include Auth::RedirectSignedInUsers + + get "/password_reset_requests/new" do + html NewPage, operation: RequestPasswordReset.new + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr new file mode 100644 index 00000000..da1e711b --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/create.cr @@ -0,0 +1,17 @@ +class PasswordResets::Create < BrowserAction + include Auth::PasswordResets::Base + include Auth::PasswordResets::TokenFromSession + + post "/password_resets/:user_id" do + ResetPassword.update(user, params) do |operation, user| + if operation.saved? + session.delete(:password_reset_token) + sign_in user + flash.success = "Your password has been reset" + redirect to: Home::Index + else + html NewPage, operation: operation, user_id: user_id.to_i64 + end + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr new file mode 100644 index 00000000..9408109c --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/edit.cr @@ -0,0 +1,8 @@ +class PasswordResets::Edit < BrowserAction + include Auth::PasswordResets::Base + include Auth::PasswordResets::TokenFromSession + + get "/password_resets/:user_id/edit" do + html NewPage, operation: ResetPassword.new, user_id: user_id.to_i64 + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr new file mode 100644 index 00000000..55034686 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/password_resets/new.cr @@ -0,0 +1,20 @@ +class PasswordResets::New < BrowserAction + include Auth::PasswordResets::Base + + param token : String + + get "/password_resets/:user_id" do + redirect_to_edit_form_without_token_param + end + + # This is to prevent password reset tokens from being scraped in the HTTP Referer header + # See more info here: https://github.com/thoughtbot/clearance/pull/707 + private def redirect_to_edit_form_without_token_param + make_token_available_to_future_actions + redirect to: PasswordResets::Edit.with(user_id) + end + + private def make_token_available_to_future_actions + session.set(:password_reset_token, token) + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr new file mode 100644 index 00000000..af225888 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/create.cr @@ -0,0 +1,16 @@ +class SignIns::Create < BrowserAction + include Auth::RedirectSignedInUsers + + post "/sign_in" do + SignInUser.run(params) do |operation, authenticated_user| + if authenticated_user + sign_in(authenticated_user) + flash.success = "You're now signed in" + Authentic.redirect_to_originally_requested_path(self, fallback: Home::Index) + else + flash.failure = "Sign in failed" + html NewPage, operation: operation + end + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr new file mode 100644 index 00000000..8d34612f --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/delete.cr @@ -0,0 +1,7 @@ +class SignIns::Delete < BrowserAction + delete "/sign_out" do + sign_out + flash.info = "You have been signed out" + redirect to: SignIns::New + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr new file mode 100644 index 00000000..3275b40f --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ins/new.cr @@ -0,0 +1,7 @@ +class SignIns::New < BrowserAction + include Auth::RedirectSignedInUsers + + get "/sign_in" do + html NewPage, operation: SignInUser.new + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr new file mode 100644 index 00000000..a291ca68 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/create.cr @@ -0,0 +1,16 @@ +class SignUps::Create < BrowserAction + include Auth::RedirectSignedInUsers + + post "/sign_up" do + SignUpUser.create(params) do |operation, user| + if user + flash.info = "Thanks for signing up" + sign_in(user) + redirect to: Home::Index + else + flash.info = "Couldn't sign you up" + html NewPage, operation: operation + end + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr new file mode 100644 index 00000000..2299df6e --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/actions/sign_ups/new.cr @@ -0,0 +1,7 @@ +class SignUps::New < BrowserAction + include Auth::RedirectSignedInUsers + + get "/sign_up" do + html NewPage, operation: SignUpUser.new + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr b/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr new file mode 100644 index 00000000..a41c8ba1 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/emails/password_reset_request_email.cr @@ -0,0 +1,13 @@ +class PasswordResetRequestEmail < BaseEmail + Habitat.create { setting stubbed_token : String? } + delegate stubbed_token, to: :settings + + def initialize(@user : User) + @token = stubbed_token || Authentic.generate_password_reset_token(@user) + end + + to @user + from "myapp@support.com" # or set a default in src/emails/base_email.cr + subject "Reset your password" + templates html, text +end diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr new file mode 100644 index 00000000..00c24fc9 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/html.ecr @@ -0,0 +1,3 @@ +

Please reset your password

+ +Reset password diff --git a/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr new file mode 100644 index 00000000..7a7a0ab7 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/emails/templates/password_reset_request_email/text.ecr @@ -0,0 +1,3 @@ +Please reset your password: + +<%= PasswordResets::New.url(@user.id, @token) %> diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr b/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr new file mode 100644 index 00000000..c2ac1b09 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/auth_layout.cr @@ -0,0 +1,27 @@ +abstract class AuthLayout + include Lucky::HTMLPage + + abstract def content + abstract def page_title + + # The default page title. It is passed to `Shared::LayoutHead`. + # + # Add a `page_title` method to pages to override it. You can also remove + # This method so every page is required to have its own page title. + def page_title + "Welcome" + end + + def render + html_doctype + + html lang: "en" do + mount Shared::LayoutHead, page_title: page_title + + body do + mount Shared::FlashMessages, context.flash + content + end + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr b/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr new file mode 100644 index 00000000..06a1ed7a --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/main_layout.cr @@ -0,0 +1,45 @@ +abstract class MainLayout + include Lucky::HTMLPage + + # 'needs current_user : User' makes it so that the current_user + # is always required for pages using MainLayout + needs current_user : User + + abstract def content + abstract def page_title + + # MainLayout defines a default 'page_title'. + # + # Add a 'page_title' method to your indivual pages to customize each page's + # title. + # + # Or, if you want to require every page to set a title, change the + # 'page_title' method in this layout to: + # + # abstract def page_title : String + # + # This will force pages to define their own 'page_title' method. + def page_title + "Welcome" + end + + def render + html_doctype + + html lang: "en" do + mount Shared::LayoutHead, page_title: page_title + + body do + mount Shared::FlashMessages, context.flash + render_signed_in_user + content + end + end + end + + private def render_signed_in_user + text current_user.email + text " - " + link "Sign out", to: SignIns::Delete, flow_id: "sign-out-button" + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr new file mode 100644 index 00000000..6a6bd879 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/me/show_page.cr @@ -0,0 +1,21 @@ +class Me::ShowPage < MainLayout + def content + h1 "This is your profile" + h3 "Email: #{@current_user.email}" + helpful_tips + end + + private def helpful_tips + h3 "Next, you may want to:" + ul do + li { link_to_authentication_guides } + li "Modify this page: src/pages/me/show_page.cr" + li "Change where you go after sign in: src/actions/home/index.cr" + end + end + + private def link_to_authentication_guides + a "Check out the authentication guides", + href: "https://luckyframework.org/guides/authentication" + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr new file mode 100644 index 00000000..368784c9 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/password_reset_requests/new_page.cr @@ -0,0 +1,15 @@ +class PasswordResetRequests::NewPage < AuthLayout + needs operation : RequestPasswordReset + + def content + h1 "Reset your password" + render_form(@operation) + end + + private def render_form(op) + form_for PasswordResetRequests::Create do + mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input + submit "Reset Password", flow_id: "request-password-reset-button" + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr new file mode 100644 index 00000000..16a6635e --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/password_resets/new_page.cr @@ -0,0 +1,18 @@ +class PasswordResets::NewPage < AuthLayout + needs operation : ResetPassword + needs user_id : Int64 + + def content + h1 "Reset your password" + render_password_reset_form(@operation) + end + + private def render_password_reset_form(op) + form_for PasswordResets::Create.with(@user_id) do + mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input(autofocus: "true") + mount Shared::Field, attribute: op.password_confirmation, label_text: "Confirm Password", &.password_input + + submit "Update Password", flow_id: "update-password-button" + end + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr new file mode 100644 index 00000000..10188135 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ins/new_page.cr @@ -0,0 +1,23 @@ +class SignIns::NewPage < AuthLayout + needs operation : SignInUser + + def content + h1 "Sign In" + render_sign_in_form(@operation) + end + + private def render_sign_in_form(op) + form_for SignIns::Create do + sign_in_fields(op) + submit "Sign In", flow_id: "sign-in-button" + end + link "Reset password", to: PasswordResetRequests::New + text " | " + link "Sign up", to: SignUps::New + end + + private def sign_in_fields(op) + mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input(autofocus: "true") + mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input + end +end diff --git a/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr new file mode 100644 index 00000000..24f6cb25 --- /dev/null +++ b/fixtures/browser_authentication_src_template/expected/src/pages/sign_ups/new_page.cr @@ -0,0 +1,22 @@ +class SignUps::NewPage < AuthLayout + needs operation : SignUpUser + + def content + h1 "Sign Up" + render_sign_up_form(@operation) + end + + private def render_sign_up_form(op) + form_for SignUps::Create do + sign_up_fields(op) + submit "Sign Up", flow_id: "sign-up-button" + end + link "Sign in instead", to: SignIns::New + end + + private def sign_up_fields(op) + mount Shared::Field, attribute: op.email, label_text: "Email", &.email_input(autofocus: "true") + mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input + mount Shared::Field, attribute: op.password_confirmation, label_text: "Confirm Password", &.password_input + end +end diff --git a/fixtures/browser_src_template/expected/bs-config.js b/fixtures/browser_src_template/expected/bs-config.js new file mode 100644 index 00000000..dbba89ce --- /dev/null +++ b/fixtures/browser_src_template/expected/bs-config.js @@ -0,0 +1,26 @@ +/* + | Browser-sync config file + | + | For up-to-date information about the options: + | http://www.browsersync.io/docs/options/ + | + */ + +module.exports = { + snippetOptions: { + rule: { + match: /<\/head>/i, + fn: function (snippet, match) { + return snippet + match; + } + } + }, + files: ["public/css/**/*.css", "public/js/**/*.js"], + watchEvents: ["change"], + open: false, + browser: "default", + ghostMode: false, + ui: false, + online: false, + logConnections: false +}; diff --git a/fixtures/browser_src_template/expected/config/html_page.cr b/fixtures/browser_src_template/expected/config/html_page.cr new file mode 100644 index 00000000..dca168e5 --- /dev/null +++ b/fixtures/browser_src_template/expected/config/html_page.cr @@ -0,0 +1,3 @@ +Lucky::HTMLPage.configure do |settings| + settings.render_component_comments = !LuckyEnv.production? +end diff --git a/fixtures/browser_src_template/expected/db/migrations/.keep b/fixtures/browser_src_template/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/package.json b/fixtures/browser_src_template/expected/package.json new file mode 100644 index 00000000..fd6b12fd --- /dev/null +++ b/fixtures/browser_src_template/expected/package.json @@ -0,0 +1,24 @@ +{ + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@rails/ujs": "^6.0.0", + "modern-normalize": "^1.1.0" + }, + "scripts": { + "heroku-postbuild": "yarn prod", + "dev": "yarn run mix", + "watch": "yarn run mix watch", + "prod": "yarn run mix --production" + }, + "devDependencies": { + "@babel/compat-data": "^7.9.0", + "browser-sync": "^2.26.7", + "compression-webpack-plugin": "^7.0.0", + "laravel-mix": "^6.0.0", + "postcss": "^8.1.0", + "resolve-url-loader": "^3.1.1", + "sass": "^1.26.10", + "sass-loader": "^10.0.2" + } +} diff --git a/fixtures/browser_src_template/expected/public/assets/images/.keep b/fixtures/browser_src_template/expected/public/assets/images/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/public/favicon.ico b/fixtures/browser_src_template/expected/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/public/mix-manifest.json b/fixtures/browser_src_template/expected/public/mix-manifest.json new file mode 100644 index 00000000..d70320ad --- /dev/null +++ b/fixtures/browser_src_template/expected/public/mix-manifest.json @@ -0,0 +1,4 @@ +{ + "/css/app.css": "/css/app.css", + "/js/app.js": "/js/app.js" +} diff --git a/fixtures/browser_src_template/expected/public/robots.txt b/fixtures/browser_src_template/expected/public/robots.txt new file mode 100644 index 00000000..12009050 --- /dev/null +++ b/fixtures/browser_src_template/expected/public/robots.txt @@ -0,0 +1,4 @@ +# Learn more about robots.txt: https://www.robotstxt.org/robotstxt.html +User-agent: * +# 'Disallow' with an empty value allows all paths to be crawled +Disallow: diff --git a/fixtures/browser_src_template/expected/spec/flows/.keep b/fixtures/browser_src_template/expected/spec/flows/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/spec/setup/.keep b/fixtures/browser_src_template/expected/spec/setup/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/spec/setup/configure_lucky_flow.cr b/fixtures/browser_src_template/expected/spec/setup/configure_lucky_flow.cr new file mode 100644 index 00000000..504a3d34 --- /dev/null +++ b/fixtures/browser_src_template/expected/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/fixtures/browser_src_template/expected/spec/support/.keep b/fixtures/browser_src_template/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/spec/support/factories/.keep b/fixtures/browser_src_template/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/spec/support/flows/base_flow.cr b/fixtures/browser_src_template/expected/spec/support/flows/base_flow.cr new file mode 100644 index 00000000..93709b25 --- /dev/null +++ b/fixtures/browser_src_template/expected/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/fixtures/browser_src_template/expected/src/actions/browser_action.cr b/fixtures/browser_src_template/expected/src/actions/browser_action.cr new file mode 100644 index 00000000..674f2088 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/actions/browser_action.cr @@ -0,0 +1,45 @@ +abstract class BrowserAction < Lucky::Action + include Lucky::ProtectFromForgery + + # By default all actions are required to use underscores. + # Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute + + # This module disables Google FLoC by setting the + # [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`. + # + # This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used + # to track browsing history instead of using 3rd-party cookies. + # + # Remove this include if you want to use the FLoC tracking. + include Lucky::SecureHeaders::DisableFLoC + + accepted_formats [:html, :json], default: :html + + # This module provides current_user, sign_in, and sign_out methods + include Authentic::ActionHelpers(User) + + # When testing you can skip normal sign in by using `visit` with the `as` param + # + # flow.visit Me::Show, as: UserFactory.create + include Auth::TestBackdoor + + # By default all actions that inherit 'BrowserAction' require sign in. + # + # You can remove the 'include Auth::RequireSignIn' below to allow anyone to + # access actions that inherit from 'BrowserAction' or you can + # 'include Auth::AllowGuests' in individual actions to skip sign in. + include Auth::RequireSignIn + + # `expose` means that `current_user` will be passed to pages automatically. + # + # In default Lucky apps, the `MainLayout` declares it `needs current_user : User` + # so that any page that inherits from MainLayout can use the `current_user` + expose current_user + + # This method tells Authentic how to find the current user + # The 'memoize' macro makes sure only one query is issued to find the user + private memoize def find_current_user(id : String | User::PrimaryKeyType) : User? + UserQuery.new.id(id).first? + end +end diff --git a/fixtures/browser_src_template/expected/src/components/.keep b/fixtures/browser_src_template/expected/src/components/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/src/components/base_component.cr b/fixtures/browser_src_template/expected/src/components/base_component.cr new file mode 100644 index 00000000..c9829b48 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/components/base_component.cr @@ -0,0 +1,2 @@ +abstract class BaseComponent < Lucky::BaseComponent +end diff --git a/fixtures/browser_src_template/expected/src/components/shared/field.cr b/fixtures/browser_src_template/expected/src/components/shared/field.cr new file mode 100644 index 00000000..5c32e8a8 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/components/shared/field.cr @@ -0,0 +1,57 @@ +# This component is used to make it easier to render the same fields styles +# throughout your app. +# +# Extensive documentation at: https://luckyframework.org/guides/frontend/html-forms#shared-components +# +# ## Basic usage: +# +# # Renders a text input by default and will guess the label name "Name" +# mount Shared::Field, op.name +# # Call any of the input methods on the block +# mount Shared::Field, op.email, &.email_input +# # Add other HTML attributes +# mount Shared::Field, op.email, &.email_input(autofocus: "true") +# # Pass an explicit label name +# mount Shared::Field, attribute: op.username, label_text: "Your username" +# +# ## Customization +# +# You can customize this component so that fields render like you expect. +# For example, you might wrap it in a div with a "field-wrapper" class. +# +# div class: "field-wrapper" +# label_for field +# yield field +# mount Shared::FieldErrors, field +# end +# +# You may also want to have more components if your fields look +# different in different parts of your app, e.g. `CompactField` or +# `InlineTextField` +class Shared::Field(T) < BaseComponent + # Raises a helpful error if component receives an unpermitted attribute + include Lucky::CatchUnpermittedAttribute + + needs attribute : Avram::PermittedAttribute(T) + needs label_text : String? + + def render(&) + label_for attribute, label_text + + # You can add more default options here. For example: + # + # tag_defaults field: attribute, class: "input" + # + # Will add the class "input" to the generated HTML. + tag_defaults field: attribute do |tag_builder| + yield tag_builder + end + + mount Shared::FieldErrors, attribute + end + + # Use a text_input by default + def render + render &.text_input + end +end diff --git a/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr b/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr new file mode 100644 index 00000000..3f2937a0 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/components/shared/field_errors.cr @@ -0,0 +1,16 @@ +class Shared::FieldErrors(T) < BaseComponent + needs attribute : Avram::PermittedAttribute(T) + + # Customize the markup and styles to match your application + def render + unless attribute.valid? + div class: "error" do + text "#{label_text} #{attribute.errors.first}" + end + end + end + + def label_text : String + Wordsmith::Inflector.humanize(attribute.name.to_s) + end +end diff --git a/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr b/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr new file mode 100644 index 00000000..bc44440d --- /dev/null +++ b/fixtures/browser_src_template/expected/src/components/shared/flash_messages.cr @@ -0,0 +1,11 @@ +class Shared::FlashMessages < BaseComponent + needs flash : Lucky::FlashStore + + def render + flash.each do |flash_type, flash_message| + div class: "flash-#{flash_type}", flow_id: "flash" do + text flash_message + end + end + end +end diff --git a/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr b/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr new file mode 100644 index 00000000..5a053315 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/components/shared/layout_head.cr @@ -0,0 +1,18 @@ +class Shared::LayoutHead < BaseComponent + needs page_title : String + + def render + head do + utf8_charset + title "My App - #{@page_title}" + css_link asset("css/app.css") + js_link asset("js/app.js"), defer: "true" + csrf_meta_tags + responsive_meta_tag + + # Development helper used with the `lucky watch` command. + # Reloads the browser when files are updated. + live_reload_connect_tag if LuckyEnv.development? + end + end +end diff --git a/fixtures/browser_src_template/expected/src/css/app.scss b/fixtures/browser_src_template/expected/src/css/app.scss new file mode 100644 index 00000000..68b60cc9 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/css/app.scss @@ -0,0 +1,66 @@ +// Lucky generates 3 folders to help you organize your CSS: +// +// - src/css/variables # Files for colors, spacing, etc. +// - src/css/mixins # Put your mixin functions in files here +// - src/css/components # CSS for your components +// +// Remember to import your new CSS files or they won't be loaded: +// +// @import "./variables/colors" # Imports the file in src/css/variables/_colors.scss +// +// Note: importing with `~` tells webpack to look in the installed npm packages +// https://stackoverflow.com/questions/39535760/what-does-a-tilde-in-a-css-url-do + +@import 'modern-normalize/modern-normalize.css'; +// Add your own components and import them like this: +// +// @import "components/my_new_component"; + +// Default Lucky styles. +// Delete these when you're ready to bring in your own CSS. +body { + font-family: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, + Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, + sans-serif; + margin: 0 auto; + max-width: 800px; + padding: 20px 40px; +} + +label, input { + display: flex; +} + +label { + font-weight: 500; +} + +[type='color'], +[type='date'], +[type='datetime'], +[type='datetime-local'], +[type='email'], +[type='month'], +[type='number'], +[type='password'], +[type='search'], +[type='tel'], +[type='text'], +[type='time'], +[type='url'], +[type='week'], +input:not([type]), +textarea { + border-radius: 3px; + border: 1px solid #bbb; + margin: 7px 0 14px 0; + max-width: 400px; + padding: 8px 6px; + width: 100%; +} + +[type='submit'] { + font-weight: 900; + margin: 9px 0; + padding: 6px 9px; +} diff --git a/fixtures/browser_src_template/expected/src/emails/.keep b/fixtures/browser_src_template/expected/src/emails/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/src/js/app.js b/fixtures/browser_src_template/expected/src/js/app.js new file mode 100644 index 00000000..5722d9d8 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/js/app.js @@ -0,0 +1,6 @@ +/* eslint no-console:0 */ + +// Rails Unobtrusive JavaScript (UJS) is *required* for links in Lucky that use DELETE, POST and PUT. +// Though it says "Rails" it actually works with any framework. +require("@rails/ujs").start(); + diff --git a/fixtures/browser_src_template/expected/src/models/mixins/.keep b/fixtures/browser_src_template/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/src/operations/.keep b/fixtures/browser_src_template/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/src/operations/mixins/.keep b/fixtures/browser_src_template/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr b/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr new file mode 100644 index 00000000..e7636de9 --- /dev/null +++ b/fixtures/browser_src_template/expected/src/pages/errors/show_page.cr @@ -0,0 +1,93 @@ +class Errors::ShowPage + include Lucky::HTMLPage + + needs message : String + needs status_code : Int32 + + def render + html_doctype + html lang: "en" do + head do + utf8_charset + title "Something went wrong" + load_lato_font + normalize_styles + error_page_styles + end + + body do + div class: "container" do + h2 status_code, class: "status-code" + h1 message, class: "message" + + ul class: "helpful-links" do + li do + a "Try heading back to home", href: "/", class: "helpful-link" + end + end + end + end + end + end + + def load_lato_font + css_link "https://fonts.googleapis.com/css?family=Lato" + end + + def normalize_styles + style <<-CSS + /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none} + CSS + end + + def error_page_styles + style <<-CSS + body { + background-color: #f5f5f5; + color: #000; + font-family: 'Lato', sans-serif; + padding-top: 100px; + } + + .helpful-links { + list-style-type: none; + margin: 0; + padding: 0; + } + + .helpful-link { + color: #15A38B; + } + + .status-code { + opacity: 0.4; + font-size: 26px; + font-weight: normal; + } + + .message { + font-size: 34px; + line-height: 56px; + font-weight: normal; + } + + .container { + margin: 0 auto; + max-width: 450px; + padding: 55px; + } + + @media only screen and (max-width: 500px) { + .status-code { + font-size: 18px; + } + + .message { + font-size: 26px; + line-height: 40px; + margin: 20px 0 35px 0; + } + } + CSS + end +end diff --git a/fixtures/browser_src_template/expected/src/pages/main_layout.cr b/fixtures/browser_src_template/expected/src/pages/main_layout.cr new file mode 100644 index 00000000..40f7c5ce --- /dev/null +++ b/fixtures/browser_src_template/expected/src/pages/main_layout.cr @@ -0,0 +1,27 @@ +abstract class MainLayout + include Lucky::HTMLPage + + abstract def content + abstract def page_title + + # The default page title. It is passed to `Shared::LayoutHead`. + # + # Add a `page_title` method to pages to override it. You can also remove + # This method so every page is required to have its own page title. + def page_title + "Welcome" + end + + def render + html_doctype + + html lang: "en" do + mount Shared::LayoutHead, page_title: page_title + + body do + mount Shared::FlashMessages, context.flash + content + end + end + end +end diff --git a/fixtures/browser_src_template/expected/webpack.mix.js b/fixtures/browser_src_template/expected/webpack.mix.js new file mode 100644 index 00000000..0c3ecc9b --- /dev/null +++ b/fixtures/browser_src_template/expected/webpack.mix.js @@ -0,0 +1,113 @@ +/* + | Mix Asset Management + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your application. + | + | Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme + */ + +let mix = require("laravel-mix"); +let plugins = []; + +// Customize the notifier to be less noisy +let WebpackNotifierPlugin = require('webpack-notifier'); +let webpackNotifier = new WebpackNotifierPlugin({ + alwaysNotify: false, + skipFirstNotification: true +}) +plugins.push(webpackNotifier) + +// Compress static assets in production +if (mix.inProduction()) { + let CompressionWepackPlugin = require('compression-webpack-plugin'); + let gzipCompression = new CompressionWepackPlugin({ + compressionOptions: { level: 9 }, + test: /\.js$|\.css$|\.html$|\.svg$/ + }) + plugins.push(gzipCompression) + + // Add additional compression plugins here. + // For example if you want to add Brotli compression: + // + // let brotliCompression = new CompressionWepackPlugin({ + // compressionOptions: { level: 11 }, + // filename: '[path].br[query]', + // algorithm: 'brotliCompress', + // test: /\.js$|\.css$|\.html$|\.svg$/ + // }) + // plugins.push(brotliCompression) +} + +mix + // Set public path so manifest gets output here + .setPublicPath("public") + // JS entry file. Supports Vue, and uses Babel + // + // More info and options (like React support) here: + // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/mixjs.md + .js("src/js/app.js", "js") + // SASS entry file. Uses autoprefixer automatically. + .sass("src/css/app.scss", "css") + // Customize postCSS: + // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/css-preprocessors.md#postcss-plugins + .options({ + // If you want to process images, change this to true and add options from + // https://github.com/tcoopman/image-webpack-loader + imgLoaderOptions: { enabled: false }, + // Stops Mix from clearing the console when compilation succeeds + clearConsole: false + }) + // Add assets to the manifest + .version(["public/assets"]) + // Reduce noise in Webpack output + .webpackConfig({ + stats: "errors-only", + plugins: plugins, + watchOptions: { + ignored: /node_modules/ + } + }) + // Disable default Mix notifications because we're using our own notifier + .disableNotifications() + +// Full API +// Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme +// +// mix.js(src, output); +// mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation. +// mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation. +// mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation. +// mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js +// mix.extract(vendorLibs); +// mix.sass(src, output); +// mix.less(src, output); +// mix.stylus(src, output); +// mix.postCss(src, output, [require('postcss-some-plugin')()]); +// mix.browserSync('my-site.test'); +// mix.combine(files, destination); +// mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation. +// mix.copy(from, to); +// mix.copyDirectory(fromDir, toDir); +// mix.minify(file); +// mix.sourceMaps(); // Enable sourcemaps +// mix.version(); // Enable versioning. +// mix.disableNotifications(); +// mix.setPublicPath('path/to/public'); +// mix.setResourceRoot('prefix/for/resource/locators'); +// mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin. +// mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly. +// mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default. +// mix.then(function () {}) <-- Will be triggered each time Webpack finishes building. +// mix.when(condition, function (mix) {}) <-- Call function if condition is true. +// mix.override(function (webpackConfig) {}) <-- Will be triggered once the webpack config object has been fully generated by Mix. +// mix.dump(); <-- Dump the generated webpack config object to the console. +// mix.extend(name, handler) <-- Extend Mix's API with your own components. +// mix.options({ +// extractVueStyles: false, // Extract .vue component styling to file, rather than inline. +// globalVueStyles: file, // Variables file to be imported in every component. +// processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched. +// purifyCss: false, // Remove unused CSS selectors. +// terser: {}, // Terser-specific options. https://github.com/webpack-contrib/terser-webpack-plugin#options +// postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md +// }); diff --git a/fixtures/browser_src_template__generate_auth/expected/bs-config.js b/fixtures/browser_src_template__generate_auth/expected/bs-config.js new file mode 100644 index 00000000..dbba89ce --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/bs-config.js @@ -0,0 +1,26 @@ +/* + | Browser-sync config file + | + | For up-to-date information about the options: + | http://www.browsersync.io/docs/options/ + | + */ + +module.exports = { + snippetOptions: { + rule: { + match: /<\/head>/i, + fn: function (snippet, match) { + return snippet + match; + } + } + }, + files: ["public/css/**/*.css", "public/js/**/*.js"], + watchEvents: ["change"], + open: false, + browser: "default", + ghostMode: false, + ui: false, + online: false, + logConnections: false +}; diff --git a/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr b/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr new file mode 100644 index 00000000..dca168e5 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/config/html_page.cr @@ -0,0 +1,3 @@ +Lucky::HTMLPage.configure do |settings| + settings.render_component_comments = !LuckyEnv.production? +end diff --git a/fixtures/browser_src_template__generate_auth/expected/db/migrations/.keep b/fixtures/browser_src_template__generate_auth/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/package.json b/fixtures/browser_src_template__generate_auth/expected/package.json new file mode 100644 index 00000000..fd6b12fd --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/package.json @@ -0,0 +1,24 @@ +{ + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@rails/ujs": "^6.0.0", + "modern-normalize": "^1.1.0" + }, + "scripts": { + "heroku-postbuild": "yarn prod", + "dev": "yarn run mix", + "watch": "yarn run mix watch", + "prod": "yarn run mix --production" + }, + "devDependencies": { + "@babel/compat-data": "^7.9.0", + "browser-sync": "^2.26.7", + "compression-webpack-plugin": "^7.0.0", + "laravel-mix": "^6.0.0", + "postcss": "^8.1.0", + "resolve-url-loader": "^3.1.1", + "sass": "^1.26.10", + "sass-loader": "^10.0.2" + } +} diff --git a/fixtures/browser_src_template__generate_auth/expected/public/assets/images/.keep b/fixtures/browser_src_template__generate_auth/expected/public/assets/images/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/public/favicon.ico b/fixtures/browser_src_template__generate_auth/expected/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json b/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json new file mode 100644 index 00000000..d70320ad --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/public/mix-manifest.json @@ -0,0 +1,4 @@ +{ + "/css/app.css": "/css/app.css", + "/js/app.js": "/js/app.js" +} diff --git a/fixtures/browser_src_template__generate_auth/expected/public/robots.txt b/fixtures/browser_src_template__generate_auth/expected/public/robots.txt new file mode 100644 index 00000000..12009050 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/public/robots.txt @@ -0,0 +1,4 @@ +# Learn more about robots.txt: https://www.robotstxt.org/robotstxt.html +User-agent: * +# 'Disallow' with an empty value allows all paths to be crawled +Disallow: diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/flows/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/flows/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/setup/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/setup/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/setup/configure_lucky_flow.cr b/fixtures/browser_src_template__generate_auth/expected/spec/setup/configure_lucky_flow.cr new file mode 100644 index 00000000..504a3d34 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/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/fixtures/browser_src_template__generate_auth/expected/spec/support/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/support/factories/.keep b/fixtures/browser_src_template__generate_auth/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/spec/support/flows/base_flow.cr b/fixtures/browser_src_template__generate_auth/expected/spec/support/flows/base_flow.cr new file mode 100644 index 00000000..93709b25 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/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/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr b/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr new file mode 100644 index 00000000..a72377f4 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/actions/browser_action.cr @@ -0,0 +1,18 @@ +abstract class BrowserAction < Lucky::Action + include Lucky::ProtectFromForgery + + # By default all actions are required to use underscores. + # Add `include Lucky::SkipRouteStyleCheck` to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute + + # This module disables Google FLoC by setting the + # [Permissions-Policy](https://github.com/WICG/floc) HTTP header to `interest-cohort=()`. + # + # This header is a part of Google's Federated Learning of Cohorts (FLoC) which is used + # to track browsing history instead of using 3rd-party cookies. + # + # Remove this include if you want to use the FLoC tracking. + include Lucky::SecureHeaders::DisableFLoC + + accepted_formats [:html, :json], default: :html +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/.keep b/fixtures/browser_src_template__generate_auth/expected/src/components/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr new file mode 100644 index 00000000..c9829b48 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/components/base_component.cr @@ -0,0 +1,2 @@ +abstract class BaseComponent < Lucky::BaseComponent +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr new file mode 100644 index 00000000..5c32e8a8 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field.cr @@ -0,0 +1,57 @@ +# This component is used to make it easier to render the same fields styles +# throughout your app. +# +# Extensive documentation at: https://luckyframework.org/guides/frontend/html-forms#shared-components +# +# ## Basic usage: +# +# # Renders a text input by default and will guess the label name "Name" +# mount Shared::Field, op.name +# # Call any of the input methods on the block +# mount Shared::Field, op.email, &.email_input +# # Add other HTML attributes +# mount Shared::Field, op.email, &.email_input(autofocus: "true") +# # Pass an explicit label name +# mount Shared::Field, attribute: op.username, label_text: "Your username" +# +# ## Customization +# +# You can customize this component so that fields render like you expect. +# For example, you might wrap it in a div with a "field-wrapper" class. +# +# div class: "field-wrapper" +# label_for field +# yield field +# mount Shared::FieldErrors, field +# end +# +# You may also want to have more components if your fields look +# different in different parts of your app, e.g. `CompactField` or +# `InlineTextField` +class Shared::Field(T) < BaseComponent + # Raises a helpful error if component receives an unpermitted attribute + include Lucky::CatchUnpermittedAttribute + + needs attribute : Avram::PermittedAttribute(T) + needs label_text : String? + + def render(&) + label_for attribute, label_text + + # You can add more default options here. For example: + # + # tag_defaults field: attribute, class: "input" + # + # Will add the class "input" to the generated HTML. + tag_defaults field: attribute do |tag_builder| + yield tag_builder + end + + mount Shared::FieldErrors, attribute + end + + # Use a text_input by default + def render + render &.text_input + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr new file mode 100644 index 00000000..3f2937a0 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/field_errors.cr @@ -0,0 +1,16 @@ +class Shared::FieldErrors(T) < BaseComponent + needs attribute : Avram::PermittedAttribute(T) + + # Customize the markup and styles to match your application + def render + unless attribute.valid? + div class: "error" do + text "#{label_text} #{attribute.errors.first}" + end + end + end + + def label_text : String + Wordsmith::Inflector.humanize(attribute.name.to_s) + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr new file mode 100644 index 00000000..bc44440d --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/flash_messages.cr @@ -0,0 +1,11 @@ +class Shared::FlashMessages < BaseComponent + needs flash : Lucky::FlashStore + + def render + flash.each do |flash_type, flash_message| + div class: "flash-#{flash_type}", flow_id: "flash" do + text flash_message + end + end + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr new file mode 100644 index 00000000..5a053315 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/components/shared/layout_head.cr @@ -0,0 +1,18 @@ +class Shared::LayoutHead < BaseComponent + needs page_title : String + + def render + head do + utf8_charset + title "My App - #{@page_title}" + css_link asset("css/app.css") + js_link asset("js/app.js"), defer: "true" + csrf_meta_tags + responsive_meta_tag + + # Development helper used with the `lucky watch` command. + # Reloads the browser when files are updated. + live_reload_connect_tag if LuckyEnv.development? + end + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss b/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss new file mode 100644 index 00000000..68b60cc9 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/css/app.scss @@ -0,0 +1,66 @@ +// Lucky generates 3 folders to help you organize your CSS: +// +// - src/css/variables # Files for colors, spacing, etc. +// - src/css/mixins # Put your mixin functions in files here +// - src/css/components # CSS for your components +// +// Remember to import your new CSS files or they won't be loaded: +// +// @import "./variables/colors" # Imports the file in src/css/variables/_colors.scss +// +// Note: importing with `~` tells webpack to look in the installed npm packages +// https://stackoverflow.com/questions/39535760/what-does-a-tilde-in-a-css-url-do + +@import 'modern-normalize/modern-normalize.css'; +// Add your own components and import them like this: +// +// @import "components/my_new_component"; + +// Default Lucky styles. +// Delete these when you're ready to bring in your own CSS. +body { + font-family: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, + Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, + sans-serif; + margin: 0 auto; + max-width: 800px; + padding: 20px 40px; +} + +label, input { + display: flex; +} + +label { + font-weight: 500; +} + +[type='color'], +[type='date'], +[type='datetime'], +[type='datetime-local'], +[type='email'], +[type='month'], +[type='number'], +[type='password'], +[type='search'], +[type='tel'], +[type='text'], +[type='time'], +[type='url'], +[type='week'], +input:not([type]), +textarea { + border-radius: 3px; + border: 1px solid #bbb; + margin: 7px 0 14px 0; + max-width: 400px; + padding: 8px 6px; + width: 100%; +} + +[type='submit'] { + font-weight: 900; + margin: 9px 0; + padding: 6px 9px; +} diff --git a/fixtures/browser_src_template__generate_auth/expected/src/emails/.keep b/fixtures/browser_src_template__generate_auth/expected/src/emails/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/src/js/app.js b/fixtures/browser_src_template__generate_auth/expected/src/js/app.js new file mode 100644 index 00000000..5722d9d8 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/js/app.js @@ -0,0 +1,6 @@ +/* eslint no-console:0 */ + +// Rails Unobtrusive JavaScript (UJS) is *required* for links in Lucky that use DELETE, POST and PUT. +// Though it says "Rails" it actually works with any framework. +require("@rails/ujs").start(); + diff --git a/fixtures/browser_src_template__generate_auth/expected/src/models/mixins/.keep b/fixtures/browser_src_template__generate_auth/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/src/operations/.keep b/fixtures/browser_src_template__generate_auth/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/src/operations/mixins/.keep b/fixtures/browser_src_template__generate_auth/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr b/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr new file mode 100644 index 00000000..e7636de9 --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/pages/errors/show_page.cr @@ -0,0 +1,93 @@ +class Errors::ShowPage + include Lucky::HTMLPage + + needs message : String + needs status_code : Int32 + + def render + html_doctype + html lang: "en" do + head do + utf8_charset + title "Something went wrong" + load_lato_font + normalize_styles + error_page_styles + end + + body do + div class: "container" do + h2 status_code, class: "status-code" + h1 message, class: "message" + + ul class: "helpful-links" do + li do + a "Try heading back to home", href: "/", class: "helpful-link" + end + end + end + end + end + end + + def load_lato_font + css_link "https://fonts.googleapis.com/css?family=Lato" + end + + def normalize_styles + style <<-CSS + /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none} + CSS + end + + def error_page_styles + style <<-CSS + body { + background-color: #f5f5f5; + color: #000; + font-family: 'Lato', sans-serif; + padding-top: 100px; + } + + .helpful-links { + list-style-type: none; + margin: 0; + padding: 0; + } + + .helpful-link { + color: #15A38B; + } + + .status-code { + opacity: 0.4; + font-size: 26px; + font-weight: normal; + } + + .message { + font-size: 34px; + line-height: 56px; + font-weight: normal; + } + + .container { + margin: 0 auto; + max-width: 450px; + padding: 55px; + } + + @media only screen and (max-width: 500px) { + .status-code { + font-size: 18px; + } + + .message { + font-size: 26px; + line-height: 40px; + margin: 20px 0 35px 0; + } + } + CSS + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr b/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr new file mode 100644 index 00000000..40f7c5ce --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/src/pages/main_layout.cr @@ -0,0 +1,27 @@ +abstract class MainLayout + include Lucky::HTMLPage + + abstract def content + abstract def page_title + + # The default page title. It is passed to `Shared::LayoutHead`. + # + # Add a `page_title` method to pages to override it. You can also remove + # This method so every page is required to have its own page title. + def page_title + "Welcome" + end + + def render + html_doctype + + html lang: "en" do + mount Shared::LayoutHead, page_title: page_title + + body do + mount Shared::FlashMessages, context.flash + content + end + end + end +end diff --git a/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js b/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js new file mode 100644 index 00000000..0c3ecc9b --- /dev/null +++ b/fixtures/browser_src_template__generate_auth/expected/webpack.mix.js @@ -0,0 +1,113 @@ +/* + | Mix Asset Management + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your application. + | + | Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme + */ + +let mix = require("laravel-mix"); +let plugins = []; + +// Customize the notifier to be less noisy +let WebpackNotifierPlugin = require('webpack-notifier'); +let webpackNotifier = new WebpackNotifierPlugin({ + alwaysNotify: false, + skipFirstNotification: true +}) +plugins.push(webpackNotifier) + +// Compress static assets in production +if (mix.inProduction()) { + let CompressionWepackPlugin = require('compression-webpack-plugin'); + let gzipCompression = new CompressionWepackPlugin({ + compressionOptions: { level: 9 }, + test: /\.js$|\.css$|\.html$|\.svg$/ + }) + plugins.push(gzipCompression) + + // Add additional compression plugins here. + // For example if you want to add Brotli compression: + // + // let brotliCompression = new CompressionWepackPlugin({ + // compressionOptions: { level: 11 }, + // filename: '[path].br[query]', + // algorithm: 'brotliCompress', + // test: /\.js$|\.css$|\.html$|\.svg$/ + // }) + // plugins.push(brotliCompression) +} + +mix + // Set public path so manifest gets output here + .setPublicPath("public") + // JS entry file. Supports Vue, and uses Babel + // + // More info and options (like React support) here: + // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/mixjs.md + .js("src/js/app.js", "js") + // SASS entry file. Uses autoprefixer automatically. + .sass("src/css/app.scss", "css") + // Customize postCSS: + // https://github.com/JeffreyWay/laravel-mix/blob/master/docs/css-preprocessors.md#postcss-plugins + .options({ + // If you want to process images, change this to true and add options from + // https://github.com/tcoopman/image-webpack-loader + imgLoaderOptions: { enabled: false }, + // Stops Mix from clearing the console when compilation succeeds + clearConsole: false + }) + // Add assets to the manifest + .version(["public/assets"]) + // Reduce noise in Webpack output + .webpackConfig({ + stats: "errors-only", + plugins: plugins, + watchOptions: { + ignored: /node_modules/ + } + }) + // Disable default Mix notifications because we're using our own notifier + .disableNotifications() + +// Full API +// Docs: https://github.com/JeffreyWay/laravel-mix/tree/master/docs#readme +// +// mix.js(src, output); +// mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation. +// mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation. +// mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation. +// mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js +// mix.extract(vendorLibs); +// mix.sass(src, output); +// mix.less(src, output); +// mix.stylus(src, output); +// mix.postCss(src, output, [require('postcss-some-plugin')()]); +// mix.browserSync('my-site.test'); +// mix.combine(files, destination); +// mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation. +// mix.copy(from, to); +// mix.copyDirectory(fromDir, toDir); +// mix.minify(file); +// mix.sourceMaps(); // Enable sourcemaps +// mix.version(); // Enable versioning. +// mix.disableNotifications(); +// mix.setPublicPath('path/to/public'); +// mix.setResourceRoot('prefix/for/resource/locators'); +// mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin. +// mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly. +// mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default. +// mix.then(function () {}) <-- Will be triggered each time Webpack finishes building. +// mix.when(condition, function (mix) {}) <-- Call function if condition is true. +// mix.override(function (webpackConfig) {}) <-- Will be triggered once the webpack config object has been fully generated by Mix. +// mix.dump(); <-- Dump the generated webpack config object to the console. +// mix.extend(name, handler) <-- Extend Mix's API with your own components. +// mix.options({ +// extractVueStyles: false, // Extract .vue component styling to file, rather than inline. +// globalVueStyles: file, // Variables file to be imported in every component. +// processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched. +// purifyCss: false, // Remove unused CSS selectors. +// terser: {}, // Terser-specific options. https://github.com/webpack-contrib/terser-webpack-plugin#options +// postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md +// }); diff --git a/fixtures/shard_file_template/expected/shard.yml b/fixtures/shard_file_template/expected/shard.yml new file mode 100644 index 00000000..8b3c3ec9 --- /dev/null +++ b/fixtures/shard_file_template/expected/shard.yml @@ -0,0 +1,39 @@ +--- +name: test-shard +version: 0.1.0 +targets: + test-shard: + main: src/test-shard.cr +crystal: '>= 1.6.2' +dependencies: + lucky: + github: luckyframework/lucky + version: ~> 1.0.0 + avram: + github: luckyframework/avram + version: ~> 1.0.0 + carbon: + github: luckyframework/carbon + version: ~> 0.3.0 + carbon_sendgrid_adapter: + github: luckyframework/carbon_sendgrid_adapter + version: ~> 0.3.0 + lucky_env: + github: luckyframework/lucky_env + version: ~> 0.1.4 + lucky_task: + github: luckyframework/lucky_task + version: ~> 0.1.1 + authentic: + github: luckyframework/authentic + version: ~> 1.0.0 + jwt: + github: crystal-community/jwt + version: ~> 1.6.0 +development_dependencies: + lucky_flow: + github: luckyframework/lucky_flow + version: ~> 0.9.0 + lucky_sec_tester: + github: luckyframework/lucky_sec_tester + version: ~> 0.2.0 diff --git a/fixtures/shard_file_template__browser/expected/shard.yml b/fixtures/shard_file_template__browser/expected/shard.yml new file mode 100644 index 00000000..f34293a8 --- /dev/null +++ b/fixtures/shard_file_template__browser/expected/shard.yml @@ -0,0 +1,30 @@ +--- +name: test-shard +version: 0.1.0 +targets: + test-shard: + main: src/test-shard.cr +crystal: '>= 1.6.2' +dependencies: + lucky: + github: luckyframework/lucky + version: ~> 1.0.0 + avram: + github: luckyframework/avram + version: ~> 1.0.0 + carbon: + github: luckyframework/carbon + version: ~> 0.3.0 + carbon_sendgrid_adapter: + github: luckyframework/carbon_sendgrid_adapter + version: ~> 0.3.0 + lucky_env: + github: luckyframework/lucky_env + version: ~> 0.1.4 + lucky_task: + github: luckyframework/lucky_task + version: ~> 0.1.1 +development_dependencies: + lucky_flow: + github: luckyframework/lucky_flow + version: ~> 0.9.0 diff --git a/fixtures/shard_file_template__generate_auth/expected/shard.yml b/fixtures/shard_file_template__generate_auth/expected/shard.yml new file mode 100644 index 00000000..74574554 --- /dev/null +++ b/fixtures/shard_file_template__generate_auth/expected/shard.yml @@ -0,0 +1,33 @@ +--- +name: test-shard +version: 0.1.0 +targets: + test-shard: + main: src/test-shard.cr +crystal: '>= 1.6.2' +dependencies: + lucky: + github: luckyframework/lucky + version: ~> 1.0.0 + avram: + github: luckyframework/avram + version: ~> 1.0.0 + carbon: + github: luckyframework/carbon + version: ~> 0.3.0 + carbon_sendgrid_adapter: + github: luckyframework/carbon_sendgrid_adapter + version: ~> 0.3.0 + lucky_env: + github: luckyframework/lucky_env + version: ~> 0.1.4 + lucky_task: + github: luckyframework/lucky_task + version: ~> 0.1.1 + authentic: + github: luckyframework/authentic + version: ~> 1.0.0 + jwt: + github: crystal-community/jwt + version: ~> 1.6.0 +development_dependencies: {} diff --git a/fixtures/shard_file_template__with_sec_tester/expected/shard.yml b/fixtures/shard_file_template__with_sec_tester/expected/shard.yml new file mode 100644 index 00000000..6892e8e5 --- /dev/null +++ b/fixtures/shard_file_template__with_sec_tester/expected/shard.yml @@ -0,0 +1,30 @@ +--- +name: test-shard +version: 0.1.0 +targets: + test-shard: + main: src/test-shard.cr +crystal: '>= 1.6.2' +dependencies: + lucky: + github: luckyframework/lucky + version: ~> 1.0.0 + avram: + github: luckyframework/avram + version: ~> 1.0.0 + carbon: + github: luckyframework/carbon + version: ~> 0.3.0 + carbon_sendgrid_adapter: + github: luckyframework/carbon_sendgrid_adapter + version: ~> 0.3.0 + lucky_env: + github: luckyframework/lucky_env + version: ~> 0.1.4 + lucky_task: + github: luckyframework/lucky_task + version: ~> 0.1.1 +development_dependencies: + lucky_sec_tester: + github: luckyframework/lucky_sec_tester + version: ~> 0.2.0 diff --git a/fixtures/src_template/expected/.crystal-version b/fixtures/src_template/expected/.crystal-version new file mode 100644 index 00000000..fdd3be6d --- /dev/null +++ b/fixtures/src_template/expected/.crystal-version @@ -0,0 +1 @@ +1.6.2 diff --git a/fixtures/src_template/expected/.env b/fixtures/src_template/expected/.env new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/.github/workflows/ci.yml b/fixtures/src_template/expected/.github/workflows/ci.yml new file mode 100644 index 00000000..84d0c1cf --- /dev/null +++ b/fixtures/src_template/expected/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: test-project CI + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + check-format: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + - name: Format + run: crystal tool format --check + + specs: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + env: + LUCKY_ENV: test + DB_HOST: localhost + continue-on-error: ${{ matrix.experimental }} + services: + postgres: + image: postgres:12-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + + - name: Set up Crystal cache + uses: actions/cache@v2 + id: crystal-cache + with: + path: | + ~/.cache/crystal + lib + lucky_tasks + key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }} + restore-keys: | + ${{ runner.os }}-crystal- + + - name: Install shards + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: shards check || shards install + + - name: Install npm and Nexploit Repeater + run: | + sudo npm install -g @neuralegion/nexploit-cli --unsafe-perm=true + - name: Build lucky_tasks + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: crystal build tasks.cr -o ./lucky_tasks + + - name: Prepare database + run: | + ./lucky_tasks db.create + ./lucky_tasks db.migrate + ./lucky_tasks db.seed.required_data + + - name: Run tests + run: crystal spec -Dwith_sec_tests \ No newline at end of file diff --git a/fixtures/src_template/expected/Procfile b/fixtures/src_template/expected/Procfile new file mode 100644 index 00000000..e524d70d --- /dev/null +++ b/fixtures/src_template/expected/Procfile @@ -0,0 +1,2 @@ +web: bin/app +release: lucky db.migrate diff --git a/fixtures/src_template/expected/Procfile.dev b/fixtures/src_template/expected/Procfile.dev new file mode 100644 index 00000000..9cca7346 --- /dev/null +++ b/fixtures/src_template/expected/Procfile.dev @@ -0,0 +1,2 @@ +system_check: script/system_check && sleep 100000 +web: lucky watch diff --git a/fixtures/src_template/expected/README.md b/fixtures/src_template/expected/README.md new file mode 100644 index 00000000..da2d97cc --- /dev/null +++ b/fixtures/src_template/expected/README.md @@ -0,0 +1,23 @@ +# test-project + +This is a project written using [Lucky](https://luckyframework.org). Enjoy! + +### Setting up the project + +1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies) +1. Update database settings in `config/database.cr` +1. Run `script/setup` +1. Run `lucky dev` to start the app + +### Using Docker for development + +1. [Install Docker](https://docs.docker.com/engine/install/) +1. Run `docker compose up` + +The Docker container will boot all of the necessary components needed to run your Lucky application. +To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file. + + +### Learning Lucky + +Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky). diff --git a/fixtures/src_template/expected/config/application.cr b/fixtures/src_template/expected/config/application.cr new file mode 100644 index 00000000..c807149a --- /dev/null +++ b/fixtures/src_template/expected/config/application.cr @@ -0,0 +1,24 @@ +# This file may be used for custom Application configurations. +# It will be loaded before other config files. +# +# Read more on configuration: +# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code + +# Use this code as an example: +# +# ``` +# module Application +# Habitat.create do +# setting support_email : String +# setting lock_with_basic_auth : Bool +# end +# end +# +# Application.configure do |settings| +# settings.support_email = "support@myapp.io" +# settings.lock_with_basic_auth = LuckyEnv.staging? +# end +# +# # In your application, call +# # `Application.settings.support_email` anywhere you need it. +# ``` diff --git a/fixtures/src_template/expected/config/colors.cr b/fixtures/src_template/expected/config/colors.cr new file mode 100644 index 00000000..761ae940 --- /dev/null +++ b/fixtures/src_template/expected/config/colors.cr @@ -0,0 +1,4 @@ +# This enables the color output when in development or test +# Check out the Colorize docs for more information +# https://crystal-lang.org/api/Colorize.html +Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? diff --git a/fixtures/src_template/expected/config/cookies.cr b/fixtures/src_template/expected/config/cookies.cr new file mode 100644 index 00000000..2d5055f2 --- /dev/null +++ b/fixtures/src_template/expected/config/cookies.cr @@ -0,0 +1,25 @@ +require "./server" + +Lucky::Session.configure do |settings| + settings.key = "_test_project_session" +end + +Lucky::CookieJar.configure do |settings| + settings.on_set = ->(cookie : HTTP::Cookie) { + # If ForceSSLHandler is enabled, only send cookies over HTTPS + cookie.secure(Lucky::ForceSSLHandler.settings.enabled) + + # By default, don't allow reading cookies with JavaScript + cookie.http_only(true) + + # Restrict cookies to a first-party or same-site context + cookie.samesite(:lax) + + # Set all cookies to the root path by default + cookie.path("/") + + # You can set other defaults for cookies here. For example: + # + # cookie.expires(1.year.from_now).domain("mydomain.com") + } +end diff --git a/fixtures/src_template/expected/config/database.cr b/fixtures/src_template/expected/config/database.cr new file mode 100644 index 00000000..f614299a --- /dev/null +++ b/fixtures/src_template/expected/config/database.cr @@ -0,0 +1,29 @@ +database_name = "test_project_#{LuckyEnv.environment}" + +AppDatabase.configure do |settings| + if LuckyEnv.production? + settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"]) + else + settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new( + database: database_name, + hostname: ENV["DB_HOST"]? || "localhost", + port: ENV["DB_PORT"]?.try(&.to_i) || 5432, + # Some common usernames are "postgres", "root", or your system username (run 'whoami') + username: ENV["DB_USERNAME"]? || "postgres", + # Some Postgres installations require no password. Use "" if that is the case. + password: ENV["DB_PASSWORD"]? || "postgres" + ) + end +end + +Avram.configure do |settings| + settings.database_to_migrate = AppDatabase + + # In production, allow lazy loading (N+1). + # In development and test, raise an error if you forget to preload associations + settings.lazy_load_enabled = LuckyEnv.production? + + # Always parse `Time` values with these specific formats. + # Used for both database values, and datetime input fields. + # settings.time_formats << "%F" +end diff --git a/fixtures/src_template/expected/config/email.cr b/fixtures/src_template/expected/config/email.cr new file mode 100644 index 00000000..7c875449 --- /dev/null +++ b/fixtures/src_template/expected/config/email.cr @@ -0,0 +1,26 @@ +require "carbon_sendgrid_adapter" + +BaseEmail.configure do |settings| + if LuckyEnv.production? + # If you don't need to send emails, set the adapter to DevAdapter instead: + # + # settings.adapter = Carbon::DevAdapter.new + # + # If you do need emails, get a key from SendGrid and set an ENV variable + send_grid_key = send_grid_key_from_env + settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key) + elsif LuckyEnv.development? + settings.adapter = Carbon::DevAdapter.new(print_emails: true) + else + settings.adapter = Carbon::DevAdapter.new + end +end + +private def send_grid_key_from_env + ENV["SEND_GRID_KEY"]? || raise_missing_key_message +end + +private def raise_missing_key_message + puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red + exit(1) +end diff --git a/fixtures/src_template/expected/config/env.cr b/fixtures/src_template/expected/config/env.cr new file mode 100644 index 00000000..3f364072 --- /dev/null +++ b/fixtures/src_template/expected/config/env.cr @@ -0,0 +1,33 @@ +# Environments are managed using `LuckyEnv`. By default, development, production +# and test are supported. See +# https://luckyframework.org/guides/getting-started/configuration for details. +# +# The default environment is development unless the environment variable +# LUCKY_ENV is set. +# +# Example: +# ``` +# LuckyEnv.environment # => "development" +# LuckyEnv.development? # => true +# LuckyEnv.production? # => false +# LuckyEnv.test? # => false +# ``` +# +# New environments can be added using the `LuckyEnv.add_env` macro. +# +# Example: +# ``` +# LuckyEnv.add_env :staging +# LuckyEnv.staging? # => false +# ``` +# +# To determine whether or not a `LuckyTask` is currently running, you can use +# the `LuckyEnv.task?` predicate. +# +# Example: +# ``` +# LuckyEnv.task? # => false +# ``` + +# Add a staging environment. +# LuckyEnv.add_env :staging diff --git a/fixtures/src_template/expected/config/error_handler.cr b/fixtures/src_template/expected/config/error_handler.cr new file mode 100644 index 00000000..c6b736e3 --- /dev/null +++ b/fixtures/src_template/expected/config/error_handler.cr @@ -0,0 +1,3 @@ +Lucky::ErrorHandler.configure do |settings| + settings.show_debug_output = !LuckyEnv.production? +end diff --git a/fixtures/src_template/expected/config/log.cr b/fixtures/src_template/expected/config/log.cr new file mode 100644 index 00000000..0e8d7636 --- /dev/null +++ b/fixtures/src_template/expected/config/log.cr @@ -0,0 +1,45 @@ +require "file_utils" + +if LuckyEnv.test? + # Logs to `tmp/test.log` so you can see what's happening without having + # a bunch of log output in your spec results. + FileUtils.mkdir_p("tmp") + + backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w")) + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) +elsif LuckyEnv.production? + # Lucky uses JSON in production so logs can be searched more easily + # + # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'. + backend = Log::IOBackend.new + backend.formatter = Dexter::JSONLogFormatter.proc + Log.dexter.configure(:info, backend) +else + # Use a pretty formatter printing to STDOUT in development + backend = Log::IOBackend.new + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) + DB::Log.level = :info +end + +# Lucky only logs when before/after pipes halt by redirecting, or rendering a +# response. Pipes that run without halting are not logged. +# +# If you want to log every pipe that runs, set the log level to ':info' +Lucky::ContinuedPipeLog.dexter.configure(:none) + +# Lucky only logs failed queries by default. +# +# Set the log to ':info' to log all queries +Avram::QueryLog.dexter.configure(:none) + +# Skip logging static assets requests in development +Lucky::LogHandler.configure do |settings| + if LuckyEnv.development? + settings.skip_if = ->(context : HTTP::Server::Context) { + context.request.method.downcase == "get" && + context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/) + } + end +end diff --git a/fixtures/src_template/expected/config/route_helper.cr b/fixtures/src_template/expected/config/route_helper.cr new file mode 100644 index 00000000..ede1f328 --- /dev/null +++ b/fixtures/src_template/expected/config/route_helper.cr @@ -0,0 +1,10 @@ +# This is used when generating URLs for your application +Lucky::RouteHelper.configure do |settings| + if LuckyEnv.production? + # Example: https://my_app.com + settings.base_uri = ENV.fetch("APP_DOMAIN") + else + # Set domain to the default host/port in development/test + settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" + end +end diff --git a/fixtures/src_template/expected/config/server.cr b/fixtures/src_template/expected/config/server.cr new file mode 100644 index 00000000..c72314be --- /dev/null +++ b/fixtures/src_template/expected/config/server.cr @@ -0,0 +1,65 @@ +# Here is where you configure the Lucky server +# +# Look at config/route_helper.cr if you want to change the domain used when +# generating links with `Action.url`. +Lucky::Server.configure do |settings| + if LuckyEnv.production? + settings.secret_key_base = secret_key_from_env + settings.host = "0.0.0.0" + settings.port = ENV["PORT"].to_i + settings.gzip_enabled = true + # By default certain content types will be gzipped. + # For a full list look in + # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr + # To add additional extensions do something like this: + # settings.gzip_content_types << "content/type" + else + settings.secret_key_base = "1234567890" + # Change host/port in config/watch.yml + # Alternatively, you can set the DEV_PORT env to set the port for local development + settings.host = Lucky::ServerSettings.host + settings.port = Lucky::ServerSettings.port + end + + # By default Lucky will serve static assets in development and production. + # + # However you could use a CDN when in production like this: + # + # Lucky::Server.configure do |settings| + # if LuckyEnv.production? + # settings.asset_host = "https://mycdnhost.com" + # else + # settings.asset_host = "" + # end + # end + settings.asset_host = "" # Lucky will serve assets +end + +Lucky::ForceSSLHandler.configure do |settings| + # To force SSL in production, uncomment the lines below. + # This will cause http requests to be redirected to https: + # + # settings.enabled = LuckyEnv.production? + # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true} + # + # Or, leave it disabled: + settings.enabled = false +end + +# Set a unique ID for each HTTP request. +# To enable the request ID, uncomment the lines below. +# You can set your own custom String, or use a random UUID. +# Lucky::RequestIdHandler.configure do |settings| +# settings.set_request_id = ->(context : HTTP::Server::Context) { +# UUID.random.to_s +# } +# end + +private def secret_key_from_env + ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production +end + +private def raise_missing_secret_key_in_production + puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red + exit(1) +end diff --git a/fixtures/src_template/expected/config/watch.yml b/fixtures/src_template/expected/config/watch.yml new file mode 100644 index 00000000..3a59b410 --- /dev/null +++ b/fixtures/src_template/expected/config/watch.yml @@ -0,0 +1,3 @@ +host: 127.0.0.1 +port: 3000 +reload_port: 3001 diff --git a/fixtures/src_template/expected/db/migrations/.keep b/fixtures/src_template/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/docker-compose.yml b/fixtures/src_template/expected/docker-compose.yml new file mode 100644 index 00000000..d00779db --- /dev/null +++ b/fixtures/src_template/expected/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + lucky: + build: + context: . + dockerfile: docker/development.dockerfile + environment: + DATABASE_URL: postgres://lucky:password@postgres:5432/lucky + DEV_HOST: "0.0.0.0" + volumes: + - .:/app + - node_modules:/app/node_modules + - shards_lib:/app/lib + - app_bin:/app/bin + - build_cache:/root/.cache + depends_on: + - postgres + ports: + - 3000:3000 # This is the Lucky Server port + - 3001:3001 # This is the Lucky watcher reload port + + entrypoint: ["docker/dev_entrypoint.sh"] + + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: lucky + POSTGRES_PASSWORD: password + POSTGRES_DB: lucky + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + # The postgres database container is exposed on the host at port 6543 to + # allow connecting directly to it with postgres clients. The port differs + # from the postgres default to avoid conflict with existing postgres + # servers. Connect to a running postgres container with: + # postgres://lucky:password@localhost:6543/lucky + - 6543:5432 + +volumes: + postgres_data: + node_modules: + shards_lib: + app_bin: + build_cache: diff --git a/fixtures/src_template/expected/docker/dev_entrypoint.sh b/fixtures/src_template/expected/docker/dev_entrypoint.sh new file mode 100755 index 00000000..f5aab877 --- /dev/null +++ b/fixtures/src_template/expected/docker/dev_entrypoint.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -euo pipefail + +# This is the entrypoint script used for development docker workflows. +# By default it will: +# - Install dependencies. +# - Run migrations. +# - Start the dev server. +# It also accepts any commands to be run instead. + + +warnfail () { + echo "$@" >&2 + exit 1 +} + +case ${1:-} in + "") # If no arguments are provided, start lucky dev server. + ;; + + *) # If any arguments are provided, execute them instead. + exec "$@" +esac + +if ! [ -d bin ] ; then + echo 'Creating bin directory' + mkdir bin +fi +if ! shards check ; then + echo 'Installing shards...' + shards install +fi + +echo 'Waiting for postgres to be available...' +./docker/wait-for-it.sh -q postgres:5432 + +if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then + echo 'Finishing database setup...' + lucky db.migrate +fi + +echo 'Starting lucky dev server...' +exec lucky dev diff --git a/fixtures/src_template/expected/docker/development.dockerfile b/fixtures/src_template/expected/docker/development.dockerfile new file mode 100644 index 00000000..11222751 --- /dev/null +++ b/fixtures/src_template/expected/docker/development.dockerfile @@ -0,0 +1,25 @@ +FROM crystallang/crystal:1.6.2 + +# Install utilities required to make this Dockerfile run +RUN apt-get update && \ + apt-get install -y wget + +# Apt installs: +# - Postgres cli tools are required for lucky-cli. +# - tmux is required for the Overmind process manager. +RUN apt-get update && \ + apt-get install -y postgresql-client tmux && \ + rm -rf /var/lib/apt/lists/* + +# Install lucky cli +WORKDIR /lucky/cli +RUN git clone https://github.com/luckyframework/lucky_cli . && \ + git checkout v1.0.0 && \ + shards build --without-development && \ + cp bin/lucky /usr/bin + +WORKDIR /app +ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres +EXPOSE 3000 +EXPOSE 3001 + diff --git a/fixtures/src_template/expected/docker/wait-for-it.sh b/fixtures/src_template/expected/docker/wait-for-it.sh new file mode 100755 index 00000000..06e0638c --- /dev/null +++ b/fixtures/src_template/expected/docker/wait-for-it.sh @@ -0,0 +1,189 @@ +#!/usr/bin/bash +# +# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28. +# Licensed under the MIT license as of 81b1373f. +# +# Below this line, wait-for-it is the original work of the author. +# +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/fixtures/src_template/expected/script/helpers/function_helpers b/fixtures/src_template/expected/script/helpers/function_helpers new file mode 100644 index 00000000..388fa67e --- /dev/null +++ b/fixtures/src_template/expected/script/helpers/function_helpers @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used as helpers +# for various tasks. Read the examples for each one for +# more information. Feel free to put any additional helper +# functions you may need for your app + + +# Returns true if the command $1 is not found +# example: +# if command_not_found "yarn"; then +# echo "no yarn" +# fi +command_not_found() { + ! command -v $1 > /dev/null + return $? +} + +# Returns true if the command $1 is not running +# You must supply the full command to check as an argument +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running" +# fi +command_not_running() { + $1 + if [ $? -ne 0 ]; then + true + else + false + fi +} + +# Returns true if the OS is macOS +# example: +# if is_mac; then +# echo "do mac stuff" +# fi +is_mac() { + if [[ "$OSTYPE" == "darwin"* ]]; then + true + else + false + fi +} + +# Returns true if the OS is linux based +# example: +# if is_linux; then +# echo "do linux stuff" +# fi +is_linux() { + if [[ "$OSTYPE" == "linux"* ]]; then + true + else + false + fi +} + +# Prints error and exit. +# example: +# print_error "Redis is not running. Run it with some_command" +print_error() { + printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n" + printf "${BOLD_RED_COLOR}$1 \n\n" | indent + exit 1 +} diff --git a/fixtures/src_template/expected/script/helpers/text_helpers b/fixtures/src_template/expected/script/helpers/text_helpers new file mode 100644 index 00000000..34b77a8c --- /dev/null +++ b/fixtures/src_template/expected/script/helpers/text_helpers @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used to format text, +# and make printing text a little easier. Feel free to put +# any additional functions you need for formatting your shell +# output text. + +# Colors +BOLD_RED_COLOR="\e[1m\e[31m" + +# Indents the text 2 spaces +# example: +# printf "Hello" | indent +indent() { + while read LINE; do + echo " $LINE" || true + done +} + +# Prints out an arrow to your custom notice +# example: +# notice "Installing new magic" +notice() { + printf "\n▸ $1\n" +} + +# Prints out a check mark and Done. +# example: +# print_done +print_done() { + printf "✔ Done\n" | indent +} diff --git a/fixtures/src_template/expected/script/setup b/fixtures/src_template/expected/script/setup new file mode 100755 index 00000000..5a97531e --- /dev/null +++ b/fixtures/src_template/expected/script/setup @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Exit if any subcommand fails +set -e +set -o pipefail + +source script/helpers/text_helpers + + +notice "Running System Check" +./script/system_check +print_done + + +notice "Installing shards" +shards install --ignore-crystal-version | indent + +if [ ! -f ".env" ]; then + notice "No .env found. Creating one." + touch .env + print_done +fi + +notice "Creating the database" +lucky db.create | indent + +notice "Verifying postgres connection" +lucky db.verify_connection | indent + +notice "Migrating the database" +lucky db.migrate | indent + +notice "Seeding the database with required and sample records" +lucky db.seed.required_data | indent +lucky db.seed.sample_data | indent + +print_done +notice "Run 'lucky dev' to start the app" diff --git a/fixtures/src_template/expected/script/system_check b/fixtures/src_template/expected/script/system_check new file mode 100755 index 00000000..eea2533a --- /dev/null +++ b/fixtures/src_template/expected/script/system_check @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +source script/helpers/text_helpers +source script/helpers/function_helpers + +# Use this script to check the system for required tools and process that your app needs. +# A few helper functions are provided to make writing bash a little easier. See the +# script/helpers/function_helpers file for more examples. +# +# A few examples you might use here: +# * 'lucky db.verify_connection' to test postgres can be connected +# * Checking that elasticsearch, redis, or postgres is installed and/or booted +# * Note: Booting additional processes for things like mail, background jobs, etc... +# should go in your Procfile.dev. + + +if command_not_found "createdb"; then + MSG="Please install the postgres CLI tools, then try again." + if is_mac; then + MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html." + fi + MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions." + + print_error "$MSG" +fi + + +## CUSTOM PRE-BOOT CHECKS ## +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running." +# fi + + diff --git a/fixtures/src_template/expected/spec/setup/clean_database.cr b/fixtures/src_template/expected/spec/setup/clean_database.cr new file mode 100644 index 00000000..a1bc631c --- /dev/null +++ b/fixtures/src_template/expected/spec/setup/clean_database.cr @@ -0,0 +1,3 @@ +Spec.before_each do + AppDatabase.truncate +end diff --git a/fixtures/src_template/expected/spec/setup/reset_emails.cr b/fixtures/src_template/expected/spec/setup/reset_emails.cr new file mode 100644 index 00000000..140ab416 --- /dev/null +++ b/fixtures/src_template/expected/spec/setup/reset_emails.cr @@ -0,0 +1,3 @@ +Spec.before_each do + Carbon::DevAdapter.reset +end diff --git a/fixtures/src_template/expected/spec/setup/setup_database.cr b/fixtures/src_template/expected/spec/setup/setup_database.cr new file mode 100644 index 00000000..393c6da3 --- /dev/null +++ b/fixtures/src_template/expected/spec/setup/setup_database.cr @@ -0,0 +1,2 @@ +Db::Create.new(quiet: true).call +Db::Migrate.new(quiet: true).call diff --git a/fixtures/src_template/expected/spec/setup/start_app_server.cr b/fixtures/src_template/expected/spec/setup/start_app_server.cr new file mode 100644 index 00000000..3a64c702 --- /dev/null +++ b/fixtures/src_template/expected/spec/setup/start_app_server.cr @@ -0,0 +1,9 @@ +app_server = AppServer.new + +spawn do + app_server.listen +end + +Spec.after_suite do + app_server.close +end diff --git a/fixtures/src_template/expected/spec/spec_helper.cr b/fixtures/src_template/expected/spec/spec_helper.cr new file mode 100644 index 00000000..d876014d --- /dev/null +++ b/fixtures/src_template/expected/spec/spec_helper.cr @@ -0,0 +1,19 @@ +ENV["LUCKY_ENV"] = "test" +ENV["DEV_PORT"] = "5001" +require "spec" +require "../src/app" +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 + +Avram::Migrator::Runner.new.ensure_migrated! +Avram::SchemaEnforcer.ensure_correct_column_mappings! +Habitat.raise_if_missing_settings! diff --git a/fixtures/src_template/expected/spec/support/.keep b/fixtures/src_template/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/spec/support/api_client.cr b/fixtures/src_template/expected/spec/support/api_client.cr new file mode 100644 index 00000000..46d449a8 --- /dev/null +++ b/fixtures/src_template/expected/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/fixtures/src_template/expected/spec/support/factories/.keep b/fixtures/src_template/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/actions/api_action.cr b/fixtures/src_template/expected/src/actions/api_action.cr new file mode 100644 index 00000000..a16fd09e --- /dev/null +++ b/fixtures/src_template/expected/src/actions/api_action.cr @@ -0,0 +1,17 @@ +# Include modules and add methods that are for all API requests +abstract class ApiAction < Lucky::Action + # APIs typically do not need to send cookie/session data. + # Remove this line if you want to send cookies in the response header. + disable_cookies + accepted_formats [:json] + + include Api::Auth::Helpers + + # By default all actions require sign in. + # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests. + include Api::Auth::RequireAuthToken + + # By default all actions are required to use underscores to separate words. + # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute +end diff --git a/fixtures/src_template/expected/src/actions/errors/show.cr b/fixtures/src_template/expected/src/actions/errors/show.cr new file mode 100644 index 00000000..a80eaa4d --- /dev/null +++ b/fixtures/src_template/expected/src/actions/errors/show.cr @@ -0,0 +1,42 @@ +# This class handles error responses and reporting. +# +# https://luckyframework.org/guides/http-and-routing/error-handling +class Errors::Show < Lucky::ErrorAction + DEFAULT_MESSAGE = "Something went wrong." + default_format :json + dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] + + def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) + error_json "Not found", status: 404 + end + + # When an InvalidOperationError is raised, show a helpful error with the + # param that is invalid, and what was wrong with it. + def render(error : Avram::InvalidOperationError) + error_json \ + message: error.renderable_message, + details: error.renderable_details, + param: error.invalid_attribute_name, + status: 400 + end + + # Always keep this below other 'render' methods or it may override your + # custom 'render' methods. + def render(error : Lucky::RenderableError) + error_json error.renderable_message, status: error.renderable_status + end + + # If none of the 'render' methods return a response for the raised Exception, + # Lucky will use this method. + def default_render(error : Exception) : Lucky::Response + error_json DEFAULT_MESSAGE, status: 500 + end + + private def error_json(message : String, status : Int, details = nil, param = nil) + json ErrorSerializer.new(message: message, details: details, param: param), status: status + end + + private def report(error : Exception) : Nil + # Send to Rollbar, send an email, etc. + end +end diff --git a/fixtures/src_template/expected/src/actions/home/index.cr b/fixtures/src_template/expected/src/actions/home/index.cr new file mode 100644 index 00000000..5a72b779 --- /dev/null +++ b/fixtures/src_template/expected/src/actions/home/index.cr @@ -0,0 +1,7 @@ +class Home::Index < ApiAction + include Api::Auth::SkipRequireAuthToken + + get "/" do + json({hello: "Hello World from Home::Index"}) + end +end diff --git a/fixtures/src_template/expected/src/actions/mixins/.keep b/fixtures/src_template/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/app.cr b/fixtures/src_template/expected/src/app.cr new file mode 100644 index 00000000..21fd1eac --- /dev/null +++ b/fixtures/src_template/expected/src/app.cr @@ -0,0 +1,20 @@ +require "./shards" + +require "../config/server" +require "./app_database" +require "../config/**" +require "./models/base_model" +require "./models/mixins/**" +require "./models/**" +require "./queries/mixins/**" +require "./queries/**" +require "./operations/mixins/**" +require "./operations/**" +require "./serializers/base_serializer" +require "./serializers/**" +require "./emails/base_email" +require "./emails/**" +require "./actions/mixins/**" +require "./actions/**" +require "../db/migrations/**" +require "./app_server" diff --git a/fixtures/src_template/expected/src/app_database.cr b/fixtures/src_template/expected/src/app_database.cr new file mode 100644 index 00000000..0efd4f50 --- /dev/null +++ b/fixtures/src_template/expected/src/app_database.cr @@ -0,0 +1,2 @@ +class AppDatabase < Avram::Database +end diff --git a/fixtures/src_template/expected/src/app_server.cr b/fixtures/src_template/expected/src/app_server.cr new file mode 100644 index 00000000..53f483d1 --- /dev/null +++ b/fixtures/src_template/expected/src/app_server.cr @@ -0,0 +1,28 @@ +class AppServer < Lucky::BaseAppServer + # Learn about middleware with HTTP::Handlers: + # https://luckyframework.org/guides/http-and-routing/http-handlers + def middleware : Array(HTTP::Handler) + [ + Lucky::RequestIdHandler.new, + Lucky::ForceSSLHandler.new, + Lucky::HttpMethodOverrideHandler.new, + Lucky::LogHandler.new, + Lucky::ErrorHandler.new(action: Errors::Show), + Lucky::RemoteIpHandler.new, + Lucky::RouteHandler.new, + + # Disabled in API mode: + # Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), + # Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), + Lucky::RouteNotFoundHandler.new, + ] of HTTP::Handler + end + + def protocol + "http" + end + + def listen + server.listen(host, port, reuse_port: false) + end +end diff --git a/fixtures/src_template/expected/src/emails/base_email.cr b/fixtures/src_template/expected/src/emails/base_email.cr new file mode 100644 index 00000000..656f4f11 --- /dev/null +++ b/fixtures/src_template/expected/src/emails/base_email.cr @@ -0,0 +1,15 @@ +# Learn about sending emails +# https://luckyframework.org/guides/emails/sending-emails-with-carbon +abstract class BaseEmail < Carbon::Email + # You can add defaults using the 'inherited' hook + # + # Example: + # + # macro inherited + # from default_from + # end + # + # def default_from + # Carbon::Address.new("support@app.com") + # end +end diff --git a/fixtures/src_template/expected/src/models/base_model.cr b/fixtures/src_template/expected/src/models/base_model.cr new file mode 100644 index 00000000..6bafeb84 --- /dev/null +++ b/fixtures/src_template/expected/src/models/base_model.cr @@ -0,0 +1,5 @@ +abstract class BaseModel < Avram::Model + def self.database : Avram::Database.class + AppDatabase + end +end diff --git a/fixtures/src_template/expected/src/models/mixins/.keep b/fixtures/src_template/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/operations/.keep b/fixtures/src_template/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/operations/mixins/.keep b/fixtures/src_template/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/queries/.keep b/fixtures/src_template/expected/src/queries/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/queries/mixins/.keep b/fixtures/src_template/expected/src/queries/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/serializers/.keep b/fixtures/src_template/expected/src/serializers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/src/serializers/base_serializer.cr b/fixtures/src_template/expected/src/serializers/base_serializer.cr new file mode 100644 index 00000000..3ad0a669 --- /dev/null +++ b/fixtures/src_template/expected/src/serializers/base_serializer.cr @@ -0,0 +1,7 @@ +abstract class BaseSerializer < Lucky::Serializer + def self.for_collection(collection : Enumerable, *args, **named_args) + collection.map do |object| + new(object, *args, **named_args) + end + end +end diff --git a/fixtures/src_template/expected/src/serializers/error_serializer.cr b/fixtures/src_template/expected/src/serializers/error_serializer.cr new file mode 100644 index 00000000..21a53aa2 --- /dev/null +++ b/fixtures/src_template/expected/src/serializers/error_serializer.cr @@ -0,0 +1,14 @@ +# This is the default error serializer generated by Lucky. +# Feel free to customize it in any way you like. +class ErrorSerializer < BaseSerializer + def initialize( + @message : String, + @details : String? = nil, + @param : String? = nil # so you can track which param (if any) caused the problem + ) + end + + def render + {message: @message, param: @param, details: @details} + end +end diff --git a/fixtures/src_template/expected/src/shards.cr b/fixtures/src_template/expected/src/shards.cr new file mode 100644 index 00000000..7cadec18 --- /dev/null +++ b/fixtures/src_template/expected/src/shards.cr @@ -0,0 +1,10 @@ +# Load .env file before any other config or app code +require "lucky_env" +LuckyEnv.load?(".env") + +# Require your shards here +require "lucky" +require "avram/lucky" +require "carbon" +require "authentic" +require "jwt" diff --git a/fixtures/src_template/expected/src/start_server.cr b/fixtures/src_template/expected/src/start_server.cr new file mode 100644 index 00000000..de8af78e --- /dev/null +++ b/fixtures/src_template/expected/src/start_server.cr @@ -0,0 +1,17 @@ +require "./app" + +Habitat.raise_if_missing_settings! + +if LuckyEnv.development? + Avram::Migrator::Runner.new.ensure_migrated! + Avram::SchemaEnforcer.ensure_correct_column_mappings! +end + +app_server = AppServer.new +puts "Listening on http://#{app_server.host}:#{app_server.port}" + +Signal::INT.trap do + app_server.close +end + +app_server.listen diff --git a/fixtures/src_template/expected/src/test_project.cr b/fixtures/src_template/expected/src/test_project.cr new file mode 100644 index 00000000..68e1a8d2 --- /dev/null +++ b/fixtures/src_template/expected/src/test_project.cr @@ -0,0 +1,6 @@ +# Typically you will not use or modify this file. 'shards build' and some +# other crystal tools will sometimes use this. +# +# When this file is compiled/run it will require and run 'start_server', +# which as its name implies will start the server for you app. +require "./start_server" diff --git a/fixtures/src_template/expected/tasks.cr b/fixtures/src_template/expected/tasks.cr new file mode 100644 index 00000000..5a892d4d --- /dev/null +++ b/fixtures/src_template/expected/tasks.cr @@ -0,0 +1,25 @@ +# This file loads your app and all your tasks when running 'lucky' +# +# Run 'lucky --help' to see all available tasks. +# +# Learn to create your own tasks: +# https://luckyframework.org/guides/command-line-tasks/custom-tasks + +# See `LuckyEnv#task?` +ENV["LUCKY_TASK"] = "true" + +# Load Lucky and the app (actions, models, etc.) +require "./src/app" +require "lucky_task" + +# You can add your own tasks here in the ./tasks folder +require "./tasks/**" + +# Load migrations +require "./db/migrations/**" + +# Load Lucky tasks (dev, routes, etc.) +require "lucky/tasks/**" +require "avram/lucky/tasks" + +LuckyTask::Runner.run diff --git a/fixtures/src_template/expected/tasks/.keep b/fixtures/src_template/expected/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template/expected/tasks/db/seed/required_data.cr b/fixtures/src_template/expected/tasks/db/seed/required_data.cr new file mode 100644 index 00000000..d866040f --- /dev/null +++ b/fixtures/src_template/expected/tasks/db/seed/required_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add seeds here that are *required* for your app to work. +# For example, you might need at least one admin user or you might need at least +# one category for your blog posts for the app to work. +# +# Use `Db::Seed::SampleData` if your only want to add sample data helpful for +# development. +class Db::Seed::RequiredData < LuckyTask::Task + summary "Add database records required for the app to work" + + def call + # Using a Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # + # SaveUser.create!(email: "me@example.com", name: "Jane") + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # + # unless UserQuery.new.email("me@example.com").first? + # SaveUser.create!(email: "me@example.com", name: "Jane") + # end + puts "Done adding required data" + end +end diff --git a/fixtures/src_template/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template/expected/tasks/db/seed/sample_data.cr new file mode 100644 index 00000000..231d7e8d --- /dev/null +++ b/fixtures/src_template/expected/tasks/db/seed/sample_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add sample data helpful for development, e.g. (fake users, blog posts, etc.) +# +# Use `Db::Seed::RequiredData` if you need to create data *required* for your +# app to work. +class Db::Seed::SampleData < LuckyTask::Task + summary "Add sample database records helpful for development" + + def call + # Using an Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # ``` + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # ``` + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # ``` + # if UserQuery.new.email("me@example.com").none? + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # end + # ``` + puts "Done adding sample data" + end +end diff --git a/fixtures/src_template__api_only/expected/.crystal-version b/fixtures/src_template__api_only/expected/.crystal-version new file mode 100644 index 00000000..fdd3be6d --- /dev/null +++ b/fixtures/src_template__api_only/expected/.crystal-version @@ -0,0 +1 @@ +1.6.2 diff --git a/fixtures/src_template__api_only/expected/.env b/fixtures/src_template__api_only/expected/.env new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/.github/workflows/ci.yml b/fixtures/src_template__api_only/expected/.github/workflows/ci.yml new file mode 100644 index 00000000..2f4ae13b --- /dev/null +++ b/fixtures/src_template__api_only/expected/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: test-project CI + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + check-format: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + - name: Format + run: crystal tool format --check + + specs: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + env: + LUCKY_ENV: test + DB_HOST: localhost + continue-on-error: ${{ matrix.experimental }} + services: + postgres: + image: postgres:12-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + + - name: Set up Crystal cache + uses: actions/cache@v2 + id: crystal-cache + with: + path: | + ~/.cache/crystal + lib + lucky_tasks + key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }} + restore-keys: | + ${{ runner.os }}-crystal- + + - name: Install shards + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: shards check || shards install + + - name: Build lucky_tasks + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: crystal build tasks.cr -o ./lucky_tasks + + - name: Prepare database + run: | + ./lucky_tasks db.create + ./lucky_tasks db.migrate + ./lucky_tasks db.seed.required_data + + - name: Run tests + run: crystal spec \ No newline at end of file diff --git a/fixtures/src_template__api_only/expected/Procfile b/fixtures/src_template__api_only/expected/Procfile new file mode 100644 index 00000000..e524d70d --- /dev/null +++ b/fixtures/src_template__api_only/expected/Procfile @@ -0,0 +1,2 @@ +web: bin/app +release: lucky db.migrate diff --git a/fixtures/src_template__api_only/expected/Procfile.dev b/fixtures/src_template__api_only/expected/Procfile.dev new file mode 100644 index 00000000..9cca7346 --- /dev/null +++ b/fixtures/src_template__api_only/expected/Procfile.dev @@ -0,0 +1,2 @@ +system_check: script/system_check && sleep 100000 +web: lucky watch diff --git a/fixtures/src_template__api_only/expected/README.md b/fixtures/src_template__api_only/expected/README.md new file mode 100644 index 00000000..da2d97cc --- /dev/null +++ b/fixtures/src_template__api_only/expected/README.md @@ -0,0 +1,23 @@ +# test-project + +This is a project written using [Lucky](https://luckyframework.org). Enjoy! + +### Setting up the project + +1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies) +1. Update database settings in `config/database.cr` +1. Run `script/setup` +1. Run `lucky dev` to start the app + +### Using Docker for development + +1. [Install Docker](https://docs.docker.com/engine/install/) +1. Run `docker compose up` + +The Docker container will boot all of the necessary components needed to run your Lucky application. +To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file. + + +### Learning Lucky + +Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky). diff --git a/fixtures/src_template__api_only/expected/config/application.cr b/fixtures/src_template__api_only/expected/config/application.cr new file mode 100644 index 00000000..c807149a --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/application.cr @@ -0,0 +1,24 @@ +# This file may be used for custom Application configurations. +# It will be loaded before other config files. +# +# Read more on configuration: +# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code + +# Use this code as an example: +# +# ``` +# module Application +# Habitat.create do +# setting support_email : String +# setting lock_with_basic_auth : Bool +# end +# end +# +# Application.configure do |settings| +# settings.support_email = "support@myapp.io" +# settings.lock_with_basic_auth = LuckyEnv.staging? +# end +# +# # In your application, call +# # `Application.settings.support_email` anywhere you need it. +# ``` diff --git a/fixtures/src_template__api_only/expected/config/colors.cr b/fixtures/src_template__api_only/expected/config/colors.cr new file mode 100644 index 00000000..761ae940 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/colors.cr @@ -0,0 +1,4 @@ +# This enables the color output when in development or test +# Check out the Colorize docs for more information +# https://crystal-lang.org/api/Colorize.html +Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? diff --git a/fixtures/src_template__api_only/expected/config/cookies.cr b/fixtures/src_template__api_only/expected/config/cookies.cr new file mode 100644 index 00000000..2d5055f2 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/cookies.cr @@ -0,0 +1,25 @@ +require "./server" + +Lucky::Session.configure do |settings| + settings.key = "_test_project_session" +end + +Lucky::CookieJar.configure do |settings| + settings.on_set = ->(cookie : HTTP::Cookie) { + # If ForceSSLHandler is enabled, only send cookies over HTTPS + cookie.secure(Lucky::ForceSSLHandler.settings.enabled) + + # By default, don't allow reading cookies with JavaScript + cookie.http_only(true) + + # Restrict cookies to a first-party or same-site context + cookie.samesite(:lax) + + # Set all cookies to the root path by default + cookie.path("/") + + # You can set other defaults for cookies here. For example: + # + # cookie.expires(1.year.from_now).domain("mydomain.com") + } +end diff --git a/fixtures/src_template__api_only/expected/config/database.cr b/fixtures/src_template__api_only/expected/config/database.cr new file mode 100644 index 00000000..f614299a --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/database.cr @@ -0,0 +1,29 @@ +database_name = "test_project_#{LuckyEnv.environment}" + +AppDatabase.configure do |settings| + if LuckyEnv.production? + settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"]) + else + settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new( + database: database_name, + hostname: ENV["DB_HOST"]? || "localhost", + port: ENV["DB_PORT"]?.try(&.to_i) || 5432, + # Some common usernames are "postgres", "root", or your system username (run 'whoami') + username: ENV["DB_USERNAME"]? || "postgres", + # Some Postgres installations require no password. Use "" if that is the case. + password: ENV["DB_PASSWORD"]? || "postgres" + ) + end +end + +Avram.configure do |settings| + settings.database_to_migrate = AppDatabase + + # In production, allow lazy loading (N+1). + # In development and test, raise an error if you forget to preload associations + settings.lazy_load_enabled = LuckyEnv.production? + + # Always parse `Time` values with these specific formats. + # Used for both database values, and datetime input fields. + # settings.time_formats << "%F" +end diff --git a/fixtures/src_template__api_only/expected/config/email.cr b/fixtures/src_template__api_only/expected/config/email.cr new file mode 100644 index 00000000..7c875449 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/email.cr @@ -0,0 +1,26 @@ +require "carbon_sendgrid_adapter" + +BaseEmail.configure do |settings| + if LuckyEnv.production? + # If you don't need to send emails, set the adapter to DevAdapter instead: + # + # settings.adapter = Carbon::DevAdapter.new + # + # If you do need emails, get a key from SendGrid and set an ENV variable + send_grid_key = send_grid_key_from_env + settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key) + elsif LuckyEnv.development? + settings.adapter = Carbon::DevAdapter.new(print_emails: true) + else + settings.adapter = Carbon::DevAdapter.new + end +end + +private def send_grid_key_from_env + ENV["SEND_GRID_KEY"]? || raise_missing_key_message +end + +private def raise_missing_key_message + puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red + exit(1) +end diff --git a/fixtures/src_template__api_only/expected/config/env.cr b/fixtures/src_template__api_only/expected/config/env.cr new file mode 100644 index 00000000..3f364072 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/env.cr @@ -0,0 +1,33 @@ +# Environments are managed using `LuckyEnv`. By default, development, production +# and test are supported. See +# https://luckyframework.org/guides/getting-started/configuration for details. +# +# The default environment is development unless the environment variable +# LUCKY_ENV is set. +# +# Example: +# ``` +# LuckyEnv.environment # => "development" +# LuckyEnv.development? # => true +# LuckyEnv.production? # => false +# LuckyEnv.test? # => false +# ``` +# +# New environments can be added using the `LuckyEnv.add_env` macro. +# +# Example: +# ``` +# LuckyEnv.add_env :staging +# LuckyEnv.staging? # => false +# ``` +# +# To determine whether or not a `LuckyTask` is currently running, you can use +# the `LuckyEnv.task?` predicate. +# +# Example: +# ``` +# LuckyEnv.task? # => false +# ``` + +# Add a staging environment. +# LuckyEnv.add_env :staging diff --git a/fixtures/src_template__api_only/expected/config/error_handler.cr b/fixtures/src_template__api_only/expected/config/error_handler.cr new file mode 100644 index 00000000..c6b736e3 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/error_handler.cr @@ -0,0 +1,3 @@ +Lucky::ErrorHandler.configure do |settings| + settings.show_debug_output = !LuckyEnv.production? +end diff --git a/fixtures/src_template__api_only/expected/config/log.cr b/fixtures/src_template__api_only/expected/config/log.cr new file mode 100644 index 00000000..0e8d7636 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/log.cr @@ -0,0 +1,45 @@ +require "file_utils" + +if LuckyEnv.test? + # Logs to `tmp/test.log` so you can see what's happening without having + # a bunch of log output in your spec results. + FileUtils.mkdir_p("tmp") + + backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w")) + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) +elsif LuckyEnv.production? + # Lucky uses JSON in production so logs can be searched more easily + # + # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'. + backend = Log::IOBackend.new + backend.formatter = Dexter::JSONLogFormatter.proc + Log.dexter.configure(:info, backend) +else + # Use a pretty formatter printing to STDOUT in development + backend = Log::IOBackend.new + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) + DB::Log.level = :info +end + +# Lucky only logs when before/after pipes halt by redirecting, or rendering a +# response. Pipes that run without halting are not logged. +# +# If you want to log every pipe that runs, set the log level to ':info' +Lucky::ContinuedPipeLog.dexter.configure(:none) + +# Lucky only logs failed queries by default. +# +# Set the log to ':info' to log all queries +Avram::QueryLog.dexter.configure(:none) + +# Skip logging static assets requests in development +Lucky::LogHandler.configure do |settings| + if LuckyEnv.development? + settings.skip_if = ->(context : HTTP::Server::Context) { + context.request.method.downcase == "get" && + context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/) + } + end +end diff --git a/fixtures/src_template__api_only/expected/config/route_helper.cr b/fixtures/src_template__api_only/expected/config/route_helper.cr new file mode 100644 index 00000000..ede1f328 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/route_helper.cr @@ -0,0 +1,10 @@ +# This is used when generating URLs for your application +Lucky::RouteHelper.configure do |settings| + if LuckyEnv.production? + # Example: https://my_app.com + settings.base_uri = ENV.fetch("APP_DOMAIN") + else + # Set domain to the default host/port in development/test + settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" + end +end diff --git a/fixtures/src_template__api_only/expected/config/server.cr b/fixtures/src_template__api_only/expected/config/server.cr new file mode 100644 index 00000000..c72314be --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/server.cr @@ -0,0 +1,65 @@ +# Here is where you configure the Lucky server +# +# Look at config/route_helper.cr if you want to change the domain used when +# generating links with `Action.url`. +Lucky::Server.configure do |settings| + if LuckyEnv.production? + settings.secret_key_base = secret_key_from_env + settings.host = "0.0.0.0" + settings.port = ENV["PORT"].to_i + settings.gzip_enabled = true + # By default certain content types will be gzipped. + # For a full list look in + # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr + # To add additional extensions do something like this: + # settings.gzip_content_types << "content/type" + else + settings.secret_key_base = "1234567890" + # Change host/port in config/watch.yml + # Alternatively, you can set the DEV_PORT env to set the port for local development + settings.host = Lucky::ServerSettings.host + settings.port = Lucky::ServerSettings.port + end + + # By default Lucky will serve static assets in development and production. + # + # However you could use a CDN when in production like this: + # + # Lucky::Server.configure do |settings| + # if LuckyEnv.production? + # settings.asset_host = "https://mycdnhost.com" + # else + # settings.asset_host = "" + # end + # end + settings.asset_host = "" # Lucky will serve assets +end + +Lucky::ForceSSLHandler.configure do |settings| + # To force SSL in production, uncomment the lines below. + # This will cause http requests to be redirected to https: + # + # settings.enabled = LuckyEnv.production? + # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true} + # + # Or, leave it disabled: + settings.enabled = false +end + +# Set a unique ID for each HTTP request. +# To enable the request ID, uncomment the lines below. +# You can set your own custom String, or use a random UUID. +# Lucky::RequestIdHandler.configure do |settings| +# settings.set_request_id = ->(context : HTTP::Server::Context) { +# UUID.random.to_s +# } +# end + +private def secret_key_from_env + ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production +end + +private def raise_missing_secret_key_in_production + puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red + exit(1) +end diff --git a/fixtures/src_template__api_only/expected/config/watch.yml b/fixtures/src_template__api_only/expected/config/watch.yml new file mode 100644 index 00000000..3a59b410 --- /dev/null +++ b/fixtures/src_template__api_only/expected/config/watch.yml @@ -0,0 +1,3 @@ +host: 127.0.0.1 +port: 3000 +reload_port: 3001 diff --git a/fixtures/src_template__api_only/expected/db/migrations/.keep b/fixtures/src_template__api_only/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/docker-compose.yml b/fixtures/src_template__api_only/expected/docker-compose.yml new file mode 100644 index 00000000..d00779db --- /dev/null +++ b/fixtures/src_template__api_only/expected/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + lucky: + build: + context: . + dockerfile: docker/development.dockerfile + environment: + DATABASE_URL: postgres://lucky:password@postgres:5432/lucky + DEV_HOST: "0.0.0.0" + volumes: + - .:/app + - node_modules:/app/node_modules + - shards_lib:/app/lib + - app_bin:/app/bin + - build_cache:/root/.cache + depends_on: + - postgres + ports: + - 3000:3000 # This is the Lucky Server port + - 3001:3001 # This is the Lucky watcher reload port + + entrypoint: ["docker/dev_entrypoint.sh"] + + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: lucky + POSTGRES_PASSWORD: password + POSTGRES_DB: lucky + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + # The postgres database container is exposed on the host at port 6543 to + # allow connecting directly to it with postgres clients. The port differs + # from the postgres default to avoid conflict with existing postgres + # servers. Connect to a running postgres container with: + # postgres://lucky:password@localhost:6543/lucky + - 6543:5432 + +volumes: + postgres_data: + node_modules: + shards_lib: + app_bin: + build_cache: diff --git a/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh b/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh new file mode 100755 index 00000000..f5aab877 --- /dev/null +++ b/fixtures/src_template__api_only/expected/docker/dev_entrypoint.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -euo pipefail + +# This is the entrypoint script used for development docker workflows. +# By default it will: +# - Install dependencies. +# - Run migrations. +# - Start the dev server. +# It also accepts any commands to be run instead. + + +warnfail () { + echo "$@" >&2 + exit 1 +} + +case ${1:-} in + "") # If no arguments are provided, start lucky dev server. + ;; + + *) # If any arguments are provided, execute them instead. + exec "$@" +esac + +if ! [ -d bin ] ; then + echo 'Creating bin directory' + mkdir bin +fi +if ! shards check ; then + echo 'Installing shards...' + shards install +fi + +echo 'Waiting for postgres to be available...' +./docker/wait-for-it.sh -q postgres:5432 + +if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then + echo 'Finishing database setup...' + lucky db.migrate +fi + +echo 'Starting lucky dev server...' +exec lucky dev diff --git a/fixtures/src_template__api_only/expected/docker/development.dockerfile b/fixtures/src_template__api_only/expected/docker/development.dockerfile new file mode 100644 index 00000000..11222751 --- /dev/null +++ b/fixtures/src_template__api_only/expected/docker/development.dockerfile @@ -0,0 +1,25 @@ +FROM crystallang/crystal:1.6.2 + +# Install utilities required to make this Dockerfile run +RUN apt-get update && \ + apt-get install -y wget + +# Apt installs: +# - Postgres cli tools are required for lucky-cli. +# - tmux is required for the Overmind process manager. +RUN apt-get update && \ + apt-get install -y postgresql-client tmux && \ + rm -rf /var/lib/apt/lists/* + +# Install lucky cli +WORKDIR /lucky/cli +RUN git clone https://github.com/luckyframework/lucky_cli . && \ + git checkout v1.0.0 && \ + shards build --without-development && \ + cp bin/lucky /usr/bin + +WORKDIR /app +ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres +EXPOSE 3000 +EXPOSE 3001 + diff --git a/fixtures/src_template__api_only/expected/docker/wait-for-it.sh b/fixtures/src_template__api_only/expected/docker/wait-for-it.sh new file mode 100755 index 00000000..06e0638c --- /dev/null +++ b/fixtures/src_template__api_only/expected/docker/wait-for-it.sh @@ -0,0 +1,189 @@ +#!/usr/bin/bash +# +# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28. +# Licensed under the MIT license as of 81b1373f. +# +# Below this line, wait-for-it is the original work of the author. +# +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/fixtures/src_template__api_only/expected/script/helpers/function_helpers b/fixtures/src_template__api_only/expected/script/helpers/function_helpers new file mode 100644 index 00000000..388fa67e --- /dev/null +++ b/fixtures/src_template__api_only/expected/script/helpers/function_helpers @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used as helpers +# for various tasks. Read the examples for each one for +# more information. Feel free to put any additional helper +# functions you may need for your app + + +# Returns true if the command $1 is not found +# example: +# if command_not_found "yarn"; then +# echo "no yarn" +# fi +command_not_found() { + ! command -v $1 > /dev/null + return $? +} + +# Returns true if the command $1 is not running +# You must supply the full command to check as an argument +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running" +# fi +command_not_running() { + $1 + if [ $? -ne 0 ]; then + true + else + false + fi +} + +# Returns true if the OS is macOS +# example: +# if is_mac; then +# echo "do mac stuff" +# fi +is_mac() { + if [[ "$OSTYPE" == "darwin"* ]]; then + true + else + false + fi +} + +# Returns true if the OS is linux based +# example: +# if is_linux; then +# echo "do linux stuff" +# fi +is_linux() { + if [[ "$OSTYPE" == "linux"* ]]; then + true + else + false + fi +} + +# Prints error and exit. +# example: +# print_error "Redis is not running. Run it with some_command" +print_error() { + printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n" + printf "${BOLD_RED_COLOR}$1 \n\n" | indent + exit 1 +} diff --git a/fixtures/src_template__api_only/expected/script/helpers/text_helpers b/fixtures/src_template__api_only/expected/script/helpers/text_helpers new file mode 100644 index 00000000..34b77a8c --- /dev/null +++ b/fixtures/src_template__api_only/expected/script/helpers/text_helpers @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used to format text, +# and make printing text a little easier. Feel free to put +# any additional functions you need for formatting your shell +# output text. + +# Colors +BOLD_RED_COLOR="\e[1m\e[31m" + +# Indents the text 2 spaces +# example: +# printf "Hello" | indent +indent() { + while read LINE; do + echo " $LINE" || true + done +} + +# Prints out an arrow to your custom notice +# example: +# notice "Installing new magic" +notice() { + printf "\n▸ $1\n" +} + +# Prints out a check mark and Done. +# example: +# print_done +print_done() { + printf "✔ Done\n" | indent +} diff --git a/fixtures/src_template__api_only/expected/script/setup b/fixtures/src_template__api_only/expected/script/setup new file mode 100755 index 00000000..5a97531e --- /dev/null +++ b/fixtures/src_template__api_only/expected/script/setup @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Exit if any subcommand fails +set -e +set -o pipefail + +source script/helpers/text_helpers + + +notice "Running System Check" +./script/system_check +print_done + + +notice "Installing shards" +shards install --ignore-crystal-version | indent + +if [ ! -f ".env" ]; then + notice "No .env found. Creating one." + touch .env + print_done +fi + +notice "Creating the database" +lucky db.create | indent + +notice "Verifying postgres connection" +lucky db.verify_connection | indent + +notice "Migrating the database" +lucky db.migrate | indent + +notice "Seeding the database with required and sample records" +lucky db.seed.required_data | indent +lucky db.seed.sample_data | indent + +print_done +notice "Run 'lucky dev' to start the app" diff --git a/fixtures/src_template__api_only/expected/script/system_check b/fixtures/src_template__api_only/expected/script/system_check new file mode 100755 index 00000000..eea2533a --- /dev/null +++ b/fixtures/src_template__api_only/expected/script/system_check @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +source script/helpers/text_helpers +source script/helpers/function_helpers + +# Use this script to check the system for required tools and process that your app needs. +# A few helper functions are provided to make writing bash a little easier. See the +# script/helpers/function_helpers file for more examples. +# +# A few examples you might use here: +# * 'lucky db.verify_connection' to test postgres can be connected +# * Checking that elasticsearch, redis, or postgres is installed and/or booted +# * Note: Booting additional processes for things like mail, background jobs, etc... +# should go in your Procfile.dev. + + +if command_not_found "createdb"; then + MSG="Please install the postgres CLI tools, then try again." + if is_mac; then + MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html." + fi + MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions." + + print_error "$MSG" +fi + + +## CUSTOM PRE-BOOT CHECKS ## +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running." +# fi + + diff --git a/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr b/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr new file mode 100644 index 00000000..a1bc631c --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/setup/clean_database.cr @@ -0,0 +1,3 @@ +Spec.before_each do + AppDatabase.truncate +end diff --git a/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr b/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr new file mode 100644 index 00000000..140ab416 --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/setup/reset_emails.cr @@ -0,0 +1,3 @@ +Spec.before_each do + Carbon::DevAdapter.reset +end diff --git a/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr b/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr new file mode 100644 index 00000000..393c6da3 --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/setup/setup_database.cr @@ -0,0 +1,2 @@ +Db::Create.new(quiet: true).call +Db::Migrate.new(quiet: true).call diff --git a/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr b/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr new file mode 100644 index 00000000..3a64c702 --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/setup/start_app_server.cr @@ -0,0 +1,9 @@ +app_server = AppServer.new + +spawn do + app_server.listen +end + +Spec.after_suite do + app_server.close +end diff --git a/fixtures/src_template__api_only/expected/spec/spec_helper.cr b/fixtures/src_template__api_only/expected/spec/spec_helper.cr new file mode 100644 index 00000000..d876014d --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/spec_helper.cr @@ -0,0 +1,19 @@ +ENV["LUCKY_ENV"] = "test" +ENV["DEV_PORT"] = "5001" +require "spec" +require "../src/app" +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 + +Avram::Migrator::Runner.new.ensure_migrated! +Avram::SchemaEnforcer.ensure_correct_column_mappings! +Habitat.raise_if_missing_settings! diff --git a/fixtures/src_template__api_only/expected/spec/support/.keep b/fixtures/src_template__api_only/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/spec/support/api_client.cr b/fixtures/src_template__api_only/expected/spec/support/api_client.cr new file mode 100644 index 00000000..ef251251 --- /dev/null +++ b/fixtures/src_template__api_only/expected/spec/support/api_client.cr @@ -0,0 +1,8 @@ +class ApiClient < Lucky::BaseHTTPClient + app AppServer.new + + def initialize + super + headers("Content-Type": "application/json") + end +end diff --git a/fixtures/src_template__api_only/expected/spec/support/factories/.keep b/fixtures/src_template__api_only/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/actions/api_action.cr b/fixtures/src_template__api_only/expected/src/actions/api_action.cr new file mode 100644 index 00000000..fac02c8b --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/actions/api_action.cr @@ -0,0 +1,11 @@ +# Include modules and add methods that are for all API requests +abstract class ApiAction < Lucky::Action + # APIs typically do not need to send cookie/session data. + # Remove this line if you want to send cookies in the response header. + disable_cookies + accepted_formats [:json] + + # By default all actions are required to use underscores to separate words. + # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute +end diff --git a/fixtures/src_template__api_only/expected/src/actions/errors/show.cr b/fixtures/src_template__api_only/expected/src/actions/errors/show.cr new file mode 100644 index 00000000..a80eaa4d --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/actions/errors/show.cr @@ -0,0 +1,42 @@ +# This class handles error responses and reporting. +# +# https://luckyframework.org/guides/http-and-routing/error-handling +class Errors::Show < Lucky::ErrorAction + DEFAULT_MESSAGE = "Something went wrong." + default_format :json + dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] + + def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) + error_json "Not found", status: 404 + end + + # When an InvalidOperationError is raised, show a helpful error with the + # param that is invalid, and what was wrong with it. + def render(error : Avram::InvalidOperationError) + error_json \ + message: error.renderable_message, + details: error.renderable_details, + param: error.invalid_attribute_name, + status: 400 + end + + # Always keep this below other 'render' methods or it may override your + # custom 'render' methods. + def render(error : Lucky::RenderableError) + error_json error.renderable_message, status: error.renderable_status + end + + # If none of the 'render' methods return a response for the raised Exception, + # Lucky will use this method. + def default_render(error : Exception) : Lucky::Response + error_json DEFAULT_MESSAGE, status: 500 + end + + private def error_json(message : String, status : Int, details = nil, param = nil) + json ErrorSerializer.new(message: message, details: details, param: param), status: status + end + + private def report(error : Exception) : Nil + # Send to Rollbar, send an email, etc. + end +end diff --git a/fixtures/src_template__api_only/expected/src/actions/home/index.cr b/fixtures/src_template__api_only/expected/src/actions/home/index.cr new file mode 100644 index 00000000..eb326da9 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/actions/home/index.cr @@ -0,0 +1,5 @@ +class Home::Index < ApiAction + get "/" do + json({hello: "Hello World from Home::Index"}) + end +end diff --git a/fixtures/src_template__api_only/expected/src/actions/mixins/.keep b/fixtures/src_template__api_only/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/app.cr b/fixtures/src_template__api_only/expected/src/app.cr new file mode 100644 index 00000000..21fd1eac --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/app.cr @@ -0,0 +1,20 @@ +require "./shards" + +require "../config/server" +require "./app_database" +require "../config/**" +require "./models/base_model" +require "./models/mixins/**" +require "./models/**" +require "./queries/mixins/**" +require "./queries/**" +require "./operations/mixins/**" +require "./operations/**" +require "./serializers/base_serializer" +require "./serializers/**" +require "./emails/base_email" +require "./emails/**" +require "./actions/mixins/**" +require "./actions/**" +require "../db/migrations/**" +require "./app_server" diff --git a/fixtures/src_template__api_only/expected/src/app_database.cr b/fixtures/src_template__api_only/expected/src/app_database.cr new file mode 100644 index 00000000..0efd4f50 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/app_database.cr @@ -0,0 +1,2 @@ +class AppDatabase < Avram::Database +end diff --git a/fixtures/src_template__api_only/expected/src/app_server.cr b/fixtures/src_template__api_only/expected/src/app_server.cr new file mode 100644 index 00000000..53f483d1 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/app_server.cr @@ -0,0 +1,28 @@ +class AppServer < Lucky::BaseAppServer + # Learn about middleware with HTTP::Handlers: + # https://luckyframework.org/guides/http-and-routing/http-handlers + def middleware : Array(HTTP::Handler) + [ + Lucky::RequestIdHandler.new, + Lucky::ForceSSLHandler.new, + Lucky::HttpMethodOverrideHandler.new, + Lucky::LogHandler.new, + Lucky::ErrorHandler.new(action: Errors::Show), + Lucky::RemoteIpHandler.new, + Lucky::RouteHandler.new, + + # Disabled in API mode: + # Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), + # Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), + Lucky::RouteNotFoundHandler.new, + ] of HTTP::Handler + end + + def protocol + "http" + end + + def listen + server.listen(host, port, reuse_port: false) + end +end diff --git a/fixtures/src_template__api_only/expected/src/emails/base_email.cr b/fixtures/src_template__api_only/expected/src/emails/base_email.cr new file mode 100644 index 00000000..656f4f11 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/emails/base_email.cr @@ -0,0 +1,15 @@ +# Learn about sending emails +# https://luckyframework.org/guides/emails/sending-emails-with-carbon +abstract class BaseEmail < Carbon::Email + # You can add defaults using the 'inherited' hook + # + # Example: + # + # macro inherited + # from default_from + # end + # + # def default_from + # Carbon::Address.new("support@app.com") + # end +end diff --git a/fixtures/src_template__api_only/expected/src/models/base_model.cr b/fixtures/src_template__api_only/expected/src/models/base_model.cr new file mode 100644 index 00000000..6bafeb84 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/models/base_model.cr @@ -0,0 +1,5 @@ +abstract class BaseModel < Avram::Model + def self.database : Avram::Database.class + AppDatabase + end +end diff --git a/fixtures/src_template__api_only/expected/src/models/mixins/.keep b/fixtures/src_template__api_only/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/operations/.keep b/fixtures/src_template__api_only/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/operations/mixins/.keep b/fixtures/src_template__api_only/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/queries/.keep b/fixtures/src_template__api_only/expected/src/queries/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/queries/mixins/.keep b/fixtures/src_template__api_only/expected/src/queries/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/serializers/.keep b/fixtures/src_template__api_only/expected/src/serializers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr b/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr new file mode 100644 index 00000000..3ad0a669 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/serializers/base_serializer.cr @@ -0,0 +1,7 @@ +abstract class BaseSerializer < Lucky::Serializer + def self.for_collection(collection : Enumerable, *args, **named_args) + collection.map do |object| + new(object, *args, **named_args) + end + end +end diff --git a/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr b/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr new file mode 100644 index 00000000..21a53aa2 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/serializers/error_serializer.cr @@ -0,0 +1,14 @@ +# This is the default error serializer generated by Lucky. +# Feel free to customize it in any way you like. +class ErrorSerializer < BaseSerializer + def initialize( + @message : String, + @details : String? = nil, + @param : String? = nil # so you can track which param (if any) caused the problem + ) + end + + def render + {message: @message, param: @param, details: @details} + end +end diff --git a/fixtures/src_template__api_only/expected/src/shards.cr b/fixtures/src_template__api_only/expected/src/shards.cr new file mode 100644 index 00000000..1afc72cb --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/shards.cr @@ -0,0 +1,8 @@ +# Load .env file before any other config or app code +require "lucky_env" +LuckyEnv.load?(".env") + +# Require your shards here +require "lucky" +require "avram/lucky" +require "carbon" diff --git a/fixtures/src_template__api_only/expected/src/start_server.cr b/fixtures/src_template__api_only/expected/src/start_server.cr new file mode 100644 index 00000000..de8af78e --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/start_server.cr @@ -0,0 +1,17 @@ +require "./app" + +Habitat.raise_if_missing_settings! + +if LuckyEnv.development? + Avram::Migrator::Runner.new.ensure_migrated! + Avram::SchemaEnforcer.ensure_correct_column_mappings! +end + +app_server = AppServer.new +puts "Listening on http://#{app_server.host}:#{app_server.port}" + +Signal::INT.trap do + app_server.close +end + +app_server.listen diff --git a/fixtures/src_template__api_only/expected/src/test_project.cr b/fixtures/src_template__api_only/expected/src/test_project.cr new file mode 100644 index 00000000..68e1a8d2 --- /dev/null +++ b/fixtures/src_template__api_only/expected/src/test_project.cr @@ -0,0 +1,6 @@ +# Typically you will not use or modify this file. 'shards build' and some +# other crystal tools will sometimes use this. +# +# When this file is compiled/run it will require and run 'start_server', +# which as its name implies will start the server for you app. +require "./start_server" diff --git a/fixtures/src_template__api_only/expected/tasks.cr b/fixtures/src_template__api_only/expected/tasks.cr new file mode 100644 index 00000000..5a892d4d --- /dev/null +++ b/fixtures/src_template__api_only/expected/tasks.cr @@ -0,0 +1,25 @@ +# This file loads your app and all your tasks when running 'lucky' +# +# Run 'lucky --help' to see all available tasks. +# +# Learn to create your own tasks: +# https://luckyframework.org/guides/command-line-tasks/custom-tasks + +# See `LuckyEnv#task?` +ENV["LUCKY_TASK"] = "true" + +# Load Lucky and the app (actions, models, etc.) +require "./src/app" +require "lucky_task" + +# You can add your own tasks here in the ./tasks folder +require "./tasks/**" + +# Load migrations +require "./db/migrations/**" + +# Load Lucky tasks (dev, routes, etc.) +require "lucky/tasks/**" +require "avram/lucky/tasks" + +LuckyTask::Runner.run diff --git a/fixtures/src_template__api_only/expected/tasks/.keep b/fixtures/src_template__api_only/expected/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr new file mode 100644 index 00000000..d866040f --- /dev/null +++ b/fixtures/src_template__api_only/expected/tasks/db/seed/required_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add seeds here that are *required* for your app to work. +# For example, you might need at least one admin user or you might need at least +# one category for your blog posts for the app to work. +# +# Use `Db::Seed::SampleData` if your only want to add sample data helpful for +# development. +class Db::Seed::RequiredData < LuckyTask::Task + summary "Add database records required for the app to work" + + def call + # Using a Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # + # SaveUser.create!(email: "me@example.com", name: "Jane") + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # + # unless UserQuery.new.email("me@example.com").first? + # SaveUser.create!(email: "me@example.com", name: "Jane") + # end + puts "Done adding required data" + end +end diff --git a/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr new file mode 100644 index 00000000..231d7e8d --- /dev/null +++ b/fixtures/src_template__api_only/expected/tasks/db/seed/sample_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add sample data helpful for development, e.g. (fake users, blog posts, etc.) +# +# Use `Db::Seed::RequiredData` if you need to create data *required* for your +# app to work. +class Db::Seed::SampleData < LuckyTask::Task + summary "Add sample database records helpful for development" + + def call + # Using an Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # ``` + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # ``` + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # ``` + # if UserQuery.new.email("me@example.com").none? + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # end + # ``` + puts "Done adding sample data" + end +end diff --git a/fixtures/src_template__generate_auth/expected/.crystal-version b/fixtures/src_template__generate_auth/expected/.crystal-version new file mode 100644 index 00000000..fdd3be6d --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/.crystal-version @@ -0,0 +1 @@ +1.6.2 diff --git a/fixtures/src_template__generate_auth/expected/.env b/fixtures/src_template__generate_auth/expected/.env new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml b/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml new file mode 100644 index 00000000..278614cf --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: test-project CI + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + check-format: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + - name: Format + run: crystal tool format --check + + specs: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + env: + LUCKY_ENV: test + DB_HOST: localhost + continue-on-error: ${{ matrix.experimental }} + services: + postgres: + image: postgres:12-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Set up Yarn cache + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Set up Node cache + uses: actions/cache@v2 + id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`) + with: + path: '**/node_modules' + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Set up Crystal cache + uses: actions/cache@v2 + id: crystal-cache + with: + path: | + ~/.cache/crystal + lib + lucky_tasks + key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }} + restore-keys: | + ${{ runner.os }}-crystal- + + - name: Install shards + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: shards check || shards install + + - name: Install yarn packages + if: steps.node-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --no-progress + - name: Compiling assets + run: yarn prod + - name: Build lucky_tasks + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: crystal build tasks.cr -o ./lucky_tasks + + - name: Prepare database + run: | + ./lucky_tasks db.create + ./lucky_tasks db.migrate + ./lucky_tasks db.seed.required_data + + - name: Run tests + run: crystal spec \ No newline at end of file diff --git a/fixtures/src_template__generate_auth/expected/Procfile b/fixtures/src_template__generate_auth/expected/Procfile new file mode 100644 index 00000000..e524d70d --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/Procfile @@ -0,0 +1,2 @@ +web: bin/app +release: lucky db.migrate diff --git a/fixtures/src_template__generate_auth/expected/Procfile.dev b/fixtures/src_template__generate_auth/expected/Procfile.dev new file mode 100644 index 00000000..d9a8173a --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/Procfile.dev @@ -0,0 +1,3 @@ +system_check: script/system_check && sleep 100000 +web: lucky watch --reload-browser +assets: yarn watch diff --git a/fixtures/src_template__generate_auth/expected/README.md b/fixtures/src_template__generate_auth/expected/README.md new file mode 100644 index 00000000..da2d97cc --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/README.md @@ -0,0 +1,23 @@ +# test-project + +This is a project written using [Lucky](https://luckyframework.org). Enjoy! + +### Setting up the project + +1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies) +1. Update database settings in `config/database.cr` +1. Run `script/setup` +1. Run `lucky dev` to start the app + +### Using Docker for development + +1. [Install Docker](https://docs.docker.com/engine/install/) +1. Run `docker compose up` + +The Docker container will boot all of the necessary components needed to run your Lucky application. +To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file. + + +### Learning Lucky + +Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky). diff --git a/fixtures/src_template__generate_auth/expected/config/application.cr b/fixtures/src_template__generate_auth/expected/config/application.cr new file mode 100644 index 00000000..c807149a --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/application.cr @@ -0,0 +1,24 @@ +# This file may be used for custom Application configurations. +# It will be loaded before other config files. +# +# Read more on configuration: +# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code + +# Use this code as an example: +# +# ``` +# module Application +# Habitat.create do +# setting support_email : String +# setting lock_with_basic_auth : Bool +# end +# end +# +# Application.configure do |settings| +# settings.support_email = "support@myapp.io" +# settings.lock_with_basic_auth = LuckyEnv.staging? +# end +# +# # In your application, call +# # `Application.settings.support_email` anywhere you need it. +# ``` diff --git a/fixtures/src_template__generate_auth/expected/config/colors.cr b/fixtures/src_template__generate_auth/expected/config/colors.cr new file mode 100644 index 00000000..761ae940 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/colors.cr @@ -0,0 +1,4 @@ +# This enables the color output when in development or test +# Check out the Colorize docs for more information +# https://crystal-lang.org/api/Colorize.html +Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? diff --git a/fixtures/src_template__generate_auth/expected/config/cookies.cr b/fixtures/src_template__generate_auth/expected/config/cookies.cr new file mode 100644 index 00000000..2d5055f2 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/cookies.cr @@ -0,0 +1,25 @@ +require "./server" + +Lucky::Session.configure do |settings| + settings.key = "_test_project_session" +end + +Lucky::CookieJar.configure do |settings| + settings.on_set = ->(cookie : HTTP::Cookie) { + # If ForceSSLHandler is enabled, only send cookies over HTTPS + cookie.secure(Lucky::ForceSSLHandler.settings.enabled) + + # By default, don't allow reading cookies with JavaScript + cookie.http_only(true) + + # Restrict cookies to a first-party or same-site context + cookie.samesite(:lax) + + # Set all cookies to the root path by default + cookie.path("/") + + # You can set other defaults for cookies here. For example: + # + # cookie.expires(1.year.from_now).domain("mydomain.com") + } +end diff --git a/fixtures/src_template__generate_auth/expected/config/database.cr b/fixtures/src_template__generate_auth/expected/config/database.cr new file mode 100644 index 00000000..f614299a --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/database.cr @@ -0,0 +1,29 @@ +database_name = "test_project_#{LuckyEnv.environment}" + +AppDatabase.configure do |settings| + if LuckyEnv.production? + settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"]) + else + settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new( + database: database_name, + hostname: ENV["DB_HOST"]? || "localhost", + port: ENV["DB_PORT"]?.try(&.to_i) || 5432, + # Some common usernames are "postgres", "root", or your system username (run 'whoami') + username: ENV["DB_USERNAME"]? || "postgres", + # Some Postgres installations require no password. Use "" if that is the case. + password: ENV["DB_PASSWORD"]? || "postgres" + ) + end +end + +Avram.configure do |settings| + settings.database_to_migrate = AppDatabase + + # In production, allow lazy loading (N+1). + # In development and test, raise an error if you forget to preload associations + settings.lazy_load_enabled = LuckyEnv.production? + + # Always parse `Time` values with these specific formats. + # Used for both database values, and datetime input fields. + # settings.time_formats << "%F" +end diff --git a/fixtures/src_template__generate_auth/expected/config/email.cr b/fixtures/src_template__generate_auth/expected/config/email.cr new file mode 100644 index 00000000..7c875449 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/email.cr @@ -0,0 +1,26 @@ +require "carbon_sendgrid_adapter" + +BaseEmail.configure do |settings| + if LuckyEnv.production? + # If you don't need to send emails, set the adapter to DevAdapter instead: + # + # settings.adapter = Carbon::DevAdapter.new + # + # If you do need emails, get a key from SendGrid and set an ENV variable + send_grid_key = send_grid_key_from_env + settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key) + elsif LuckyEnv.development? + settings.adapter = Carbon::DevAdapter.new(print_emails: true) + else + settings.adapter = Carbon::DevAdapter.new + end +end + +private def send_grid_key_from_env + ENV["SEND_GRID_KEY"]? || raise_missing_key_message +end + +private def raise_missing_key_message + puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red + exit(1) +end diff --git a/fixtures/src_template__generate_auth/expected/config/env.cr b/fixtures/src_template__generate_auth/expected/config/env.cr new file mode 100644 index 00000000..3f364072 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/env.cr @@ -0,0 +1,33 @@ +# Environments are managed using `LuckyEnv`. By default, development, production +# and test are supported. See +# https://luckyframework.org/guides/getting-started/configuration for details. +# +# The default environment is development unless the environment variable +# LUCKY_ENV is set. +# +# Example: +# ``` +# LuckyEnv.environment # => "development" +# LuckyEnv.development? # => true +# LuckyEnv.production? # => false +# LuckyEnv.test? # => false +# ``` +# +# New environments can be added using the `LuckyEnv.add_env` macro. +# +# Example: +# ``` +# LuckyEnv.add_env :staging +# LuckyEnv.staging? # => false +# ``` +# +# To determine whether or not a `LuckyTask` is currently running, you can use +# the `LuckyEnv.task?` predicate. +# +# Example: +# ``` +# LuckyEnv.task? # => false +# ``` + +# Add a staging environment. +# LuckyEnv.add_env :staging diff --git a/fixtures/src_template__generate_auth/expected/config/error_handler.cr b/fixtures/src_template__generate_auth/expected/config/error_handler.cr new file mode 100644 index 00000000..c6b736e3 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/error_handler.cr @@ -0,0 +1,3 @@ +Lucky::ErrorHandler.configure do |settings| + settings.show_debug_output = !LuckyEnv.production? +end diff --git a/fixtures/src_template__generate_auth/expected/config/log.cr b/fixtures/src_template__generate_auth/expected/config/log.cr new file mode 100644 index 00000000..0e8d7636 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/log.cr @@ -0,0 +1,45 @@ +require "file_utils" + +if LuckyEnv.test? + # Logs to `tmp/test.log` so you can see what's happening without having + # a bunch of log output in your spec results. + FileUtils.mkdir_p("tmp") + + backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w")) + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) +elsif LuckyEnv.production? + # Lucky uses JSON in production so logs can be searched more easily + # + # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'. + backend = Log::IOBackend.new + backend.formatter = Dexter::JSONLogFormatter.proc + Log.dexter.configure(:info, backend) +else + # Use a pretty formatter printing to STDOUT in development + backend = Log::IOBackend.new + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) + DB::Log.level = :info +end + +# Lucky only logs when before/after pipes halt by redirecting, or rendering a +# response. Pipes that run without halting are not logged. +# +# If you want to log every pipe that runs, set the log level to ':info' +Lucky::ContinuedPipeLog.dexter.configure(:none) + +# Lucky only logs failed queries by default. +# +# Set the log to ':info' to log all queries +Avram::QueryLog.dexter.configure(:none) + +# Skip logging static assets requests in development +Lucky::LogHandler.configure do |settings| + if LuckyEnv.development? + settings.skip_if = ->(context : HTTP::Server::Context) { + context.request.method.downcase == "get" && + context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/) + } + end +end diff --git a/fixtures/src_template__generate_auth/expected/config/route_helper.cr b/fixtures/src_template__generate_auth/expected/config/route_helper.cr new file mode 100644 index 00000000..ede1f328 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/route_helper.cr @@ -0,0 +1,10 @@ +# This is used when generating URLs for your application +Lucky::RouteHelper.configure do |settings| + if LuckyEnv.production? + # Example: https://my_app.com + settings.base_uri = ENV.fetch("APP_DOMAIN") + else + # Set domain to the default host/port in development/test + settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" + end +end diff --git a/fixtures/src_template__generate_auth/expected/config/server.cr b/fixtures/src_template__generate_auth/expected/config/server.cr new file mode 100644 index 00000000..c72314be --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/server.cr @@ -0,0 +1,65 @@ +# Here is where you configure the Lucky server +# +# Look at config/route_helper.cr if you want to change the domain used when +# generating links with `Action.url`. +Lucky::Server.configure do |settings| + if LuckyEnv.production? + settings.secret_key_base = secret_key_from_env + settings.host = "0.0.0.0" + settings.port = ENV["PORT"].to_i + settings.gzip_enabled = true + # By default certain content types will be gzipped. + # For a full list look in + # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr + # To add additional extensions do something like this: + # settings.gzip_content_types << "content/type" + else + settings.secret_key_base = "1234567890" + # Change host/port in config/watch.yml + # Alternatively, you can set the DEV_PORT env to set the port for local development + settings.host = Lucky::ServerSettings.host + settings.port = Lucky::ServerSettings.port + end + + # By default Lucky will serve static assets in development and production. + # + # However you could use a CDN when in production like this: + # + # Lucky::Server.configure do |settings| + # if LuckyEnv.production? + # settings.asset_host = "https://mycdnhost.com" + # else + # settings.asset_host = "" + # end + # end + settings.asset_host = "" # Lucky will serve assets +end + +Lucky::ForceSSLHandler.configure do |settings| + # To force SSL in production, uncomment the lines below. + # This will cause http requests to be redirected to https: + # + # settings.enabled = LuckyEnv.production? + # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true} + # + # Or, leave it disabled: + settings.enabled = false +end + +# Set a unique ID for each HTTP request. +# To enable the request ID, uncomment the lines below. +# You can set your own custom String, or use a random UUID. +# Lucky::RequestIdHandler.configure do |settings| +# settings.set_request_id = ->(context : HTTP::Server::Context) { +# UUID.random.to_s +# } +# end + +private def secret_key_from_env + ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production +end + +private def raise_missing_secret_key_in_production + puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red + exit(1) +end diff --git a/fixtures/src_template__generate_auth/expected/config/watch.yml b/fixtures/src_template__generate_auth/expected/config/watch.yml new file mode 100644 index 00000000..3a59b410 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/config/watch.yml @@ -0,0 +1,3 @@ +host: 127.0.0.1 +port: 3000 +reload_port: 3001 diff --git a/fixtures/src_template__generate_auth/expected/db/migrations/.keep b/fixtures/src_template__generate_auth/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/docker-compose.yml b/fixtures/src_template__generate_auth/expected/docker-compose.yml new file mode 100644 index 00000000..d00779db --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + lucky: + build: + context: . + dockerfile: docker/development.dockerfile + environment: + DATABASE_URL: postgres://lucky:password@postgres:5432/lucky + DEV_HOST: "0.0.0.0" + volumes: + - .:/app + - node_modules:/app/node_modules + - shards_lib:/app/lib + - app_bin:/app/bin + - build_cache:/root/.cache + depends_on: + - postgres + ports: + - 3000:3000 # This is the Lucky Server port + - 3001:3001 # This is the Lucky watcher reload port + + entrypoint: ["docker/dev_entrypoint.sh"] + + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: lucky + POSTGRES_PASSWORD: password + POSTGRES_DB: lucky + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + # The postgres database container is exposed on the host at port 6543 to + # allow connecting directly to it with postgres clients. The port differs + # from the postgres default to avoid conflict with existing postgres + # servers. Connect to a running postgres container with: + # postgres://lucky:password@localhost:6543/lucky + - 6543:5432 + +volumes: + postgres_data: + node_modules: + shards_lib: + app_bin: + build_cache: diff --git a/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh new file mode 100755 index 00000000..d16ef6ab --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -euo pipefail + +# This is the entrypoint script used for development docker workflows. +# By default it will: +# - Install dependencies. +# - Run migrations. +# - Start the dev server. +# It also accepts any commands to be run instead. + + +warnfail () { + echo "$@" >&2 + exit 1 +} + +case ${1:-} in + "") # If no arguments are provided, start lucky dev server. + ;; + + *) # If any arguments are provided, execute them instead. + exec "$@" +esac + +if ! [ -d bin ] ; then + echo 'Creating bin directory' + mkdir bin +fi +echo 'Installing npm packages...' +yarn install +if ! shards check ; then + echo 'Installing shards...' + shards install +fi + +echo 'Waiting for postgres to be available...' +./docker/wait-for-it.sh -q postgres:5432 + +if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then + echo 'Finishing database setup...' + lucky db.migrate +fi + +echo 'Starting lucky dev server...' +exec lucky dev diff --git a/fixtures/src_template__generate_auth/expected/docker/development.dockerfile b/fixtures/src_template__generate_auth/expected/docker/development.dockerfile new file mode 100644 index 00000000..be3dba5f --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/docker/development.dockerfile @@ -0,0 +1,34 @@ +FROM crystallang/crystal:1.6.2 + +# Install utilities required to make this Dockerfile run +RUN apt-get update && \ + apt-get install -y wget +# Add the nodesource ppa to apt. Update this to change the nodejs version. +RUN wget https://deb.nodesource.com/setup_16.x -O- | bash + +# Apt installs: +# - nodejs (from above ppa) is required for front-end apps. +# - Postgres cli tools are required for lucky-cli. +# - tmux is required for the Overmind process manager. +RUN apt-get update && \ + apt-get install -y nodejs postgresql-client tmux && \ + rm -rf /var/lib/apt/lists/* + +# NPM global installs: +# - Yarn is the default package manager for the node component of a lucky +# browser app. +# - Mix is the default asset compiler. +RUN npm install -g yarn mix + +# Install lucky cli +WORKDIR /lucky/cli +RUN git clone https://github.com/luckyframework/lucky_cli . && \ + git checkout v1.0.0 && \ + shards build --without-development && \ + cp bin/lucky /usr/bin + +WORKDIR /app +ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres +EXPOSE 3000 +EXPOSE 3001 + diff --git a/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh b/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh new file mode 100755 index 00000000..06e0638c --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/docker/wait-for-it.sh @@ -0,0 +1,189 @@ +#!/usr/bin/bash +# +# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28. +# Licensed under the MIT license as of 81b1373f. +# +# Below this line, wait-for-it is the original work of the author. +# +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers b/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers new file mode 100644 index 00000000..388fa67e --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/script/helpers/function_helpers @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used as helpers +# for various tasks. Read the examples for each one for +# more information. Feel free to put any additional helper +# functions you may need for your app + + +# Returns true if the command $1 is not found +# example: +# if command_not_found "yarn"; then +# echo "no yarn" +# fi +command_not_found() { + ! command -v $1 > /dev/null + return $? +} + +# Returns true if the command $1 is not running +# You must supply the full command to check as an argument +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running" +# fi +command_not_running() { + $1 + if [ $? -ne 0 ]; then + true + else + false + fi +} + +# Returns true if the OS is macOS +# example: +# if is_mac; then +# echo "do mac stuff" +# fi +is_mac() { + if [[ "$OSTYPE" == "darwin"* ]]; then + true + else + false + fi +} + +# Returns true if the OS is linux based +# example: +# if is_linux; then +# echo "do linux stuff" +# fi +is_linux() { + if [[ "$OSTYPE" == "linux"* ]]; then + true + else + false + fi +} + +# Prints error and exit. +# example: +# print_error "Redis is not running. Run it with some_command" +print_error() { + printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n" + printf "${BOLD_RED_COLOR}$1 \n\n" | indent + exit 1 +} diff --git a/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers b/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers new file mode 100644 index 00000000..34b77a8c --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/script/helpers/text_helpers @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used to format text, +# and make printing text a little easier. Feel free to put +# any additional functions you need for formatting your shell +# output text. + +# Colors +BOLD_RED_COLOR="\e[1m\e[31m" + +# Indents the text 2 spaces +# example: +# printf "Hello" | indent +indent() { + while read LINE; do + echo " $LINE" || true + done +} + +# Prints out an arrow to your custom notice +# example: +# notice "Installing new magic" +notice() { + printf "\n▸ $1\n" +} + +# Prints out a check mark and Done. +# example: +# print_done +print_done() { + printf "✔ Done\n" | indent +} diff --git a/fixtures/src_template__generate_auth/expected/script/setup b/fixtures/src_template__generate_auth/expected/script/setup new file mode 100755 index 00000000..bf0bc317 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/script/setup @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Exit if any subcommand fails +set -e +set -o pipefail + +source script/helpers/text_helpers + + +notice "Running System Check" +./script/system_check +print_done + +notice "Installing node dependencies" +yarn install --no-progress | indent + +notice "Compiling assets" +yarn dev | indent + +print_done + +notice "Installing shards" +shards install --ignore-crystal-version | indent + +if [ ! -f ".env" ]; then + notice "No .env found. Creating one." + touch .env + print_done +fi + +notice "Creating the database" +lucky db.create | indent + +notice "Verifying postgres connection" +lucky db.verify_connection | indent + +notice "Migrating the database" +lucky db.migrate | indent + +notice "Seeding the database with required and sample records" +lucky db.seed.required_data | indent +lucky db.seed.sample_data | indent + +print_done +notice "Run 'lucky dev' to start the app" diff --git a/fixtures/src_template__generate_auth/expected/script/system_check b/fixtures/src_template__generate_auth/expected/script/system_check new file mode 100755 index 00000000..c27c926b --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/script/system_check @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +source script/helpers/text_helpers +source script/helpers/function_helpers + +# Use this script to check the system for required tools and process that your app needs. +# A few helper functions are provided to make writing bash a little easier. See the +# script/helpers/function_helpers file for more examples. +# +# A few examples you might use here: +# * 'lucky db.verify_connection' to test postgres can be connected +# * Checking that elasticsearch, redis, or postgres is installed and/or booted +# * Note: Booting additional processes for things like mail, background jobs, etc... +# should go in your Procfile.dev. + +if command_not_found "yarn"; then + print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions." +fi + +if command_not_found "createdb"; then + MSG="Please install the postgres CLI tools, then try again." + if is_mac; then + MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html." + fi + MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions." + + print_error "$MSG" +fi + + +## CUSTOM PRE-BOOT CHECKS ## +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running." +# fi + + diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr b/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr new file mode 100644 index 00000000..a1bc631c --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/spec/setup/clean_database.cr @@ -0,0 +1,3 @@ +Spec.before_each do + AppDatabase.truncate +end diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr b/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr new file mode 100644 index 00000000..140ab416 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/spec/setup/reset_emails.cr @@ -0,0 +1,3 @@ +Spec.before_each do + Carbon::DevAdapter.reset +end diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr b/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr new file mode 100644 index 00000000..393c6da3 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/spec/setup/setup_database.cr @@ -0,0 +1,2 @@ +Db::Create.new(quiet: true).call +Db::Migrate.new(quiet: true).call diff --git a/fixtures/src_template__generate_auth/expected/spec/setup/start_app_server.cr b/fixtures/src_template__generate_auth/expected/spec/setup/start_app_server.cr new file mode 100644 index 00000000..ff0bfeeb --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/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/fixtures/src_template__generate_auth/expected/spec/spec_helper.cr b/fixtures/src_template__generate_auth/expected/spec/spec_helper.cr new file mode 100644 index 00000000..93914648 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/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/fixtures/src_template__generate_auth/expected/spec/support/.keep b/fixtures/src_template__generate_auth/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/spec/support/api_client.cr b/fixtures/src_template__generate_auth/expected/spec/support/api_client.cr new file mode 100644 index 00000000..46d449a8 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/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/fixtures/src_template__generate_auth/expected/spec/support/factories/.keep b/fixtures/src_template__generate_auth/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr b/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr new file mode 100644 index 00000000..a16fd09e --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/actions/api_action.cr @@ -0,0 +1,17 @@ +# Include modules and add methods that are for all API requests +abstract class ApiAction < Lucky::Action + # APIs typically do not need to send cookie/session data. + # Remove this line if you want to send cookies in the response header. + disable_cookies + accepted_formats [:json] + + include Api::Auth::Helpers + + # By default all actions require sign in. + # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests. + include Api::Auth::RequireAuthToken + + # By default all actions are required to use underscores to separate words. + # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute +end diff --git a/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr b/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr new file mode 100644 index 00000000..d01ed541 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/actions/errors/show.cr @@ -0,0 +1,63 @@ +# This class handles error responses and reporting. +# +# https://luckyframework.org/guides/http-and-routing/error-handling +class Errors::Show < Lucky::ErrorAction + DEFAULT_MESSAGE = "Something went wrong." + default_format :html + dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] + + def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) + if html? + error_html "Sorry, we couldn't find that page.", status: 404 + else + error_json "Not found", status: 404 + end + end + + # When the request is JSON and an InvalidOperationError is raised, show a + # helpful error with the param that is invalid, and what was wrong with it. + def render(error : Avram::InvalidOperationError) + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json \ + message: error.renderable_message, + details: error.renderable_details, + param: error.invalid_attribute_name, + status: 400 + end + end + + # Always keep this below other 'render' methods or it may override your + # custom 'render' methods. + def render(error : Lucky::RenderableError) + if html? + error_html DEFAULT_MESSAGE, status: error.renderable_status + else + error_json error.renderable_message, status: error.renderable_status + end + end + + # If none of the 'render' methods return a response for the raised Exception, + # Lucky will use this method. + def default_render(error : Exception) : Lucky::Response + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json DEFAULT_MESSAGE, status: 500 + end + end + + private def error_html(message : String, status : Int) + context.response.status_code = status + html_with_status Errors::ShowPage, status, message: message, status_code: status + end + + private def error_json(message : String, status : Int, details = nil, param = nil) + json ErrorSerializer.new(message: message, details: details, param: param), status: status + end + + private def report(error : Exception) : Nil + # Send to Rollbar, send an email, etc. + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr b/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr new file mode 100644 index 00000000..f780130a --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/actions/home/index.cr @@ -0,0 +1,18 @@ +class Home::Index < BrowserAction + include Auth::AllowGuests + + get "/" do + if current_user? + redirect Me::Show + else + # When you're ready change this line to: + # + # redirect SignIns::New + # + # Or maybe show signed out users a marketing page: + # + # html Marketing::IndexPage + html Lucky::WelcomePage + end + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/actions/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/app.cr b/fixtures/src_template__generate_auth/expected/src/app.cr new file mode 100644 index 00000000..9d3ee3c7 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/app.cr @@ -0,0 +1,26 @@ +require "./shards" + +# Load the asset manifest +Lucky::AssetHelpers.load_manifest "public/mix-manifest.json" + +require "../config/server" +require "./app_database" +require "../config/**" +require "./models/base_model" +require "./models/mixins/**" +require "./models/**" +require "./queries/mixins/**" +require "./queries/**" +require "./operations/mixins/**" +require "./operations/**" +require "./serializers/base_serializer" +require "./serializers/**" +require "./emails/base_email" +require "./emails/**" +require "./actions/mixins/**" +require "./actions/**" +require "./components/base_component" +require "./components/**" +require "./pages/**" +require "../db/migrations/**" +require "./app_server" diff --git a/fixtures/src_template__generate_auth/expected/src/app_database.cr b/fixtures/src_template__generate_auth/expected/src/app_database.cr new file mode 100644 index 00000000..0efd4f50 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/app_database.cr @@ -0,0 +1,2 @@ +class AppDatabase < Avram::Database +end diff --git a/fixtures/src_template__generate_auth/expected/src/app_server.cr b/fixtures/src_template__generate_auth/expected/src/app_server.cr new file mode 100644 index 00000000..8ec16c3c --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/app_server.cr @@ -0,0 +1,26 @@ +class AppServer < Lucky::BaseAppServer + # Learn about middleware with HTTP::Handlers: + # https://luckyframework.org/guides/http-and-routing/http-handlers + def middleware : Array(HTTP::Handler) + [ + Lucky::RequestIdHandler.new, + Lucky::ForceSSLHandler.new, + Lucky::HttpMethodOverrideHandler.new, + Lucky::LogHandler.new, + Lucky::ErrorHandler.new(action: Errors::Show), + Lucky::RemoteIpHandler.new, + Lucky::RouteHandler.new, + Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), + Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), + Lucky::RouteNotFoundHandler.new, + ] of HTTP::Handler + end + + def protocol + "http" + end + + def listen + server.listen(host, port, reuse_port: false) + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr b/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr new file mode 100644 index 00000000..656f4f11 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/emails/base_email.cr @@ -0,0 +1,15 @@ +# Learn about sending emails +# https://luckyframework.org/guides/emails/sending-emails-with-carbon +abstract class BaseEmail < Carbon::Email + # You can add defaults using the 'inherited' hook + # + # Example: + # + # macro inherited + # from default_from + # end + # + # def default_from + # Carbon::Address.new("support@app.com") + # end +end diff --git a/fixtures/src_template__generate_auth/expected/src/models/base_model.cr b/fixtures/src_template__generate_auth/expected/src/models/base_model.cr new file mode 100644 index 00000000..6bafeb84 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/models/base_model.cr @@ -0,0 +1,5 @@ +abstract class BaseModel < Avram::Model + def self.database : Avram::Database.class + AppDatabase + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/models/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/operations/.keep b/fixtures/src_template__generate_auth/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/operations/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/queries/.keep b/fixtures/src_template__generate_auth/expected/src/queries/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/queries/mixins/.keep b/fixtures/src_template__generate_auth/expected/src/queries/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/.keep b/fixtures/src_template__generate_auth/expected/src/serializers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr b/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr new file mode 100644 index 00000000..3ad0a669 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/serializers/base_serializer.cr @@ -0,0 +1,7 @@ +abstract class BaseSerializer < Lucky::Serializer + def self.for_collection(collection : Enumerable, *args, **named_args) + collection.map do |object| + new(object, *args, **named_args) + end + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr b/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr new file mode 100644 index 00000000..21a53aa2 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/serializers/error_serializer.cr @@ -0,0 +1,14 @@ +# This is the default error serializer generated by Lucky. +# Feel free to customize it in any way you like. +class ErrorSerializer < BaseSerializer + def initialize( + @message : String, + @details : String? = nil, + @param : String? = nil # so you can track which param (if any) caused the problem + ) + end + + def render + {message: @message, param: @param, details: @details} + end +end diff --git a/fixtures/src_template__generate_auth/expected/src/shards.cr b/fixtures/src_template__generate_auth/expected/src/shards.cr new file mode 100644 index 00000000..7cadec18 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/shards.cr @@ -0,0 +1,10 @@ +# Load .env file before any other config or app code +require "lucky_env" +LuckyEnv.load?(".env") + +# Require your shards here +require "lucky" +require "avram/lucky" +require "carbon" +require "authentic" +require "jwt" diff --git a/fixtures/src_template__generate_auth/expected/src/start_server.cr b/fixtures/src_template__generate_auth/expected/src/start_server.cr new file mode 100644 index 00000000..9df5d1fd --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/start_server.cr @@ -0,0 +1,16 @@ +require "./app" + +Habitat.raise_if_missing_settings! + +if LuckyEnv.development? + Avram::Migrator::Runner.new.ensure_migrated! + Avram::SchemaEnforcer.ensure_correct_column_mappings! +end + +app_server = AppServer.new + +Signal::INT.trap do + app_server.close +end + +app_server.listen diff --git a/fixtures/src_template__generate_auth/expected/src/test_project.cr b/fixtures/src_template__generate_auth/expected/src/test_project.cr new file mode 100644 index 00000000..68e1a8d2 --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/src/test_project.cr @@ -0,0 +1,6 @@ +# Typically you will not use or modify this file. 'shards build' and some +# other crystal tools will sometimes use this. +# +# When this file is compiled/run it will require and run 'start_server', +# which as its name implies will start the server for you app. +require "./start_server" diff --git a/fixtures/src_template__generate_auth/expected/tasks.cr b/fixtures/src_template__generate_auth/expected/tasks.cr new file mode 100644 index 00000000..5a892d4d --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/tasks.cr @@ -0,0 +1,25 @@ +# This file loads your app and all your tasks when running 'lucky' +# +# Run 'lucky --help' to see all available tasks. +# +# Learn to create your own tasks: +# https://luckyframework.org/guides/command-line-tasks/custom-tasks + +# See `LuckyEnv#task?` +ENV["LUCKY_TASK"] = "true" + +# Load Lucky and the app (actions, models, etc.) +require "./src/app" +require "lucky_task" + +# You can add your own tasks here in the ./tasks folder +require "./tasks/**" + +# Load migrations +require "./db/migrations/**" + +# Load Lucky tasks (dev, routes, etc.) +require "lucky/tasks/**" +require "avram/lucky/tasks" + +LuckyTask::Runner.run diff --git a/fixtures/src_template__generate_auth/expected/tasks/.keep b/fixtures/src_template__generate_auth/expected/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr new file mode 100644 index 00000000..d866040f --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/tasks/db/seed/required_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add seeds here that are *required* for your app to work. +# For example, you might need at least one admin user or you might need at least +# one category for your blog posts for the app to work. +# +# Use `Db::Seed::SampleData` if your only want to add sample data helpful for +# development. +class Db::Seed::RequiredData < LuckyTask::Task + summary "Add database records required for the app to work" + + def call + # Using a Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # + # SaveUser.create!(email: "me@example.com", name: "Jane") + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # + # unless UserQuery.new.email("me@example.com").first? + # SaveUser.create!(email: "me@example.com", name: "Jane") + # end + puts "Done adding required data" + end +end diff --git a/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr new file mode 100644 index 00000000..231d7e8d --- /dev/null +++ b/fixtures/src_template__generate_auth/expected/tasks/db/seed/sample_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add sample data helpful for development, e.g. (fake users, blog posts, etc.) +# +# Use `Db::Seed::RequiredData` if you need to create data *required* for your +# app to work. +class Db::Seed::SampleData < LuckyTask::Task + summary "Add sample database records helpful for development" + + def call + # Using an Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # ``` + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # ``` + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # ``` + # if UserQuery.new.email("me@example.com").none? + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # end + # ``` + puts "Done adding sample data" + end +end diff --git a/fixtures/src_template__sec_tester/expected/.crystal-version b/fixtures/src_template__sec_tester/expected/.crystal-version new file mode 100644 index 00000000..fdd3be6d --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/.crystal-version @@ -0,0 +1 @@ +1.6.2 diff --git a/fixtures/src_template__sec_tester/expected/.env b/fixtures/src_template__sec_tester/expected/.env new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml b/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml new file mode 100644 index 00000000..9b10bbcb --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/.github/workflows/ci.yml @@ -0,0 +1,118 @@ +name: test-project CI + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + check-format: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + - name: Format + run: crystal tool format --check + + specs: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.6.2 + experimental: + - false + runs-on: ubuntu-latest + env: + LUCKY_ENV: test + DB_HOST: localhost + continue-on-error: ${{ matrix.experimental }} + services: + postgres: + image: postgres:12-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Set up Yarn cache + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Set up Node cache + uses: actions/cache@v2 + id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`) + with: + path: '**/node_modules' + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Set up Crystal cache + uses: actions/cache@v2 + id: crystal-cache + with: + path: | + ~/.cache/crystal + lib + lucky_tasks + key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }} + restore-keys: | + ${{ runner.os }}-crystal- + + - name: Install shards + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: shards check || shards install + + - name: Install yarn packages + if: steps.node-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --no-progress + - name: Compiling assets + run: yarn prod + - name: Install npm and Nexploit Repeater + run: | + sudo npm install -g @neuralegion/nexploit-cli --unsafe-perm=true + - name: Build lucky_tasks + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: crystal build tasks.cr -o ./lucky_tasks + + - name: Prepare database + run: | + ./lucky_tasks db.create + ./lucky_tasks db.migrate + ./lucky_tasks db.seed.required_data + + - name: Run tests + run: crystal spec -Dwith_sec_tests \ No newline at end of file diff --git a/fixtures/src_template__sec_tester/expected/Procfile b/fixtures/src_template__sec_tester/expected/Procfile new file mode 100644 index 00000000..e524d70d --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/Procfile @@ -0,0 +1,2 @@ +web: bin/app +release: lucky db.migrate diff --git a/fixtures/src_template__sec_tester/expected/Procfile.dev b/fixtures/src_template__sec_tester/expected/Procfile.dev new file mode 100644 index 00000000..d9a8173a --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/Procfile.dev @@ -0,0 +1,3 @@ +system_check: script/system_check && sleep 100000 +web: lucky watch --reload-browser +assets: yarn watch diff --git a/fixtures/src_template__sec_tester/expected/README.md b/fixtures/src_template__sec_tester/expected/README.md new file mode 100644 index 00000000..da2d97cc --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/README.md @@ -0,0 +1,23 @@ +# test-project + +This is a project written using [Lucky](https://luckyframework.org). Enjoy! + +### Setting up the project + +1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies) +1. Update database settings in `config/database.cr` +1. Run `script/setup` +1. Run `lucky dev` to start the app + +### Using Docker for development + +1. [Install Docker](https://docs.docker.com/engine/install/) +1. Run `docker compose up` + +The Docker container will boot all of the necessary components needed to run your Lucky application. +To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file. + + +### Learning Lucky + +Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky). diff --git a/fixtures/src_template__sec_tester/expected/config/application.cr b/fixtures/src_template__sec_tester/expected/config/application.cr new file mode 100644 index 00000000..c807149a --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/application.cr @@ -0,0 +1,24 @@ +# This file may be used for custom Application configurations. +# It will be loaded before other config files. +# +# Read more on configuration: +# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code + +# Use this code as an example: +# +# ``` +# module Application +# Habitat.create do +# setting support_email : String +# setting lock_with_basic_auth : Bool +# end +# end +# +# Application.configure do |settings| +# settings.support_email = "support@myapp.io" +# settings.lock_with_basic_auth = LuckyEnv.staging? +# end +# +# # In your application, call +# # `Application.settings.support_email` anywhere you need it. +# ``` diff --git a/fixtures/src_template__sec_tester/expected/config/colors.cr b/fixtures/src_template__sec_tester/expected/config/colors.cr new file mode 100644 index 00000000..761ae940 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/colors.cr @@ -0,0 +1,4 @@ +# This enables the color output when in development or test +# Check out the Colorize docs for more information +# https://crystal-lang.org/api/Colorize.html +Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? diff --git a/fixtures/src_template__sec_tester/expected/config/cookies.cr b/fixtures/src_template__sec_tester/expected/config/cookies.cr new file mode 100644 index 00000000..2d5055f2 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/cookies.cr @@ -0,0 +1,25 @@ +require "./server" + +Lucky::Session.configure do |settings| + settings.key = "_test_project_session" +end + +Lucky::CookieJar.configure do |settings| + settings.on_set = ->(cookie : HTTP::Cookie) { + # If ForceSSLHandler is enabled, only send cookies over HTTPS + cookie.secure(Lucky::ForceSSLHandler.settings.enabled) + + # By default, don't allow reading cookies with JavaScript + cookie.http_only(true) + + # Restrict cookies to a first-party or same-site context + cookie.samesite(:lax) + + # Set all cookies to the root path by default + cookie.path("/") + + # You can set other defaults for cookies here. For example: + # + # cookie.expires(1.year.from_now).domain("mydomain.com") + } +end diff --git a/fixtures/src_template__sec_tester/expected/config/database.cr b/fixtures/src_template__sec_tester/expected/config/database.cr new file mode 100644 index 00000000..f614299a --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/database.cr @@ -0,0 +1,29 @@ +database_name = "test_project_#{LuckyEnv.environment}" + +AppDatabase.configure do |settings| + if LuckyEnv.production? + settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"]) + else + settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new( + database: database_name, + hostname: ENV["DB_HOST"]? || "localhost", + port: ENV["DB_PORT"]?.try(&.to_i) || 5432, + # Some common usernames are "postgres", "root", or your system username (run 'whoami') + username: ENV["DB_USERNAME"]? || "postgres", + # Some Postgres installations require no password. Use "" if that is the case. + password: ENV["DB_PASSWORD"]? || "postgres" + ) + end +end + +Avram.configure do |settings| + settings.database_to_migrate = AppDatabase + + # In production, allow lazy loading (N+1). + # In development and test, raise an error if you forget to preload associations + settings.lazy_load_enabled = LuckyEnv.production? + + # Always parse `Time` values with these specific formats. + # Used for both database values, and datetime input fields. + # settings.time_formats << "%F" +end diff --git a/fixtures/src_template__sec_tester/expected/config/email.cr b/fixtures/src_template__sec_tester/expected/config/email.cr new file mode 100644 index 00000000..7c875449 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/email.cr @@ -0,0 +1,26 @@ +require "carbon_sendgrid_adapter" + +BaseEmail.configure do |settings| + if LuckyEnv.production? + # If you don't need to send emails, set the adapter to DevAdapter instead: + # + # settings.adapter = Carbon::DevAdapter.new + # + # If you do need emails, get a key from SendGrid and set an ENV variable + send_grid_key = send_grid_key_from_env + settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key) + elsif LuckyEnv.development? + settings.adapter = Carbon::DevAdapter.new(print_emails: true) + else + settings.adapter = Carbon::DevAdapter.new + end +end + +private def send_grid_key_from_env + ENV["SEND_GRID_KEY"]? || raise_missing_key_message +end + +private def raise_missing_key_message + puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red + exit(1) +end diff --git a/fixtures/src_template__sec_tester/expected/config/env.cr b/fixtures/src_template__sec_tester/expected/config/env.cr new file mode 100644 index 00000000..3f364072 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/env.cr @@ -0,0 +1,33 @@ +# Environments are managed using `LuckyEnv`. By default, development, production +# and test are supported. See +# https://luckyframework.org/guides/getting-started/configuration for details. +# +# The default environment is development unless the environment variable +# LUCKY_ENV is set. +# +# Example: +# ``` +# LuckyEnv.environment # => "development" +# LuckyEnv.development? # => true +# LuckyEnv.production? # => false +# LuckyEnv.test? # => false +# ``` +# +# New environments can be added using the `LuckyEnv.add_env` macro. +# +# Example: +# ``` +# LuckyEnv.add_env :staging +# LuckyEnv.staging? # => false +# ``` +# +# To determine whether or not a `LuckyTask` is currently running, you can use +# the `LuckyEnv.task?` predicate. +# +# Example: +# ``` +# LuckyEnv.task? # => false +# ``` + +# Add a staging environment. +# LuckyEnv.add_env :staging diff --git a/fixtures/src_template__sec_tester/expected/config/error_handler.cr b/fixtures/src_template__sec_tester/expected/config/error_handler.cr new file mode 100644 index 00000000..c6b736e3 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/error_handler.cr @@ -0,0 +1,3 @@ +Lucky::ErrorHandler.configure do |settings| + settings.show_debug_output = !LuckyEnv.production? +end diff --git a/fixtures/src_template__sec_tester/expected/config/log.cr b/fixtures/src_template__sec_tester/expected/config/log.cr new file mode 100644 index 00000000..0e8d7636 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/log.cr @@ -0,0 +1,45 @@ +require "file_utils" + +if LuckyEnv.test? + # Logs to `tmp/test.log` so you can see what's happening without having + # a bunch of log output in your spec results. + FileUtils.mkdir_p("tmp") + + backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w")) + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) +elsif LuckyEnv.production? + # Lucky uses JSON in production so logs can be searched more easily + # + # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'. + backend = Log::IOBackend.new + backend.formatter = Dexter::JSONLogFormatter.proc + Log.dexter.configure(:info, backend) +else + # Use a pretty formatter printing to STDOUT in development + backend = Log::IOBackend.new + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) + DB::Log.level = :info +end + +# Lucky only logs when before/after pipes halt by redirecting, or rendering a +# response. Pipes that run without halting are not logged. +# +# If you want to log every pipe that runs, set the log level to ':info' +Lucky::ContinuedPipeLog.dexter.configure(:none) + +# Lucky only logs failed queries by default. +# +# Set the log to ':info' to log all queries +Avram::QueryLog.dexter.configure(:none) + +# Skip logging static assets requests in development +Lucky::LogHandler.configure do |settings| + if LuckyEnv.development? + settings.skip_if = ->(context : HTTP::Server::Context) { + context.request.method.downcase == "get" && + context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/) + } + end +end diff --git a/fixtures/src_template__sec_tester/expected/config/route_helper.cr b/fixtures/src_template__sec_tester/expected/config/route_helper.cr new file mode 100644 index 00000000..ede1f328 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/route_helper.cr @@ -0,0 +1,10 @@ +# This is used when generating URLs for your application +Lucky::RouteHelper.configure do |settings| + if LuckyEnv.production? + # Example: https://my_app.com + settings.base_uri = ENV.fetch("APP_DOMAIN") + else + # Set domain to the default host/port in development/test + settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" + end +end diff --git a/fixtures/src_template__sec_tester/expected/config/server.cr b/fixtures/src_template__sec_tester/expected/config/server.cr new file mode 100644 index 00000000..c72314be --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/server.cr @@ -0,0 +1,65 @@ +# Here is where you configure the Lucky server +# +# Look at config/route_helper.cr if you want to change the domain used when +# generating links with `Action.url`. +Lucky::Server.configure do |settings| + if LuckyEnv.production? + settings.secret_key_base = secret_key_from_env + settings.host = "0.0.0.0" + settings.port = ENV["PORT"].to_i + settings.gzip_enabled = true + # By default certain content types will be gzipped. + # For a full list look in + # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr + # To add additional extensions do something like this: + # settings.gzip_content_types << "content/type" + else + settings.secret_key_base = "1234567890" + # Change host/port in config/watch.yml + # Alternatively, you can set the DEV_PORT env to set the port for local development + settings.host = Lucky::ServerSettings.host + settings.port = Lucky::ServerSettings.port + end + + # By default Lucky will serve static assets in development and production. + # + # However you could use a CDN when in production like this: + # + # Lucky::Server.configure do |settings| + # if LuckyEnv.production? + # settings.asset_host = "https://mycdnhost.com" + # else + # settings.asset_host = "" + # end + # end + settings.asset_host = "" # Lucky will serve assets +end + +Lucky::ForceSSLHandler.configure do |settings| + # To force SSL in production, uncomment the lines below. + # This will cause http requests to be redirected to https: + # + # settings.enabled = LuckyEnv.production? + # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true} + # + # Or, leave it disabled: + settings.enabled = false +end + +# Set a unique ID for each HTTP request. +# To enable the request ID, uncomment the lines below. +# You can set your own custom String, or use a random UUID. +# Lucky::RequestIdHandler.configure do |settings| +# settings.set_request_id = ->(context : HTTP::Server::Context) { +# UUID.random.to_s +# } +# end + +private def secret_key_from_env + ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production +end + +private def raise_missing_secret_key_in_production + puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red + exit(1) +end diff --git a/fixtures/src_template__sec_tester/expected/config/watch.yml b/fixtures/src_template__sec_tester/expected/config/watch.yml new file mode 100644 index 00000000..3a59b410 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/config/watch.yml @@ -0,0 +1,3 @@ +host: 127.0.0.1 +port: 3000 +reload_port: 3001 diff --git a/fixtures/src_template__sec_tester/expected/db/migrations/.keep b/fixtures/src_template__sec_tester/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/docker-compose.yml b/fixtures/src_template__sec_tester/expected/docker-compose.yml new file mode 100644 index 00000000..d00779db --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + lucky: + build: + context: . + dockerfile: docker/development.dockerfile + environment: + DATABASE_URL: postgres://lucky:password@postgres:5432/lucky + DEV_HOST: "0.0.0.0" + volumes: + - .:/app + - node_modules:/app/node_modules + - shards_lib:/app/lib + - app_bin:/app/bin + - build_cache:/root/.cache + depends_on: + - postgres + ports: + - 3000:3000 # This is the Lucky Server port + - 3001:3001 # This is the Lucky watcher reload port + + entrypoint: ["docker/dev_entrypoint.sh"] + + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: lucky + POSTGRES_PASSWORD: password + POSTGRES_DB: lucky + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + # The postgres database container is exposed on the host at port 6543 to + # allow connecting directly to it with postgres clients. The port differs + # from the postgres default to avoid conflict with existing postgres + # servers. Connect to a running postgres container with: + # postgres://lucky:password@localhost:6543/lucky + - 6543:5432 + +volumes: + postgres_data: + node_modules: + shards_lib: + app_bin: + build_cache: diff --git a/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh new file mode 100755 index 00000000..d16ef6ab --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -euo pipefail + +# This is the entrypoint script used for development docker workflows. +# By default it will: +# - Install dependencies. +# - Run migrations. +# - Start the dev server. +# It also accepts any commands to be run instead. + + +warnfail () { + echo "$@" >&2 + exit 1 +} + +case ${1:-} in + "") # If no arguments are provided, start lucky dev server. + ;; + + *) # If any arguments are provided, execute them instead. + exec "$@" +esac + +if ! [ -d bin ] ; then + echo 'Creating bin directory' + mkdir bin +fi +echo 'Installing npm packages...' +yarn install +if ! shards check ; then + echo 'Installing shards...' + shards install +fi + +echo 'Waiting for postgres to be available...' +./docker/wait-for-it.sh -q postgres:5432 + +if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then + echo 'Finishing database setup...' + lucky db.migrate +fi + +echo 'Starting lucky dev server...' +exec lucky dev diff --git a/fixtures/src_template__sec_tester/expected/docker/development.dockerfile b/fixtures/src_template__sec_tester/expected/docker/development.dockerfile new file mode 100644 index 00000000..be3dba5f --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/docker/development.dockerfile @@ -0,0 +1,34 @@ +FROM crystallang/crystal:1.6.2 + +# Install utilities required to make this Dockerfile run +RUN apt-get update && \ + apt-get install -y wget +# Add the nodesource ppa to apt. Update this to change the nodejs version. +RUN wget https://deb.nodesource.com/setup_16.x -O- | bash + +# Apt installs: +# - nodejs (from above ppa) is required for front-end apps. +# - Postgres cli tools are required for lucky-cli. +# - tmux is required for the Overmind process manager. +RUN apt-get update && \ + apt-get install -y nodejs postgresql-client tmux && \ + rm -rf /var/lib/apt/lists/* + +# NPM global installs: +# - Yarn is the default package manager for the node component of a lucky +# browser app. +# - Mix is the default asset compiler. +RUN npm install -g yarn mix + +# Install lucky cli +WORKDIR /lucky/cli +RUN git clone https://github.com/luckyframework/lucky_cli . && \ + git checkout v1.0.0 && \ + shards build --without-development && \ + cp bin/lucky /usr/bin + +WORKDIR /app +ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres +EXPOSE 3000 +EXPOSE 3001 + diff --git a/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh b/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh new file mode 100755 index 00000000..06e0638c --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/docker/wait-for-it.sh @@ -0,0 +1,189 @@ +#!/usr/bin/bash +# +# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28. +# Licensed under the MIT license as of 81b1373f. +# +# Below this line, wait-for-it is the original work of the author. +# +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers b/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers new file mode 100644 index 00000000..388fa67e --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/script/helpers/function_helpers @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used as helpers +# for various tasks. Read the examples for each one for +# more information. Feel free to put any additional helper +# functions you may need for your app + + +# Returns true if the command $1 is not found +# example: +# if command_not_found "yarn"; then +# echo "no yarn" +# fi +command_not_found() { + ! command -v $1 > /dev/null + return $? +} + +# Returns true if the command $1 is not running +# You must supply the full command to check as an argument +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running" +# fi +command_not_running() { + $1 + if [ $? -ne 0 ]; then + true + else + false + fi +} + +# Returns true if the OS is macOS +# example: +# if is_mac; then +# echo "do mac stuff" +# fi +is_mac() { + if [[ "$OSTYPE" == "darwin"* ]]; then + true + else + false + fi +} + +# Returns true if the OS is linux based +# example: +# if is_linux; then +# echo "do linux stuff" +# fi +is_linux() { + if [[ "$OSTYPE" == "linux"* ]]; then + true + else + false + fi +} + +# Prints error and exit. +# example: +# print_error "Redis is not running. Run it with some_command" +print_error() { + printf "${BOLD_RED_COLOR}There is a problem with your system setup:\n\n" + printf "${BOLD_RED_COLOR}$1 \n\n" | indent + exit 1 +} diff --git a/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers b/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers new file mode 100644 index 00000000..34b77a8c --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/script/helpers/text_helpers @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This file contains a set of functions used to format text, +# and make printing text a little easier. Feel free to put +# any additional functions you need for formatting your shell +# output text. + +# Colors +BOLD_RED_COLOR="\e[1m\e[31m" + +# Indents the text 2 spaces +# example: +# printf "Hello" | indent +indent() { + while read LINE; do + echo " $LINE" || true + done +} + +# Prints out an arrow to your custom notice +# example: +# notice "Installing new magic" +notice() { + printf "\n▸ $1\n" +} + +# Prints out a check mark and Done. +# example: +# print_done +print_done() { + printf "✔ Done\n" | indent +} diff --git a/fixtures/src_template__sec_tester/expected/script/setup b/fixtures/src_template__sec_tester/expected/script/setup new file mode 100755 index 00000000..bf0bc317 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/script/setup @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Exit if any subcommand fails +set -e +set -o pipefail + +source script/helpers/text_helpers + + +notice "Running System Check" +./script/system_check +print_done + +notice "Installing node dependencies" +yarn install --no-progress | indent + +notice "Compiling assets" +yarn dev | indent + +print_done + +notice "Installing shards" +shards install --ignore-crystal-version | indent + +if [ ! -f ".env" ]; then + notice "No .env found. Creating one." + touch .env + print_done +fi + +notice "Creating the database" +lucky db.create | indent + +notice "Verifying postgres connection" +lucky db.verify_connection | indent + +notice "Migrating the database" +lucky db.migrate | indent + +notice "Seeding the database with required and sample records" +lucky db.seed.required_data | indent +lucky db.seed.sample_data | indent + +print_done +notice "Run 'lucky dev' to start the app" diff --git a/fixtures/src_template__sec_tester/expected/script/system_check b/fixtures/src_template__sec_tester/expected/script/system_check new file mode 100755 index 00000000..c27c926b --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/script/system_check @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +source script/helpers/text_helpers +source script/helpers/function_helpers + +# Use this script to check the system for required tools and process that your app needs. +# A few helper functions are provided to make writing bash a little easier. See the +# script/helpers/function_helpers file for more examples. +# +# A few examples you might use here: +# * 'lucky db.verify_connection' to test postgres can be connected +# * Checking that elasticsearch, redis, or postgres is installed and/or booted +# * Note: Booting additional processes for things like mail, background jobs, etc... +# should go in your Procfile.dev. + +if command_not_found "yarn"; then + print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions." +fi + +if command_not_found "createdb"; then + MSG="Please install the postgres CLI tools, then try again." + if is_mac; then + MSG="$MSG\nIf you're using Postgres.app, see https://postgresapp.com/documentation/cli-tools.html." + fi + MSG="$MSG\nSee https://www.postgresql.org/docs/current/tutorial-install.html for install instructions." + + print_error "$MSG" +fi + + +## CUSTOM PRE-BOOT CHECKS ## +# example: +# if command_not_running "redis-cli ping"; then +# print_error "Redis is not running." +# fi + + diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr b/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr new file mode 100644 index 00000000..a1bc631c --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/spec/setup/clean_database.cr @@ -0,0 +1,3 @@ +Spec.before_each do + AppDatabase.truncate +end diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr b/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr new file mode 100644 index 00000000..140ab416 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/spec/setup/reset_emails.cr @@ -0,0 +1,3 @@ +Spec.before_each do + Carbon::DevAdapter.reset +end diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr b/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr new file mode 100644 index 00000000..393c6da3 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/spec/setup/setup_database.cr @@ -0,0 +1,2 @@ +Db::Create.new(quiet: true).call +Db::Migrate.new(quiet: true).call diff --git a/fixtures/src_template__sec_tester/expected/spec/setup/start_app_server.cr b/fixtures/src_template__sec_tester/expected/spec/setup/start_app_server.cr new file mode 100644 index 00000000..ff0bfeeb --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/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/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr b/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr new file mode 100644 index 00000000..187f4753 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/spec/spec_helper.cr @@ -0,0 +1,24 @@ +ENV["LUCKY_ENV"] = "test" +ENV["DEV_PORT"] = "5001" +require "spec" +require "lucky_flow" +require "lucky_flow/ext/lucky" +require "lucky_flow/ext/avram" +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/fixtures/src_template__sec_tester/expected/spec/support/.keep b/fixtures/src_template__sec_tester/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr b/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr new file mode 100644 index 00000000..ef251251 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/spec/support/api_client.cr @@ -0,0 +1,8 @@ +class ApiClient < Lucky::BaseHTTPClient + app AppServer.new + + def initialize + super + headers("Content-Type": "application/json") + end +end diff --git a/fixtures/src_template__sec_tester/expected/spec/support/factories/.keep b/fixtures/src_template__sec_tester/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr b/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr new file mode 100644 index 00000000..fac02c8b --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/actions/api_action.cr @@ -0,0 +1,11 @@ +# Include modules and add methods that are for all API requests +abstract class ApiAction < Lucky::Action + # APIs typically do not need to send cookie/session data. + # Remove this line if you want to send cookies in the response header. + disable_cookies + accepted_formats [:json] + + # By default all actions are required to use underscores to separate words. + # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute +end diff --git a/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr b/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr new file mode 100644 index 00000000..d01ed541 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/actions/errors/show.cr @@ -0,0 +1,63 @@ +# This class handles error responses and reporting. +# +# https://luckyframework.org/guides/http-and-routing/error-handling +class Errors::Show < Lucky::ErrorAction + DEFAULT_MESSAGE = "Something went wrong." + default_format :html + dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] + + def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) + if html? + error_html "Sorry, we couldn't find that page.", status: 404 + else + error_json "Not found", status: 404 + end + end + + # When the request is JSON and an InvalidOperationError is raised, show a + # helpful error with the param that is invalid, and what was wrong with it. + def render(error : Avram::InvalidOperationError) + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json \ + message: error.renderable_message, + details: error.renderable_details, + param: error.invalid_attribute_name, + status: 400 + end + end + + # Always keep this below other 'render' methods or it may override your + # custom 'render' methods. + def render(error : Lucky::RenderableError) + if html? + error_html DEFAULT_MESSAGE, status: error.renderable_status + else + error_json error.renderable_message, status: error.renderable_status + end + end + + # If none of the 'render' methods return a response for the raised Exception, + # Lucky will use this method. + def default_render(error : Exception) : Lucky::Response + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json DEFAULT_MESSAGE, status: 500 + end + end + + private def error_html(message : String, status : Int) + context.response.status_code = status + html_with_status Errors::ShowPage, status, message: message, status_code: status + end + + private def error_json(message : String, status : Int, details = nil, param = nil) + json ErrorSerializer.new(message: message, details: details, param: param), status: status + end + + private def report(error : Exception) : Nil + # Send to Rollbar, send an email, etc. + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr b/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr new file mode 100644 index 00000000..2d39a100 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/actions/home/index.cr @@ -0,0 +1,5 @@ +class Home::Index < BrowserAction + get "/" do + html Lucky::WelcomePage + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/actions/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/app.cr b/fixtures/src_template__sec_tester/expected/src/app.cr new file mode 100644 index 00000000..9d3ee3c7 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/app.cr @@ -0,0 +1,26 @@ +require "./shards" + +# Load the asset manifest +Lucky::AssetHelpers.load_manifest "public/mix-manifest.json" + +require "../config/server" +require "./app_database" +require "../config/**" +require "./models/base_model" +require "./models/mixins/**" +require "./models/**" +require "./queries/mixins/**" +require "./queries/**" +require "./operations/mixins/**" +require "./operations/**" +require "./serializers/base_serializer" +require "./serializers/**" +require "./emails/base_email" +require "./emails/**" +require "./actions/mixins/**" +require "./actions/**" +require "./components/base_component" +require "./components/**" +require "./pages/**" +require "../db/migrations/**" +require "./app_server" diff --git a/fixtures/src_template__sec_tester/expected/src/app_database.cr b/fixtures/src_template__sec_tester/expected/src/app_database.cr new file mode 100644 index 00000000..0efd4f50 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/app_database.cr @@ -0,0 +1,2 @@ +class AppDatabase < Avram::Database +end diff --git a/fixtures/src_template__sec_tester/expected/src/app_server.cr b/fixtures/src_template__sec_tester/expected/src/app_server.cr new file mode 100644 index 00000000..8ec16c3c --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/app_server.cr @@ -0,0 +1,26 @@ +class AppServer < Lucky::BaseAppServer + # Learn about middleware with HTTP::Handlers: + # https://luckyframework.org/guides/http-and-routing/http-handlers + def middleware : Array(HTTP::Handler) + [ + Lucky::RequestIdHandler.new, + Lucky::ForceSSLHandler.new, + Lucky::HttpMethodOverrideHandler.new, + Lucky::LogHandler.new, + Lucky::ErrorHandler.new(action: Errors::Show), + Lucky::RemoteIpHandler.new, + Lucky::RouteHandler.new, + Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), + Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), + Lucky::RouteNotFoundHandler.new, + ] of HTTP::Handler + end + + def protocol + "http" + end + + def listen + server.listen(host, port, reuse_port: false) + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr b/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr new file mode 100644 index 00000000..656f4f11 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/emails/base_email.cr @@ -0,0 +1,15 @@ +# Learn about sending emails +# https://luckyframework.org/guides/emails/sending-emails-with-carbon +abstract class BaseEmail < Carbon::Email + # You can add defaults using the 'inherited' hook + # + # Example: + # + # macro inherited + # from default_from + # end + # + # def default_from + # Carbon::Address.new("support@app.com") + # end +end diff --git a/fixtures/src_template__sec_tester/expected/src/models/base_model.cr b/fixtures/src_template__sec_tester/expected/src/models/base_model.cr new file mode 100644 index 00000000..6bafeb84 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/models/base_model.cr @@ -0,0 +1,5 @@ +abstract class BaseModel < Avram::Model + def self.database : Avram::Database.class + AppDatabase + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/models/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/operations/.keep b/fixtures/src_template__sec_tester/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/operations/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/queries/.keep b/fixtures/src_template__sec_tester/expected/src/queries/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/queries/mixins/.keep b/fixtures/src_template__sec_tester/expected/src/queries/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/.keep b/fixtures/src_template__sec_tester/expected/src/serializers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr b/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr new file mode 100644 index 00000000..3ad0a669 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/serializers/base_serializer.cr @@ -0,0 +1,7 @@ +abstract class BaseSerializer < Lucky::Serializer + def self.for_collection(collection : Enumerable, *args, **named_args) + collection.map do |object| + new(object, *args, **named_args) + end + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr b/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr new file mode 100644 index 00000000..21a53aa2 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/serializers/error_serializer.cr @@ -0,0 +1,14 @@ +# This is the default error serializer generated by Lucky. +# Feel free to customize it in any way you like. +class ErrorSerializer < BaseSerializer + def initialize( + @message : String, + @details : String? = nil, + @param : String? = nil # so you can track which param (if any) caused the problem + ) + end + + def render + {message: @message, param: @param, details: @details} + end +end diff --git a/fixtures/src_template__sec_tester/expected/src/shards.cr b/fixtures/src_template__sec_tester/expected/src/shards.cr new file mode 100644 index 00000000..1afc72cb --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/shards.cr @@ -0,0 +1,8 @@ +# Load .env file before any other config or app code +require "lucky_env" +LuckyEnv.load?(".env") + +# Require your shards here +require "lucky" +require "avram/lucky" +require "carbon" diff --git a/fixtures/src_template__sec_tester/expected/src/start_server.cr b/fixtures/src_template__sec_tester/expected/src/start_server.cr new file mode 100644 index 00000000..9df5d1fd --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/start_server.cr @@ -0,0 +1,16 @@ +require "./app" + +Habitat.raise_if_missing_settings! + +if LuckyEnv.development? + Avram::Migrator::Runner.new.ensure_migrated! + Avram::SchemaEnforcer.ensure_correct_column_mappings! +end + +app_server = AppServer.new + +Signal::INT.trap do + app_server.close +end + +app_server.listen diff --git a/fixtures/src_template__sec_tester/expected/src/test_project.cr b/fixtures/src_template__sec_tester/expected/src/test_project.cr new file mode 100644 index 00000000..68e1a8d2 --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/src/test_project.cr @@ -0,0 +1,6 @@ +# Typically you will not use or modify this file. 'shards build' and some +# other crystal tools will sometimes use this. +# +# When this file is compiled/run it will require and run 'start_server', +# which as its name implies will start the server for you app. +require "./start_server" diff --git a/fixtures/src_template__sec_tester/expected/tasks.cr b/fixtures/src_template__sec_tester/expected/tasks.cr new file mode 100644 index 00000000..5a892d4d --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/tasks.cr @@ -0,0 +1,25 @@ +# This file loads your app and all your tasks when running 'lucky' +# +# Run 'lucky --help' to see all available tasks. +# +# Learn to create your own tasks: +# https://luckyframework.org/guides/command-line-tasks/custom-tasks + +# See `LuckyEnv#task?` +ENV["LUCKY_TASK"] = "true" + +# Load Lucky and the app (actions, models, etc.) +require "./src/app" +require "lucky_task" + +# You can add your own tasks here in the ./tasks folder +require "./tasks/**" + +# Load migrations +require "./db/migrations/**" + +# Load Lucky tasks (dev, routes, etc.) +require "lucky/tasks/**" +require "avram/lucky/tasks" + +LuckyTask::Runner.run diff --git a/fixtures/src_template__sec_tester/expected/tasks/.keep b/fixtures/src_template__sec_tester/expected/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr new file mode 100644 index 00000000..d866040f --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/tasks/db/seed/required_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add seeds here that are *required* for your app to work. +# For example, you might need at least one admin user or you might need at least +# one category for your blog posts for the app to work. +# +# Use `Db::Seed::SampleData` if your only want to add sample data helpful for +# development. +class Db::Seed::RequiredData < LuckyTask::Task + summary "Add database records required for the app to work" + + def call + # Using a Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # + # SaveUser.create!(email: "me@example.com", name: "Jane") + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # + # unless UserQuery.new.email("me@example.com").first? + # SaveUser.create!(email: "me@example.com", name: "Jane") + # end + puts "Done adding required data" + end +end diff --git a/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr new file mode 100644 index 00000000..231d7e8d --- /dev/null +++ b/fixtures/src_template__sec_tester/expected/tasks/db/seed/sample_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add sample data helpful for development, e.g. (fake users, blog posts, etc.) +# +# Use `Db::Seed::RequiredData` if you need to create data *required* for your +# app to work. +class Db::Seed::SampleData < LuckyTask::Task + summary "Add sample database records helpful for development" + + def call + # Using an Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # ``` + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # ``` + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # ``` + # if UserQuery.new.email("me@example.com").none? + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # end + # ``` + puts "Done adding sample data" + end +end diff --git a/script/test b/script/test index 363859c5..329124b5 100755 --- a/script/test +++ b/script/test @@ -16,13 +16,14 @@ else printf "\nCrystal format checks passed.\n\n" fi -if ! crystal ./bin/ameba.cr; then - printf "\nCode did not pass Ameba checks.\n" - printf "\nResolve Ameba linter errors, then run this checker again\n\n" - exit 1 -else - printf "\nAmeba linter checks passed.\n\n" -fi +# TODO: check for ambea in PATH +#if ! crystal ./bin/ameba.cr; then + #printf "\nCode did not pass Ameba checks.\n" + #printf "\nResolve Ameba linter errors, then run this checker again\n\n" + #exit 1 +#else + #printf "\nAmeba linter checks passed.\n\n" +#fi printf "\nRunning specs with 'crystal spec'\n\n" crystal spec "$@" diff --git a/shard.yml b/shard.yml index 3b8129fb..7e0bb45b 100644 --- a/shard.yml +++ b/shard.yml @@ -22,8 +22,3 @@ dependencies: nox: github: crystal-loot/nox version: ~> 0.2.2 - -development_dependencies: - ameba: - github: crystal-ameba/ameba - version: ~> 1.5.0 diff --git a/spec/generators_spec.cr b/spec/generators_spec.cr new file mode 100644 index 00000000..255e9d03 --- /dev/null +++ b/spec/generators_spec.cr @@ -0,0 +1,195 @@ +require "./spec_helper2" + +describe "Generators" do + around_each do |example| + with_tempfile("tmp") do |tmp| + Dir.mkdir_p(tmp) + Dir.cd(tmp) do + example.run + end + end + end + + describe ApiAuthenticationTemplate do + it "generates api authentication template" do + generate_snapshot("api_authentication_template") do + ApiAuthenticationTemplate.new + end + end + end + + describe AppWithSecTesterTemplate do + it "generates app with sec tester template" do + generate_snapshot("app_sec_tester_template") do + AppWithSecTesterTemplate.new( + generate_auth: false, + browser: false + ) + end + end + + it "generates app with sec tester template with only generate auth option" do + generate_snapshot("app_sec_tester_template__generate_auth") do + AppWithSecTesterTemplate.new( + generate_auth: true, + browser: false + ) + end + end + + it "generates app with sec tester template with only browser option" do + generate_snapshot("app_sec_tester_template__browser") do + AppWithSecTesterTemplate.new( + generate_auth: false, + browser: true + ) + end + end + end + + describe ShardFileGenerator do + it "generates shard file template" do + generate_snapshot("shard_file_template") do + ShardFileGenerator.new( + "test-shard", + generate_auth: true, + browser: true, + with_sec_tester: true + ).tap do |instance| + instance.crystal_version = "1.6.2" + end + end + end + + it "generates shard file template with only browser option" do + generate_snapshot("shard_file_template__browser") do + ShardFileGenerator.new( + "test-shard", + generate_auth: false, + browser: true, + with_sec_tester: false + ).tap do |instance| + instance.crystal_version = "1.6.2" + end + end + end + + it "generates shard file template with only generate auth option" do + generate_snapshot("shard_file_template__generate_auth") do + ShardFileGenerator.new( + "test-shard", + generate_auth: true, + browser: false, + with_sec_tester: false + ).tap do |instance| + instance.crystal_version = "1.6.2" + end + end + end + + it "generates shard file template with only sec tester option" do + generate_snapshot("shard_file_template__with_sec_tester") do + ShardFileGenerator.new( + "test-shard", + generate_auth: false, + browser: false, + with_sec_tester: true + ).tap do |instance| + instance.crystal_version = "1.6.2" + end + end + end + end + + describe SrcTemplate do + it "generates src template" do + generate_snapshot("src_template") do + SrcTemplate.new( + "test-project", + generate_auth: true, + api_only: true, + with_sec_tester: true + ).tap do |instance| + instance.secret_key_base = "1234567890" + instance.crystal_version = "1.6.2" + instance.lucky_cli_version = "1.0.0" + end + end + end + + it "generates src template with only generate auth option" do + generate_snapshot("src_template__generate_auth") do + SrcTemplate.new( + "test-project", + generate_auth: true, + api_only: false, + with_sec_tester: false + ).tap do |instance| + instance.secret_key_base = "1234567890" + instance.crystal_version = "1.6.2" + instance.lucky_cli_version = "1.0.0" + end + end + end + + it "generates src template with only api option" do + generate_snapshot("src_template__api_only") do + SrcTemplate.new( + "test-project", + generate_auth: false, + api_only: true, + with_sec_tester: false + ).tap do |instance| + instance.secret_key_base = "1234567890" + instance.crystal_version = "1.6.2" + instance.lucky_cli_version = "1.0.0" + end + end + end + + it "generates src template with only sec tester option" do + generate_snapshot("src_template__sec_tester") do + SrcTemplate.new( + "test-project", + generate_auth: false, + api_only: false, + with_sec_tester: true + ).tap do |instance| + instance.secret_key_base = "1234567890" + instance.crystal_version = "1.6.2" + instance.lucky_cli_version = "1.0.0" + end + end + end + end + + describe BaseAuthenticationSrcTemplate do + it "generates base authentication src template" do + generate_snapshot("base_authentication_src_template") do + BaseAuthenticationSrcTemplate.new + end + end + end + + describe BrowserAuthenticationSrcTemplate do + it "generates browser authentication src template" do + generate_snapshot("browser_authentication_src_template") do + BrowserAuthenticationSrcTemplate.new + end + end + end + + describe BrowserSrcTemplate do + it "generates browser src template" do + generate_snapshot("browser_src_template") do + BrowserSrcTemplate.new(generate_auth: true) + end + end + + it "generates browser src template without generate auth" do + generate_snapshot("browser_src_template__generate_auth") do + BrowserSrcTemplate.new(generate_auth: false) + end + end + end +end diff --git a/spec/spec_helper2.cr b/spec/spec_helper2.cr new file mode 100644 index 00000000..3717428d --- /dev/null +++ b/spec/spec_helper2.cr @@ -0,0 +1,43 @@ +require "spec" +require "lucky_template/spec" +require "./support/tempfile" +require "../src/lucky_cli" + +include LuckyTemplate::Spec + +SPEC_UPDATE_SNAPSHOT = ENV["SPEC_UPDATE_SNAPSHOT"]? == "1" + +def generate_snapshot(fixture_name, file = __FILE__, line = __LINE__, &) + generator = yield + + actual_path = Path[Dir.current] + expected_path = Path["#{__DIR__}/../fixtures"] / fixture_name / "expected" + + if SPEC_UPDATE_SNAPSHOT + FileUtils.rm_rf(expected_path) + FileUtils.mkdir_p(expected_path) + generator.render(expected_path) + end + + FileUtils.mkdir_p(actual_path) + generator.render(actual_path) + + generator.template_folder.should be_valid_at(actual_path), file: file, line: line + + snapshot = LuckyTemplate.snapshot(generator.template_folder) + snapshot.select { |_, type| type.file? }.each do |filename, _| + actual_filename = actual_path / filename + expected_filename = expected_path / filename + + unless File.same_content?(actual_filename, expected_filename) + actual_lines = File.read_lines(actual_filename) + expected_lines = File.read_lines(expected_filename) + + actual_lines.each_with_index do |actual_line, index| + actual_line.should eq(expected_lines[index]), file: file, line: line + end + end + end + + generator +end diff --git a/spec/support/tempfile.cr b/spec/support/tempfile.cr new file mode 100644 index 00000000..d683a4db --- /dev/null +++ b/spec/support/tempfile.cr @@ -0,0 +1,61 @@ +# https://github.com/crystal-lang/crystal/blob/master/spec/support/tempfile.cr + +require "file_utils" + +SPEC_TEMPFILE_PATH = File.join(Dir.tempdir, "cr-spec-#{Random.new.hex(4)}") +SPEC_TEMPFILE_CLEANUP = ENV["SPEC_TEMPFILE_CLEANUP"]? != "0" + +# Expands *paths* in a unique temp folder and yield them to the block. +# +# The *paths* are interpreted relative to a unique folder for every spec run and +# prefixed by the name of the spec file that requests them. +# +# The constructed path is yielded to the block and cleaned up afterwards. +# +# Paths should still be uniquely chosen inside a spec file. This helper +# ensures they're placed in the temporary location (`Dir.tempdir`), +# avoids name clashes between parallel spec runs and cleans up afterwards. +# +# The unique directory for the spec run is removed `at_exit`. +# +# If the environment variable `SPEC_TEMPFILE_CLEANUP` is set to `0`, no paths +# will be cleaned up, enabling easier debugging. +def with_tempfile(*paths, file = __FILE__, &) + calling_spec = File.basename(file).rchop("_spec.cr") + paths = paths.map { |path| File.join(SPEC_TEMPFILE_PATH, calling_spec, path) } + FileUtils.mkdir_p(File.join(SPEC_TEMPFILE_PATH, calling_spec)) + + begin + yield *paths + ensure + if SPEC_TEMPFILE_CLEANUP + paths.each do |path| + rm_rf(path) if File.exists?(path) + end + end + end +end + +if SPEC_TEMPFILE_CLEANUP + at_exit do + rm_rf(SPEC_TEMPFILE_PATH) if Dir.exists?(SPEC_TEMPFILE_PATH) + end +end + +private def rm_rf(path : String) : Nil + if Dir.exists?(path) && !File.symlink?(path) + Dir.each_child(path) do |entry| + src = File.join(path, entry) + rm_rf(src) + end + Dir.delete(path) + else + begin + File.delete(path) + rescue File::AccessDeniedError + # To be able to delete read-only files (e.g. ones under .git/) on Windows. + File.chmod(path, 0o666) + File.delete(path) + end + end +end diff --git a/src/lucky.cr b/src/lucky.cr index 0808bde9..fc3fed3f 100755 --- a/src/lucky.cr +++ b/src/lucky.cr @@ -1,5 +1,6 @@ require "colorize" require "option_parser" +require "ecr" require "./lucky_cli/spinner" require "./lucky_cli" require "./generators/*" diff --git a/src/lucky_cli.cr b/src/lucky_cli.cr index 97f114bc..87d20b50 100644 --- a/src/lucky_cli.cr +++ b/src/lucky_cli.cr @@ -1,6 +1,7 @@ require "lucky_template" require "lucky_task" require "nox" +require "ecr" require "./lucky_cli/**" require "./generators/*" require "./init" diff --git a/src/lucky_cli/shard_file_generator.cr b/src/lucky_cli/shard_file_generator.cr index 3e04c045..ebd31d13 100644 --- a/src/lucky_cli/shard_file_generator.cr +++ b/src/lucky_cli/shard_file_generator.cr @@ -6,6 +6,7 @@ class ShardFileGenerator getter? generate_auth : Bool getter? browser : Bool getter? with_sec_tester : Bool + property(crystal_version) { Crystal::VERSION } def initialize( @project_name : String, @@ -34,7 +35,7 @@ class ShardFileGenerator "main" => Path.new("src", "#{project_name}.cr").to_s, }, }, - "crystal" => ">= #{Crystal::VERSION}", + "crystal" => ">= #{crystal_version}", "dependencies" => shard_deps, "development_dependencies" => shard_dev_deps, } diff --git a/src/lucky_cli/src_template.cr b/src/lucky_cli/src_template.cr index ca052279..49c07f2f 100644 --- a/src/lucky_cli/src_template.cr +++ b/src/lucky_cli/src_template.cr @@ -5,8 +5,16 @@ class SrcTemplate getter project_name getter? api_only, generate_auth, with_sec_tester getter crystal_project_name : String + property(secret_key_base) { Random::Secure.base64(32) } + property(crystal_version) { Crystal::VERSION } + property(lucky_cli_version) { LuckyCli::VERSION } - def initialize(@project_name : String, @generate_auth : Bool, @api_only : Bool, @with_sec_tester : Bool) + def initialize( + @project_name : String, + @generate_auth : Bool, + @api_only : Bool, + @with_sec_tester : Bool + ) @crystal_project_name = @project_name.gsub("-", "_") end @@ -18,10 +26,6 @@ class SrcTemplate !api_only? end - def secret_key_base - Random::Secure.base64(32) - end - def render(path : Path) LuckyTemplate.write!(path, template_folder) end diff --git a/src/web_app_skeleton/.crystal-version.ecr b/src/web_app_skeleton/.crystal-version.ecr index bc6a0336..5c2537ca 100644 --- a/src/web_app_skeleton/.crystal-version.ecr +++ b/src/web_app_skeleton/.crystal-version.ecr @@ -1 +1 @@ -<%= Crystal::VERSION %> +<%= crystal_version %> diff --git a/src/web_app_skeleton/.github/workflows/ci.yml.ecr b/src/web_app_skeleton/.github/workflows/ci.yml.ecr index 5a3113ca..77c7428f 100644 --- a/src/web_app_skeleton/.github/workflows/ci.yml.ecr +++ b/src/web_app_skeleton/.github/workflows/ci.yml.ecr @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: crystal_version: - - <%= Crystal::VERSION %> + - <%= crystal_version %> experimental: - false runs-on: ubuntu-latest @@ -31,7 +31,7 @@ jobs: fail-fast: false matrix: crystal_version: - - <%= Crystal::VERSION %> + - <%= crystal_version %> experimental: - false runs-on: ubuntu-latest diff --git a/src/web_app_skeleton/docker/development.dockerfile.ecr b/src/web_app_skeleton/docker/development.dockerfile.ecr index 5cc00c85..13abd8a5 100644 --- a/src/web_app_skeleton/docker/development.dockerfile.ecr +++ b/src/web_app_skeleton/docker/development.dockerfile.ecr @@ -1,4 +1,4 @@ -FROM crystallang/crystal:<%= Crystal::VERSION %> +FROM crystallang/crystal:<%= crystal_version %> # Install utilities required to make this Dockerfile run RUN apt-get update && \ @@ -33,7 +33,7 @@ RUN apt-get update && \ # Install lucky cli WORKDIR /lucky/cli RUN git clone https://github.com/luckyframework/lucky_cli . && \ - git checkout v<%= LuckyCli::VERSION %> && \ + git checkout v<%= lucky_cli_version %> && \ shards build --without-development && \ cp bin/lucky /usr/bin