diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 594b0cce..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,93 +0,0 @@ -version: 2 - -orbs: - browser-tools: circleci/browser-tools@1.4.0 - -jobs: - build: - working_directory: ~/otb-api - - # Primary container image where all commands run - - docker: - - image: cimg/ruby:3.1.2-browsers - environment: - PGUSER: root - RAILS_ENV: test - DB_HOSTNAME: 127.0.0.1 - DB_USERNAME: user - DB_PASSWORD: password - TEST_DB_NAME: otb - MYSQL_ALLOW_EMPTY_PASSWORD: true - CI: 'circleci' - BROWSER_PATH: /usr/bin/google-chrome - - - image: circleci/postgres:10 - environment: - POSTGRES_USER: user - POSTGRES_DB: otb - POSTGRES_PASSWORD: password - - - image: circleci/mysql:latest - command: [--default-authentication-plugin=mysql_native_password] - environment: - MYSQL_DATABASE: otb - MYSQL_USER: user - MYSQL_PASSWORD: password - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_ROOT_PASSWORD: password - - steps: - - checkout - - run: - name: Install dependencies - command: | - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - sudo apt update - sudo apt install -y postgresql-client || true - sudo apt install -y imagemagick libappindicator1 fonts-liberation - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome*.deb - gem install bundle - bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs 4 --retry 3 - - - run: - name: Make Tmp Directory - command: | - sudo mkdir -p /data/tmp - sudo chmod 777 /data/tmp - sudo chown $USER:$USER /data/tmp - sudo mkdir -p public/storage/tmp - sudo chmod 777 public/storage/tmp - sudo chown $USER:$USER public/storage/tmp - - # Test building MySQL - # TODO: Figure out how to allow the database user to create new tenants in - # the MySQL Docker instance. For now, we'll just test that it can setup the - # initial database using MySQL. - # - run: - # name: Wait for DB - # command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m - - # - run: - # name: MySQL Setup - # command: | - # export DB_ADAPTER=mysql2 - # bundle exec rake db:drop RAILS_ENV=test DB_ADAPTER=mysql2 - # bundle exec rake db:create RAILS_ENV=test DB_ADAPTER=mysql2 - # bundle exec rake db:schema:load RAILS_ENV=test DB_ADAPTER=mysql2 - # bundle exec rake db:migrate RAILS_ENV=test DB_ADAPTER=mysql2 - - # Test using PostgreSQL - - run: - name: PostgreSQL Setup - command: | - export DB_ADAPTER=postgresql - bundle exec rake db:drop RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:create RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:schema:load RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:migrate RAILS_ENV=test DB_ADAPTER=postgresql - - - run: - name: Parallel RSpec with PostgreSQL - command: DB_ADAPTER=postgresql bundle exec rspec spec/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4faa2c7f..09a9d233 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,17 +2,17 @@ name: Deploy on: push: - branches: [ develop, main ] + branches: [develop, main] workflow_dispatch: inputs: environment: - description: 'Environment to deploy to' + description: "Environment to deploy to" required: true - default: 'staging' + default: "staging" type: choice options: - - staging - - production + - staging + - production env: RAILS_ENV: production @@ -22,91 +22,82 @@ permissions: contents: read jobs: + # Runs the exact same job defined in test.yml (via workflow_call) instead + # of keeping a separate copy here. deploy only runs if this passes. test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:13 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: otb_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.1.2 - bundler-cache: true - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y postgresql-client imagemagick libpq-dev gdal-bin libgdal-dev - - - name: Create directories - run: | - sudo mkdir -p /data/tmp - sudo chmod 777 /data/tmp - mkdir -p public/storage/tmp - - - name: Set up database - run: | - export DB_ADAPTER=postgresql - until pg_isready -h localhost -p 5432 -U postgres; do sleep 2; done - bundle exec rake db:create RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:schema:load RAILS_ENV=test DB_ADAPTER=postgresql - - - name: Run tests - run: | - DB_ADAPTER=postgresql bundle exec rspec --format progress + uses: ./.github/workflows/test.yml + secrets: inherit deploy: needs: test runs-on: ubuntu-latest - + # For a manual run, the picker below decides. For an ordinary push, + # there's no picker input, so this falls back to branch: main -> + # production, anything else (develop) -> staging. + environment: ${{ github.event.inputs.environment || (github.ref_name == 'main' && 'production' || 'staging') }} + steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE }} - aws-region: us-east-1 - role-session-name: GitHub-OIDC-OTB + # A manual dispatch could otherwise pick "production" while running + # against a non-main branch, silently deploying untested branch code + # to production. This only ever fires for that specific case — a + # normal push always resolves its environment from the branch itself, + # never from this input, so it can't trip this guard. + - name: Refuse to deploy production from a non-main branch + if: github.event.inputs.environment == 'production' && github.ref_name != 'main' + run: | + echo "::error::Refusing to deploy to production from branch '${{ github.ref_name }}'. Production deploys must be triggered from main." + exit 1 - - name: Deploy to ECS - env: - AWS_ECR: ${{ secrets.AWS_ECR }} - AWS_ECS_CLUSTER_DEV: ${{ secrets.AWS_ECS_CLUSTER_DEV }} - AWS_ECS_CLUSTER_PROD: ${{ secrets.AWS_ECS_CLUSTER_PROD }} - AWS_ECS_SERVICE_DEV: ${{ secrets.AWS_ECS_SERVICE_DEV }} - AWS_ECS_SERVICE_PROD: ${{ secrets.AWS_ECS_SERVICE_PROD }} - AWS_REGION: ${{ secrets.AWS_REGION }} - BRANCH: ${{ github.ref_name }} - run: | - chmod +x build.sh - ./build.sh + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE }} + aws-region: us-east-1 + role-session-name: GitHub-OIDC-OTB + + # Turns the resolved environment (production/staging) into the actual + # image tag + ECS cluster/service build.sh needs. This is the one + # place that mapping lives now — build.sh just builds/pushes/deploys + # whatever it's told, it doesn't decide anything on its own anymore. + - name: Resolve deploy target + id: target + env: + DEPLOY_ENV: ${{ github.event.inputs.environment || (github.ref_name == 'main' && 'production' || 'staging') }} + run: | + if [ "$DEPLOY_ENV" = "production" ]; then + { + echo "tag=stable" + echo "cluster=${{ secrets.AWS_ECS_CLUSTER_PROD }}" + echo "service=${{ secrets.AWS_ECS_SERVICE_PROD }}" + } >> "$GITHUB_OUTPUT" + else + { + echo "tag=latest" + echo "cluster=${{ secrets.AWS_ECS_CLUSTER_DEV }}" + echo "service=${{ secrets.AWS_ECS_SERVICE_DEV }}" + } >> "$GITHUB_OUTPUT" + fi - - name: Notify deployment status - if: always() - run: | - branch="${{ github.ref_name }}" - environment=$([ "$branch" == "main" ] && echo "production" || echo "staging") - if [[ "${{ job.status }}" == "success" ]]; then - echo "Successfully deployed to $environment via ECS" - else - echo "Deployment to $environment failed" - fi \ No newline at end of file + - name: Build, tag, push, and deploy + env: + AWS_ECR: ${{ secrets.AWS_ECR }} + TAG: ${{ steps.target.outputs.tag }} + AWS_ECS_CLUSTER: ${{ steps.target.outputs.cluster }} + AWS_ECS_SERVICE: ${{ steps.target.outputs.service }} + run: | + chmod +x build.sh + ./build.sh + + - name: Notify deployment status + if: always() + env: + DEPLOY_ENV: ${{ github.event.inputs.environment || (github.ref_name == 'main' && 'production' || 'staging') }} + run: | + if [ "${{ job.status }}" = "success" ]; then + echo "Successfully deployed to $DEPLOY_ENV via ECS" + else + echo "Deployment to $DEPLOY_ENV failed" + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55109b48..fc2c6242 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,13 @@ name: Test on: - push: - branches: [ main, develop ] pull_request: - branches: [ main, develop ] + branches: [main, develop] + # Lets deploy.yml call this same job for push-triggered runs instead of + # keeping a second, separate copy of the test job that can drift out of + # sync (which is what happened before — the old copy in deploy.yml was + # missing the elasticsearch/redis services and RAILS_MASTER_KEY below). + workflow_call: env: RAILS_ENV: test @@ -13,14 +16,38 @@ env: DB_USERNAME: postgres DB_PASSWORD: postgres TEST_DB_NAME: otb_test - CI: 'github-actions' - BROWSER_PATH: /usr/bin/google-chrome + CI: "github-actions" + BROWSER_PATH: /usr/bin/chromium-browser + RAILS_MASTER_KEY: ${{ secrets.MASTER_KEY }} jobs: test: - runs-on: ubuntu-latest - + runs-on: ubuntu-24.04-arm + services: + elasticsearch: + # Gemfile.lock uses the elasticsearch gem (~> 8), whose client + # refuses to talk to a pre-8.x server (it checks for the + # X-Elastic-Product header, which 7.x doesn't send) — that + # mismatch is what was causing + # Elasticsearch::UnsupportedProductError in delete_test_indices. + image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3 + env: + # 8.x's default heap sizing is much larger than 7.x's and gets + # OOM-killed on a standard runner without this. + ES_JAVA_OPTS: -Xms512m -Xmx512m + xpack.security.enabled: false + cluster.name: otb-elasticsearch + http.port: 9200 + discovery.type: single-node + options: >- + --health-cmd "curl http://localhost:9200/_cluster/health" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + ports: + - 9200:9200 + postgres: image: postgres:13 env: @@ -35,47 +62,57 @@ jobs: ports: - 5432:5432 + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.1.2 - bundler-cache: true + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4.4 + bundler-cache: true - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y postgresql-client imagemagick libpq-dev gdal-bin libgdal-dev fonts-liberation - wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome-stable_current_amd64.deb || true - sudo apt-get install -f -y + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y postgresql-client imagemagick libpq-dev gdal-bin libgdal-dev fonts-liberation libmagickwand-dev libvips chromium-browser + wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + sudo dpkg -i google-chrome-stable_current_amd64.deb || true + sudo apt-get install -f -y - - name: Create required directories - run: | - sudo mkdir -p /data/tmp - sudo chmod 777 /data/tmp - sudo chown $USER:$USER /data/tmp - mkdir -p public/storage/tmp - chmod 777 public/storage/tmp + - name: Create required directories + run: | + sudo mkdir -p /data/tmp + sudo chmod 777 /data/tmp + sudo chown $USER:$USER /data/tmp + mkdir -p public/storage/tmp + chmod 777 public/storage/tmp - - name: Wait for database to be ready - run: | - until pg_isready -h localhost -p 5432 -U postgres; do - echo "Waiting for PostgreSQL..." - sleep 2 - done + - name: Wait for database to be ready + run: | + until pg_isready -h localhost -p 5432 -U postgres; do + echo "Waiting for PostgreSQL..." + sleep 2 + done - - name: Set up PostgreSQL database - run: | - export DB_ADAPTER=postgresql - bundle exec rake db:drop RAILS_ENV=test DB_ADAPTER=postgresql || true - bundle exec rake db:create RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:schema:load RAILS_ENV=test DB_ADAPTER=postgresql - bundle exec rake db:migrate RAILS_ENV=test DB_ADAPTER=postgresql + - name: Set up PostgreSQL database + run: | + export DB_ADAPTER=postgresql + bundle exec rake db:drop RAILS_ENV=test DB_ADAPTER=postgresql || true + bundle exec rake db:create RAILS_ENV=test DB_ADAPTER=postgresql + bundle exec rake db:schema:load RAILS_ENV=test DB_ADAPTER=postgresql + bundle exec rake db:migrate RAILS_ENV=test DB_ADAPTER=postgresql - - name: Run RSpec tests - run: | - DB_ADAPTER=postgresql bundle exec rspec --format progress \ No newline at end of file + - name: Run RSpec tests + run: | + DB_ADAPTER=postgresql bundle exec rspec --format progress diff --git a/.rubocop.yml b/.rubocop.yml index 615b2430..06002162 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,148 +1,26 @@ -AllCops: - TargetRubyVersion: 2.5 - # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop - # to ignore them, so only the ones explicitly set in this file are enabled. - DisabledByDefault: true - Exclude: - - '**/templates/**/*' - - '**/vendor/**/*' - - 'actionpack/lib/action_dispatch/journey/parser.rb' +inherit_gem: + rubocop-shopify: rubocop.yml -# Prefer &&/|| over and/or. -Style/AndOr: - Enabled: true +Metrics/AbcSize: + Max: 20 -# Do not use braces for hash literals when they are the last argument of a -# method call. -# Style/BracesAroundHashParameters: -# Enabled: true -# EnforcedStyle: context_dependent +Metrics/CyclomaticComplexity: + Max: 10 -# Align `when` with `case`. -Layout/CaseIndentation: - Enabled: true +Metrics/PerceivedComplexity: + Max: 10 -# Align comments with method definitions. -Layout/CommentIndentation: - Enabled: true - -Layout/EmptyLineAfterMagicComment: - Enabled: true - -# In a regular class definition, no empty lines around the body. -Layout/EmptyLinesAroundClassBody: - Enabled: true - -# In a regular method definition, no empty lines around the body. -Layout/EmptyLinesAroundMethodBody: - Enabled: true - -# In a regular module definition, no empty lines around the body. -Layout/EmptyLinesAroundModuleBody: - Enabled: true - -Layout/FirstParameterIndentation: - Enabled: true - -# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. -Style/HashSyntax: - Enabled: true - -# Method definitions after `private` or `protected` isolated calls need one -# extra level of indentation. -Layout/IndentationConsistency: - Enabled: true - EnforcedStyle: indented_internal_methods - -# Two spaces, no tabs (for indentation). -Layout/IndentationWidth: - Enabled: true - -Layout/SpaceAfterColon: - Enabled: true - -Layout/SpaceAfterComma: - Enabled: true - -Layout/SpaceAroundEqualsInParameterDefault: - Enabled: true - -Layout/SpaceAroundKeyword: - Enabled: true - -Layout/SpaceAroundOperators: - Enabled: true - -Layout/SpaceBeforeFirstArg: - Enabled: true - -Style/DefWithParentheses: - Enabled: true - -# Defining a method with parameters needs parentheses. -Style/MethodDefParentheses: - Enabled: true - -Style/FrozenStringLiteralComment: - Enabled: true - EnforcedStyle: always - Exclude: - - 'actionview/test/**/*.builder' - - 'actionview/test/**/*.ruby' - - 'actionpack/test/**/*.builder' - - 'actionpack/test/**/*.ruby' - - 'activestorage/db/migrate/**/*.rb' - -# Use `foo {}` not `foo{}`. -Layout/SpaceBeforeBlockBraces: - Enabled: true - -# Use `foo { bar }` not `foo {bar}`. -Layout/SpaceInsideBlockBraces: - Enabled: true - -# Use `{ a: 1 }` not `{a:1}`. -Layout/SpaceInsideHashLiteralBraces: - Enabled: true - -Layout/SpaceInsideParens: - Enabled: true - -# Check quotes usage according to lint rule below. Style/StringLiterals: - Enabled: true EnforcedStyle: single_quotes -# Detect hard tabs, no hard tabs. -Layout/IndentationStyle: - Enabled: true - -# Blank lines should not have any spaces. -Layout/TrailingEmptyLines: - Enabled: true - -# No trailing whitespace. -Layout/TrailingWhitespace: - Enabled: true - -# Use quotes for string literals when they are enough. -Style/RedundantPercentQ: - Enabled: true - -# Align `end` with the matching keyword or starting expression except for -# assignments, where it should be aligned with the LHS. -Layout/EndAlignment: - Enabled: true - EnforcedStyleAlignWith: variable - -# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. -Lint/RequireParentheses: - Enabled: true +RSpec/ExampleLength: + Max: 20 -Style/RedundantReturn: - Enabled: true - AllowMultipleReturnValues: true +RSpec/MultipleExpectations: + Max: 10 -Style/Semicolon: - Enabled: true - AllowAsExpressionSeparator: true +plugins: + - rubocop-factory_bot + - rubocop-rails + - rubocop-rspec + - rubocop-rspec_rails diff --git a/.ruby-version b/.ruby-version index ef538c28..f9892605 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.2 +3.4.4 diff --git a/Capfile b/Capfile index 5fad1378..a9767c20 100644 --- a/Capfile +++ b/Capfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Load DSL and set up stages require 'capistrano/setup' diff --git a/Dockerfile b/Dockerfile index a2610361..ae36c8bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,47 @@ -FROM ruby:3.1.2-slim +FROM ruby:3.4.4-slim + +ENV RAILS_ENV=production RUN apt-get update -qq && \ apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - git \ - postgresql-client \ - libpq-dev \ - libgdal-dev \ - gdal-bin \ - imagemagick \ - libmagickwand-dev \ - libvips \ - pkg-config \ - && rm -rf /var/lib/apt/lists/* + build-essential \ + curl \ + git \ + postgresql-client \ + libpq-dev \ + libgdal-dev \ + gdal-bin \ + imagemagick \ + libmagickwand-dev \ + libvips \ + libyaml-dev \ + pkg-config \ + vim \ + && rm -rf /var/lib/apt/lists/* WORKDIR /rails COPY Gemfile Gemfile.lock ./ -RUN gem install bundler:2.2.22 +RUN gem install bundler:2.3.7 RUN bundle install COPY . . -RUN mkdir -p /data/tmp public/storage/tmp tmp/pids && \ - chmod 777 /data/tmp public/storage/tmp tmp/pids +RUN mkdir -p /data/tmp public/storage/tmp tmp/pids EXPOSE 3000 RUN chmod +x ./entrypoint.sh +# Run as a non-root user rather than the default root. Nothing entrypoint.sh +# does (migrations, sidekiq, puma on the unprivileged port 3000) needs root +# at runtime. UID/GID are fixed rather than auto-assigned so ownership is +# consistent across rebuilds. +RUN groupadd --gid 1000 rails && \ + useradd --uid 1000 --gid rails --create-home --shell /bin/bash rails && \ + chown -R rails:rails /rails /data/tmp + +USER rails + ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/Gemfile b/Gemfile index d948983e..59936d40 100644 --- a/Gemfile +++ b/Gemfile @@ -3,20 +3,21 @@ source 'https://rubygems.org' git_source(:github) do |repo_name| - repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') + repo_name = "#{repo_name}/#{repo_name}" if repo_name.exclude?('/') "https://github.com/#{repo_name}.git" end - # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # gem 'rails', '~> 7.0.4' -gem 'rails', '~> 6.1.0' +gem 'rails', '~> 8.0.0' -gem 'rack', '>= 2.0.6' gem 'pg' -gem 'mysql2' + +# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] +gem 'rubocop-rails-omakase', require: false + # Multitenancy for Rails and ActiveRecord -gem 'ros-apartment', require: 'apartment' +gem 'ros-apartment', '~> 3.4.0', require: 'apartment' # For JSONAPI responses gem 'active_model_serializers', '~> 0.10.12' # For pagination @@ -25,71 +26,72 @@ gem 'kaminari' # Use Puma as the app server gem 'puma', '~> 4.3.0' # Use Redis adapter to run Action Cable in production -gem 'redis', '~> 3.0' gem 'actionview', '>= 5.2.2.1' +gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' +gem 'http' # gem 'ecds_rails_auth_engine', path: '../ecds_auth_engine' gem 'ecds_rails_auth_engine', git: 'https://github.com/ecds/ecds_rails_auth_engine.git', branch: 'feature/fauxoauth' # Active Storage will land in 5.2 +gem 'active_storage_validations', '~> 3.0.3' +gem 'aws-actionmailer-ses', '~> 1' +gem 'aws-sdk-rails', '~> 5' +gem 'aws-sdk-s3', '~> 1' gem 'carrierwave', '~> 1.0' -gem 'mini_magick' -gem 'image_processing', '~> 1.2' gem 'ferrum' -gem 'aws-sdk-s3', '~> 1' +gem 'image_processing', '~> 1.2' +gem 'mini_magick' # RGeo is a geospatial data library for Ruby. # https://github.com/rgeo/rgeo -gem 'rgeo' +gem 'geocoder', '~> 1.8.6' gem 'ipinfo-rails' +gem 'rgeo' +# Elasticsearch +gem 'elasticsearch', '~> 8' +gem 'faraday-httpclient', '~> 2.0' +gem 'searchkick' +gem 'sidekiq', '>=7.2.2', '<8' # Vidoe provider APIs gem 'vimeo' -gem 'yt' gem 'youtube_rails' +gem 'yt' # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible gem 'rack-cors' # TODO: should probably only require this for :test -gem 'faker' - -group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console - # gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] - # gem "test-prof" -end group :development do gem 'listen' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'rspec-rails', '~> 6.1.0' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' - gem 'rspec-rails', '~> 5.1.2' - # Use Capistrano for deployment - gem 'capistrano-rails' - gem 'capistrano-rbenv', '~> 2.0' - gem 'capistrano-passenger' end - group :test do + gem 'database_cleaner' gem 'factory_bot' gem 'factory_bot_rails' - gem 'shoulda-matchers', '~> 4.5.1' #git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5' - gem 'database_cleaner' - gem 'webmock' - gem 'coveralls', require: false - gem 'simplecov', require: false - gem 'simplecov-lcov', require: false + gem 'shoulda-matchers', '~> 4.5.1' # git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5' gem 'term-ansicolor' + gem 'webmock' + gem 'rubocop-factory_bot' # (https://rubygems.org/gems/rubocop-factory_bot) + gem 'rubocop-rspec' # (https://rubygems.org/gems/rubocop-rspec) + gem 'rubocop-rspec_rails' # (https://rubygems.org/gems/rubocop-rspec_rails) + gem 'faker' + gem 'ruby-lsp' + gem 'rubocop-shopify' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false +gem 'net-smtp', require: false +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock index 48a30662..3c8c8bbf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,209 +1,238 @@ GIT remote: https://github.com/ecds/ecds_rails_auth_engine.git - revision: fe56d7d214403689a0ab0ab79cbc9d343fdc63f6 + revision: cb0228ce2293e02519217769ee02d9cff323f3f7 branch: feature/fauxoauth specs: ecds_rails_auth_engine (0.2.0) cancancan httparty - jwt + jwt (<= 2.2.2) rails GEM remote: https://rubygems.org/ specs: - IPinfo (1.0.1) - faraday (~> 1.0) + IPinfo (2.4.0) + faraday (~> 2.0) json (~> 2.1) lru_redux (~> 1.1) - actioncable (6.1.6.1) - actionpack (= 6.1.6.1) - activesupport (= 6.1.6.1) + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.6.1) - actionpack (= 6.1.6.1) - activejob (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) - mail (>= 2.7.1) - actionmailer (6.1.6.1) - actionpack (= 6.1.6.1) - actionview (= 6.1.6.1) - activejob (= 6.1.6.1) - activesupport (= 6.1.6.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.6.1) - actionview (= 6.1.6.1) - activesupport (= 6.1.6.1) - rack (~> 2.0, >= 2.0.9) + zeitwerk (~> 2.6) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.6.1) - actionpack (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.6.1) - activesupport (= 6.1.6.1) + actionview (8.0.4) + activesupport (= 8.0.4) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - active_model_serializers (0.10.13) - actionpack (>= 4.1, < 7.1) - activemodel (>= 4.1, < 7.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + active_model_serializers (0.10.16) + actionpack (>= 4.1) + activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.6.1) - activesupport (= 6.1.6.1) + active_storage_validations (3.0.3) + activejob (>= 6.1.4) + activemodel (>= 6.1.4) + activestorage (>= 6.1.4) + activesupport (>= 6.1.4) + marcel (>= 1.0.3) + activejob (8.0.4) + activesupport (= 8.0.4) globalid (>= 0.3.6) - activemodel (6.1.6.1) - activesupport (= 6.1.6.1) - activerecord (6.1.6.1) - activemodel (= 6.1.6.1) - activesupport (= 6.1.6.1) - activestorage (6.1.6.1) - actionpack (= 6.1.6.1) - activejob (= 6.1.6.1) - activerecord (= 6.1.6.1) - activesupport (= 6.1.6.1) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) + timeout (>= 0.4.0) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (6.1.6.1) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.0.4) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - airbrussh (1.4.1) - sshkit (>= 1.6.1, != 1.7.0) - aws-eventstream (1.2.0) - aws-partitions (1.613.0) - aws-sdk-core (3.131.5) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + aws-actionmailer-ses (1.0.0) + actionmailer (>= 7.1.0) + aws-sdk-ses (~> 1, >= 1.50.0) + aws-sdk-sesv2 (~> 1, >= 1.34.0) + aws-eventstream (1.4.0) + aws-partitions (1.1204.0) + aws-sdk-core (3.241.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.58.0) - aws-sdk-core (~> 3, >= 3.127.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.114.0) - aws-sdk-core (~> 3, >= 3.127.0) + logger + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) + aws-sigv4 (~> 1.5) + aws-sdk-rails (5.1.0) + aws-sdk-core (~> 3) + railties (>= 7.1.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.1) + aws-sigv4 (~> 1.5) + aws-sdk-ses (1.95.0) + aws-sdk-core (~> 3, >= 3.241.3) + aws-sigv4 (~> 1.5) + aws-sdk-sesv2 (1.94.0) + aws-sdk-core (~> 3, >= 3.241.3) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - builder (3.2.4) - cancancan (3.4.0) - capistrano (3.17.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) - capistrano (~> 3.1) - capistrano-passenger (0.2.1) - capistrano (~> 3.0) - capistrano-rails (1.6.2) - capistrano (~> 3.1) - capistrano-bundler (>= 1.1, < 3) - capistrano-rbenv (2.2.0) - capistrano (~> 3.1) - sshkit (~> 1.3) - carrierwave (1.3.2) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + builder (3.3.0) + cancancan (3.6.1) + carrierwave (1.3.4) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) - ssrf_filter (~> 1.0) + ssrf_filter (~> 1.0, < 1.1.0) case_transform (0.2) activesupport - cliver (0.3.2) - concurrent-ruby (1.1.10) - coveralls (0.7.1) - multi_json (~> 1.3) - rest-client - simplecov (>= 0.7) - term-ansicolor - thor - crack (0.4.5) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crack (1.0.1) + bigdecimal rexml crass (1.0.6) - database_cleaner (2.0.1) - database_cleaner-active_record (~> 2.0.0) - database_cleaner-active_record (2.0.1) + csv (3.3.5) + database_cleaner (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) activerecord (>= 5.a) - database_cleaner-core (~> 2.0.0) + database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) - diff-lcs (1.5.0) - docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - erubi (1.10.0) - factory_bot (6.2.1) - activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) - railties (>= 5.0.0) - faker (3.5.2) + date (3.5.1) + diff-lcs (1.6.2) + domain_name (0.6.20240107) + drb (2.2.3) + elastic-transport (8.5.2) + faraday (< 3) + multi_json + elasticsearch (8.19.3) + elastic-transport (~> 8.3) + elasticsearch-api (= 8.19.3) + ostruct + elasticsearch-api (8.19.3) + multi_json + erb (6.0.2) + erubi (1.13.1) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.5.3) i18n (>= 1.8.11, < 2) - faraday (1.10.0) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - ferrum (0.11) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-httpclient (2.0.2) + httpclient (>= 2.2) + faraday-net_http (3.4.2) + net-http (~> 0.5) + ferrum (0.17.1) addressable (~> 2.5) - cliver (~> 0.3) + base64 (~> 0.2) concurrent-ruby (~> 1.1) - websocket-driver (>= 0.6, < 0.8) - ffi (1.15.5) - globalid (1.0.0) - activesupport (>= 5.0) - hashdiff (1.0.1) - http-accept (1.7.0) - http-cookie (1.0.5) + webrick (~> 1.7) + websocket-driver (~> 0.7) + ffi (1.17.3-aarch64-linux-gnu) + ffi (1.17.3-arm64-darwin) + geocoder (1.8.6) + base64 (>= 0.1.0) + csv (>= 3.0.0) + globalid (1.3.0) + activesupport (>= 6.1) + hashdiff (1.2.1) + hashie (5.1.0) + logger + http (6.0.3) + http-cookie (~> 1.0) + llhttp (~> 0.6.1) + http-cookie (1.1.6) domain_name (~> 0.5) - httparty (0.20.0) - mime-types (~> 3.0) + httparty (0.24.2) + csv + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - httpclient (2.8.3) - i18n (1.12.0) + httpclient (2.9.0) + mutex_m + i18n (1.14.8) concurrent-ruby (~> 1.0) - image_processing (1.12.2) - mini_magick (>= 4.9.5, < 5) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - ipinfo-rails (1.0.1) - IPinfo (~> 1.0.1) + io-console (0.8.2) + ipinfo-rails (1.2.0) + IPinfo (~> 2.4) rack (~> 2.0) - jmespath (1.6.1) - json (2.6.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.18.0) jsonapi-renderer (0.2.2) - jwt (2.4.1) + jwt (2.2.2) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -216,220 +245,321 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - listen (3.7.1) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.18.0) + llhttp (0.6.1) + logger (1.7.0) + loofah (2.25.1) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) lru_redux (1.1.0) - mail (2.7.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) - marcel (1.0.2) - method_source (1.0.0) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) - mini_magick (4.11.0) - mini_mime (1.1.2) - minitest (5.16.2) - multi_json (1.15.0) - multi_xml (0.6.0) - multipart-post (2.2.3) - mysql2 (0.5.4) - net-imap (0.3.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2026.0113) + mini_magick (5.3.1) + logger + mini_mime (1.1.5) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + mize (0.6.1) + multi_json (1.21.1) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) + multipart-post (2.4.1) + mutex_m (0.3.0) + net-http (0.9.1) + uri (>= 0.11.1) + net-imap (0.6.3) + date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.1.3) + net-protocol (0.2.2) timeout - net-scp (1.2.1) - net-ssh (>= 2.6.5) - net-smtp (0.3.2) + net-smtp (0.5.1) net-protocol - net-ssh (7.0.1) - netrc (0.11.0) - nio4r (2.5.8) - nokogiri (1.13.8-aarch64-linux) - racc (~> 1.4) - nokogiri (1.13.8-arm64-darwin) - racc (~> 1.4) - nokogiri (1.13.8-x86_64-darwin) + nio4r (2.7.5) + nokogiri (1.19.1-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.13.8-x86_64-linux) + nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) - oauth (0.5.10) - parallel (1.22.1) - pg (1.4.2) - public_suffix (4.0.7) + oauth (1.1.3) + base64 (~> 0.1) + oauth-tty (~> 1.0, >= 1.0.6) + snaky_hash (~> 2.0) + version_gem (~> 1.1, >= 1.1.9) + oauth-tty (1.0.6) + version_gem (~> 1.1, >= 1.1.9) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.10.1) + ast (~> 2.4.1) + racc + pg (1.6.3-aarch64-linux) + pg (1.6.3-arm64-darwin) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + public_suffix (6.0.2) puma (4.3.12) nio4r (~> 2.0) - racc (1.6.0) - rack (2.2.4) - rack-cors (1.1.1) + racc (1.8.1) + rack (2.2.22) + rack-cors (2.0.2) rack (>= 2.0.0) - rack-test (2.0.2) + rack-session (1.0.2) + rack (< 3) + rack-test (2.2.0) rack (>= 1.3) - rails (6.1.6.1) - actioncable (= 6.1.6.1) - actionmailbox (= 6.1.6.1) - actionmailer (= 6.1.6.1) - actionpack (= 6.1.6.1) - actiontext (= 6.1.6.1) - actionview (= 6.1.6.1) - activejob (= 6.1.6.1) - activemodel (= 6.1.6.1) - activerecord (= 6.1.6.1) - activestorage (= 6.1.6.1) - activesupport (= 6.1.6.1) + rackup (1.0.1) + rack (< 3) + webrick + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) bundler (>= 1.15.0) - railties (= 6.1.6.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 8.0.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) - railties (6.1.6.1) - actionpack (= 6.1.6.1) - activesupport (= 6.1.6.1) - method_source + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - rake (13.0.6) - rb-fsevent (0.11.1) - rb-inotify (0.10.1) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) + rbs (4.0.2) + logger + prism (>= 1.6.0) + tsort + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + readline (0.0.4) + reline redis (3.3.5) - rest-client (2.1.0) - http-accept (>= 1.7.0, < 2.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - rexml (3.2.5) - rgeo (2.4.0) - ros-apartment (2.11.0) - activerecord (>= 5.0.0, < 7.1) + redis-client (0.26.3) + connection_pool + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rgeo (3.0.1) + ros-apartment (3.4.1) + activerecord (>= 7.0.0, < 8.2) + activesupport (>= 7.0.0, < 8.2) parallel (< 2.0) - public_suffix (>= 2.0.5, < 5.0) - rack (>= 1.3.6, < 3.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + public_suffix (>= 2.0.5, < 7) + rack (>= 1.3.6, < 4.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.11.0) - ruby-vips (2.1.4) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + rubocop (1.82.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.48.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-factory_bot (2.28.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rails-omakase (1.1.0) + rubocop (>= 1.72) + rubocop-performance (>= 1.24) + rubocop-rails (>= 2.30) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + rubocop-rspec_rails (2.32.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) + rubocop-shopify (2.18.0) + rubocop (~> 1.62) + ruby-lsp (0.26.9) + language_server-protocol (~> 3.17.0) + prism (>= 1.2, < 2.0) + rbs (>= 3, < 5) + ruby-progressbar (1.13.0) + ruby-vips (2.3.0) ffi (~> 1.12) - ruby2_keywords (0.0.5) + logger + searchkick (6.0.3) + activemodel (>= 7.2) + securerandom (0.4.1) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) - simplecov (0.21.2) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov-lcov (0.8.0) - simplecov_json_formatter (0.1.4) + sidekiq (7.3.9) + base64 + connection_pool (>= 2.3.0) + logger + rack (>= 2.2.4) + redis-client (>= 0.22.2) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.1.1) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sshkit (1.21.2) - net-scp (>= 1.1.2) - net-ssh (>= 2.8.0) - ssrf_filter (1.0.7) + ssrf_filter (1.0.8) + stringio (3.2.0) sync (0.5.0) - term-ansicolor (1.7.1) - tins (~> 1.0) - thor (1.2.1) - timeout (0.3.0) - tins (1.31.1) + term-ansicolor (1.11.3) + tins (~> 1) + thor (1.5.0) + timeout (0.6.1) + tins (1.51.1) + bigdecimal + mize (~> 0.6) + readline sync - tzinfo (2.0.5) + tsort (0.2.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + version_gem (1.1.9) vimeo (1.5.4) httparty (>= 0.4.5) httpclient (>= 2.1.5.2) json (>= 1.1.9) multipart-post (>= 1.0.1) oauth (>= 0.4.3) - webmock (3.14.0) + webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.5) + webrick (1.9.2) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - youtube_rails (1.2.2) - yt (0.33.4) + youtube_rails (1.3.0) + yt (0.34.1) activesupport - zeitwerk (2.6.0) + zeitwerk (2.7.5) PLATFORMS aarch64-linux arm64-darwin-24 - x86_64-darwin-20 - x86_64-darwin-21 - x86_64-linux DEPENDENCIES actionview (>= 5.2.2.1) active_model_serializers (~> 0.10.12) + active_storage_validations (~> 3.0.3) + aws-actionmailer-ses (~> 1) + aws-sdk-rails (~> 5) aws-sdk-s3 (~> 1) - capistrano-passenger - capistrano-rails - capistrano-rbenv (~> 2.0) carrierwave (~> 1.0) - coveralls database_cleaner ecds_rails_auth_engine! + elasticsearch (~> 8) factory_bot factory_bot_rails faker + faraday-httpclient (~> 2.0) ferrum + geocoder (~> 1.8.6) + http image_processing (~> 1.2) ipinfo-rails kaminari listen mini_magick - mysql2 net-imap net-pop net-smtp pg puma (~> 4.3.0) - rack (>= 2.0.6) rack-cors - rails (~> 6.1.0) + rails (~> 8.0.0) redis (~> 3.0) rgeo - ros-apartment - rspec-rails (~> 5.1.2) + ros-apartment (~> 3.4.0) + rspec-rails (~> 6.1.0) + rubocop-factory_bot + rubocop-rails-omakase + rubocop-rspec + rubocop-rspec_rails + rubocop-shopify + ruby-lsp + searchkick shoulda-matchers (~> 4.5.1) - simplecov - simplecov-lcov + sidekiq (>= 7.2.2, < 8) spring spring-watcher-listen (~> 2.0.0) term-ansicolor @@ -440,4 +570,4 @@ DEPENDENCIES yt BUNDLED WITH - 2.2.22 + 2.3.7 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9a16d01c..6235aad9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,9 +5,7 @@ class ApplicationController < ActionController::API include Response include ExceptionHandler include EcdsRailsAuthEngine::CurrentUser - if Rails.env == 'test' - include ActiveStorage::SetCurrent - end + include ActiveStorage::SetCurrent if Rails.env.test? before_action :set_no_cache_control, only: [:index, :show] @@ -25,7 +23,7 @@ def set_no_cache_control response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '-1' - expires_now() + expires_now stale?(SecureRandom.hex(10)) end end diff --git a/app/controllers/concerns/response.rb b/app/controllers/concerns/response.rb index 29e6534f..0cd37d98 100644 --- a/app/controllers/concerns/response.rb +++ b/app/controllers/concerns/response.rb @@ -3,6 +3,6 @@ # app/controllers/concerns/response.rb module Response def json_response(object, status = :ok) - render json: object, status: status + render(json: object, status: status) end end diff --git a/app/controllers/v3/flat_pages_controller.rb b/app/controllers/v3/flat_pages_controller.rb index f1f736bc..72f06189 100644 --- a/app/controllers/v3/flat_pages_controller.rb +++ b/app/controllers/v3/flat_pages_controller.rb @@ -3,18 +3,18 @@ module V3 class FlatPagesController < V3Controller before_action :set_record, only: [:show, :update, :destroy] - #authorize_resource + # authorize_resource # GET /v3/records def index @records = if current_user.current_tenant_admin? FlatPage.all elsif current_user.tours.present? - current_user.tours.map { |tour| tour.flat_pages }.flatten.uniq + current_user.tours.map(&:flat_pages).flatten.uniq else - Tour.published.map { |tour| tour.flat_pages }.flatten.uniq + Tour.published.map(&:flat_pages).flatten.uniq end - render json: @records + render(json: @records) end # POST /v3/records @@ -23,12 +23,12 @@ def create @record = FlatPage.new(record_params) if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/flat-pages/#{@record.id}" + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/flat-pages/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end @@ -36,37 +36,37 @@ def create def update if @allowed if @record.update(record_params) - render json: @record + render(json: @record) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end # DELETE /v3/records/1 - private - # Use callbacks to share common setup or constraints between actions. - def set_record - _record = FlatPage.find(params[:id]) - @record = _record.published || @allowed ? _record : FlatPage.new(id: params[:id]) - end - # Only allow a trusted parameter "white list" through. - def record_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :title, :body, :tours - ] - ) - end + # Use callbacks to share common setup or constraints between actions. + def set_record + _record = FlatPage.find(params[:id]) + @record = _record.published || @allowed ? _record : FlatPage.new(id: params[:id]) + end - def allowed? - @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } - end + # Only allow a trusted parameter "white list" through. + def record_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :title, :body, :tours, + ] + ) + end + + def allowed? + @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } + end end end diff --git a/app/controllers/v3/geojson_tours_controller.rb b/app/controllers/v3/geojson_tours_controller.rb index 3a00ff4e..8a1c5866 100644 --- a/app/controllers/v3/geojson_tours_controller.rb +++ b/app/controllers/v3/geojson_tours_controller.rb @@ -11,17 +11,17 @@ def show @tour = Tour.find(params[:id]) # if @tour.published geojosn = { - type: 'FeatureCollection', - crs: { - type: 'name', - properties: { - name: 'urn:ogc:def:crs:EPSG::4326' - } - }, - meta: meta_content, - features: @tour.tour_stops.map { |tour_stop| feature(tour_stop.position, tour_stop.stop) } - } - render json: geojosn.to_json + type: 'FeatureCollection', + crs: { + type: 'name', + properties: { + name: 'urn:ogc:def:crs:EPSG::4326', + }, + }, + meta: meta_content, + features: @tour.tour_stops.map { |tour_stop| feature(tour_stop.position, tour_stop.stop) }, + } + render(json: geojosn.to_json) # render json: { type: 'FeatureCollection', meta: meta_content, features: @tour.tour_stops.map { |tour_stop| feature(tour_stop.position, tour_stop.stop) } }.to_json # else # head 401 @@ -30,41 +30,41 @@ def show private - def meta_content - { - title: @tour.title, - description: sanitize(@tour.description), - images: @tour.media.map do |m| + def meta_content + { + title: @tour.title, + description: sanitize(@tour.description), + images: @tour.media.map do |m| + { + caption: m.caption, + full: m.files[:desktop], + thumb: m.files[:mobile], + } + end, + } + end + + def feature(position, stop) + stop.media.map { |m| m.caption = nil if m.caption.blank? } + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [stop.lng.to_f, stop.lat.to_f], + }, + properties: { + title: stop.title, + description: sanitize(stop.description), + position: position, + images: stop.media.map do |m| { caption: m.caption, full: m.files[:desktop], - thumb: m.files[:mobile] + thumb: m.files[:mobile], } - end - } - end - - def feature(position, stop) - stop.media.map { |m| m.caption = nil if m.caption.blank? } - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [stop.lng.to_f, stop.lat.to_f] - }, - properties: { - title: stop.title, - description: sanitize(stop.description), - position: position, - images: stop.media.map do |m| - { - caption: m.caption, - full: m.files[:desktop], - thumb: m.files[:mobile] - } - end - } - } - end + end, + }, + } + end end end diff --git a/app/controllers/v3/map_icons_controller.rb b/app/controllers/v3/map_icons_controller.rb index e1f1e72f..c143012c 100644 --- a/app/controllers/v3/map_icons_controller.rb +++ b/app/controllers/v3/map_icons_controller.rb @@ -1,38 +1,39 @@ +# frozen_string_literal: true + module V3 class MapIconsController < V3Controller - def index - render json: MapIcon.all + render(json: MapIcon.all) end def create if crud_allowed? @record = MapIcon.new(record_params) if @record.save - render json: @record, status: :created + render(json: @record, status: :created) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end private - # Only allow a trusted parameter "white list" through. - def record_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :base_sixty_four, :filename, :stop - ] - ) - end + # Only allow a trusted parameter "white list" through. + def record_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :base_sixty_four, :filename, :stop, + ] + ) + end - def set_record - _record = MapIcon.find(params[:id]) - @record = _record&.published || @allowed ? _record : MapIcon.new(id: params[:id]) - end + def set_record + _record = MapIcon.find(params[:id]) + @record = _record&.published || @allowed ? _record : MapIcon.new(id: params[:id]) + end end end diff --git a/app/controllers/v3/map_overlays_controller.rb b/app/controllers/v3/map_overlays_controller.rb index 0f71ed97..b59374fa 100644 --- a/app/controllers/v3/map_overlays_controller.rb +++ b/app/controllers/v3/map_overlays_controller.rb @@ -1,33 +1,35 @@ +# frozen_string_literal: true + module V3 class MapOverlaysController < V3Controller def create if crud_allowed? @record = MapOverlay.new(record_params) if @record.save - render json: @record, status: :created + render(json: @record, status: :created) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end private - # Only allow a trusted parameter "white list" through. - def record_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :south, :east, :north, :west, :base_sixty_four, :filename, :tour, :stop - ] - ) - end + # Only allow a trusted parameter "white list" through. + def record_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :south, :east, :north, :west, :base_sixty_four, :filename, :tour, :stop, + ] + ) + end - def set_record - _record = MapOverlay.find(params[:id]) - @record = _record&.published || @allowed ? _record : MapOverlay.new(id: params[:id]) - end + def set_record + _record = MapOverlay.find(params[:id]) + @record = _record&.published || @allowed ? _record : MapOverlay.new(id: params[:id]) + end end end diff --git a/app/controllers/v3/media_controller.rb b/app/controllers/v3/media_controller.rb index a6b09925..297ccc6d 100644 --- a/app/controllers/v3/media_controller.rb +++ b/app/controllers/v3/media_controller.rb @@ -3,33 +3,31 @@ # app/controllers/v3/media_controller.rb module V3 class MediaController < V3Controller - # include Pagy::Backend before_action :set_record, only: [:show, :update, :destroy, :file] # GET /media def index - # TODO: This ins not ideal, we use these `not_in_*` scopes to make the list of media avaliable to add - # to a stop or tour. But the paramerter does not make sense when just looking at it. Needs clearer language. - @media = if (current_user && current_user.current_tenant_admin?) + # TODO: This ins not ideal, we use these `not_in_*` scopes to make the list of media available to add + # to a stop or tour. But the parameter does not make sense when just looking at it. Needs clearer language. + @media = if current_user&.current_tenant_admin? Medium.all else Medium.all.map { |medium| medium if medium.published }.compact end - # pagy, @media = pagy(@media) - # pagy_headers_merge(pagy) + if params[:page].present? - @media = @media.page params[:page] - self.set_pagination_header + @media = @media.page(params[:page]) + set_pagination_header end - render json: @media + render(json: @media) end # GET /media/1 def show if @record.published || current_user.id.present? - render json: @record + render(json: @record) else - render json: { data: { id: 0, type: 'media', attributes: { title: '....' } } } + render(json: { data: { id: 0, type: 'media', attributes: { title: '....' } } }) end end @@ -39,12 +37,12 @@ def create @record = Medium.new(record_params) if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/media/#{@record.id}" + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/media/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - render json: {}, status: :unauthorized + render(json: {}, status: :unauthorized) end end @@ -56,19 +54,21 @@ def set_record # Only allow a trusted parameter "white list" through. def record_params ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :title, :caption, :original_image, :stops, :tours, :video, :stop_id, :tour_id, :base_sixty_four, :video_provider, :embed, :filename + .jsonapi_parse( + params, only: [ + :title, :caption, :original_image, :stops, :tours, :video, :stop_id, :tour_id, :base_sixty_four, :video_provider, :embed, :filename, ] - ) + ) end private - def set_pagination_header(name=:media, options = {}) + def set_pagination_header(name = :media, _options = {}) scope = instance_variable_get("@#{name}") request_params = request.query_parameters - url_without_params = request.original_url.slice(0..(request.original_url.index("?")-1)) unless request_params.empty? + unless request_params.empty? + url_without_params = request.original_url.slice(0..(request.original_url.index('?') - 1)) + end url_without_params ||= request.original_url page = {} @@ -79,10 +79,10 @@ def set_pagination_header(name=:media, options = {}) pagination_links = [] page.each do |k, v| - new_request_hash= request_params.merge({:page => v}) + new_request_hash = request_params.merge({ page: v }) pagination_links << "<#{url_without_params}?#{new_request_hash.to_param}>; rel=\"#{k}\"" end - headers["Link"] = pagination_links.join(", ") + headers['Link'] = pagination_links.join(', ') end end end diff --git a/app/controllers/v3/modes_controller.rb b/app/controllers/v3/modes_controller.rb index 678e29de..bc67614f 100644 --- a/app/controllers/v3/modes_controller.rb +++ b/app/controllers/v3/modes_controller.rb @@ -5,11 +5,11 @@ module V3 class ModesController < ApplicationController # GET /modes def index - json_response Mode.all + json_response(Mode.all) end def show - json_response Mode.find(params[:id]) + json_response(Mode.find(params[:id])) end end end diff --git a/app/controllers/v3/stop_media_controller.rb b/app/controllers/v3/stop_media_controller.rb index 3f42d292..f19dcf56 100644 --- a/app/controllers/v3/stop_media_controller.rb +++ b/app/controllers/v3/stop_media_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class StopMediaController < V3::TourRelationsController # GET /v3/stop_media @@ -14,23 +16,24 @@ def index @stop_media = @stop_media.reject { |stop_medium| !stop_medium.stop.published } end - render json: @stop_media + render(json: @stop_media) end private - # Only allow a trusted parameter "white list" through. - def record_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :medium, :stop, :position - ] - ) - end - def set_record - _record = StopMedium.find(params[:id]) - @record = _record&.published || @allowed ? _record : StopMedium.new(id: params[:id]) - end + # Only allow a trusted parameter "white list" through. + def record_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :medium, :stop, :position, + ] + ) + end + + def set_record + _record = StopMedium.find(params[:id]) + @record = _record&.published || @allowed ? _record : StopMedium.new(id: params[:id]) + end end end diff --git a/app/controllers/v3/stops_controller.rb b/app/controllers/v3/stops_controller.rb index 6d7b94f7..eb1f0fc3 100644 --- a/app/controllers/v3/stops_controller.rb +++ b/app/controllers/v3/stops_controller.rb @@ -3,27 +3,25 @@ # /app/controllers/v3/stops_controller.rb module V3 class StopsController < V3::TourRelationsController - # GET /stops + # GET /stops def index @records = if current_user.current_tenant_admin? Stop.all elsif current_user.tours.present? - current_user.tours.map { |tour| tour.stops }.flatten.uniq + current_user.tours.map(&:stops).flatten.uniq else - Tour.published.map { |tour| tour.stops }.flatten.uniq + Tour.published.map(&:stops).flatten.uniq end - render json: @records + render(json: @records) end # POST /stops def create if crud_allowed? @record = Stop.new(stop_params) - if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/#{@record.id}" - end + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/#{@record.id}") if @record.save else - head 401 + head(:unauthorized) end end @@ -31,43 +29,52 @@ def create def update if crud_allowed? if @record&.update(stop_params) - render json: @record, location: "/#{Apartment::Tenant.current}/stops/#{@record.id}" + render(json: @record, location: "/#{Apartment::Tenant.current}/stops/#{@record.id}") end else - head 401 + head(:unauthorized) end end def destroy if !crud_allowed? - head 401 + head(:unauthorized) elsif crud_allowed? && @record.orphaned @record.destroy elsif crud_allowed? && !@record.orphaned - head 405 + head(:method_not_allowed) end end - private + private - # Only allow a trusted parameter "white list" through. - def stop_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :title, :description, :lat, :lng, - :parking_lat, :parking_lng, :media, - :address, :tours, :direction_notes, - :meta_description, :parking_address, - :icon_color, :map_icon - ] - ) - end + # Only allow a trusted parameter "white list" through. + def stop_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :title, + :description, + :lat, + :lng, + :parking_lat, + :parking_lng, + :media, + :address, + :tours, + :direction_notes, + :meta_description, + :parking_address, + :icon_color, + :map_icon, + ] + ) + end - # Callbacks - def set_record - _record = Stop.find_by(id: params[:id]) - @record = _record&.published || @allowed ? _record : Stop.new(id: params[:id]) - end + # Callbacks + def set_record + _record = Stop.find_by(id: params[:id]) + @record = _record&.published || @allowed ? _record : Stop.new(id: params[:id]) + end end end diff --git a/app/controllers/v3/themes_controller.rb b/app/controllers/v3/themes_controller.rb index c2739267..4f3ebf20 100644 --- a/app/controllers/v3/themes_controller.rb +++ b/app/controllers/v3/themes_controller.rb @@ -5,30 +5,31 @@ module V3 class ThemesController < V3Controller # GET /themes def index - render json: Theme.all + render(json: Theme.all) end # GET /themes/1 def show - render json: @record + render(json: @record) end # POST /themes def create - head 405 + head(:method_not_allowed) end # PATCH/PUT /themes/1 def update - head 405 + head(:method_not_allowed) end # DELETE /themes/1 def destroy - head 405 + head(:method_not_allowed) end - private + private + # Only allow a trusted parameter "white list" through. def set_record @record = Theme.find(params[:id]) diff --git a/app/controllers/v3/tour_authors_controller.rb b/app/controllers/v3/tour_authors_controller.rb index bd8e4b73..ec23c229 100644 --- a/app/controllers/v3/tour_authors_controller.rb +++ b/app/controllers/v3/tour_authors_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class TourAuthorsController < ApplicationController before_action :set_tour_author, only: [:show] @@ -5,40 +7,41 @@ class TourAuthorsController < ApplicationController # GET /tour_authors def index if current_user&.current_tenant_admin? - render json: TourAuthor.all + render(json: TourAuthor.all) else - head 401 + head(:unauthorized) end end # GET /tour_authors/1 def show if current_user&.current_tenant_admin? - render json: @tour_author + render(json: @tour_author) else - head 401 + head(:unauthorized) end end # POST /tour_authors def create - head 405 + head(:method_not_allowed) end # PATCH/PUT /tour_set_admins/1 def update - head 405 + head(:method_not_allowed) end # DELETE /tour_set_admins/1 def destroy - head 405 + head(:method_not_allowed) end private - # Use callbacks to share common setup or constraints between actions. - def set_tour_author - @tour_author = TourAuthor.find(params[:id]) - end + + # Use callbacks to share common setup or constraints between actions. + def set_tour_author + @tour_author = TourAuthor.find(params[:id]) + end end end diff --git a/app/controllers/v3/tour_flat_pages_controller.rb b/app/controllers/v3/tour_flat_pages_controller.rb index 4126f7c9..f527eadf 100644 --- a/app/controllers/v3/tour_flat_pages_controller.rb +++ b/app/controllers/v3/tour_flat_pages_controller.rb @@ -11,15 +11,15 @@ def index @tour_flat_pages = @tour_flat_pages.reject { |tour_flat_page| !tour_flat_page.tour.published } end - render json: @tour_flat_pages + render(json: @tour_flat_pages) end # GET /stops/1 def show - if @record&.tour.published || allowed? - render json: @record + if @record&.tour&.published || allowed? + render(json: @record) else - render json: { data: {} } + render(json: { data: {} }) end # render json: { data: {} } if @record.nil? # render json: @record, include: ['stop'] @@ -28,47 +28,47 @@ def show # POST /stops def create # Not created via the API - head 405 + head(:method_not_allowed) end # PATCH/PUT /stops/1 def update if @allowed if @record.update(tour_stop_params) - render json: @record, location: "/#{Apartment::Tenant.current}/tour_stops/#{@record.id}" + render(json: @record, location: "/#{Apartment::Tenant.current}/tour_stops/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end # DELETE /stops/1 def destroy # Not deleted via the API - head 405 + head(:method_not_allowed) end - private + private - # Only allow a trusted parameter "white list" through. - def tour_stop_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :stop, :tour, :position - ] - ) - end + # Only allow a trusted parameter "white list" through. + def tour_stop_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :stop, :tour, :position, + ] + ) + end - def set_record - @record = TourFlatPage.find(params[:id]) - end + def set_record + @record = TourFlatPage.find(params[:id]) + end - def allowed? - @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } - return @allowed - end + def allowed? + @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } + @allowed + end end end diff --git a/app/controllers/v3/tour_media_controller.rb b/app/controllers/v3/tour_media_controller.rb index 730e12b9..f988c050 100644 --- a/app/controllers/v3/tour_media_controller.rb +++ b/app/controllers/v3/tour_media_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class TourMediaController < V3::TourRelationsController # GET /v3/tour_media @@ -14,23 +16,24 @@ def index @tour_media = @tour_media.reject { |tour_medium| !tour_medium.tour.published } end - render json: @tour_media + render(json: @tour_media) end private - # Only allow a trusted parameter "white list" through. - def record_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :medium, :tour, :position - ] - ) - end - def set_record - _record = TourMedium.find(params[:id]) - @record = _record&.published || @allowed ? _record : TourMedium.new(id: params[:id]) - end + # Only allow a trusted parameter "white list" through. + def record_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :medium, :tour, :position, + ] + ) + end + + def set_record + _record = TourMedium.find(params[:id]) + @record = _record&.published || @allowed ? _record : TourMedium.new(id: params[:id]) + end end end diff --git a/app/controllers/v3/tour_relations_controller.rb b/app/controllers/v3/tour_relations_controller.rb index a4a94b86..1aa3f89b 100644 --- a/app/controllers/v3/tour_relations_controller.rb +++ b/app/controllers/v3/tour_relations_controller.rb @@ -3,9 +3,8 @@ # /app/controllers/v3/tour_relations_controller.rb module V3 class TourRelationsController < V3Controller - def destroy - head 405 + head(:method_not_allowed) end def allowed? @@ -15,7 +14,7 @@ def allowed? def crud_allowed? current_user&.current_tenant_admin? || - current_user.tours&.any? { |tour| Tour.all.include?(tour) } + current_user.tours&.any? { |tour| Tour.all.include?(tour) } end end end diff --git a/app/controllers/v3/tour_set_admins_controller.rb b/app/controllers/v3/tour_set_admins_controller.rb index 33f9b2a1..c47497c4 100644 --- a/app/controllers/v3/tour_set_admins_controller.rb +++ b/app/controllers/v3/tour_set_admins_controller.rb @@ -5,15 +5,15 @@ class TourSetAdminsController < V3Controller # GET /tour_set_admins def index if current_user&.super || current_user&.current_tenant_admin? - render json: TourSetAdmin.all + render(json: TourSetAdmin.all) else - head 401 + head(:unauthorized) end end # GET /tour_set_admins/1 def show - head 405 + head(:method_not_allowed) # if current_user&.super || current_user&.current_tenant_admin? # render json: @record # else @@ -23,7 +23,7 @@ def show # POST /tour_set_admins def create - head 405 + head(:method_not_allowed) # @record = TourSetAdmin.new(tour_set_admin_params) # if @record.save @@ -35,22 +35,23 @@ def create # PATCH/PUT /tour_set_admins/1 def update - head 405 + head(:method_not_allowed) end # DELETE /tour_set_admins/1 def destroy - head 405 + head(:method_not_allowed) end private - # Only allow a trusted parameter "white list" through. - # def tour_set_admin_params - # params.fetch(:tour_set_admin, {}) - # end - def set_record - @record = TourSetAdmin.find(params[:id]) - end + # Only allow a trusted parameter "white list" through. + # def tour_set_admin_params + # params.fetch(:tour_set_admin, {}) + # end + + def set_record + @record = TourSetAdmin.find(params[:id]) + end end end diff --git a/app/controllers/v3/tour_sets_controller.rb b/app/controllers/v3/tour_sets_controller.rb index 600c7f65..ccecc770 100644 --- a/app/controllers/v3/tour_sets_controller.rb +++ b/app/controllers/v3/tour_sets_controller.rb @@ -9,10 +9,10 @@ def index @records = [] if params[:subdir] && params[:subdir] != 'public' @records = TourSet.where(subdir: params[:subdir]) - if !@records.first&.published_tours&.empty? || current_user&.tour_sets.include?(@records.first) || current_user&.super - render json: @records + if !@records.first&.published_tours&.empty? || current_user&.tour_sets&.include?(@records.first) || current_user&.super + render(json: @records) else - render json: TourSet.none + render(json: TourSet.none) end return elsif current_user&.tour_sets.present? && !current_user.super @@ -22,21 +22,19 @@ def index end if current_user.tour_sets.present? || current_user.super - render json: @records, include: [ 'admins' ] + render(json: @records, include: ['admins']) else - if current_user&.tour_sets.empty? - @records = published - end - render json: @records + @records = published if current_user&.tour_sets&.empty? + render(json: @records) end end # GET /tour_sets/1 def show if @allowed - render json: @record + render(json: @record) else - render json: { data: { id: 0, type: 'tour_sets', attributes: { name: '....' } } } + render(json: { data: { id: 0, type: 'tour_sets', attributes: { name: '....' } } }) end end @@ -46,12 +44,12 @@ def create @record = TourSet.new(record_params) if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/#{@record.id}" + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end @@ -59,16 +57,16 @@ def create def update if crud_allowed? if @record.update(record_params) - render json: @record + render(json: @record) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end - private + private # Use callbacks to share common setup or constraints between actions. def set_record @@ -95,11 +93,11 @@ def published # Only allow a trusted parameter "white list" through. def record_params ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :name, :tours, :admins, :base_sixty_four, :logo_title - ] - ) + .jsonapi_parse( + params, only: [ + :name, :tours, :admins, :base_sixty_four, :logo_title, :logo, + ] + ) end end end diff --git a/app/controllers/v3/tour_stops_controller.rb b/app/controllers/v3/tour_stops_controller.rb index ecad029f..2a85bf0d 100644 --- a/app/controllers/v3/tour_stops_controller.rb +++ b/app/controllers/v3/tour_stops_controller.rb @@ -18,21 +18,21 @@ def index elsif current_user.current_tenant_admin? TourStop.all else - Tour.published.map { |tour| tour.tour_stops }.flatten.uniq + Tour.published.map(&:tour_stops).flatten.uniq end if @records.nil? - render json: { data: { type: 'tour_stops', id: 0 } } + render(json: { data: { type: 'tour_stops', id: 0 } }) else - render json: @records, include: ['stop'] + render(json: @records, include: ['stop']) end end # GET /stops/1 def show - if @record&.tour.published || allowed? - render json: @record + if @record&.tour&.published || allowed? + render(json: @record) else - render json: { data: {} } + render(json: { data: {} }) end # render json: { data: {} } if @record.nil? # render json: @record, include: ['stop'] @@ -41,46 +41,46 @@ def show # POST /stops def create # Not created via the API - head 405 + head(:method_not_allowed) end # PATCH/PUT /stops/1 def update if @allowed if @record.update(tour_stop_params) - render json: @record, location: "/#{Apartment::Tenant.current}/tour_stops/#{@record.id}" + render(json: @record, location: "/#{Apartment::Tenant.current}/tour_stops/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end # DELETE /stops/1 def destroy - head 405 + head(:method_not_allowed) end - private + private - # Only allow a trusted parameter "white list" through. - def tour_stop_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :stop, :tour, :position - ] - ) - end + # Only allow a trusted parameter "white list" through. + def tour_stop_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :stop, :tour, :position, + ] + ) + end - def set_record - @record = TourStop.find(params[:id]) - end + def set_record + @record = TourStop.find(params[:id]) + end - def allowed? - @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } - return @allowed - end + def allowed? + @allowed = current_user&.current_tenant_admin? || current_user.tours&.any? { |tour| Tour.all.include?(tour) } + @allowed + end end end diff --git a/app/controllers/v3/tours_controller.rb b/app/controllers/v3/tours_controller.rb index 12abeec4..f808a42c 100644 --- a/app/controllers/v3/tours_controller.rb +++ b/app/controllers/v3/tours_controller.rb @@ -5,24 +5,20 @@ module V3 class ToursController < V3Controller # GET /tours def index - @records = if (params[:slug]) + @records = if params[:slug] @record = Slug.find_by(slug: params[:slug]).tour - if @record.published || crud_allowed? - @record - else - nil - end - elsif (current_user && current_user.current_tenant_admin?) + @record if @record.published || crud_allowed? + elsif current_user&.current_tenant_admin? Tour.all - elsif (current_user && current_user.id) + elsif current_user&.id (current_user.tours + Tour.published).uniq else Tour.published end if @records.nil? - render json: { data: { id: 0, type: 'tours', attributes: { title: '....' } } } + render(json: { data: { id: 0, type: 'tours', attributes: { title: '....' } } }) else - render json: @records, each_serializer: V3::TourBaseSerializer + render(json: @records, each_serializer: V3::TourBaseSerializer) end end @@ -35,9 +31,9 @@ def show end if @record&.published || crud_allowed? - render json: @record, loc: request_loc + render(json: @record, loc: request_loc) else - render json: { data: { id: 0, type: 'tours', attributes: { title: '....' } } } + render(json: { data: { id: 0, type: 'tours', attributes: { title: '....' } } }) end end @@ -46,12 +42,12 @@ def create if crud_allowed? @record = Tour.new(tour_params) if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/tours/#{@record.id}" + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/tours/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end @@ -59,44 +55,59 @@ def create def update if crud_allowed? if @record.update(tour_params) - render json: @record, location: "/#{Apartment::Tenant.current}/tours/#{@record.id}" + render(json: @record, location: "/#{Apartment::Tenant.current}/tours/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end - private - # Only allow a trusted parameter "white list" through. - def tour_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :title, :description, - :is_geo, :modes, :published, :theme_id, - :mode, :meta_description, :stops, - :media, :users, :flat_pages, :map_type, - :theme, :use_directions, :default_lng, - :link_address, :link_text, :restrict_bounds, - :restrict_bounds_to_overlay, :blank_map - ] - ) - end + private - def set_record - _record = Tour.find(params[:id]) - @record = _record&.published || @allowed ? _record : Tour.new(id: params[:id]) - end + # Only allow a trusted parameter "white list" through. + def tour_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :title, + :description, + :is_geo, + :modes, + :published, + :theme_id, + :mode, + :meta_description, + :stops, + :media, + :users, + :flat_pages, + :map_type, + :theme, + :use_directions, + :default_lng, + :link_address, + :link_text, + :restrict_bounds, + :restrict_bounds_to_overlay, + :blank_map, + ] + ) + end - # def allowed? - # @allowed = crud_allowed? || - # end + def set_record + _record = Tour.find(params[:id]) + @record = _record&.published || @allowed ? _record : Tour.new(id: params[:id]) + end - def crud_allowed? - set_record if @record.nil? && params[:id].present? - current_user&.current_tenant_admin? || current_user.tours.include?(@record) - end + # def allowed? + # @allowed = crud_allowed? || + # end + + def crud_allowed? + set_record if @record.nil? && params[:id].present? + current_user&.current_tenant_admin? || current_user.tours.include?(@record) + end end end diff --git a/app/controllers/v3/users_controller.rb b/app/controllers/v3/users_controller.rb index 1a7bbd3c..df4a8ffe 100644 --- a/app/controllers/v3/users_controller.rb +++ b/app/controllers/v3/users_controller.rb @@ -10,23 +10,23 @@ class UsersController < V3Controller # GET /users def index - if current_user.present? - if params['me'] - render json: current_user - elsif current_user.current_tenant_admin? - render json: User.all - else - render json: { data: [] } - end + return if current_user.blank? + + if params['me'] + render(json: current_user) + elsif current_user.current_tenant_admin? + render(json: User.all) + else + render(json: { data: [] }) end end # GET /users/1 def show if current_user == @record || current_user.super - render json: @record, include_tours: true + render(json: @record, include_tours: true) else - render json: { message: 'You are not autorized to to view this resource.' }.to_json, status: 401 + render(json: { message: 'You are not authorized to to view this resource.' }.to_json, status: :unauthorized) end end @@ -37,12 +37,12 @@ def create @record = User.new(user_params) if @record.save - render json: @record, status: :created, location: "/#{Apartment::Tenant.current}/users/#{@record.id}" + render(json: @record, status: :created, location: "/#{Apartment::Tenant.current}/users/#{@record.id}") else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end @@ -50,12 +50,12 @@ def create def update if current_user&.super || current_user == @record if @record.update(user_params) - render json: @record + render(json: @record) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - head 401 + head(:unauthorized) end end @@ -64,25 +64,33 @@ def destroy if current_user&.super @record.destroy else - head 401 + head(:unauthorized) end end - private - # Only allow a trusted parameter "white list" through. - def user_params - ActiveModelSerializers::Deserialization - .jsonapi_parse( - params, only: [ - :display_name, :identification, :password, - :password_confirmation, :uid, :tour_sets, - :tours, :super, :email, :terms_accepted - ] - ) - end + private - def set_record - @record = User.find(params[:id]) - end + # Only allow a trusted parameter "white list" through. + def user_params + ActiveModelSerializers::Deserialization + .jsonapi_parse( + params, only: [ + :display_name, + :identification, + :password, + :password_confirmation, + :uid, + :tour_sets, + :tours, + :super, + :email, + :terms_accepted, + ] + ) + end + + def set_record + @record = User.find(params[:id]) + end end end diff --git a/app/controllers/v3_controller.rb b/app/controllers/v3_controller.rb index d87c08df..83a1b9dd 100644 --- a/app/controllers/v3_controller.rb +++ b/app/controllers/v3_controller.rb @@ -7,24 +7,24 @@ class V3Controller < ApplicationController # GET //1 def show - render json: @record + render(json: @record) end # POST /v3/tour_media def create - render json: {}, status: :unauthorized + render(json: {}, status: :unauthorized) end # PATCH/PUT /media/1 def update if crud_allowed? if @record.update(record_params) - render json: @record + render(json: @record) else - render json: serialize_errors, status: :unprocessable_entity + render(json: serialize_errors, status: :unprocessable_entity) end else - render json: {}, status: :unauthorized + render(json: {}, status: :unauthorized) end end @@ -32,32 +32,30 @@ def destroy if crud_allowed? @record.destroy else - render json: {}, status: :unauthorized + render(json: {}, status: :unauthorized) end end def serialize_errors errors = [] - if @record&.errors - @record.errors.full_messages.each do |error| - errors.push({ - detail: error, - source: { - pointer: 'data/attributes' - } - }) - end + @record&.errors&.full_messages&.each do |error| + errors.push({ + detail: error, + source: { + pointer: 'data/attributes', + }, + }) end { errors: errors } end private - def allowed? - @allowed = @record&.published || crud_allowed? - end + def allowed? + @allowed = @record&.published || crud_allowed? + end - def crud_allowed? - current_user&.current_tenant_admin? || current_user.tours.present? - end + def crud_allowed? + current_user&.current_tenant_admin? || current_user.tours.present? + end end diff --git a/app/controllers/v4/admin/access_requests_controller.rb b/app/controllers/v4/admin/access_requests_controller.rb new file mode 100644 index 00000000..56d73036 --- /dev/null +++ b/app/controllers/v4/admin/access_requests_controller.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module V4 + module Admin + class AccessRequestsController < V4Controller + before_action :tenant_admin, except: [:create, :destroy] + + def index + render_json(AccessRequest.where(tour_set:).map(&:search_data).uniq.as_json, status: :ok) and return + end + + def update + if @record.update(update_params) + render(json: @record, status: :ok) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + def create + head(:unauthorized) and return unless current_user.id + + @record = AccessRequest.new(create_params) + if @record.save + mailer = AccessRequestMailer.with(access_request: @record) + mailer.access_request_email.deliver_later if params[:tour_ids].nil? + mailer.access_request_tour_email.deliver_later if params[:tour_ids].present? + render(json: @record, status: :created) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + def destroy + head(:unauthorized) and return unless requester? + + if @record.delete + head(:no_content) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + private + + def tenant_admin + Apartment::Tenant.switch!(params[:tenant]) + begin + head(:unauthorized) and return unless current_user&.super || current_user&.current_tenant_admin? + rescue NoMethodError + head(:unauthorized) and return + end + end + + def create_params + { + user: User.find(params[:user]), + tour_set:, + tour_ids: params[:tour_ids]&.map(&:to_i) || [], + } + end + + def update_params + params.require(:access_request).permit(:approved, :tour_ids, tour_ids: []) + end + + def set_record + @record = AccessRequest.find(params[:id]) + end + + def requester? + @record.user == current_user + end + + def tour_set + TourSet.find_by(subdir: params[:tenant]) + end + end + end +end diff --git a/app/controllers/v4/admin/crud_controller.rb b/app/controllers/v4/admin/crud_controller.rb new file mode 100644 index 00000000..e4c02b08 --- /dev/null +++ b/app/controllers/v4/admin/crud_controller.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module V4 + module Admin + class CrudController < V4Controller + after_action :update_index + + def create + head(:unauthorized) and return unless crud_allowed? + + render(json: { error: 'Cannot create tour using this endpoint.' }, status: :unauthorized) and return if params[:model] == 'tour' + + model = params[:model].camelize.constantize + @record = model.new(allowed_params) + + if @record.save + render(json: @record.search_data, status: :created) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + def update + head(:unauthorized) and return unless crud_allowed? + + set_record + if @record.update(update_params) + @record.reload + render(json: @record.search_data, status: :ok) + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + def destroy + head(:unauthorized) and return unless crud_allowed? + + if @record.destroy + head(:no_content) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + private + + def set_record + id = params[:id] + model = params[:model].camelize.constantize + @record = model.find(id) + set_related_record if params[:related_model] + end + + def set_related_record + related_model = params[:related_model].camelize.constantize + @related_record = related_model.find(params[:value].to_i) + end + + def update_index + ReindexService.call(@record) + end + + def update_params + if @related_record + { params[:attribute].to_sym => @related_record } + else + allowed_params + end + end + + def allowed_params + case params[:model] + when 'medium' + params.require(:medium).permit(:file, :filename, :embed, :video_provider, :video, :embed_id, :title) + when 'tour_medium' + params.require(:tour_medium).permit(:medium_id, :tour_id, :position) + when 'stop_medium' + params.require(:stop_medium).permit(:medium_id, :stop_id, :position) + when 'map_overlay' + params.require(:map_overlay).permit(:file, :filename, :south, :east, :north, :west) + when 'map_icon' + params.require(:map_icon).permit(:file, :filename) + when 'voice_over' + params.require(:voice_over).permit(:file, :language, :tour_id, :stop_id) + when 'flat_page' + params.require(:flat_page).permit(:title, :body) + when 'tour_flat_page' + params.require(:tour_flat_page).permit(:tour_id, :flat_page_id, :position) + when 'stop' + default_location + params.require(:stop).permit( + :title, + :lat, + :lng, + :address, + :article_link, + :description, + :direction_intro, + :direction_notes, + :icon_color, + :meta_description, + :parking_lat, + :parking_lng, + ) + when 'tour_stop' + params.require(:tour_stop).permit(:tour_id, :stop_id, :position) + when 'tour_mode' + params.require(:tour_mode).permit(:tour_id, :mode_id) + when 'tour' + params.require(:tour).permit( + :title, + :published, + :map_icon_id, + :default_lng, + :map_type, + :icon_color, + :description, + :meta_description, + :link_address, + :link_text, + :is_geo, + :restrict_bounds, + :restrict_bounds_to_overlay, + :use_directions, + :blank_map, + :mode_id, + ) + when 'tour_set' + params.require(:tour_set).permit(:name, :logo, :description) + when 'tour_set_admin' + params.require(:tour_set_admin).permit(:user_id, :tour_set_id) + end + end + + def default_location + return unless params[:stop][:lng].nil? || params[:stop][:lat].nil? + + location = Geocoder.search(request.remote_ip).first + params[:stop][:lat] = location.latitude.to_f + params[:stop][:lng] = location.longitude.to_f + end + end + end +end diff --git a/app/controllers/v4/admin/flat_pages_controller.rb b/app/controllers/v4/admin/flat_pages_controller.rb new file mode 100644 index 00000000..c2b1dfc9 --- /dev/null +++ b/app/controllers/v4/admin/flat_pages_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module V4 + module Admin + class FlatPagesController < V4Controller + def index + head(:unauthorized) and return unless crud_allowed? + + @records = FlatPage.page(params[:page] || 1).per(params[:per] || FlatPage.count) + @records = @records.where.not(id: params[:exclude].split(',').map(&:to_i)) if params[:exclude] + set_pagination_header + render(json: @records.map(&:search_data).sort_by { |flat_page| flat_page[:title] }) + end + end + end +end diff --git a/app/controllers/v4/admin/media_controller.rb b/app/controllers/v4/admin/media_controller.rb new file mode 100644 index 00000000..35ff736c --- /dev/null +++ b/app/controllers/v4/admin/media_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module V4 + module Admin + class MediaController < V4Controller + def index + head(:unauthorized) and return unless crud_allowed? + + @records = Medium.page(params[:page] || 1).per(params[:per] || Medium.count) + @records = @records.where.not(id: params[:exclude].split(',').map(&:to_i)) if params[:exclude] + set_pagination_header + render(json: @records.map(&:search_data)) + end + end + end +end diff --git a/app/controllers/v4/admin/services_controller.rb b/app/controllers/v4/admin/services_controller.rb new file mode 100644 index 00000000..a674d678 --- /dev/null +++ b/app/controllers/v4/admin/services_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module V4 + module Admin + class ServicesController < V4Controller + def url_resolver + render(json: { error: 'you must provide a URL' }, status: :unprocessable_entity) and return unless params[:url] + + resolved_url = UrlResolver.resolve(params[:url]) + render(json: { error: 'could not resolve url' }, status: :unprocessable_entity) and return unless resolved_url + + render(json: { resolved_url: }, status: :ok) + end + end + end +end diff --git a/app/controllers/v4/admin/stops_controller.rb b/app/controllers/v4/admin/stops_controller.rb new file mode 100644 index 00000000..3dc02f66 --- /dev/null +++ b/app/controllers/v4/admin/stops_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module V4 + module Admin + class StopsController < V4Controller + def index + head(:unauthorized) and return unless crud_allowed? + + @records = Stop.page(params[:page] || 1).per(params[:per] || Stop.count) + @records = @records.where.not(id: params[:exclude].split(',').map(&:to_i)) if params[:exclude] + set_pagination_header + render(json: @records.map(&:search_data).sort_by { |stop| stop[:title] }) + end + end + end +end diff --git a/app/controllers/v4/admin/tour_authors_controller.rb b/app/controllers/v4/admin/tour_authors_controller.rb new file mode 100644 index 00000000..947df257 --- /dev/null +++ b/app/controllers/v4/admin/tour_authors_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module V4 + module Admin + class TourAuthorsController < V4Controller + def index + head(:unauthorized) and return unless authorized? + + @records = TourAuthor.where(tour_id: params[:tour_id]) + + render(json: [].to_json, status: :ok) and return if @records.empty? + + render(json: @records.map(&:user), status: :ok) + end + + def create + head(:unauthorized) and return unless authorized? + + @record = if params[:username] + TourAuthor.new(user: User.find_by(display_name: params[:username]), tour_id: params[:tour_id]) + else + TourAuthor.new(user_id: params[:user_id], tour_id: params[:tour_id]) + end + + if @record.save + render(json: @record, status: :created) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + def destroy + head(:unauthorized) and return unless authorized? + head(:not_found) and return if @record.nil? + + if @record.delete + head(:no_content) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + private + + def set_record + @record = TourAuthor.find_by(tour_id: params[:tour_id], user_id: params[:user_id]) + end + + def authorized? + current_user&.super || current_user&.current_tenant_admin? + end + end + end +end diff --git a/app/controllers/v4/admin/tour_sets_controller.rb b/app/controllers/v4/admin/tour_sets_controller.rb new file mode 100644 index 00000000..89c93917 --- /dev/null +++ b/app/controllers/v4/admin/tour_sets_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module V4 + module Admin + class TourSetsController < V4Controller + def index + render(json: { error: 'unauthorized' }, status: :unauthorized) and return if current_user.id.nil? + if current_user.id + render( + json: TourSet.all.sort_by(&:name).map do |ts| + { name: ts.name, subdir: ts.subdir, id: ts.id } + end, + status: :ok, + ) and return + end + + render(json: current_user.tour_sets.sort_by(&:name), status: :ok) + end + + def show + render(json: { error: 'not found' }, status: :not_found) and return if @record.nil? + if current_user.super || current_user.tour_sets.include?(@record) + render( + json: { **@record.admin_data, tours: all_tours }, + status: :ok, + ) and return + end + head(:no_content) and return if current_user.all_tours.map { |t| t[:tenant] }.include?(@record.subdir) + + render(json: { error: 'unauthorized' }, status: :unauthorized) + end + + private + + def set_record + @record = TourSet.find_by(subdir: params[:slug]) + end + + def all_tours + Apartment::Tenant.switch!(@record.subdir) + Tour.order(:title).map { |tour| tour_summary(tour) } + end + + def tour_summary(tour) + { id: tour.id, title: tour.title, slug: tour.slug, published: tour.published, published_on: tour.published_on } + end + + def user_authorized?; end + end + end +end diff --git a/app/controllers/v4/admin/tours_controller.rb b/app/controllers/v4/admin/tours_controller.rb new file mode 100644 index 00000000..768644af --- /dev/null +++ b/app/controllers/v4/admin/tours_controller.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module V4 + module Admin + class ToursController < V4Controller + def index + if params[:all].present? + render( + json: Tour.all.map do |tour| + { title: tour.title, slug: tour.slug, id: tour.id } + end, + status: :ok, + ) and return + end + render(json: { error: 'unauthorized' }, status: :unauthorized) and return unless crud_allowed? + + @records = if current_user.current_tenant_admin? + Tour.all + else + current_user.tours + end + + render(json: [].to_json, status: :ok) and return if @records.empty? + + render(json: @records.sort_by(&:title), status: :ok) + end + + def show + render(json: { error: 'unauthorized' }, status: :unauthorized) and return unless crud_allowed? + + render(json: @record, status: :ok) and return unless @record.nil? + + head(:not_found) + end + + def create + set_tour_set + render(json: { error: 'unauthorized' }, status: :unauthorized) and return unless current_user&.current_tenant_tour_creator? + + @record = Tour.new(allowed_params) + + if @record.save + current_user.tours << @record unless current_user.super || current_user.current_tenant_admin? + render(json: @record.search_data, status: :created) and return + else + render(json: serialize_errors, status: :unprocessable_entity) + end + end + + private + + def set_record + @record = Tour.search('*', load: false, limit: 1).where(id: params[:id]).first + end + + def allowed_params + params.require(:tour).permit( + :title, + :published, + :map_icon_id, + :default_lng, + :map_type, + :description, + :meta_description, + :link_address, + :link_text, + :is_geo, + :restrict_bounds, + :restrict_bounds_to_overlay, + :use_directions, + :blank_map, + ) + end + end + end +end diff --git a/app/controllers/v4/admin/users_controller.rb b/app/controllers/v4/admin/users_controller.rb new file mode 100644 index 00000000..9ad79a06 --- /dev/null +++ b/app/controllers/v4/admin/users_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module V4 + module Admin + class UsersController < V4Controller + def index + head(:unauthorized) and return unless current_user&.super + + render( + json: User.all.map(&:preview_data).sort_by do |u| + u[:email] + end, + status: :ok, + ) and return + end + + def show + render(json: User.new.search_data, status: :unauthorized) and return if current_user.id.nil? + + render(json: @record, status: :ok) and return + end + + private + + def set_record + return unless current_user.id + + @record = current_user.search_data + end + end + end +end diff --git a/app/controllers/v4/public/media_controller.rb b/app/controllers/v4/public/media_controller.rb new file mode 100644 index 00000000..3dc1c69f --- /dev/null +++ b/app/controllers/v4/public/media_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module V4 + module Public + class MediaController < V4Controller + def show + return head(:not_found) if @record.nil? + + if params[:variant] + redirect_to(@record.files[params[:variant].to_sym], allow_other_host: true) + else + redirect_to(@record.file.blob.url, allow_other_host: true) + end + end + + private + + def set_record + blob = ActiveStorage::Blob.find_by(key: params[:key]) + @record = ActiveStorage::Attachment.find_by(blob:)&.record + end + end + end +end diff --git a/app/controllers/v4/public/modes_controller.rb b/app/controllers/v4/public/modes_controller.rb new file mode 100644 index 00000000..66d01b2a --- /dev/null +++ b/app/controllers/v4/public/modes_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module V4 + module Public + class ModesController < V4Controller + def index + render(json: Mode.all.map(&:search_data)) + end + end + end +end diff --git a/app/controllers/v4/public/stops_controller.rb b/app/controllers/v4/public/stops_controller.rb new file mode 100644 index 00000000..e718fb5f --- /dev/null +++ b/app/controllers/v4/public/stops_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module V4 + module Public + class StopsController < V4Controller + @model_class = Stop + + def index + head(:unauthorized) and return unless crud_allowed? + + render(json: Stop.all.map(&:search_data)) + end + + def show + render(json: { errors: ['Not found'] }, status: :not_found) and return if @record.nil? + + return head(:not_found) unless @record.published + + render(json: @record.search_data) + end + + def set_record + @record = StopSlug.find_by(slug: params[:slug])&.stop + end + end + end +end diff --git a/app/controllers/v4/public/tour_sets_controller.rb b/app/controllers/v4/public/tour_sets_controller.rb new file mode 100644 index 00000000..681e3ee2 --- /dev/null +++ b/app/controllers/v4/public/tour_sets_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module V4 + module Public + class TourSetsController < V4Controller + def index + render(json: tour_sets.sort_by { |ts| ts[:name] }) + end + + private + + def tour_sets + if current_user.super + TourSet.all + elsif current_user.tour_sets.present? + [*published_tour_sets, *current_user.tour_sets].uniq + else + published_tour_sets + end.map do |ts| + { + name: ts.name, + subdir: ts.subdir, + mapable_tours: ts.mapable_tours, + published_tours: ts.published_tours.empty? ? [] : ts.published_tours.sort_by { |t| t[:title] }, + } + end + end + + def published_tour_sets + TourSet.all.select(&:should_index?) + end + end + end +end diff --git a/app/controllers/v4/public/tours_controller.rb b/app/controllers/v4/public/tours_controller.rb new file mode 100644 index 00000000..6fbd894c --- /dev/null +++ b/app/controllers/v4/public/tours_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module V4 + module Public + class ToursController < V4Controller + def index + @records = if current_user&.current_tenant_admin? + Array(Tour.search('*', load: false, limit: Tour.count)) if current_user&.current_tenant_admin? + elsif current_user + if current_user + user_tours = current_user.tours.map do |t| + Tour.search('*', load: false, limit: 1).where(id: t.id, published: false).first + end.flatten.compact + end + published_tours = Array(Tour.search( + '*', + load: false, + limit: Tour.published.count, + ).where(published: true)) + [*published_tours, *user_tours] + else + Array(Tour.search('*', load: false, limit: Tour.published.count).where(published: true)) + end.sort_by { |t| t[:title] } + + render(json: [], status: :not_found) and return if @records.empty? + + render(json: { tour_set: @tour_set, tours: @records }, status: :ok) + end + + def show + render(json: { errors: ['Not found'] }, status: :not_found) and return unless @record&.then do + @record.published || crud_allowed?(@record) + end + + render(json: TourPresenter.new(@record, @tour_set), status: :ok) + end + + def create + raise NotImplementedError, 'Create is not implemented in V4 Public' + end + + def update + raise NotImplementedError, 'Update is not implemented in V4 Public' + end + + def destroy + raise NotImplementedError, 'Destroy is not implemented in V4 Public' + end + + private + + def crud_allowed?(record = @record) + tour = Tour.find(Integer(record.id)) unless record.nil? + current_user&.current_tenant_admin? || current_user.tours.include?(tour) + end + + def set_record + @record = Tour.search('*', where: { slugs: params[:slug] }, limit: 1, load: false).first + end + end + end +end diff --git a/app/controllers/v4_controller.rb b/app/controllers/v4_controller.rb new file mode 100644 index 00000000..e0b5adb3 --- /dev/null +++ b/app/controllers/v4_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class V4Controller < V3Controller + before_action :set_tour_set, only: [:index, :show] + + def default_serializer_options + {} + end + + def render_json(data, status: :ok) + render(plain: data.to_json, content_type: 'application/json', status:) + end + + def index + raise NotImplementedError, 'Index is not implemented in V4' + end + + def show + raise NotImplementedError, 'Show is not implemented in V4' + end + + def create + raise NotImplementedError, 'Create is not implemented in V4' + end + + def update + raise NotImplementedError, 'Update is not implemented in V4' + end + + def destroy + raise NotImplementedError, 'Destroy is not implemented in V4' + end + + def set_pagination_header(name = :records, _options = {}) + scope = instance_variable_get("@#{name}") + base_url = request.base_url + request.path + + links = [] + links << build_link(base_url, 1, 'first') if scope.total_pages > 1 && !scope.first_page? + links << build_link(base_url, scope.current_page - 1, 'prev') unless scope.first_page? + links << build_link(base_url, scope.current_page + 1, 'next') unless scope.last_page? + links << build_link(base_url, scope.total_pages, 'last') if scope.total_pages > 1 && !scope.last_page? + + headers['Link'] = links.join(', ') if links.any? + end + + private + + def build_link(base_url, page_num, rel) + params = request.query_parameters.merge(page: page_num) + "<#{base_url}?#{params.to_query}>; rel=\"#{rel}\"" + end + + def set_tour_set + @tour_set = TourSet.find_by(subdir: params[:tenant])&.preview_data + Apartment::Tenant.switch!(params[:tenant]) + end +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb new file mode 100644 index 00000000..c0604f63 --- /dev/null +++ b/app/controllers/welcome_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class WelcomeController < ApplicationController + def index + render(json: {}) + end +end diff --git a/app/jobs/process_medium_variants_job.rb b/app/jobs/process_medium_variants_job.rb new file mode 100644 index 00000000..7af781ac --- /dev/null +++ b/app/jobs/process_medium_variants_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ProcessMediumVariantsJob < ApplicationJob + queue_as :default + + def perform(medium_id) + medium = Medium.find_by(id: medium_id) + return unless medium&.file&.attached? + return if medium.file.content_type.include?('gif') + + medium.file.variant(resize_to_limit: [5, 5]).processed + medium.file.variant(resize_to_limit: [300, 300]).processed + medium.file.variant(resize_to_limit: [400, 400]).processed + medium.file.variant(resize_to_limit: [750, 750]).processed + end +end diff --git a/app/jobs/reindex_job.rb b/app/jobs/reindex_job.rb new file mode 100644 index 00000000..00b3f70d --- /dev/null +++ b/app/jobs/reindex_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ReindexJob < ApplicationJob + queue_as :searchkick + + def perform(tenant:, class_name:, id:) + Apartment::Tenant.switch!(tenant) + model = class_name.constantize + ReindexService.call(model.find_by(id:)) + end +end diff --git a/app/lib/api_version.rb b/app/lib/api_version.rb index ac4be814..f35a7c31 100644 --- a/app/lib/api_version.rb +++ b/app/lib/api_version.rb @@ -14,11 +14,11 @@ def matches?(request) check_headers(request.headers) || default end - private + private - def check_headers(headers) - # check version from Accept headers; expect custom media type `tours` - accept = headers[:accept] - accept && accept.include?("application/vnd.tours.#{version}+json") - end + def check_headers(headers) + # check version from Accept headers; expect custom media type `tours` + accept = headers[:accept] + accept&.include?("application/vnd.tours.#{version}+json") + end end diff --git a/app/lib/rename_site.rb b/app/lib/rename_site.rb new file mode 100644 index 00000000..553e50c7 --- /dev/null +++ b/app/lib/rename_site.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class RenameSite + def rename_site(old_name, new_name) + Apartment::Tenant.switch!('public') + ActiveRecord::Base.transaction do + # 1. Rename the schema + ActiveRecord::Base.connection.execute( + "ALTER SCHEMA \"#{old_name}\" RENAME TO \"#{new_name}\"", + ) + + # 2. Update the tenant record + TourSite.find_by!(subdir: old_name).update!(subdomain: new_name) + end + end +end diff --git a/app/mailers/access_request_mailer.rb b/app/mailers/access_request_mailer.rb new file mode 100644 index 00000000..79b0e0d6 --- /dev/null +++ b/app/mailers/access_request_mailer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AccessRequestMailer < ApplicationMailer + def access_request_email + @access_request = params[:access_request] + @tour_set = @access_request.tour_set + Apartment::Tenant.switch!(@tour_set.subdir) + recipients = (@tour_set.admins.pluck(:email) + User.where(super: true).pluck(:email)).uniq + mail(to: recipients, subject: "OpenTour Access Request #{@tour_set.name}") + end + + def access_request_tour_email + @access_request = params[:access_request] + @tour_set = @access_request.tour_set + Apartment::Tenant.switch!(@tour_set.subdir) + recipients = (@tour_set.admins.pluck(:email) + User.where(super: true).pluck(:email)).uniq + mail(to: recipients, subject: "OpenTour Access Request for #{@tour_set.name}") + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d84cb6e7..ade7de96 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' + default from: 'noreply@opentour.site' layout 'mailer' end diff --git a/app/models/access_request.rb b/app/models/access_request.rb new file mode 100644 index 00000000..3d927209 --- /dev/null +++ b/app/models/access_request.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AccessRequest < ApplicationRecord + after_update :authorize + validates :tour_set, uniqueness: { scope: [:user] } + belongs_to :user + belongs_to :tour_set + + def search_data + { + id:, + user: user.display_name, + email: user.email, + site: tour_set.name, + tours: requested_tours.map(&:title) || nil, + tour_ids: requested_tours.map(&:id) || nil, + date: created_at.strftime('%B %d, %Y'), + } + end + + def requested_tours + return [] if tour_ids.blank? + + Apartment::Tenant.switch!(tour_set.subdir) + Tour.find(tour_ids) + end + + private + + def authorize + if approved + user.tours << requested_tours unless requested_tours.empty? + user.tour_sets << tour_set if requested_tours.empty? + end + + delete + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index ef998df7..0d2f1b82 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -3,4 +3,12 @@ # Base class for models. class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + + def mappings + {} + end + + def search_data + {} + end end diff --git a/app/models/concerns/embed_props.rb b/app/models/concerns/embed_props.rb new file mode 100644 index 00000000..da9f7810 --- /dev/null +++ b/app/models/concerns/embed_props.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module EmbedProps + extend ActiveSupport::Concern + + PROVIDERS = { + 'youtube' => VideoProviders::Youtube, + 'vimeo' => VideoProviders::Vimeo, + 'soundcloud' => VideoProviders::Soundcloud, + 'sketchfab' => VideoProviders::Sketchfab, + 'matterport' => VideoProviders::Matterport, + 'morphosource' => VideoProviders::Morphosource, + }.freeze + + class << self + def props(medium) + return if medium.embed_id.blank? || medium.video_provider == 'keiner' + + provider_class = PROVIDERS.fetch(medium.video_provider, VideoProviders::Base) + downloaded_image = provider_class.new(medium).call + return if downloaded_image.nil? + + medium.filename ||= "#{medium.embed_id}.jpg" + medium.base_sixty_four = Base64.encode64(downloaded_image) + medium.attach_file unless medium.file.attached? + # TODO: remove this on release of V4. + medium.video = medium.embed_id + end + end +end diff --git a/app/models/concerns/html_saintizer.rb b/app/models/concerns/html_saintizer.rb deleted file mode 100644 index d3916421..00000000 --- a/app/models/concerns/html_saintizer.rb +++ /dev/null @@ -1,15 +0,0 @@ - -# frozen_string_literal: true - -# app/models/concerns/html_saintizer.rb -module HtmlSaintizer - extend ActiveSupport::Concern - - def self.accessable(text) - Rails::Html::FullSanitizer.new.sanitize(text).to_s.gsub(/([A-z]\.)([A-z])/, '\1 \2') - end - - def self.accessable_truncated(text) - self.accessable(text).truncate(140) - end -end diff --git a/app/models/concerns/html_sanitizer.rb b/app/models/concerns/html_sanitizer.rb new file mode 100644 index 00000000..48466e5b --- /dev/null +++ b/app/models/concerns/html_sanitizer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# app/models/concerns/html_sanitizer.rb +module HtmlSanitizer + extend ActiveSupport::Concern + + def self.accessible(text) + Rails::Html::FullSanitizer.new.sanitize(text).to_s.gsub(/([A-Za-z]\.)([A-Za-z])/, '\1 \2') + end + + def self.accessible_truncated(text) + accessible(text).truncate(140) + end +end diff --git a/app/models/concerns/searchable.rb b/app/models/concerns/searchable.rb new file mode 100644 index 00000000..2d9af4ca --- /dev/null +++ b/app/models/concerns/searchable.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Common stuff for indexing +module Searchable + extend ActiveSupport::Concern + + included do + searchkick index_name: -> { "otb_#{Apartment::Tenant.current}_#{model_name.plural}_#{Rails.env}" }, + callbacks: false, + deep_paging: true, + merge_mappings: true, + mappings: { + properties: { + slugs: { type: 'keyword' }, + }, + }, + searchable: [:slugs], + word: false + + after_commit :reindex_record + end + + private + + def reindex_record + ReindexJob.perform_later(tenant: Apartment::Tenant.current, class_name: self.class.name, id:) + end +end diff --git a/app/models/concerns/video_props.rb b/app/models/concerns/video_props.rb index 619b72e6..42c88142 100644 --- a/app/models/concerns/video_props.rb +++ b/app/models/concerns/video_props.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true require 'open-uri' +require 'httparty' +require 'json' module VideoProps extend ActiveSupport::Concern def self.props(medium) return if medium.video.nil? + case medium.video_provider when 'keiner' nil @@ -17,9 +20,14 @@ def self.props(medium) medium.embed = "//player.vimeo.com/video/#{medium.video}" thumbnail_width = metadata['thumbnail_width'] thumbnail_height = metadata['thumbnail_height'] - scale_by = 1000 / thumbnail_width - thumbnail_url = "#{metadata['thumbnail_url'].split('_')[0]}_#{thumbnail_width * scale_by}x#{thumbnail_height * scale_by}" - downloaded_image = URI.open(thumbnail_url) + if thumbnail_height.present? + scale_by = 1000 / thumbnail_width + thumbnail_url = "#{metadata["thumbnail_url"].split("_")[0]}_#{thumbnail_width * scale_by}x#{thumbnail_height * scale_by}" + downloaded_image = URI.open(thumbnail_url) + else + medium.filename ||= 'otblogo.png' + downloaded_image = File.open(Rails.root.join('public/otblogo.png').to_s).read + end when 'youtube' begin metadata = Yt::Video.new(id: medium.video) @@ -36,25 +44,15 @@ def self.props(medium) if medium.video.include?('iframe') embed_code = Nokogiri::HTML(medium.video) medium.title = embed_code.xpath('//a').map { |a| a[:title] }.compact.join(': ') - medium.video = embed_code.xpath('//iframe', 'src').first['src'].split('&').first[/(.*tracks\/)(.*)/, 2] + medium.video = embed_code.xpath('//iframe', 'src').first['src'].split('&').first[%r{(.*tracks/)(.*)}, 2] medium.embed = "//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{medium.video}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false" - # When testing, the requests made via Ferrum do not go through webmock and CircleCI does not seem to - # let requests out to the world. - # We have to use Ferrum (headless Chrome) because the SoundCloud embeds are generated by JavaScript. - begin - browser = Ferrum::Browser.new() - browser.go_to("https:#{medium.embed}") - spans = browser.at_xpath('//span[contains(@class, "sc-artwork")]') until spans.present? - image = spans.attribute('style')[/(.*\()(.*)(\).*)/, 2] - rescue Ferrum::ProcessTimeoutError - html = Nokogiri::HTML(HTTParty.get("https:#{medium.embed}").body) - style = html.xpath('//span[contains(@class, "sc-artwork")]/@style').first - image = style.value[/\((.*?)\)/, 1] - end - if image.nil? - downloaded_image = File.open(File.join(Rails.root, 'public', 'soundcloud.jpg')).read + track_url = embed_code.xpath('//a').last[:href] + track_response = HTTParty.get("https://soundcloud.com/oembed?url=#{track_url}&format=json", format: 'plan') + track_data = JSON.parse(track_response.body).deep_symbolize_keys! + downloaded_image = if track_data[:thumbnail_url].nil? + File.open(Rails.root.join('public/soundcloud.jpg').to_s).read else - downloaded_image = URI.open("https:#{image}") + URI.open(track_data[:thumbnail_url]) end end @@ -69,7 +67,7 @@ def self.props(medium) def self.encode_image(downloaded_image) begin - if downloaded_image.is_a? StringIO + if downloaded_image.is_a?(StringIO) base_sixty_four = Base64.encode64(downloaded_image.read) else base_sixty_four = Base64.encode64(downloaded_image.open.read) diff --git a/app/models/content_base.rb b/app/models/content_base.rb new file mode 100644 index 00000000..f0b11a32 --- /dev/null +++ b/app/models/content_base.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ContentBase < ApplicationRecord + self.abstract_class = true + class_attribute :html_fields, default: ['description'] + + before_save :enforce_content_accessibility + + def enforce_content_accessibility + html_fields.each do |field| + self[field] = HtmlAccessibilityEnforcer.process(self[field]) + end + end +end diff --git a/app/models/flat_page.rb b/app/models/flat_page.rb index cba879a5..7f923154 100644 --- a/app/models/flat_page.rb +++ b/app/models/flat_page.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -class FlatPage < ApplicationRecord - has_many :tour_flat_pages +class FlatPage < ContentBase + self.html_fields = ['body'] + + has_many :tour_flat_pages, dependent: :destroy has_many :tours, through: :tour_flat_pages validates :title, presence: true @@ -14,6 +16,17 @@ def orphaned end def published - tours.any? { |tour| tour.published } + tours.any?(&:published) + end + + def search_data + { + id:, + title:, + slug:, + body:, + orphaned:, + tour_count: tours.count, + } end end diff --git a/app/models/map_icon.rb b/app/models/map_icon.rb index e5376c03..e166237b 100644 --- a/app/models/map_icon.rb +++ b/app/models/map_icon.rb @@ -1,19 +1,27 @@ +# frozen_string_literal: true + class MapIcon < MediumBaseRecord validate :check_dimensions + validates :file, dimension: { width: 80, height: 80, message: 'Icons should be no bigger that 80 by 80 pixels' } - has_one :stop + has_one :stop, dependent: :nullify + has_one :tour, dependent: :nullify def published - stop.published + (stop || tour)&.published || false + end + + def search_data + { id: } end def check_dimensions return if base_sixty_four.nil? - file = MiniMagick::Image.read(Base64.decode64(base_sixty_four)) + file_to_check = MiniMagick::Image.read(Base64.decode64(base_sixty_four)) + + return if file_to_check[:height] <= 80 && file_to_check[:width] <= 80 - if file[:height] > 80 || file[:width] > 80 - errors.add(:base, 'Icons should be no bigger that 80 by 80 pixels') - end + errors.add(:base, 'Icons should be no bigger that 80 by 80 pixels') end end diff --git a/app/models/map_overlay.rb b/app/models/map_overlay.rb index 7fca50c3..d3e2b3ef 100644 --- a/app/models/map_overlay.rb +++ b/app/models/map_overlay.rb @@ -1,26 +1,34 @@ # frozen_string_literal: true # -# Model calss for map overlays. +# Model class for map overlays. # class MapOverlay < MediumBaseRecord - before_create :set_initial_bounds + before_save :set_initial_bounds belongs_to :tour, optional: true belongs_to :stop, optional: true - def published - tour.published - end + delegate :published, to: :tour def set_initial_bounds - return if tour&.bounds.nil? || tour&.stop_count < 2 + return if tour.nil? || tour&.bounds.nil? || (tour&.stop_count&.< 2) + + self.south = tour.bounds[:south] if south.to_f.zero? + self.north = tour.bounds[:north] if north.to_f.zero? + self.east = tour.bounds[:east] if east.to_f.zero? + self.west = tour.bounds[:west] if west.to_f.zero? + end - if tour - self.south = self.tour.bounds[:south] - self.north = self.tour.bounds[:north] - self.east = self.tour.bounds[:east] - self.west = self.tour.bounds[:west] - end + def search_data + { + id:, + east: east.to_f, + image_url: http_path, + north: north.to_f, + south: south.to_f, + west: west.to_f, + rotation:, + } end end diff --git a/app/models/medium.rb b/app/models/medium.rb index 48cdca2c..951a42f8 100644 --- a/app/models/medium.rb +++ b/app/models/medium.rb @@ -1,13 +1,27 @@ # frozen_string_literal: true +require 'cgi' + # Model for media associated with stops. class Medium < MediumBaseRecord - include VideoProps + include EmbedProps include Rails.application.routes.url_helpers + ALLOWED_CONTENT_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + ].freeze + + validates :file, content_type: { in: ALLOWED_CONTENT_TYPES } + + # TODO: get rid of VideoProps after next release. + before_create :v_props before_create :props before_save :add_widths before_update :replace_video + after_create_commit -> { ProcessMediumVariantsJob.perform_later(id) } # has_one_attached :file do |attachable| # attachable.variant :mobile, resize: '200x200' @@ -16,41 +30,46 @@ class Medium < MediumBaseRecord # end # mount_base64_uploader :original_image, MediumUploader - has_many :stop_media + has_many :stop_media, dependent: :destroy has_many :stops, through: :stop_media - has_many :tour_media + has_many :tour_media, dependent: :destroy has_many :tours, through: :tour_media - enum video_provider: { keiner: 0, vimeo: 1, youtube: 2, soundcloud: 3 } - + enum :video_provider, { keiner: 0, vimeo: 1, youtube: 2, soundcloud: 3, sketchfab: 4, unknown: 5, matterport: 6, morphosource: 7 } attr_accessor :insecure - def props - return if self.video.nil? || self.video.empty? + def v_props + return if embed_id.present? || video.blank? VideoProps.props(self) end + def props + return if embed_id.blank? + + EmbedProps.props(self) + end + def published - tours.any? { |tour| tour.published } || stops.any? { |stop| stop.published } + tours.any?(&:published) || stops.any?(&:published) end def files - return nil if !self.file.attached? + return unless file.attached? if file.content_type.include?('gif') return { - lqip: file.variant(resize_to_limit: [50, 50], coalesce: true, layers: 'Optimize', deconstruct: true, loader: { page: nil }).processed.url, - mobile: file.variant(resize_to_limit: [300, 300], coalesce: true, layers: 'Optimize', deconstruct: true, loader: { page: nil }).processed.url, - tablet: file.variant(resize_to_limit: [400, 400], coalesce: true, layers: 'Optimize', deconstruct: true, loader: { page: nil }).processed.url, - desktop: file.variant(resize_to_limit: [750, 750], coalesce: true, layers: 'Optimize', deconstruct: true, loader: { page: nil }).processed.url + lqip: file.url, + mobile: file.url, + tablet: file.url, + desktop: file.url, } end { - lqip: file.variant(resize_to_limit: [5, 5]).processed.url, - mobile: file.variant(resize_to_limit: [300, 300]).processed.url, - tablet: file.variant(resize_to_limit: [400, 400]).processed.url, - desktop: file.variant(resize_to_limit: [750, 750]).processed.url + lqip: file.variant(resize_to_limit: [5, 5]).url, + mobile: file.variant(resize_to_limit: [300, 300]).url, + tablet: file.variant(resize_to_limit: [400, 400]).url, + desktop: file.variant(resize_to_limit: [750, 750]).url, } end @@ -58,18 +77,45 @@ def orphaned tours.empty? && stops.empty? end + def search_data + { + caption:, + desktop_width:, + embed:, + embed_id:, + filename:, + files: { + original: http_path, + mobile: "#{http_path}?variant=mobile", + tablet: "#{http_path}?variant=tablet", + desktop: "#{http_path}?variant=desktop", + lqip: "#{http_path}?variant=lqip", + }, + id: id, + lqip_width:, + mobile_width:, + original_image:, + provider: provider || video_provider, + tablet_width:, + title:, + video:, + } + end + + private + def replace_video - if video.present? && base_sixty_four.present? - attach_file - end + return if video.blank? + + attach_file end def add_widths return unless file.attached? - self.lqip_width = MiniMagick::Image.open(files[:lqip])[:width] || 50 - self.mobile_width = MiniMagick::Image.open(files[:mobile])[:width] || 300 - self.tablet_width = MiniMagick::Image.open(files[:tablet])[:width] || 400 - self.desktop_width = MiniMagick::Image.open(files[:desktop])[:width] || 750 + self.lqip_width = 50 + self.mobile_width = 300 + self.tablet_width = 400 + self.desktop_width = 750 end end diff --git a/app/models/medium_base_record.rb b/app/models/medium_base_record.rb index 64f0e8c9..89226d35 100755 --- a/app/models/medium_base_record.rb +++ b/app/models/medium_base_record.rb @@ -8,7 +8,7 @@ class MediumBaseRecord < ApplicationRecord before_create :attach_file before_destroy :purge - validates_presence_of :filename + validates :filename, presence: true # has_one_attached "#{Apartment::Tenant.current.underscore}_file" has_one_attached 'file' @@ -22,7 +22,8 @@ class MediumBaseRecord < ApplicationRecord # end def tmp_file_path - return Rails.root.join('public', 'storage', 'tmp', filename) if self.filename + return Rails.root.join('public', 'storage', 'tmp', filename) if filename + nil end @@ -44,22 +45,23 @@ def attach_file # file.blob.delete if file.attached? - self.parse_base64 + parse_base64 File.open(tmp_file_path, 'wb') do |f| f.write(Base64.decode64(base_sixty_four)) + f.rewind end - self.file.attach( + file.attach( io: File.open(tmp_file_path), filename: filename, - content_type: self.content_type + content_type: content_type, ) self.base_sixty_four = nil end def remove_tmp_file - File.delete(tmp_file_path) if File.exists?(tmp_file_path) + File.delete(tmp_file_path) if File.exist?(tmp_file_path) end def purge @@ -70,22 +72,26 @@ def purge def check_content_type return if base_sixty_four.nil? - self.parse_base64 + parse_base64 - if self.content_type.include?('jp2') - errors.add(:base, 'JPEG 2000 fils are not supported. Plese convert the image to a reqular JPEG or WebP format.') - end + return if content_type.exclude?('jp2') + + errors.add(:base, 'JPEG 2000 fils are not supported. Please convert the image to a regular JPEG or WebP format.') end private - def parse_base64 - if base_sixty_four.include?('data:') - headers, self.base_sixty_four = base_sixty_four.split(',') - headers =~ /^data:(.*?)$/ - self.content_type = Regexp.last_match(1).split(';base64').first - else - self.content_type = 'image/jpeg' - end + def parse_base64 + if base_sixty_four.include?('data:') + headers, self.base_sixty_four = base_sixty_four.split(',') + headers =~ /^data:(.*?)$/ + self.content_type = Regexp.last_match(1).split(';base64').first + else + self.content_type = 'image/jpeg' end + end + + def http_path + "#{Rails.application.routes.default_url_options[:host]}/#{Apartment::Tenant.current}/v4/public/media/#{CGI.escape(file.key || "")}" + end end diff --git a/app/models/mode.rb b/app/models/mode.rb index efda7b29..3d040ebb 100644 --- a/app/models/mode.rb +++ b/app/models/mode.rb @@ -2,6 +2,10 @@ # Model class for modes of directions, e.g. Walking, Biking, etc. class Mode < ApplicationRecord - has_many :tour_modes + has_many :tour_modes, dependent: :destroy has_many :tours, through: :tour_modes + + def search_data + { id:, title: } + end end diff --git a/app/models/open_geographies_medium.rb b/app/models/open_geographies_medium.rb new file mode 100644 index 00000000..ef5c98a6 --- /dev/null +++ b/app/models/open_geographies_medium.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Public-schema cache for Open Geographies media records. Keyed by the OG +# UUID so the same record isn't resolved more than once across all tenants. +class OpenGeographiesMedium < ApplicationRecord + CACHE_TTL = 30.days + + validates :uuid, presence: true, uniqueness: true + + def stale? + updated_at < CACHE_TTL.ago + end +end diff --git a/app/models/role.rb b/app/models/role.rb index 5c14f940..61672f02 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Role < ApplicationRecord end diff --git a/app/models/slug.rb b/app/models/slug.rb index f4d062c9..43813ded 100644 --- a/app/models/slug.rb +++ b/app/models/slug.rb @@ -3,10 +3,4 @@ class Slug < ApplicationRecord belongs_to :tour validates :slug, uniqueness: true - - # attr_accessor :published - - # def published - # tour.published - # end end diff --git a/app/models/stop.rb b/app/models/stop.rb index 3069a1c1..45ba0acf 100644 --- a/app/models/stop.rb +++ b/app/models/stop.rb @@ -1,17 +1,19 @@ # frozen_string_literal: true # Model class for a tour stop. -class Stop < ApplicationRecord - include HtmlSaintizer +class Stop < ContentBase + include HtmlSanitizer + + self.html_fields = ['description', 'direction_notes'] has_many :tour_stops, dependent: :destroy has_many :tours, -> { distinct }, through: :tour_stops - has_many :stop_media + has_many :stop_media, dependent: :destroy has_many :media, through: :stop_media belongs_to :medium, optional: true belongs_to :map_icon, optional: true has_many :stop_slugs, dependent: :delete_all - + has_many :voice_overs, dependent: :destroy before_validation -> { self.title ||= 'untitled' } validates :title, presence: true @@ -20,14 +22,16 @@ class Stop < ApplicationRecord before_create :ensure_icon_color after_save :ensure_slug - scope :by_slug_and_tour, lambda { |slug, tour_id| joins(:stop_slugs).joins(:tours).where('stop_slugs.slug = ?', slug).where('tour_stops.tour_id = ?', tour_id) } + scope :by_slug_and_tour, lambda { |slug, tour_id| + joins(:stop_slugs).joins(:tours).where('stop_slugs.slug = ?', slug).where('tour_stops.tour_id = ?', tour_id) + } def sanitized_description - HtmlSaintizer.accessable(description) + HtmlSanitizer.accessible(description) end def sanitized_direction_notes - HtmlSaintizer.accessable(direction_notes) + HtmlSanitizer.accessible(direction_notes) end def slug @@ -39,13 +43,16 @@ def splash medium elsif stop_media.present? stop_media.order(:position).first.medium - else - nil end if splash_medium&.files - return { title: splash_medium.title, caption: splash_medium.caption, url: splash_medium.files[:desktop] } + return { + title: splash_medium.title, + caption: splash_medium.caption, + url: splash_medium.search_data[:files][:desktop], + } end + nil end @@ -54,20 +61,61 @@ def orphaned end def published - tours.any? { |tour| tour.published } + tours.any?(&:published) + end + + def should_index? + !orphaned + end + + def search_data + { + address:, + article_link:, + description:, + direction_intro:, + direction_notes:, + icon_color:, + id:, + lat: lat&.to_f, + lng: lng&.to_f, + map_icon: map_icon&.original_image_url || nil, + media: stop_media.sort_by(&:position).map(&:search_data), + meta_description:, + orphaned:, + parking_lat: parking_lat&.to_f, + parking_lng: parking_lng&.to_f, + published:, + sanitized_description:, + slug:, + slugs: stop_slugs.map(&:slug), + splash:, + title:, + type: 'stop', + video_embed:, + video_poster:, + voice_overs: voice_overs.map(&:search_data), + } end private - def default_values - self.meta_description ||= HtmlSaintizer.accessable_truncated(self.description) - end + def default_values + self.meta_description ||= HtmlSanitizer.accessible_truncated(description) + end - def ensure_slug - tour_stops.each { |ts| ts.save } - end + def ensure_slug + stop_slug = title.parameterize_intl + existing = StopSlug.find_by(slug: stop_slug) - def ensure_icon_color - self.icon_color = '#D32F2F' if icon_color.nil? + if existing + existing.update(stop: self) unless existing.stop == self + else + stop_slugs.create(slug: stop_slug) end + end + + def ensure_icon_color + self.icon_color = '#D32F2F' if icon_color.nil? + end end diff --git a/app/models/stop_medium.rb b/app/models/stop_medium.rb index b8980451..70aad2f5 100644 --- a/app/models/stop_medium.rb +++ b/app/models/stop_medium.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Model class for many to many assoition between stops and media +# Model class for many to many association between stops and media class StopMedium < ApplicationRecord belongs_to :medium belongs_to :stop @@ -10,15 +10,15 @@ def published end after_create do - self.position = self.position.nil? ? self.stop.media.length : self.position - self.save + self.position = position.nil? ? stop.media.length : position + save end - # before_destroy do - # if self.medium.nil? || self.stop.nil? - # nil - # else - # self.medium.stops.length == 1 ? self.medium.destroy! : nil - # end - # end + def search_data + { + relation_id: id, + position:, + **medium&.search_data, + } + end end diff --git a/app/models/stop_slug.rb b/app/models/stop_slug.rb index 332f30e9..7be28812 100644 --- a/app/models/stop_slug.rb +++ b/app/models/stop_slug.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + class StopSlug < ApplicationRecord belongs_to :stop - belongs_to :tour - # validates :slug, uniqueness: true + validates :slug, uniqueness: true end diff --git a/app/models/tour.rb b/app/models/tour.rb index d6116d49..0c8df440 100644 --- a/app/models/tour.rb +++ b/app/models/tour.rb @@ -3,37 +3,50 @@ require 'uri' # Model class for a tour. -class Tour < ApplicationRecord - include HtmlSaintizer - +class Tour < ContentBase + include ActionView::Helpers::DateHelper + include HtmlSanitizer + include Searchable has_many :tour_stops, autosave: true, dependent: :destroy has_many :stops, -> { distinct }, through: :tour_stops has_many :tour_modes, autosave: true, dependent: :destroy has_many :modes, through: :tour_modes belongs_to :mode, default: -> { Mode.last } - has_many :tour_media + has_many :tour_media, dependent: :destroy has_many :media, through: :tour_media belongs_to :medium, optional: true - has_many :tour_flat_pages + belongs_to :map_icon, optional: true + has_many :tour_flat_pages, dependent: :destroy has_many :flat_pages, through: :tour_flat_pages - has_many :tour_authors + has_many :tour_authors, dependent: :destroy has_many :users, through: :tour_authors has_many :slugs, dependent: :delete_all - has_one :map_overlay - - # TODO: why does the CircleCI env need to serialize here? - if ENV['CI'] == 'circleci' - serialize :saved_stop_order, Array - end + has_one :map_overlay, dependent: :destroy + has_many :voice_overs, dependent: :destroy - # belongs_to :splash_image_medium_id, class_name: 'Medium' belongs_to :theme, default: -> { Theme.first } - enum default_lng: { - "en-US": 0, "fr-FR": 1, "de-DE": 2, "pl-PL": 3, "nl-NL": 4, "fi-FI": 5, "sv-SE": 6, "it-IT": 7, "es-ES": 8, "pt-PT": 9, - "ru-RU": 10, "pt-BR": 11, "es-MX": 12, "zh-CN": 13, "zh-TW": 14, "ja-JP": 15, "ko-KR": 16 - } + enum :default_lng, + { + 'en-US': 0, + 'fr-FR': 1, + 'de-DE': 2, + 'pl-PL': 3, + 'nl-NL': 4, + 'fi-FI': 5, + 'sv-SE': 6, + 'it-IT': 7, + 'es-ES': 8, + 'pt-PT': 9, + 'ru-RU': 10, + 'pt-BR': 11, + 'es-MX': 12, + 'zh-CN': 13, + 'zh-TW': 14, + 'ja-JP': 15, + 'ko-KR': 16, + } validates :title, presence: true, uniqueness: { case_sensitive: false } @@ -44,19 +57,33 @@ class Tour < ApplicationRecord before_save :calculate_duration before_save :check_url before_save :check_for_overlay + before_save :check_published after_save :ensure_slug after_create :add_modes scope :published, -> { where(published: true) } scope :mapable, -> { where(is_geo: true) } scope :has_stops, -> { includes(:stops).where.not(stops: { id: nil }) } + scope :search_import, -> { + includes( + :slugs, + :tour_flat_pages, + :map_icon, + :map_overlay, + :mode, + :tour_modes, + :voice_overs, + tour_media: :medium, + tour_stops: { stop: [:stop_slugs, :voice_overs, { stop_media: :medium }] }, + ) + } def sanitized_description - HtmlSaintizer.accessable(description) + HtmlSanitizer.accessible(description) end def slug - title.parameterize_intl + URI.encode_uri_component(title.parameterize_intl) end def tenant @@ -67,41 +94,37 @@ def tenant_title Apartment::Tenant.current.titleize end - # def external_url - # if Apartment::Tenant.current == 'public' - # return nil - # end - # TourSet.find_by(subdir: Apartment::Tenant.current).external_url - # end - - def theme_title - theme.title - end + delegate :title, to: :theme, prefix: true def splash splash_medium = if medium.present? medium elsif tour_media.present? tour_media.order(:position).first.medium - else - nil + elsif stops.present? + first_stop_medium end - if splash_medium - return { title: splash_medium.title, caption: splash_medium.caption, url: splash_medium.files[:desktop] } + if splash_medium && splash_medium.files.present? + return { + title: splash_medium.title, + caption: splash_medium.caption, + url: splash_medium.search_data[:files][:desktop], + } end - nil + + { title: 'OpenTourBuilder', caption: 'OpenTourBuilderLogo', url: 'https://opentour.site/assets/images/otb-bg.png' } end def stop_count - self.stops.count + stops.count end def bounds - if self.restrict_bounds_to_overlay && self.map_overlay.present? + if map_overlay.present? box = RGeo::Cartesian::BoundingBox.create_from_points( - RGeo::Geographic.spherical_factory.point(self.map_overlay.east.to_f, self.map_overlay.south.to_f), - RGeo::Geographic.spherical_factory.point(self.map_overlay.west.to_f, self.map_overlay.north.to_f) + RGeo::Geographic.spherical_factory.point(map_overlay.east.to_f, map_overlay.south.to_f), + RGeo::Geographic.spherical_factory.point(map_overlay.west.to_f, map_overlay.north.to_f), ) return { @@ -110,10 +133,10 @@ def bounds east: box.max_x + (box.x_span / 8), west: box.min_x - (box.x_span / 8), centerLat: box.center_y, - centerLng: box.center_x + centerLng: box.center_x, } elsif stops.empty? - return nil + return end points = stops.map { |stop| RGeo::Geographic.spherical_factory.point(stop.lng, stop.lat) } @@ -126,7 +149,7 @@ def bounds east: box.max_x + (box.x_span / 8), west: box.min_x - (box.x_span / 8), centerLat: box.center_y, - centerLng: box.center_x + centerLng: box.center_x, } end @@ -139,58 +162,141 @@ def calculate_duration return if mode.title.nil? - return unless self.will_save_change_to_published? || self.will_save_change_to_saved_stop_order? || self.will_save_change_to_mode_id? + unless will_save_change_to_published? || will_save_change_to_saved_stop_order? || will_save_change_to_mode_id? + return + end + + waypoints = tour_stops.order(:position).map { |tour_stop| [tour_stop.stop.lat, tour_stop.stop.lng] } - durations = [] - destinations = tour_stops.order(:position).map { |tour_stop| [tour_stop.stop.lat, tour_stop.stop.lng] } + self.travel_duration = waypoints.each_cons(2).map do |origin, destination| + GoogleDirections.new(origin, [destination], 1, mode.title.downcase).durations&.sum + end.compact.sum - # The direction matrix API limits the number of destinations to 25. - # Calculate the duration in chunks to stay below the limit. - destinations.each_slice(24) do |group| - origin = group.shift - g_directions = GoogleDirections.new(origin, group, group.count + 1, mode.title) - durations.push(g_directions.duration) + # 3.3 is ~ 200 words per-minute, but we need it in seconds, so 200 / 60 = 3.3 + self.read_duration = stops.map { |stop| (stop.sanitized_description.split.size / 3.3).to_i }.sum + + self.duration = travel_duration + read_duration if travel_duration + end + + def check_published + if published? && published_on.nil? + self.published_on = Time.current + elsif !published? + self.published_on = nil end + end - self.duration = durations.compact.sum.zero? ? nil : durations.sum + def search_data + { + blank_map:, + bounds:, + default_lng:, + description:, + flat_pages: tour_flat_pages.sort_by(&:position).map(&:search_data), + icon_color:, + id:, + is_geo:, + link_address:, + link_text:, + map_icon: map_icon&.original_image_url || nil, + map_overlay: map_overlay&.search_data, + map_type: map_type || 'hybrid', + media: tour_media.sort_by(&:position).map(&:search_data), + meta_description: search_meta_description, + mode: mode.search_data, + modes: tour_modes.map(&:search_data), + open_geographies_endpoint:, + published:, + published_on:, + read_duration: [duration_in_words(read_duration), 'reading'].join(' '), + restrict_bounds:, + restrict_bounds_to_overlay:, + sanitized_description:, + splash:, + slug:, + slugs: slugs.map(&:slug), + stop_count:, + stops: begin + sorted = tour_stops.sort_by(&:position) + sorted.each_with_index.map do |ts, i| + ts.search_data_with_neighbors( + prev_stop: i > 0 ? sorted[i - 1].stop : nil, + next_stop: sorted[i + 1]&.stop, + ) + end + end, + tenant:, + tenant_title:, + title:, + travel_duration: [duration_in_words(travel_duration), mode.title.downcase].join(' '), + theme: { id: theme.id, title: theme.title }, + type: 'tour', + voice_overs: voice_overs.map(&:search_data), + use_directions:, + } end private - def ensure_slug - Slug.find_or_create_by(slug: self.slug, tour: self) + def ensure_slug + tour_slug = title.parameterize_intl + existing = Slug.find_by(slug: tour_slug) + + if existing + existing.update(tour: self) unless existing.tour == self + else + slugs.create(slug: tour_slug) end + end - def add_modes - Mode.all.each do |m| - self.modes << m - end + def add_modes + Mode.all.find_each do |m| + modes << m end + end + + def check_url + return if link_address.blank? - def check_url - return if link_address.blank? + uri = URI(link_address) + + self.link_address = "http://#{link_address}" if uri.scheme.nil? + end - uri = URI(link_address) + def update_saved_stop_order + self.saved_stop_order = tour_stops.order(:position).map(&:stop_id) + end - self.link_address = "http://#{link_address}" if uri.scheme.nil? + def check_for_overlay + # Can't restrict to overlay without an overlay present + if restrict_bounds_to_overlay? && map_overlay.nil? + self.restrict_bounds_to_overlay = false + return end - def update_saved_stop_order - self.saved_stop_order = self.tour_stops.order(:position).map(&:stop_id) + # These two flags are mutually exclusive — the most recently changed one wins + if restrict_bounds_to_overlay_changed? && restrict_bounds_to_overlay? + self.restrict_bounds = false + elsif restrict_bounds_changed? && restrict_bounds? + self.restrict_bounds_to_overlay = false end + end - def check_for_overlay - if self.restrict_bounds_to_overlay && self.map_overlay.nil? - self.restrict_bounds_to_overlay = false - # self.restrict_bounds = false - end + def search_meta_description + return meta_description unless meta_description.nil? - if !self.restrict_bounds_to_overlay_was && self.restrict_bounds_to_overlay && self.map_overlay.present? - self.restrict_bounds = false - end + sanitized_description + end - if self.restrict_bounds && !self.restrict_bounds_was - self.restrict_bounds_to_overlay = false - end - end + def first_stop_medium + return if stops.empty? + + stops.map(&:media).flatten.first + end + + def duration_in_words(dur) + return if dur.nil? || dur.zero? + + distance_of_time_in_words(dur).capitalize + end end diff --git a/app/models/tour_author.rb b/app/models/tour_author.rb index 3f419d61..092c56d5 100644 --- a/app/models/tour_author.rb +++ b/app/models/tour_author.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + class TourAuthor < ApplicationRecord belongs_to :tour belongs_to :user + + validates :user_id, uniqueness: { scope: :tour_id } end diff --git a/app/models/tour_flat_page.rb b/app/models/tour_flat_page.rb index bdcbc1ac..01abe4d6 100644 --- a/app/models/tour_flat_page.rb +++ b/app/models/tour_flat_page.rb @@ -6,7 +6,15 @@ class TourFlatPage < ApplicationRecord belongs_to :flat_page after_create do - self.position = self.tour.flat_pages.length + 1 - self.save + self.position = tour.tour_flat_pages.length + 1 + save + end + + def search_data + { + position: position, + relation_id: id, + **flat_page.search_data, + } end end diff --git a/app/models/tour_medium.rb b/app/models/tour_medium.rb index 13c44b5b..081ce3a4 100644 --- a/app/models/tour_medium.rb +++ b/app/models/tour_medium.rb @@ -7,8 +7,8 @@ class TourMedium < ApplicationRecord # validates :position, presence: true after_create do - self.position = self.tour.media.length + 1 - self.save + self.position = tour.media.length + 1 + save end # before_destroy do @@ -22,4 +22,12 @@ class TourMedium < ApplicationRecord def published tour&.published end + + def search_data + { + relation_id: id, + position:, + **medium&.search_data, + } + end end diff --git a/app/models/tour_mode.rb b/app/models/tour_mode.rb index 0127788e..b37ec88d 100644 --- a/app/models/tour_mode.rb +++ b/app/models/tour_mode.rb @@ -4,4 +4,11 @@ class TourMode < ApplicationRecord belongs_to :tour belongs_to :mode + + def search_data + { + relation_id: id, + **mode.search_data, + } + end end diff --git a/app/models/tour_set.rb b/app/models/tour_set.rb index ad205817..b2a8d2d8 100644 --- a/app/models/tour_set.rb +++ b/app/models/tour_set.rb @@ -2,7 +2,11 @@ # Model class for tour sets. This is the main model for "instances" of Open Tour Builder. class TourSet < ApplicationRecord - before_save :set_subdir + include Searchable + + before_validation :compute_subdir + validate :subdir_available, if: :will_save_change_to_subdir? + before_save :rename_tenant_schema, if: :renaming_tenant? before_save :attach_file after_create :create_tenant after_create :create_defaults @@ -12,142 +16,200 @@ class TourSet < ApplicationRecord has_one_attached 'logo' - has_many :tour_set_admins + has_many :tour_set_admins, dependent: :destroy has_many :admins, through: :tour_set_admins, source: :user - - attr_accessor :published_tours + has_many :subdir_histories, class_name: 'TourSetSubdirHistory', dependent: :destroy def published_tours - begin - Apartment::Tenant.switch! self.subdir - tours = [] - Tour.published.has_stops.each do |t| - tour = { - title: t.title, - slug: t.slug - } - tours.push(tour) - end - tours - rescue Apartment::TenantNotFound => error - # self.delete + Apartment::Tenant.switch!(subdir) + tours = [] + Tour.published.has_stops.each do |t| + tour = { + title: t.title, + slug: t.slug, + location: { lat: t.bounds[:centerLat], lng: t.bounds[:centerLng] }, + } + tours.push(tour) end + + Apartment::Tenant.switch!('public') + tours + rescue Apartment::TenantNotFound => _e + logger.warn('Tenant not found.') end def mapable_tours - begin - Apartment::Tenant.switch! self.subdir - tours = [] - Tour.published.has_stops.mapable.each do |t| - tour = { - title: t.title, - slug: t.slug, - center: { lat: t.bounds[:centerLat], lng: t.bounds[:centerLng] } - } - tours.push(tour) - end - tours - rescue Apartment::TenantNotFound => error - # self.delete + Apartment::Tenant.switch!(subdir) + tours = [] + Tour.published.has_stops.mapable.each do |t| + tour = { + title: t.title, + slug: t.slug, + center: { lat: t.bounds[:centerLat], lng: t.bounds[:centerLng] }, + } + tours.push(tour) end + + Apartment::Tenant.switch!('public') + tours + rescue Apartment::TenantNotFound => _e + logger.warn('Tenant not found.') end def logo_url - Apartment::Tenant.switch! 'public' - begin - return logo.url if logo.attached? - rescue URI::InvalidURIError - # FIXME: This seems to be a problem when testing? - end + Apartment::Tenant.switch!('public') + return logo.url if logo.attached? nil end - private + def should_index? + published_tours.count > 0 + end - def set_subdir - self.subdir = name.parameterize_intl - end + def preview_data + { + id:, + description:, + external_url:, + footer_logo:, + name:, + logo_url:, + notes:, + subdir:, + } + end - def create_tenant - Apartment::Tenant.create(subdir) - # This is a bit of hack to fake the migrations from the - # auth engine. Hopfully this will be replaced when we - # redo the auth engine. - Apartment::Tenant.reset - schemas = ActiveRecord::SchemaMigration.all - - schemas.each do |schema| - Apartment::Tenant.switch!(subdir) - migration = ActiveRecord::SchemaMigration.find_by_version(schema.version) - if migration.nil? - ActiveRecord::SchemaMigration.create(version: schema.version) - end - end - end + def search_data + { + **preview_data, + mapable_tours:, + published_tours:, + } + end - def create_defaults - # Apartment::Tenant.reset - # themes = Theme.all.collect(&:title) - Apartment::Tenant.switch! subdir - Mode.create([ - { title: 'BICYCLING', icon: 'bicycle' }, - { title: 'DRIVING', icon: 'car' }, - { title: 'TRANSIT', icon: 'subway' }, - { title: 'WALKING', icon: 'walking' } - ]) - # themes.each do |t| - # Theme.create(title: t) - # end - end + def admin_data + { + **preview_data, + admins: tour_set_admins.map { |ta| { id: ta.id, display_name: ta.user.display_name || ta.user.email } }, + tour_authors:, + } + end - def drop_tenant - Apartment::Tenant.drop(subdir) - end + private - def tmp_file_path - return nil if logo_title.nil? + def compute_subdir + self.subdir = name.parameterize_intl if name.present? && (new_record? || will_save_change_to_name?) + end - Rails.root.join('public', 'storage', 'tmp', logo_title) - end + def subdir_available + return unless TourSet.where(subdir:).where.not(id:).exists? || + TourSetSubdirHistory.where(subdir:).exists? - # - # Create and attach file from Base64 string. - # - # This should only be called once when a new medium obeject is created via the API - # It assumes - # - # Some code taken from https://github.com/rootstrap/active-storage-base64/blob/v1.2.0/lib/active_storage_support/base64_attach.rb#L17-L32 - # - # - def attach_file - return if base_sixty_four.nil? && !logo.attached? + errors.add(:subdir, 'is already in use') + end + def renaming_tenant? + !new_record? && will_save_change_to_subdir? && subdir_was.present? + end - return if !self.will_save_change_to_base_sixty_four? && logo.attached? + # Postgres schema rename is metadata-only (no data copy) and, since this + # runs inside the same before_save transaction as the record's own update, + # rolls back automatically if anything else in this save fails. + def rename_tenant_schema + old_subdir = subdir_was + ActiveRecord::Base.connection.execute( + %(ALTER SCHEMA "#{old_subdir}" RENAME TO "#{subdir}"), + ) + subdir_histories.create!(subdir: old_subdir) + end - if base_sixty_four.nil? && logo.attached? - logo.purge - else - headers, self.base_sixty_four = base_sixty_four.split(',') + def create_tenant + Apartment::Tenant.create(subdir) + versions = ActiveRecord::Base.connection.select_values('SELECT version FROM schema_migrations') + Apartment::Tenant.switch!(subdir) + versions.each do |version| + ActiveRecord::Base.connection.execute( + "INSERT INTO schema_migrations (version) VALUES ('#{version}') ON CONFLICT DO NOTHING", + ) + end + Apartment::Tenant.reset + end - return if base_sixty_four.nil? + def create_defaults + # Apartment::Tenant.reset + # themes = Theme.all.collect(&:title) + Apartment::Tenant.switch!(subdir) + Mode.create([ + { title: 'BICYCLING', icon: 'bicycle' }, + { title: 'DRIVING', icon: 'car' }, + { title: 'TRANSIT', icon: 'subway' }, + { title: 'WALKING', icon: 'walking' }, + ]) + Apartment::Tenant.switch!('public') + # themes.each do |t| + # Theme.create(title: t) + # end + end - File.open(tmp_file_path, 'wb') do |f| - f.write(Base64.decode64(base_sixty_four)) - end + def drop_tenant + Apartment::Tenant.drop(subdir) + end - image = MiniMagick::Image.open(tmp_file_path) + def tmp_file_path + return if logo_title.nil? - if image[:height] > 80 - image.resize('300x80') - image.write(tmp_file_path) - end + Rails.root.join('public', 'storage', 'tmp', logo_title) + end + + # + # Create and attach file from Base64 string. + # + # This should only be called once when a new medium object is created via the API + # It assumes + # + # Some code taken from https://github.com/rootstrap/active-storage-base64/blob/v1.2.0/lib/active_storage_support/base64_attach.rb#L17-L32 + # + # + def attach_file + return if base_sixty_four.nil? && !logo.attached? + return if !will_save_change_to_base_sixty_four? && logo.attached? + + if base_sixty_four.nil? && logo.attached? + logo.purge + else + _, self.base_sixty_four = base_sixty_four.split(',') + + return if base_sixty_four.nil? + + File.open(tmp_file_path, 'wb') do |f| + f.write(Base64.decode64(base_sixty_four)) + end + + image = MiniMagick::Image.open(tmp_file_path) - self.logo.attach( - io: File.open(tmp_file_path), - filename: logo_title - ) + if image[:height] > 80 + image.resize('300x80') + image.write(tmp_file_path) end + + logo.attach( + io: File.open(tmp_file_path), + filename: logo_title, + ) end + end + + def tour_authors + Apartment::Tenant.switch!(subdir) + TourAuthor.all.map do |ta| + user = ta.user.display_name || ta.user.email + { + id: ta.id, + user:, + tour: ta.tour.title, + may_create_tour: ta.user.current_tenant_tour_creator?, + } + end.sort_by { |ta| ta[:user] } + end end diff --git a/app/models/tour_set_admin.rb b/app/models/tour_set_admin.rb index 705336fe..3f7b44b9 100644 --- a/app/models/tour_set_admin.rb +++ b/app/models/tour_set_admin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Through model TourSet and User assoiation. +# Through model TourSet and User association. # class TourSetAdmin < ApplicationRecord belongs_to :user diff --git a/app/models/tour_set_subdir_history.rb b/app/models/tour_set_subdir_history.rb new file mode 100644 index 00000000..d2cdc4c1 --- /dev/null +++ b/app/models/tour_set_subdir_history.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Records every subdir (tenant schema name) a TourSet has ever had, so +# requests using an old, since-renamed subdir can be redirected to the +# current one. See DirectoryElevator. +class TourSetSubdirHistory < ApplicationRecord + belongs_to :tour_set + + validates :subdir, presence: true, uniqueness: true +end diff --git a/app/models/tour_stop.rb b/app/models/tour_stop.rb index b47b61c4..9437848d 100644 --- a/app/models/tour_stop.rb +++ b/app/models/tour_stop.rb @@ -1,50 +1,62 @@ # frozen_string_literal: true -# Modle class for connecting stops to tours. +# Model class for connecting stops to tours. class TourStop < ApplicationRecord belongs_to :tour belongs_to :stop validates :position, presence: true - before_save :_ensure_stop_slug + # before_save :_ensure_stop_slug before_validation :_set_position - def slug - stop.slug - end + delegate :slug, to: :stop def next - ts = self.class.where(tour_id: self.tour_id).where(position: self.position + 1).first - ts.present? ? ts : nil + ts = self.class.where(tour_id: tour_id).where(position: position + 1).first + ts.presence end # Used for UIkit's Sticky component on the desktop vew. The media is sticky when scrolling until # the next stop comes along. def next_slug - self.next.nil? ? nil : self.next.stop.slug + self.next&.stop&.slug end def previous - ts = self.class.where(tour_id: self.tour_id).where(position: self.position - 1).first - ts.present? ? ts : nil + ts = self.class.where(tour_id: tour_id).where(position: position - 1).first + ts.presence end def previous_slug - self.previous.nil? ? nil : self.previous.stop.slug + previous&.stop&.slug + end + + def search_data + search_data_with_neighbors(prev_stop: previous&.stop, next_stop: self.next&.stop) + end + + def search_data_with_neighbors(prev_stop:, next_stop:) + { + next: next_stop && { id: next_stop.id, slug: next_stop.slug, title: next_stop.title }, + position:, + previous: prev_stop && { id: prev_stop.id, slug: prev_stop.slug, title: prev_stop.title }, + relation_id: id, + **stop.search_data, + } end private - def _set_position - return if tour.nil? + def _set_position + return if tour.nil? - self.position = self.position || self.tour.stops.length + 1 - end + self.position = position || tour.stops.length + 1 + end - def _ensure_stop_slug - new_slug = StopSlug.find_or_create_by(slug: self.stop.slug, tour: self.tour) - new_slug.stop = self.stop - new_slug.save - end + # def _ensure_stop_slug + # new_slug = StopSlug.find_or_create_by(slug: self.stop.slug, tour: self.tour) + # new_slug.stop = self.stop + # new_slug.save + # end end diff --git a/app/models/user.rb b/app/models/user.rb index 1ce93c2a..e8a17969 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true -class User < ActiveRecord::Base - has_many :tour_set_admins +class User < ApplicationRecord + include Searchable + has_many :tour_set_admins, dependent: :destroy has_many :tour_sets, through: :tour_set_admins - has_many :tour_authors + has_many :tour_authors, dependent: :destroy has_many :tours, through: :tour_authors + has_many :access_requests, dependent: :destroy validates :email, presence: true @@ -17,29 +19,95 @@ class User < ActiveRecord::Base # def current_tenant_admin? return true if self.super - return false if tour_sets.empty? - tour_sets.map(&:subdir).include? Apartment::Tenant.current + + admin_role = Role.find_by(title: 'Tour Admin') + admin_tour_sets = tour_set_admins.where(role: admin_role).map(&:tour_set) + return false if admin_tour_sets.empty? + + admin_tour_sets.map(&:subdir).include?(Apartment::Tenant.current) + end + + def current_tenant_tour_creator? + return true if current_tenant_admin? + + tour_creator_role = Role.find_by(title: 'Tour Creator') + tour_creator_sets = tour_set_admins.where(role: tour_creator_role).map(&:tour_set) + return false if tour_creator_sets.empty? + + tour_creator_sets.map(&:subdir).include?(Apartment::Tenant.current) + end + + def preview_data + { + id:, + display_name:, + email:, + super:, + tour_sets: tour_set_admins.map { |tsa| { id: tsa.id, name: tsa.tour_set.name, subdir: tsa.tour_set.subdir } }, + terms_accepted:, + access_requests: access_requests.map(&:search_data), + date_joined: created_at&.strftime('%b %d, %Y'), + last_sign_in:, + } + end + + def search_data + { + **preview_data, + current_tenant_admin: current_tenant_admin?, + may_create_tour: current_tenant_tour_creator?, + tours: as_author, + } end def provider - return nil if login.nil? + return if login.nil? + login.provider end def all_tours - all = [] - TourSet.all.each do |tour_set| - Apartment::Tenant.switch! tour_set.subdir + all = TourSet.all.map do |tour_set| + Apartment::Tenant.switch!(tour_set.subdir) next if tours.empty? || current_tenant_admin? - Apartment::Tenant.switch! tour_set.subdir - _tours = TourAuthor.where(user: self) - all.push(_tours.map { |ta| { id: ta.tour.id, tenant: ta.tour.tenant, title: ta.tour.title } }) - end + + Apartment::Tenant.switch!(tour_set.subdir) + TourAuthor.where(user: self).map { |ta| { id: ta.tour.id, tenant: ta.tour.tenant, title: ta.tour.title } } + end.flatten.uniq.compact Apartment::Tenant.reset - all.flatten.uniq + all end def login - EcdsRailsAuthEngine::Login.find_by(user_id: self.id) + EcdsRailsAuthEngine::Login.find_by(user_id: id) + end + + private + + def as_author + return unless persisted? + + # preserve current tenant so we can go back + current = Apartment::Tenant.current + all = TourSet.all.map do |ts| + next if tour_sets.include?(ts) + + Apartment::Tenant.switch!(ts.subdir) + tours = TourAuthor.where(user: self).map(&:tour) + { tour_set: ts, tours: } unless tours.empty? + end.flatten.compact + # return to initial tenant + Apartment::Tenant.switch!(current) + all + end + + def as_site_editor + as_author.map { |t| t[:tour_set] } + end + + def last_sign_in + return if login.nil? || login.tokens.empty? + + login.tokens.order(created_at: :desc).first.created_at.strftime('%b %d, %Y') end end diff --git a/app/models/voice_over.rb b/app/models/voice_over.rb new file mode 100644 index 00000000..c7bebc65 --- /dev/null +++ b/app/models/voice_over.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Audio narration attached to either a Tour or a Stop, in a given language. +class VoiceOver < ApplicationRecord + include Searchable + + AUDIO_CONTENT_TYPES = [ + 'audio/mpeg', + 'audio/mp4', + 'audio/x-m4a', + ].freeze + + belongs_to :tour, optional: true + belongs_to :stop, optional: true + + has_one_attached :file + + validates :language, presence: true + validates :language, uniqueness: { scope: [:tour_id, :stop_id] } + validates :file, content_type: { + in: AUDIO_CONTENT_TYPES, + message: 'must be an MP3 or M4A audio file', + } + validate :belongs_to_exactly_one_parent + + def search_data + { + id:, + source_url: "#{Rails.application.routes.default_url_options[:host]}/#{Apartment::Tenant.current}/v4/public/media/#{CGI.escape(file.key || "")}", + language:, + filename: file.filename.to_s, + } + end + + private + + def belongs_to_exactly_one_parent + return if tour.present? ^ stop.present? + + errors.add(:base, 'must belong to exactly one of tour or stop') + end +end diff --git a/app/presenters/tour_presenter.rb b/app/presenters/tour_presenter.rb new file mode 100644 index 00000000..7095e49f --- /dev/null +++ b/app/presenters/tour_presenter.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class TourPresenter + DESERIALIZERS = { + 'v1' => OpenGeographies::DeserializerV1, + }.freeze + + def initialize(record, tour_set) + @record = record + @tour_set = tour_set + end + + def as_json(*) + @data = @record.to_h + + fetch_geography_stops if @record['open_geographies_endpoint'].present? + + { tour: @data.as_json, tour_set: @tour_set.as_json } + end + + private + + def fetch_geography_stops + response = HTTParty.get(@record['open_geographies_endpoint']) + return unless response.success? + + og_data = JSON.parse(response.body, symbolize_names: true) + version = detect_version(og_data) + deserializer_class = DESERIALIZERS.fetch(version, OpenGeographies::DeserializerV1) + @data = @data.as_json.merge(deserializer_class.new(og_data).call.stringify_keys) + rescue HTTP::TimeoutError, HTTP::Error => e + Rails.logger.error("Geography #{@record["open_geographies_endpoint"]} API failed: #{e.message}") + nil + end + + def detect_version(og_data) + context = og_data.dig(:stops, 0, :'@context').to_s + context.match(%r{/api/(v\d+)/})&.captures&.first || 'v1' + end +end diff --git a/app/serializers/v3/flat_page_serializer.rb b/app/serializers/v3/flat_page_serializer.rb index 52fb55c0..7c7c5ed3 100644 --- a/app/serializers/v3/flat_page_serializer.rb +++ b/app/serializers/v3/flat_page_serializer.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module V3 - class FlatPageSerializer < ActiveModel::Serializer + class FlatPageSerializer < ActiveModel::Serializer has_many :tours attributes :id, :title, :body, :slug, :orphaned end diff --git a/app/serializers/v3/map_icon_serializer.rb b/app/serializers/v3/map_icon_serializer.rb index 1fdf2c28..c768ea57 100644 --- a/app/serializers/v3/map_icon_serializer.rb +++ b/app/serializers/v3/map_icon_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class MapIconSerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers diff --git a/app/serializers/v3/map_overlay_serializer.rb b/app/serializers/v3/map_overlay_serializer.rb index 94446148..165a4639 100644 --- a/app/serializers/v3/map_overlay_serializer.rb +++ b/app/serializers/v3/map_overlay_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class MapOverlaySerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers diff --git a/app/serializers/v3/medium_serializer.rb b/app/serializers/v3/medium_serializer.rb index 7cbc4f87..50a7dc7b 100644 --- a/app/serializers/v3/medium_serializer.rb +++ b/app/serializers/v3/medium_serializer.rb @@ -4,20 +4,20 @@ module V3 class MediumSerializer < ActiveModel::Serializer # include Rails.application.routes.url_helpers attributes :id, - :title, - :caption, - :video, - :provider, - :original_image, - :embed, - :files, - :orphaned, - :filename, - :original_image_url, - :lqip_width, - :mobile_width, - :tablet_width, - :desktop_width + :title, + :caption, + :video, + :provider, + :original_image, + :embed, + :files, + :orphaned, + :filename, + :original_image_url, + :lqip_width, + :mobile_width, + :tablet_width, + :desktop_width # def files # return nil unless object.file.attached? diff --git a/app/serializers/v3/stop_serializer.rb b/app/serializers/v3/stop_serializer.rb index 767923ac..46e246f5 100644 --- a/app/serializers/v3/stop_serializer.rb +++ b/app/serializers/v3/stop_serializer.rb @@ -7,24 +7,24 @@ class StopSerializer < ActiveModel::Serializer has_many :tours belongs_to :map_icon attributes :id, - :title, - :slug, - :description, - :sanitized_description, - :sanitized_direction_notes, - :lat, - :lng, - :address, - :meta_description, - :article_link, - :video_embed, - :video_poster, - :parking_lat, - :parking_lng, - :direction_intro, - :direction_notes, - :splash, - :orphaned, - :icon_color + :title, + :slug, + :description, + :sanitized_description, + :sanitized_direction_notes, + :lat, + :lng, + :address, + :meta_description, + :article_link, + :video_embed, + :video_poster, + :parking_lat, + :parking_lng, + :direction_intro, + :direction_notes, + :splash, + :orphaned, + :icon_color end end diff --git a/app/serializers/v3/tour_author_serializer.rb b/app/serializers/v3/tour_author_serializer.rb index 9f8bff5d..a4e79842 100644 --- a/app/serializers/v3/tour_author_serializer.rb +++ b/app/serializers/v3/tour_author_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class TourAuthorSerializer < ActiveModel::Serializer belongs_to :tour diff --git a/app/serializers/v3/tour_base_serializer.rb b/app/serializers/v3/tour_base_serializer.rb index 13adde0d..f122d636 100644 --- a/app/serializers/v3/tour_base_serializer.rb +++ b/app/serializers/v3/tour_base_serializer.rb @@ -7,32 +7,32 @@ module V3 class TourBaseSerializer < ActiveModel::Serializer has_one :map_overlay attributes :id, - :title, - :slug, - :description, - :is_geo, - :published, - :sanitized_description, - :position, - :theme_title, - :meta_description, - :tenant, - :tenant_title, - :stop_count, - :map_type, - :splash, - :use_directions, - :default_lng, - :stop_count, - :est_time, - :link_address, - :link_text, - :restrict_bounds, - :restrict_bounds_to_overlay, - :blank_map + :title, + :slug, + :description, + :is_geo, + :published, + :sanitized_description, + :position, + :theme_title, + :meta_description, + :tenant, + :tenant_title, + :stop_count, + :map_type, + :splash, + :use_directions, + :default_lng, + :stop_count, + :est_time, + :link_address, + :link_text, + :restrict_bounds, + :restrict_bounds_to_overlay, + :blank_map def est_time - return nil if object.duration.nil? + return if object.duration.nil? "#{distance_of_time_in_words(object.duration).capitalize} #{object.mode.title.downcase}" end @@ -44,9 +44,7 @@ def map_type def bounds return object.bounds if object.bounds.present? - if @instance_options[:loc].present? - return @instance_options[:loc] - end + return @instance_options[:loc] if @instance_options[:loc].present? nil end diff --git a/app/serializers/v3/tour_flat_page_serializer.rb b/app/serializers/v3/tour_flat_page_serializer.rb index 67dfef6f..8a1436b8 100644 --- a/app/serializers/v3/tour_flat_page_serializer.rb +++ b/app/serializers/v3/tour_flat_page_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class TourFlatPageSerializer < ActiveModel::Serializer belongs_to :tour diff --git a/app/serializers/v3/tour_set_admin_serializer.rb b/app/serializers/v3/tour_set_admin_serializer.rb index 4a1508c3..f71a833f 100644 --- a/app/serializers/v3/tour_set_admin_serializer.rb +++ b/app/serializers/v3/tour_set_admin_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module V3 class TourSetAdminSerializer < ActiveModel::Serializer attributes :id diff --git a/app/serializers/v3/tour_set_serializer.rb b/app/serializers/v3/tour_set_serializer.rb index 497a509d..c3971b47 100644 --- a/app/serializers/v3/tour_set_serializer.rb +++ b/app/serializers/v3/tour_set_serializer.rb @@ -8,12 +8,10 @@ class TourSetSerializer < ActiveModel::Serializer attributes :id, :name, :subdir, :published_tours, :mapable_tours, :logo_url, :logo def admins - begin - object.admins if current_user&.super || current_user&.tour_sets.include?(object) - rescue NameError - # This is a problem when using the serializer directly - nil - end + object.admins if current_user&.super || current_user&.tour_sets&.include?(object) + rescue NameError + # This is a problem when using the serializer directly + nil end end end diff --git a/app/serializers/v3/user_serializer.rb b/app/serializers/v3/user_serializer.rb index fec6739d..6c0ca22b 100644 --- a/app/serializers/v3/user_serializer.rb +++ b/app/serializers/v3/user_serializer.rb @@ -13,9 +13,8 @@ def current_tenant_admin end def all_tours - if @instance_options[:include_tours] - return object.all_tours - end + return object.all_tours if @instance_options[:include_tours] + [] end end diff --git a/app/services/google_directions.rb b/app/services/google_directions.rb index 89547580..4b71ff59 100644 --- a/app/services/google_directions.rb +++ b/app/services/google_directions.rb @@ -1,12 +1,13 @@ -class GoogleDirections +# frozen_string_literal: true +class GoogleDirections def initialize(origin, destinations, stops_count, mode) google_key = ENV['RAILS_ENV'] == 'test' ? 'FAkeFaK-E_fAkeChv-P3nchtQYHoCLfFzn9ylr8' : Rails.application.credentials.dig(:g_maps_key) @query = { origins: origin.join(','), destinations: destinations.map { |d| d.join(',') }.join('|'), mode: mode, - key: google_key + key: google_key, } @stops_count = stops_count @@ -14,18 +15,24 @@ def initialize(origin, destinations, stops_count, mode) def matrix response = HTTParty.get( - "https://maps.googleapis.com/maps/api/distancematrix/json?#{@query.to_query}" + "https://maps.googleapis.com/maps/api/distancematrix/json?#{@query.to_query}", ).with_indifferent_access - return nil if response[:status] && response[:rows].nil? + return if response[:status] && response[:rows].nil? - return nil if response[:rows].first[:elements].first[:status] == 'ZERO_RESULTS' + return if response[:rows].first[:elements].first[:status] == 'ZERO_RESULTS' response end def durations - matrix.nil? ? nil : matrix[:rows].first[:elements].map { |e| e[:duration][:value] if e[:duration].present? }.reject { |d| d.nil? } + if matrix.nil? + nil + else + matrix[:rows].first[:elements].map do |e| + e[:duration][:value] if e[:duration].present? + end.reject(&:nil?) + end end def duration diff --git a/app/services/html_accessibility_enforcer.rb b/app/services/html_accessibility_enforcer.rb new file mode 100644 index 00000000..960f2f26 --- /dev/null +++ b/app/services/html_accessibility_enforcer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# app/services/html_accessibility_enforcer.rb +class HtmlAccessibilityEnforcer + def self.process(html) + return html if html.blank? + + doc = Nokogiri::HTML5.fragment(html) + + doc.css('a[target="_blank"]').each do |link| + # Prevent tab-napping + rel = (link['rel'] || '').split.to_set + rel << 'noopener' << 'noreferrer' + link['rel'] = rel.to_a.join(' ') + + # Add aria-label if missing + unless link['aria-label'].present? + link['aria-label'] = "#{link.text.strip} (opens in a new tab)" + end + end + + doc.to_html + end +end diff --git a/app/services/open_geographies/deserializer_v1.rb b/app/services/open_geographies/deserializer_v1.rb new file mode 100644 index 00000000..b8a357af --- /dev/null +++ b/app/services/open_geographies/deserializer_v1.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module OpenGeographies + class DeserializerV1 + PROVIDER_PATTERNS = [ + { name: 'youtube', pattern: /youtube\.com|youtu\.be/ }, + { name: 'vimeo', pattern: /vimeo\.com/ }, + { name: 'soundcloud', pattern: /soundcloud\.com/ }, + { name: 'sketchfab', pattern: /sketchfab\.com|skfb\.ly/ }, + { name: 'matterport', pattern: /matterport\.com/ }, + { name: 'morphosource', pattern: /morphosource\.org/ }, + ].freeze + + def initialize(og_data) + @og_data = og_data + end + + def call + { + bounds: bounds, + stops: stops, + stop_count: @og_data[:stops].count, + } + end + + private + + def bounds + factory = RGeo::Cartesian.factory + bbox = RGeo::Cartesian::BoundingBox.new(factory) + @og_data[:stops].each do |og_stop| + bbox.add(factory.point(og_stop[:geo][:point][:lon], og_stop[:geo][:point][:lat])) + end + { + south: bbox.min_y, + north: bbox.max_y, + west: bbox.min_x, + east: bbox.max_x, + centerLat: bbox.center_y, + centerLng: bbox.center_x, + } + end + + def stops + @og_data[:stops].each_with_index.map do |og_stop, index| + position = index + 1 + count = @og_data[:stops].count + next_stop = neighbor(@og_data[:stops][position]) unless position == count + previous_stop = neighbor(@og_data[:stops][index - 1]) if index.nonzero? + + { + address: og_stop[:address], + description: og_stop[:description], + id: og_stop[:uuid], + lat: og_stop[:geo][:point][:lat], + lng: og_stop[:geo][:point][:lon], + media: media(og_stop[:media] || []), + meta_description: og_stop[:short_description], + next: next_stop, + position:, + previous: previous_stop, + sanitized_description: HtmlSanitizer.accessible(og_stop[:description]), + slug: og_stop[:slug], + slugs: og_stop[:slugs], + title: og_stop[:name], + } + end + end + + def neighbor(og_stop) + { + id: og_stop[:uuid], + slug: og_stop[:slug], + title: og_stop[:name], + } + end + + def media(og_media) + og_media.map do |medium| + cached = MediaResolver.new(medium).call + thumbnail = cached&.thumbnail_url || medium[:thumbnail] || medium[:preview] + { + title: medium[:name], + provider: cached&.provider, + embed: medium[:embed_url], + files: { + original: medium[:content_url], + mobile: thumbnail, + tablet: medium[:preview] || thumbnail, + desktop: medium[:content_url], + }, + } + end + end + end +end diff --git a/app/services/open_geographies/media_resolver.rb b/app/services/open_geographies/media_resolver.rb new file mode 100644 index 00000000..19e5534f --- /dev/null +++ b/app/services/open_geographies/media_resolver.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module OpenGeographies + # Resolves a thumbnail URL for a single OG media item, using a DB cache so + # external API calls happen at most once per UUID per 30 days. + class MediaResolver + # How to extract a provider-specific thumbnail URL from an embed URL. + # Each entry returns a URL string, or nil to fall through to the next step. + THUMBNAIL_FETCHERS = { + 'youtube' => lambda { |embed_url| + id = embed_url.match(%r{(?:youtube\.com/(?:watch\?v=|embed/)|youtu\.be/)([A-Za-z0-9_-]+)}i)&.captures&.first + "https://img.youtube.com/vi/#{id}/0.jpg" if id + }, + 'vimeo' => lambda { |embed_url| + id = embed_url.match(%r{vimeo\.com/(?:video/)?(\d+)})&.captures&.first + if id + data = HTTParty.get("https://vimeo.com/api/oembed.json?url=https%3A//vimeo.com/video/#{id}") + data['thumbnail_url'] if data.success? + end + }, + 'soundcloud' => lambda { |embed_url| + id = embed_url.match(%r{api\.soundcloud\.com/tracks/(\d+)})&.captures&.first || + embed_url.match(%r{soundcloud\.com/tracks/(\d+)})&.captures&.first + if id + data = HTTParty.get( + "https://soundcloud.com/oembed?url=https://api.soundcloud.com/tracks/#{id}&format=json", + format: :plain, + ) + JSON.parse(data.body)['thumbnail_url'] if data.success? + end + }, + 'sketchfab' => lambda { |embed_url| + id = embed_url.match(%r{sketchfab\.com/models/([0-9a-f]{32})}i)&.captures&.first + if id + doc = Nokogiri::HTML(HTTParty.get("https://sketchfab.com/models/#{id}/embed").body) + doc.at_xpath('/html/head/meta[@property="og:image"]')&.[](:content) + end + }, + 'matterport' => lambda { |embed_url| + id = URI.parse(embed_url).query&.then { |q| URI.decode_www_form(q).to_h['m'] } + if id + doc = Nokogiri::HTML(HTTParty.get("https://my.matterport.com/show/?m=#{id}").body) + doc.at_xpath('/html/head/meta[@property="og:image"]')&.[](:content) + end + }, + }.freeze + + def initialize(og_medium) + @og_medium = og_medium + end + + def call + uuid = @og_medium[:uuid] + cached = OpenGeographiesMedium.find_by(uuid:) + return cached if cached && !cached.stale? + + resolved = resolve + attrs = { + uuid:, + provider: resolved[:provider], + embed_url: @og_medium[:embed_url], + thumbnail_url: resolved[:thumbnail_url], + } + + if cached + cached.update(attrs) + cached + else + OpenGeographiesMedium.create(attrs) + end + end + + private + + def resolve + provider = detect_provider(@og_medium[:embed_url]) + thumbnail_url = working_content_url || provider_thumbnail(provider, @og_medium[:embed_url]) + { provider:, thumbnail_url: } + end + + def working_content_url + url = @og_medium[:content_url] + return if url.blank? + + response = HTTParty.head(url) + url if response.success? + rescue StandardError + nil + end + + def provider_thumbnail(provider, embed_url) + return if provider.nil? || embed_url.blank? + + fetcher = THUMBNAIL_FETCHERS[provider] + fetcher&.call(embed_url) + rescue StandardError => e + Rails.logger.warn("OpenGeographies thumbnail fetch failed for #{embed_url}: #{e.message}") + nil + end + + def detect_provider(embed_url) + return if embed_url.blank? + + DeserializerV1::PROVIDER_PATTERNS.find { |p| p[:pattern].match?(embed_url) }&.dig(:name) + end + end +end diff --git a/app/services/reindex_service.rb b/app/services/reindex_service.rb new file mode 100644 index 00000000..bc1cb569 --- /dev/null +++ b/app/services/reindex_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Single place responsible for keeping Elasticsearch in sync with a record +# after a write. Cascades to whichever Tour(s) the record belongs to when the +# record itself isn't Searchkick-indexed (e.g. join models like StopMedium). +class ReindexService + class << self + def call(record) + return unless record + + record.reindex if record.respond_to?(:reindex) + tours_for(record).each(&:reindex) + rescue StandardError => e + Rails.logger.error("ReindexService failed for #{record.class}##{record.id}: #{e.message}") + end + + def tours_for(record) + return record.tours if record.respond_to?(:tours) + return [record.tour] if record.respond_to?(:tour) && record.tour + return record.stop.tours if record.respond_to?(:stop) && record.stop + + [] + end + end +end diff --git a/app/services/url_resolver.rb b/app/services/url_resolver.rb new file mode 100644 index 00000000..0a6aa9f2 --- /dev/null +++ b/app/services/url_resolver.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module UrlResolver + def self.resolve(url) + loop do + response = HTTParty.head(url, follow_redirects: false) + break url unless response.redirection? + url = response.headers['location'] + end + end +end diff --git a/app/services/video_providers/base.rb b/app/services/video_providers/base.rb new file mode 100644 index 00000000..48a40256 --- /dev/null +++ b/app/services/video_providers/base.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module VideoProviders + class Base + def initialize(medium) + @medium = medium + end + + def embed_url + v = @medium.embed_id + v.start_with?('http', '//') ? v : "https://#{v}" + end + + def call + @medium.embed = embed_url + fetch_url = @medium.embed.start_with?('//') ? "https:#{@medium.embed}" : @medium.embed + doc = Nokogiri::HTML(HTTParty.get(fetch_url).body) + @medium.title = doc.at_xpath('/html/head/meta[@property="og:title"]')&.[](:content) || + doc.at('title')&.text || + 'A Model' + image_url = doc.at_xpath('/html/head/meta[@property="og:image"]')&.[](:content) + if image_url + @medium.filename = File.basename(URI.parse(image_url).path) + HTTParty.get(image_url).body + else + @medium.filename ||= 'otblogo.png' + File.read(Rails.root.join('public/otblogo.png')) + end + end + end +end diff --git a/app/services/video_providers/matterport.rb b/app/services/video_providers/matterport.rb new file mode 100644 index 00000000..bdf18fa7 --- /dev/null +++ b/app/services/video_providers/matterport.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module VideoProviders + class Matterport < Base + def embed_url + "//my.matterport.com/show/?m=#{@medium.embed_id}" + end + + def call + image_body = super + @medium.filename = "#{@medium.embed_id}.jpg" + image_body + end + end +end diff --git a/app/services/video_providers/morphosource.rb b/app/services/video_providers/morphosource.rb new file mode 100644 index 00000000..ccb97fe7 --- /dev/null +++ b/app/services/video_providers/morphosource.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module VideoProviders + class Morphosource < Base + def embed_url + "//www.morphosource.org/uv.html#?manifest=/manifests/#{@medium.embed_id}&c=0&m=0&cv=0" + end + + def call + @medium.embed = embed_url + manifest = HTTParty.get("https://www.morphosource.org/manifests/#{@medium.embed_id}.json") + @medium.title = manifest.dig('label', '@none', 0) || 'A MorphoSource Model' + @medium.caption = manifest.dig('summary', '@none', 0) + @medium.filename ||= 'otblogo.png' + File.read(Rails.root.join('public/otblogo.png')) + end + end +end diff --git a/app/services/video_providers/sketchfab.rb b/app/services/video_providers/sketchfab.rb new file mode 100644 index 00000000..a8324340 --- /dev/null +++ b/app/services/video_providers/sketchfab.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module VideoProviders + class Sketchfab < Base + def embed_url + "//sketchfab.com/models/#{@medium.embed_id}/embed" + end + end +end diff --git a/app/services/video_providers/soundcloud.rb b/app/services/video_providers/soundcloud.rb new file mode 100644 index 00000000..4551d0b7 --- /dev/null +++ b/app/services/video_providers/soundcloud.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module VideoProviders + class Soundcloud < Base + def embed_url + "//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{@medium.embed_id}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false" + end + + def call + @medium.embed = embed_url + oembed = HTTParty.get( + "https://soundcloud.com/oembed?url=https://api.soundcloud.com/tracks/#{@medium.embed_id}&format=json", + format: :plain, + ) + data = JSON.parse(oembed.body).deep_symbolize_keys + @medium.title = data[:title] + if data[:thumbnail_url] + HTTParty.get(data[:thumbnail_url]).body + else + File.read(Rails.root.join('public/soundcloud.jpg')) + end + end + end +end diff --git a/app/services/video_providers/vimeo.rb b/app/services/video_providers/vimeo.rb new file mode 100644 index 00000000..44a1f381 --- /dev/null +++ b/app/services/video_providers/vimeo.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module VideoProviders + class Vimeo < Base + def embed_url + "//player.vimeo.com/video/#{@medium.embed_id}" + end + + def call + metadata = HTTParty.get("https://vimeo.com/api/oembed.json?url=https%3A//vimeo.com/video/#{@medium.embed_id}") + @medium.title = metadata['title'] + @medium.caption = metadata['description'] + @medium.embed = embed_url + thumbnail_width = metadata['thumbnail_width'] + thumbnail_height = metadata['thumbnail_height'] + scale_by = 1000 / thumbnail_width + thumbnail_url = "#{metadata["thumbnail_url"].split("_")[0]}_#{thumbnail_width * scale_by}x#{thumbnail_height * scale_by}" + HTTParty.get(thumbnail_url).body + end + end +end diff --git a/app/services/video_providers/youtube.rb b/app/services/video_providers/youtube.rb new file mode 100644 index 00000000..90398a49 --- /dev/null +++ b/app/services/video_providers/youtube.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module VideoProviders + class Youtube < Base + def embed_url + "//www.youtube.com/embed/#{@medium.embed_id}" + end + + def call + metadata = Yt::Video.new(id: @medium.embed_id) + @medium.title = metadata.title + @medium.caption = metadata.description + @medium.embed = embed_url + HTTParty.get("https://img.youtube.com/vi/#{@medium.embed_id}/0.jpg").body + rescue Yt::Errors::NoItems + @medium.provider = nil + @medium.embed_id = nil + nil + end + end +end diff --git a/app/uploaders/logo_footer_uploader.rb b/app/uploaders/logo_footer_uploader.rb index e55f44c9..69ce1055 100644 --- a/app/uploaders/logo_footer_uploader.rb +++ b/app/uploaders/logo_footer_uploader.rb @@ -3,15 +3,15 @@ # app/uploarder/medium_uploader.rb class LogoFooterUploader < LogoUploader - # process :get_dimensions + # process :dimensions - before :cache, :get_dimensions # callback, example here: http://goo.gl/9VGHI + before :cache, :dimensions # callback, example here: http://goo.gl/9VGHI private - def get_dimensions - if file && model - model.footer_width, model.footer_height = ::MiniMagick::Image.open(file.file)[:dimensions] - end + def dimensions + return unless file && model + + model.footer_width, model.footer_height = ::MiniMagick::Image.open(file.file)[:dimensions] end end diff --git a/app/uploaders/logo_uploader.rb b/app/uploaders/logo_uploader.rb index 2a63df3f..4b556fb5 100644 --- a/app/uploaders/logo_uploader.rb +++ b/app/uploaders/logo_uploader.rb @@ -13,12 +13,12 @@ class LogoUploader < CarrierWave::Uploader::Base process resize_to_limit: [1000, 80] def store_dir - "#{Rails.root}/public/uploads/#{Apartment::Tenant.current}/" + Rails.root.join("public/uploads/#{Apartment::Tenant.current}/").to_s end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_whitelist - %w(JPG jpg jpeg gif png) + ['JPG', 'jpg', 'jpeg', 'gif', 'png'] end end diff --git a/app/uploaders/medium_uploader.rb b/app/uploaders/medium_uploader.rb index 6da03e62..96fa5c5a 100644 --- a/app/uploaders/medium_uploader.rb +++ b/app/uploaders/medium_uploader.rb @@ -16,7 +16,7 @@ class MediumUploader < CarrierWave::Uploader::Base # end def store_dir - "#{Rails.root}/public/uploads/#{Apartment::Tenant.current}/" + Rails.root.join("public/uploads/#{Apartment::Tenant.current}/").to_s end # Provide a default URL as a default if there hasn't been a file uploaded: @@ -57,14 +57,14 @@ def store_dir # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_whitelist - %w(JPG jpg jpeg gif png) + ['JPG', 'jpg', 'jpeg', 'gif', 'png'] end def filename if model.video.present? "#{model.video}.jpg" elsif defined?(original_filname) && original_filename.starts_with?('original_image') - "#{DateTime.now.strftime('%Q')[0..8]}.#{file.extension}" + "#{Time.zone.now.strftime("%Q")[0..8]}.#{file.extension}" else @filename end @@ -72,21 +72,21 @@ def filename private - def store_desktop_dimensions - if file && model - model.desktop_width, model.desktop_height = ::MiniMagick::Image.open(file.file)[:dimensions] - end - end + def store_desktop_dimensions + return unless file && model - def store_tablet_dimensions - if file && model - model.tablet_width, model.tablet_height = ::MiniMagick::Image.open(file.file)[:dimensions] - end - end + model.desktop_width, model.desktop_height = ::MiniMagick::Image.open(file.file)[:dimensions] + end - def store_mobile_dimensions - if file && model - model.mobile_width, model.mobile_height = ::MiniMagick::Image.open(file.file)[:dimensions] - end - end + def store_tablet_dimensions + return unless file && model + + model.tablet_width, model.tablet_height = ::MiniMagick::Image.open(file.file)[:dimensions] + end + + def store_mobile_dimensions + return unless file && model + + model.mobile_width, model.mobile_height = ::MiniMagick::Image.open(file.file)[:dimensions] + end end diff --git a/app/views/access_request_mailer/access_request_email.html.erb b/app/views/access_request_mailer/access_request_email.html.erb new file mode 100644 index 00000000..9ccfc299 --- /dev/null +++ b/app/views/access_request_mailer/access_request_email.html.erb @@ -0,0 +1,8 @@ +

Request for access to <%= @tour_set.name %> Open Tour Site

+ +

+ <%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to your Open Tour site <%= @tour_set.name %>. +

+

+ View request to approve. +

diff --git a/app/views/access_request_mailer/access_request_email.text.erb b/app/views/access_request_mailer/access_request_email.text.erb new file mode 100644 index 00000000..c2c51c8e --- /dev/null +++ b/app/views/access_request_mailer/access_request_email.text.erb @@ -0,0 +1,5 @@ +Request for access to <%= @tour_set.name %> Open Tour Site + + <%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to your Open Tour site <%= @tour_set.name %>. + + Use this link to view and approve the request: https://opentour.site/admin/approve/<%= @access_request.id %> diff --git a/app/views/access_request_mailer/access_request_tour_email.html.erb b/app/views/access_request_mailer/access_request_tour_email.html.erb new file mode 100644 index 00000000..8e45c296 --- /dev/null +++ b/app/views/access_request_mailer/access_request_tour_email.html.erb @@ -0,0 +1,8 @@ +

Access Request for <%= @tour_set.name %> Open Tour Site

+ +

+ <%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to your Open Tour site <%= @tour_set.name %>. +

+

+ View request to approve. +

diff --git a/app/views/access_request_mailer/access_request_tour_email.text.erb b/app/views/access_request_mailer/access_request_tour_email.text.erb new file mode 100644 index 00000000..4ef1c189 --- /dev/null +++ b/app/views/access_request_mailer/access_request_tour_email.text.erb @@ -0,0 +1,5 @@ +Access Request for <%= @tour_set.name %> Open Tour Site + +<%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to your Open Tour site <%= @tour_set.name %>. + +View request to approve: https://opentour.site/admin/approve/<%= @access_request.id %> diff --git a/app/views/access_request_tour_mailer/access_request_email.html.erb b/app/views/access_request_tour_mailer/access_request_email.html.erb new file mode 100644 index 00000000..4f2f0db2 --- /dev/null +++ b/app/views/access_request_tour_mailer/access_request_email.html.erb @@ -0,0 +1,12 @@ +Request for access to edit <%= @access_request.tour_set.name %> tours on OpenTour +==================================================================================== + +<%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to edit the following tours. + +You can approve each individually by clicking the links below, or approve all here: +APPROVE ALL: <%= approve_all_url %> + +Tours requested: +<% @access_request.requested_tours.each do |tour| %> + - <%= tour.title %>: <%= tour_url(tour) %> +<% end %> \ No newline at end of file diff --git a/app/views/access_request_tour_mailer/access_request_email.text.erb b/app/views/access_request_tour_mailer/access_request_email.text.erb new file mode 100644 index 00000000..36312cc1 --- /dev/null +++ b/app/views/access_request_tour_mailer/access_request_email.text.erb @@ -0,0 +1,5 @@ +Request for access to <%= @tour.title %> on Open Tour + + <%= @access_request.user.display_name %> (<%= @access_request.user.email %>) has requested access to edit <%= @tour.title%>. + + Use this link to view and approve the request: https://opentour.site/admin/approve/<%= @access_request.id %> diff --git a/bin/bundle b/bin/bundle index f19acf5b..2dbb7176 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails index 07396602..a31728ab 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 17240489..c1999550 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/bin/setup b/bin/setup index a334d86a..f17c5668 100755 --- a/bin/setup +++ b/bin/setup @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require 'fileutils' include FileUtils diff --git a/bin/spring b/bin/spring index 9bd27ec6..c8883bcd 100755 --- a/bin/spring +++ b/bin/spring @@ -11,7 +11,7 @@ unless defined?(Spring) lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) spring = lockfile.specs.detect { |spec| spec.name == 'spring' } if spring - Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + Gem.use_paths(Gem.dir, Bundler.bundle_path.to_s, *Gem.path) gem 'spring', spring.version require 'spring/binstub' end diff --git a/bin/update b/bin/update index 67d0d496..0b5306f4 100755 --- a/bin/update +++ b/bin/update @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require 'fileutils' include FileUtils diff --git a/build.sh b/build.sh index ef706fc2..1ea79ccb 100755 --- a/build.sh +++ b/build.sh @@ -1,29 +1,29 @@ #!/bin/bash set -e -TAG=$([ "$BRANCH" == "main" ] && echo "stable" || echo "latest") +# TAG, AWS_ECS_CLUSTER, and AWS_ECS_SERVICE are resolved by the calling +# workflow (see .github/workflows/deploy.yml's "Resolve deploy target" +# step) based on which environment this deploy actually targets — this +# script just builds/pushes/deploys whatever it's told. -AWS_ECS_CLUSTER=$([ "$BRANCH" == "main" ] && echo "$AWS_ECS_CLUSTER_PROD" || echo "$AWS_ECS_CLUSTER_DEV") - -AWS_ECS_SERVICE=$([ "$BRANCH" == "main" ] && echo "$AWS_ECS_SERVICE_PROD" || echo "$AWS_ECS_SERVICE_DEV") - -echo "Building image for branch: $BRANCH with tag: $TAG" +echo "Building image with tag: latest" docker build \ + --platform linux/amd64 \ --file Dockerfile \ -t otb \ . echo "Logging in to AWS" aws ecr get-login-password --region us-east-1 | - docker login --username AWS --password-stdin "${AWS_ECR}" + docker login --username AWS --password-stdin 310867200447.dkr.ecr.us-east-1.amazonaws.com echo "Logged in successfully" -echo "Tagging image with $TAG" -docker tag otb "${AWS_ECR}/otb:${TAG}" +echo "Tagging image with latest" +docker tag otb "310867200447.dkr.ecr.us-east-1.amazonaws.com/otb:latest" echo "Pushing image" -docker push "${AWS_ECR}/otb:${TAG}" +docker push "310867200447.dkr.ecr.us-east-1.amazonaws.com/otb:latest" echo "Force update service" -aws ecs update-service --cluster ${AWS_ECS_CLUSTER} --service ${AWS_ECS_SERVICE} --force-new-deployment --region ${AWS_REGION} \ No newline at end of file +aws ecs update-service --cluster otb-dev --service otb-dev --force-new-deployment --region us-east-1 \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 3933417c..4270a9cc 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,6 +16,9 @@ require 'apartment/elevators/generic' require 'active_storage/engine' require 'ipinfo-rails' +require 'httparty' +require 'http' +require 'json' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -25,18 +28,46 @@ module OpenTourApi # Base class for the app. class Application < Rails::Application class DirectoryElevator < Apartment::Elevators::Generic + def call(env) + request = Rack::Request.new(env) + tenant_name = parse_tenant_name(request) + + return @app.call(env) unless tenant_name + + redirect = renamed_tenant_redirect(tenant_name, request) + return redirect if redirect + + Apartment::Tenant.switch(tenant_name) { @app.call(env) } + end + def parse_tenant_name(request) # request is an instance of Rack::Request tenant_name = request.fullpath.split('/')[1] + tenants_to_ignore = ['auth', 'rails', 'sidekiq', 'favicon.ico', 'health'] + + return 'public' if tenants_to_ignore.include?(tenant_name) - if tenant_name == 'auth' || tenant_name == 'rails' - return nil - end tenant_name end + + private + + # If `tenant_name` is a subdir a TourSet used to have (before a rename), + # redirect to the same path under its current subdir instead of trying + # (and failing) to switch to a Postgres schema that no longer exists + # under that name. + def renamed_tenant_redirect(tenant_name, request) + return if TourSet.exists?(subdir: tenant_name) + + history = TourSetSubdirHistory.find_by(subdir: tenant_name) + return unless history + + new_path = request.fullpath.sub("/#{tenant_name}", "/#{history.tour_set.subdir}") + [308, { 'Location' => new_path }, []] + end end # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.1 + config.load_defaults(7.0) # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -45,13 +76,16 @@ def parse_tenant_name(request) config.middleware.use(ActionDispatch::Cookies) config.middleware.use(ActionDispatch::Session::CookieStore) config.action_dispatch.cookies_serializer = :json - config.middleware.use(IPinfoMiddleware, { token: ENV['IPINFO_TOKEN'] || Rails.application.credentials.dig(:ipinfo) }) + config.middleware.use( + IPinfoMiddleware, + { token: ENV['IPINFO_TOKEN'] || Rails.application.credentials.dig(:ipinfo) }, + ) # Only loads a smaller set of middleware suitable for API only apps. # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true - config.middleware.use DirectoryElevator - # config.relative_url_root = '/*' + config.middleware.use(DirectoryElevator) + config.active_job.queue_adapter = :sidekiq config end end diff --git a/config/boot.rb b/config/boot.rb index 30e594e2..50bffe8d 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -3,3 +3,4 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'logger' diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 00000000..3fb5dcfc --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +G9zUDMGV1jZT69ElArXAqzAREeG7zmWBxrdVrXhChWlcpBGPBq/M6+5MAwrEX6ddr0wBF+Vl2kdnGdOjkVJ0y5VxQHNhB0tDo8sgyBcHLXvkDaa8A/Sl4UfJCKx8JSqRes3kyRX/p2gdrnQT/yzC9Prs6O7JThhrxl1xPStMDFINpMgzWcTBmECdvCB3plED4hFT0Ry/MpQ43hnPxQlz5lScQ8jRjYZvMo42uNagLm7HMYGtM3iMpXNRuiBgHWkWAeOlzcLA0DhDTX+6/tsEa036yKuYI4MPZKxHk0ubbt1022B5tGGpp8kis2cfsLFI2TLlD5/xuazLp7MFAasbnJo9KkBVXFfaPGtUc1nTnaAjFJ6oYmABsvih4rmvbCZFkux/X6riVjU+istTR4A2ENxk7M0M6EJ2eX65QQfbFwN3MC/DJ9QJT5n8zMiUvmFzTKuDqqzBqXxIEdPK8PqSx7tzm17CfdnRttx8YpsWfu3HyEF8nyl7VpvD3JOfQKbd3TzG+poUkwEwJdHuG++GfV2l6rx/qMAvYoFwyS+tCTL9iteHaLoR1+0o/bUjq+Ic1njYD85Mg/qoVX2rCvPYMM/BE4uAJ/q2O9T6enLvyeT2f6oyg3K7eqaIAn1WIpB/WLNIBO82OLYj1ps7HmIRVt83DvBfRf7+TgjO0WM5FhiJcauZEdZoDB19GUdIskk2RMUCZe1QbAKhlc6cUMD5u/3b+8EpINfy2NvlWBxGHonv4ptmQ6uwMNRPX24zfRRftvvHifIUaKnh+x0/3vPv/VXnkLb020PaOgNJCu3L9fid31YZykFvNi4pz5Tuf2/AxTLclgQ0lC0g8/cbCalc3+LOpoqY00uCblDn+CUmpP5H+6Y52h5WLlW3wAPhZ6a6pKmBXn3ZiaG3kC5af41JzghXJz0TNhGXfFEnRlnGu4W2IiBLuLmrxZdDOQ0CLD1k4Pw+FvshpuyPfthnu+WVfqo25SF4ijKOqRd+9UE6wYw31Gc1aflLCemMp/fmK/4oGnXVWNYPagbWwQl/MgL9RqCocB9XN0wNakK2z4uewnK/QGvrUgpV7FoOM0hrhEeronHUow9Eh0cgZGqRRp7jyVu0YG7efNQbdnkqIje1HL5E7mliZ0TJ4M4FzpUS/40UxeVQkY7bYIXEp4gMRM6U6LGmBe35l3OOlIMPnHZelqIXBdEVtWZoZwUMS2gt5BL9Vf+pRc2OJZpiFXX+5FZFpZHYD3KAJwvvcqLNVb5VRkVtveLwemoep4a8X3O0H9lgN0v0FobfoaUzJOSNN41ax1luUPSJbLxfP6eXKyiTVBakUDhe3JYUsomCYKecsTkJwVZoEXgCktZuaQrHw0Qj3y/25r2x5/T3XcQOeM+sqtt+UwcUPThYMFLapaqJAfU0a+As5INUSe7p9C25/Z+hphMsYhjzQ60psALD1JYhpAvGUN3V4gS66Rk/uzEHCVdkI1Lmvgnn2geTtDxna1SEvBZAH0ZVKz60CJIR/nHqiVntdB9HLOhRYifr5fKvd0Sl/5ZuCTLeE2opLe2o5Sxv1z+FBU0VB7jnSgc5rrylcWE2DD/KrgCYOsoWlHupTOD3ipNBTgU2oSS3co52UU/0ILPXo7VnssWHsAlNDIVJfpYoXyCDs0tCTdnpbXqpc+0FJhfcDg==--+eY6+70ZE4bTiVR3--DnWPn63svMUt3UAbeZfcxw== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index faeb8607..254d44cd 100644 --- a/config/database.yml +++ b/config/database.yml @@ -9,14 +9,30 @@ default: &default password: <%= ENV['DB_PASSWORD'] || 'password' %> database: <%= ENV['DB_NAME'] || 'otb' %> -development: - <<: *default - database: <%= ENV['DB_NAME'] || 'otb_development' %> - username: <%= ENV['DB_USERNAME'] || 'user' %> - password: <%= ENV['DB_PASSWORD'] || 'password' %> - host: <%= ENV['DB_HOSTNAME'] || 'localhost' %> +# development: +# <<: *default +# database: <%= ENV['DB_NAME'] || 'otb_development' %> +# username: <%= ENV['DB_USERNAME'] || 'user' %> +# password: <%= ENV['DB_PASSWORD'] || 'password' %> +# host: <%= ENV['DB_HOSTNAME'] || 'localhost' %> test: <<: *default database: <%= ENV['TEST_DB_NAME'] || 'otb_test' %> + password: <%= Rails.application.credentials.dig(:dbTest, :postgres, :pw) %> + username: <%= Rails.application.credentials.dig(:dbTest, :postgres, :user) %> + +production: + <<: *default + database: <%= Rails.application.credentials.dig(:rdsProduction, :db) %> + username: <%= Rails.application.credentials.dig(:rdsProduction, :user) %> + password: <%= Rails.application.credentials.dig(:rdsProduction, :pw) %> + host: <%= Rails.application.credentials.dig(:rdsProduction, :host) %> + +development: + <<: *default + database: <%= Rails.application.credentials.dig(:rdsProduction, :db) %> + username: <%= Rails.application.credentials.dig(:rdsProduction, :user) %> + password: <%= Rails.application.credentials.dig(:rdsProduction, :pw) %> + host: <%= Rails.application.credentials.dig(:rdsProduction, :host) %> \ No newline at end of file diff --git a/config/deploy/production.rb b/config/deploy/production.rb index 906861df..b43b0dba 100644 --- a/config/deploy/production.rb +++ b/config/deploy/production.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + set :branch, 'develop' # server '44.192.30.237', user: 'deploy', roles: %w{app db web}, primary: :my_value -role :app, %w{3.86.138.59 3.236.252.227}, user: 'deploy' -role :web, %w{3.86.138.59 3.236.252.227}, user: 'deploy' -role :db, %w{3.86.138.59 3.236.252.227}, user: 'deploy' +role :app, ['3.86.138.59', '3.236.252.227'], user: 'deploy' +role :web, ['3.86.138.59', '3.236.252.227'], user: 'deploy' +role :db, ['3.86.138.59', '3.236.252.227'], user: 'deploy' set :deploy_to, '/data/otb-api-server' set :ssh_options, { forward_agent: false, - auth_methods: %w(publickey) + auth_methods: ['publickey'], } diff --git a/config/deploy/staging.rb b/config/deploy/staging.rb index 16ba1d28..e748a953 100644 --- a/config/deploy/staging.rb +++ b/config/deploy/staging.rb @@ -7,12 +7,10 @@ # Defines a single server with a list of roles and multiple properties. # You can define all roles on a single server, or split them: -server "3.238.239.164", user: "deploy", roles: %w{app db web}, primary: :my_value +server '3.238.239.164', user: 'deploy', roles: ['app', 'db', 'web'], primary: :my_value # server "otb.ecdsdev.org", user: "deploy", roles: %w{app web}, other_property: :other_value # server "db.otb.ecdsdev.org", user: "deploy", roles: %w{db} - - # role-based syntax # ================== @@ -21,11 +19,9 @@ # property set. Specify the username and a domain or IP for the server. # Don't use `:all`, it's a meta role. -role :app, %w{deploy@3.238.239.164} -role :web, %w{user1@3.238.239.164} -role :db, %w{deploy@3.238.239.164} - - +role :app, ['deploy@3.238.239.164'] +role :web, ['user1@3.238.239.164'] +role :db, ['deploy@3.238.239.164'] # Configuration # ============= @@ -35,8 +31,6 @@ # http://capistranorb.com/documentation/getting-started/configuration/ # Feel free to add new variables to customise your setup. - - # Custom SSH Options # ================== # You may pass any option but keep in mind that net/ssh understands a @@ -45,13 +39,13 @@ # # Global options # -------------- - set :ssh_options, { - forward_agent: false, - auth_methods: %w(publickey) - } +set :ssh_options, { + forward_agent: false, + auth_methods: ['publickey'], +} - set :branch, 'develop' - set :deploy_to, '/data/otb-api' +set :branch, 'develop' +set :deploy_to, '/data/otb-api' # # The server-based syntax can be used to override options: # ------------------------------------ diff --git a/config/environment.rb b/config/environment.rb index f9f287fd..f6d2ab1b 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -6,5 +6,6 @@ # Initialize the Rails application. Rails.application.configure do config.force_ssl = true + config.active_support.to_time_preserves_timezone = :zone end Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 747b0597..65d8803a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,7 +16,7 @@ config.consider_all_requests_local = true # Active Storage - config.active_storage.service = :local + config.active_storage.service = :production # Enable/disable caching. By default caching is disabled. if Rails.root.join('tmp/caching-dev.txt').exist? @@ -24,7 +24,7 @@ config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}", } else config.action_controller.perform_caching = false @@ -41,8 +41,7 @@ config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = false#:page_load - + config.active_record.migration_error = false # :page_load # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 98b48202..57856bab 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Rails.application.configure do - config.hosts << 'api.opentour.site' + config.hosts = nil Rails.application.routes.default_url_options[:host] = 'https://api.opentour.site' # Settings specified here will take precedence over those in config/application.rb. @@ -11,6 +11,8 @@ # Code is not reloaded between requests. config.cache_classes = true + config.force_ssl = false + # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. @@ -47,8 +49,8 @@ # Use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug - + config.log_level = :error + config.active_record.logger = nil # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -64,6 +66,17 @@ # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + # Use the Amazon SESV2 API in the us-east-1 region + config.action_mailer.delivery_method = :ses_v2 + config.action_mailer.ses_v2_settings = { + credentials: Aws::Credentials.new( + Rails.application.credentials.dig(:s3Staging, :access_key_id), + Rails.application.credentials.dig(:s3Staging, :secret_access_key), + ), + region: 'us-east-1', + + } + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true @@ -72,14 +85,14 @@ config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV['RAILS_LOG_TO_STDOUT'].present? - logger = ActiveSupport::Logger.new(STDOUT) + logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end @@ -89,5 +102,4 @@ ENV['BASE_URL'] = 'https://api.opentour.site' ENV['INSECURE_IMAGE_BASE_URL'] = 'http://otbimages.ecdsdev.org' - end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 45c49784..7b50a5db 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -22,7 +22,7 @@ config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}", } else config.action_controller.perform_caching = false @@ -39,8 +39,7 @@ config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = false#:page_load - + config.active_record.migration_error = false # :page_load # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 668a85ce..b5652a2c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -20,7 +20,7 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}", } # Show full error reports and disable caching. @@ -48,11 +48,14 @@ # This is needed for the tests to request tests to pass when subdomain is set. config.action_dispatch.tld_length = 0 config.force_ssl = false - - # config.active_storage.service = :test + config.active_job.queue_adapter = :test + config.active_storage.service = :test + config.middleware.use(IPinfoMiddleware, { token: ENV['IPINFO_TOKEN'] || Rails.application.credentials.dig(:ipinfo) }) + # config.active_record.logger = nil + config.log_level = :info + # config.ipinfo.filter = ->(request) { false } # Never filter out, always lookup # config.consider_all_requests_local = true # config.action_controller.perform_caching = false # config.host = 'localhost:3030' # config.action_controller.default_url_options = { host: 'localhost:3030' } - end diff --git a/config/initializers/active_model_serializer.rb b/config/initializers/active_model_serializer.rb index 60241ef8..a9ff9ed6 100644 --- a/config/initializers/active_model_serializer.rb +++ b/config/initializers/active_model_serializer.rb @@ -8,4 +8,4 @@ require 'active_model_serializers/register_jsonapi_renderer' end -Mime::Type.register 'application/json', :json, %w(text/x-json application/jsonrequest application/vnd.api+json) +Mime::Type.register('application/json', :json, ['text/x-json', 'application/jsonrequest', 'application/vnd.api+json']) diff --git a/config/initializers/active_storage_apartment_fix.rb b/config/initializers/active_storage_apartment_fix.rb new file mode 100644 index 00000000..816504ff --- /dev/null +++ b/config/initializers/active_storage_apartment_fix.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActiveStorageApartmentFix + extend ActiveSupport::Concern + + def key + self[:key] ||= "#{Apartment::Tenant.current}/#{self.class.generate_unique_secure_token(length: 28)}" + end + + class_methods do + def create_before_direct_upload!(**args) + Apartment::Tenant.switch(Apartment::Tenant.current) do + super + end + end + end +end + +Rails.application.config.to_prepare do + ActiveStorage::Blob.prepend(ActiveStorageApartmentFix) + + ActiveRecord::Base.class_eval do + def self.find_signed!(signed_id, purpose: nil) + Apartment::Tenant.switch(Apartment::Tenant.current) do + super + end + end + end +end diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index 46ac452a..8a94a1bc 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -2,7 +2,18 @@ # require 'directory_elevator' Apartment.configure do |config| - config.tenant_names = -> { TourSet.pluck :subdir } - config.excluded_models = ['User', 'Role', 'TourSetAdmin', 'TourSet', 'EcdsRailsAuthEngine::Login', 'EcdsRailsAuthEngine::Token', 'Theme'] + config.tenant_names = -> { TourSet.pluck(:subdir) } + config.excluded_models = [ + 'User', + 'Role', + 'TourSetAdmin', + 'TourSet', + 'TourSetSubdirHistory', + 'EcdsRailsAuthEngine::Login', + 'EcdsRailsAuthEngine::Token', + 'Theme', + 'AccessRequest', + 'OpenGeographiesMedium', + ] config.persistent_schemas = ['shared_extensions'] end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 89d2efab..6d56e439 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf..4b63f289 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/config/initializers/cookie_session.rb b/config/initializers/cookie_session.rb index 53cfc4f4..a4354f5b 100644 --- a/config/initializers/cookie_session.rb +++ b/config/initializers/cookie_session.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true + Rails.application.config.session_store(:cookie_store, key: '_otb_session') Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index f341144d..f1b3844a 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -7,13 +7,22 @@ # Read more: https://github.com/cyu/rack-cors -Rails.application.config.middleware.insert_before 0, Rack::Cors do +Rails.application.config.middleware.insert_before(0, Rack::Cors) do allow do - origins 'https://lvh.me:4200', 'https://otb.ecdsdev.org', 'https://opentour.site', /.*\.opentour.site/, /.*\.lvh.me:4200/, /.*localhost:3000/, /.*\.urbanspatialhistory.org/ - + origins 'https://lvh.me:4200', + 'https://otb.ecdsdev.org', + 'https://opentour.site', + /.*\.opentour.site/, + /.*\.dev\.opentour.site/, + /.*\.lvh.me:4200/, + /.*localhost:3000/, + /.*\.urbanspatialhistory.org/, + /.*\.otb\.ecdsdev.org/, + /.*\.openworldatlanta.org/ resource '*', - headers: :any, - methods: [:get, :post, :put, :patch, :delete, :options, :head], - credentials: true + headers: :any, + methods: [:get, :post, :put, :patch, :delete, :options, :head], + credentials: true, + expose: ['Link'] end end diff --git a/config/initializers/disable_ssl_in_development.rb b/config/initializers/disable_ssl_in_development.rb index 13c334c7..3f5436f6 100644 --- a/config/initializers/disable_ssl_in_development.rb +++ b/config/initializers/disable_ssl_in_development.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + Rails.application.configure { config.force_ssl = false } if Rails.env.development? diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb new file mode 100644 index 00000000..e4b9ec31 --- /dev/null +++ b/config/initializers/elasticsearch.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'searchkick' + +timeout = 10 + +Searchkick.client = if ENV.fetch('RAILS_ENV', nil) == 'production' + Elasticsearch::Client.new( + host: 'https://search.ecdsdev.org', + api_key: Rails.application.credentials.dig(:rdsProduction, :es_api_key), + transport_options: { request: { timeout: }, headers: { content_type: 'application/json' } }, + retry_on_failure: 2, + ) +else + Elasticsearch::Client.new( + host: 'localhost', + transport_options: { request: { timeout: }, headers: { content_type: 'application/json' } }, + ) +end diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb new file mode 100644 index 00000000..d5236504 --- /dev/null +++ b/config/initializers/geocoder.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Geocoder.configure( + lookup: :ipinfo_io, + ipinfo_io: { + api_key: Rails.application.credentials.dig(:ipinfo), + }, +) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9..dc847422 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc189968..be6fedc5 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb index c383d072..c300f213 100644 --- a/config/initializers/new_framework_defaults_5_2.rb +++ b/config/initializers/new_framework_defaults_5_2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # # This file contains migration options to ease your Rails 5.2 upgrade. diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb index 6149760f..922a0266 100644 --- a/config/initializers/pagy.rb +++ b/config/initializers/pagy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # require 'pagy/extras/headers' # Pagy::DEFAULT[:items] = 2 # Pagy::DEFAULT.freeze diff --git a/config/initializers/rails_deprecation_listener.rb b/config/initializers/rails_deprecation_listener.rb new file mode 100644 index 00000000..3d1b90db --- /dev/null +++ b/config/initializers/rails_deprecation_listener.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +ActiveSupport::Notifications.subscribe('deprecation.rails') do |_name, _start, _finish, _id, payload| + YourLogService.notify( + message: ['RAILS 7 DEPRECATION WARNINGS'], + deprecation_warning: warn, + stack_trace: payload[:callstack], + gem_name: payload[:gem_name], + deprecation_horizon: payload[:deprecation_horizon], + ) +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 00000000..68e56dd3 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Sidekiq.configure_server do |config| + config.redis = { url: "#{Rails.application.credentials.dig(ENV["RAILS_ENV"].to_sym, :redis_url)}/8" } + config.logger.level = Logger::ERROR +end + +Sidekiq.configure_client do |config| + config.redis = { url: "#{Rails.application.credentials.dig(ENV["RAILS_ENV"].to_sym, :redis_url)}/8" } +end diff --git a/config/initializers/string.rb b/config/initializers/string.rb index d57e10a7..81bd5232 100644 --- a/config/initializers/string.rb +++ b/config/initializers/string.rb @@ -1,18 +1,20 @@ -require "active_support/inflector" +# frozen_string_literal: true + +require 'active_support/inflector' class String def parameterize_intl(separator: '-', preserve_case: false, locale: nil) # Replace accented chars with their ASCII equivalents. - transliterated_string = ActiveSupport::Inflector.transliterate(self, replacement = '~', locale: locale) + transliterated_string = ActiveSupport::Inflector.transliterate(self, '~', locale: locale) parameterized_string = if transliterated_string.include?('~') - self.gsub(/[!@#$%^&*()-=_+|;':",.<>?\s']/, separator) + gsub(/[!@#$%^&*()-=_+|;':",.<>?\s']/, separator) else transliterated_string.gsub(/[^a-z0-9\-_]+/i, separator) end - unless separator.nil? || separator.empty? - if separator == '_'.freeze + if separator.present? + if separator == '_' re_duplicate_separator = /-{2,}/ re_leading_trailing_separator = /^-|-$/ else @@ -23,7 +25,7 @@ def parameterize_intl(separator: '-', preserve_case: false, locale: nil) # No more than one of the separator in a row. parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. - parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) + parameterized_string.gsub!(re_leading_trailing_separator, '') end parameterized_string.downcase! unless preserve_case diff --git a/config/initializers/version.rb b/config/initializers/version.rb index a0e6e767..7e793723 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module OpenTourApi class Application VERSION = '3.0' end -end \ No newline at end of file +end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index bbfc3961..2f3c0db4 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which diff --git a/config/puma.rb b/config/puma.rb index b2102072..a4a1d353 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV') { 'development' } # Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } +pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 8fbfacdf..d784a31f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,18 +1,12 @@ # frozen_string_literal: true -# config/routes.rb -# The subdomain constraint determins the tenant. -# class SubdomainConstraint -# def self.matches?(request) -# subdomains = %w(www admin public) -# request.subdomain.present? && !subdomains.include?(request.subdomain) -# end -# end +require 'sidekiq/web' Rails.application.routes.draw do - + root 'welcome#index' + get 'health' => 'rails/health#show', as: :rails_health_check resources :tour_set_admins - scope ':tenant' do + scope ':tenant', defaults: { format: :json } do scope module: :v3, constraints: ApiVersion.new('v3', true) do resources :tour_authors, path: 'tour-authors' resources :users @@ -34,8 +28,33 @@ resources :flat_pages, path: 'flat-pages' resources :tour_flat_pages, path: 'tour-flat-pages' resources :geojson_tours - + end + namespace :v4 do + namespace :public do + resources :tours, only: [:index] + resources :modes, only: [:index] + resources :stops, only: [:index] + resources :tour_sets, only: [:index], path: 'tour-sets' + get 'tours/:slug', to: 'tours#show' + get 'stops/:slug', to: 'stops#show' + get 'media/:key', to: 'media#show' + end + namespace :admin do + resources :crud + resources :access_requests + resources :tours, only: [:index, :show, :create] + resources :flat_pages, only: [:index] + resources :tour_sets, only: [:index] + resources :media, only: [:index] + resources :stops, only: [:index] + resources :tour_authors, only: [:index, :create, :destroy] + get 'users', to: 'users#index' + get 'users/me', to: 'users#show' + get 'tour_sets/:slug', to: 'tour_sets#show' + get 'resolve_url', to: 'services#url_resolver' + end end end mount EcdsRailsAuthEngine::Engine, at: '/auth' + mount Sidekiq::Web => '/sidekiq' end diff --git a/config/sidekiq.yml b/config/sidekiq.yml new file mode 100644 index 00000000..e856281b --- /dev/null +++ b/config/sidekiq.yml @@ -0,0 +1,6 @@ +:queues: + - default + - searchkick +:verbose: false +:concurrency: 10 +:timeout: 60 \ No newline at end of file diff --git a/config/spring.rb b/config/spring.rb index 9fa7863f..8ac0be02 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,8 @@ -%w[ - .ruby-version - .rbenv-vars - tmp/restart.txt - tmp/caching-dev.txt +# frozen_string_literal: true + +[ + '.ruby-version', + '.rbenv-vars', + 'tmp/restart.txt', + 'tmp/caching-dev.txt', ].each { |path| Spring.watch(path) } diff --git a/db/migrate/20200213152142_rename_metadescription.rb b/db/migrate/20200213152142_rename_metadescription.rb index b9d3725d..1b9643ea 100644 --- a/db/migrate/20200213152142_rename_metadescription.rb +++ b/db/migrate/20200213152142_rename_metadescription.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + class RenameMetadescription < ActiveRecord::Migration[5.2] def change - begin - rename_column :stops, :metadescription, :meta_description - rescue - # It's fine - end + rename_column(:stops, :metadescription, :meta_description) + rescue StandardError + # It's fine end end diff --git a/db/migrate/20210518143822_add_email.rb b/db/migrate/20210518143822_add_email.rb index fcc835e7..2f30a64d 100644 --- a/db/migrate/20210518143822_add_email.rb +++ b/db/migrate/20210518143822_add_email.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddEmail < ActiveRecord::Migration[5.2] def change - add_column :users, :email, :string + add_column(:users, :email, :string) end end diff --git a/db/migrate/20210602201922_create_active_storage_tables.active_storage.rb b/db/migrate/20210602201922_create_active_storage_tables.active_storage.rb index 0b2ce257..cad9fec2 100644 --- a/db/migrate/20210602201922_create_active_storage_tables.active_storage.rb +++ b/db/migrate/20210602201922_create_active_storage_tables.active_storage.rb @@ -1,27 +1,33 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20170806125915) class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change - create_table :active_storage_blobs do |t| - t.string :key, null: false - t.string :filename, null: false - t.string :content_type - t.text :metadata - t.bigint :byte_size, null: false - t.string :checksum, null: false - t.datetime :created_at, null: false + create_table(:active_storage_blobs) do |t| + t.string(:key, null: false) + t.string(:filename, null: false) + t.string(:content_type) + t.text(:metadata) + t.bigint(:byte_size, null: false) + t.string(:checksum, null: false) + t.datetime(:created_at, null: false) - t.index [ :key ], unique: true + t.index([:key], unique: true) end - create_table :active_storage_attachments do |t| - t.string :name, null: false - t.references :record, null: false, polymorphic: true, index: false - t.references :blob, null: false + create_table(:active_storage_attachments) do |t| + t.string(:name, null: false) + t.references(:record, null: false, polymorphic: true, index: false) + t.references(:blob, null: false) - t.datetime :created_at, null: false + t.datetime(:created_at, null: false) - t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true - t.foreign_key :active_storage_blobs, column: :blob_id + t.index( + [:record_type, :record_id, :name, :blob_id], + name: 'index_active_storage_attachments_uniqueness', + unique: true, + ) + t.foreign_key(:active_storage_blobs, column: :blob_id) end end end diff --git a/db/migrate/20210604131202_add_base64.rb b/db/migrate/20210604131202_add_base64.rb index 11a7dcda..d4dfc787 100644 --- a/db/migrate/20210604131202_add_base64.rb +++ b/db/migrate/20210604131202_add_base64.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddBase64 < ActiveRecord::Migration[6.0] def change - add_column :media, :base64, :text + add_column(:media, :base64, :text) end end diff --git a/db/migrate/20210604132602_add_video_provider.rb b/db/migrate/20210604132602_add_video_provider.rb index 1c408042..6978c5a9 100644 --- a/db/migrate/20210604132602_add_video_provider.rb +++ b/db/migrate/20210604132602_add_video_provider.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddVideoProvider < ActiveRecord::Migration[6.0] def change - add_column :media, :video_provider, :integer, default: 0 + add_column(:media, :video_provider, :integer, default: 0) end end diff --git a/db/migrate/20210604145226_change_base64.rb b/db/migrate/20210604145226_change_base64.rb index 80142312..7eeb78c6 100644 --- a/db/migrate/20210604145226_change_base64.rb +++ b/db/migrate/20210604145226_change_base64.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class ChangeBase64 < ActiveRecord::Migration[6.0] def change - rename_column :media, :base64, :base_sixty_four + rename_column(:media, :base64, :base_sixty_four) end end diff --git a/db/migrate/20210607125915_add_parking_address.rb b/db/migrate/20210607125915_add_parking_address.rb index fd12fa45..c0c1fcdf 100644 --- a/db/migrate/20210607125915_add_parking_address.rb +++ b/db/migrate/20210607125915_add_parking_address.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddParkingAddress < ActiveRecord::Migration[6.0] def change - add_column :stops, :parking_address, :string + add_column(:stops, :parking_address, :string) end end diff --git a/db/migrate/20210607165827_rename_content.rb b/db/migrate/20210607165827_rename_content.rb index 054747b2..4ddadb2c 100644 --- a/db/migrate/20210607165827_rename_content.rb +++ b/db/migrate/20210607165827_rename_content.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class RenameContent < ActiveRecord::Migration[6.0] def change - rename_column :flat_pages, :content, :body + rename_column(:flat_pages, :content, :body) end end diff --git a/db/migrate/20210607221303_add_tour_to_stop_slugs.rb b/db/migrate/20210607221303_add_tour_to_stop_slugs.rb index 25244cb0..8dfbfb44 100644 --- a/db/migrate/20210607221303_add_tour_to_stop_slugs.rb +++ b/db/migrate/20210607221303_add_tour_to_stop_slugs.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddTourToStopSlugs < ActiveRecord::Migration[6.0] def change - add_reference :stop_slugs, :tour, foreign_key: true + add_reference(:stop_slugs, :tour, foreign_key: true) end end diff --git a/db/migrate/20210608211705_create_map_overlays.rb b/db/migrate/20210608211705_create_map_overlays.rb index c4ba6439..5e6fe1f3 100644 --- a/db/migrate/20210608211705_create_map_overlays.rb +++ b/db/migrate/20210608211705_create_map_overlays.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + class CreateMapOverlays < ActiveRecord::Migration[6.0] def change - create_table :map_overlays do |t| - t.decimal "south", precision: 100, scale: 8 - t.decimal "north", precision: 100, scale: 8 - t.decimal "east", precision: 100, scale: 8 - t.decimal "west", precision: 100, scale: 8 - t.references :tour, null: true - t.references :stop, null: true + create_table(:map_overlays) do |t| + t.decimal('south', precision: 100, scale: 8) + t.decimal('north', precision: 100, scale: 8) + t.decimal('east', precision: 100, scale: 8) + t.decimal('west', precision: 100, scale: 8) + t.references(:tour, null: true) + t.references(:stop, null: true) t.timestamps end diff --git a/db/migrate/20210609141823_add_base64_overlay.rb b/db/migrate/20210609141823_add_base64_overlay.rb index 31bb4a60..33f966cf 100644 --- a/db/migrate/20210609141823_add_base64_overlay.rb +++ b/db/migrate/20210609141823_add_base64_overlay.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddBase64Overlay < ActiveRecord::Migration[6.0] def change - add_column :map_overlays, :base_sixty_four, :text + add_column(:map_overlays, :base_sixty_four, :text) end end diff --git a/db/migrate/20210609142132_add_title_overlay.rb b/db/migrate/20210609142132_add_title_overlay.rb index c531694d..5dd1c22b 100644 --- a/db/migrate/20210609142132_add_title_overlay.rb +++ b/db/migrate/20210609142132_add_title_overlay.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddTitleOverlay < ActiveRecord::Migration[6.0] def change - add_column :map_overlays, :title, :text + add_column(:map_overlays, :title, :text) end end diff --git a/db/migrate/20210610141706_add_enable_directions.rb b/db/migrate/20210610141706_add_enable_directions.rb index 1913657a..dc54c4d8 100644 --- a/db/migrate/20210610141706_add_enable_directions.rb +++ b/db/migrate/20210610141706_add_enable_directions.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddEnableDirections < ActiveRecord::Migration[6.0] def change - add_column :tours, :use_directions, :boolean, default: true + add_column(:tours, :use_directions, :boolean, default: true) end end diff --git a/db/migrate/20210610152819_add_default_lang.rb b/db/migrate/20210610152819_add_default_lang.rb index d6532be1..5341b3ef 100644 --- a/db/migrate/20210610152819_add_default_lang.rb +++ b/db/migrate/20210610152819_add_default_lang.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddDefaultLang < ActiveRecord::Migration[6.0] def change - add_column :tours, :default_lng, :integer, default: 0 + add_column(:tours, :default_lng, :integer, default: 0) end end diff --git a/db/migrate/20210610180023_add_icon_color.rb b/db/migrate/20210610180023_add_icon_color.rb index 652bbd67..a94c258e 100644 --- a/db/migrate/20210610180023_add_icon_color.rb +++ b/db/migrate/20210610180023_add_icon_color.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddIconColor < ActiveRecord::Migration[6.0] def change - add_column :stops, :icon_color, :string, default: '#D32F2F' + add_column(:stops, :icon_color, :string, default: '#D32F2F') end end diff --git a/db/migrate/20210610182827_create_map_icons.rb b/db/migrate/20210610182827_create_map_icons.rb index dc200cea..fb95df8e 100644 --- a/db/migrate/20210610182827_create_map_icons.rb +++ b/db/migrate/20210610182827_create_map_icons.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class CreateMapIcons < ActiveRecord::Migration[6.0] def change - create_table :map_icons do |t| - t.text :base_sixty_four + create_table(:map_icons) do |t| + t.text(:base_sixty_four) t.timestamps end diff --git a/db/migrate/20210610183825_add_title_to_map_icons.rb b/db/migrate/20210610183825_add_title_to_map_icons.rb index 6aac4c6d..ec100247 100644 --- a/db/migrate/20210610183825_add_title_to_map_icons.rb +++ b/db/migrate/20210610183825_add_title_to_map_icons.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class AddTitleToMapIcons < ActiveRecord::Migration[6.0] def change - add_column :map_icons, :title, :string - add_reference :stops, :map_icon, null: true, foreign_key: true + add_column(:map_icons, :title, :string) + add_reference(:stops, :map_icon, null: true, foreign_key: true) end end diff --git a/db/migrate/20210614140848_add_logo64_to_tour_sets.rb b/db/migrate/20210614140848_add_logo64_to_tour_sets.rb index 486a0d34..867d77aa 100644 --- a/db/migrate/20210614140848_add_logo64_to_tour_sets.rb +++ b/db/migrate/20210614140848_add_logo64_to_tour_sets.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddLogo64ToTourSets < ActiveRecord::Migration[6.0] def change - add_column :tour_sets, :base_sixty_four, :text + add_column(:tour_sets, :base_sixty_four, :text) end end diff --git a/db/migrate/20210614154357_remove_logo_from_tour_sets.rb b/db/migrate/20210614154357_remove_logo_from_tour_sets.rb index 92e49e5f..8c43c67d 100644 --- a/db/migrate/20210614154357_remove_logo_from_tour_sets.rb +++ b/db/migrate/20210614154357_remove_logo_from_tour_sets.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class RemoveLogoFromTourSets < ActiveRecord::Migration[6.0] def change - remove_column :tour_sets, :logo + remove_column(:tour_sets, :logo) end end diff --git a/db/migrate/20210614154939_add_logo_title_to_tour_sets.rb b/db/migrate/20210614154939_add_logo_title_to_tour_sets.rb index d034390b..8b16f985 100644 --- a/db/migrate/20210614154939_add_logo_title_to_tour_sets.rb +++ b/db/migrate/20210614154939_add_logo_title_to_tour_sets.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddLogoTitleToTourSets < ActiveRecord::Migration[6.0] def change - add_column :tour_sets, :logo_title, :string + add_column(:tour_sets, :logo_title, :string) end end diff --git a/db/migrate/20210702214539_add_file_urls_to_media.rb b/db/migrate/20210702214539_add_file_urls_to_media.rb index e645d8c4..b708b96c 100644 --- a/db/migrate/20210702214539_add_file_urls_to_media.rb +++ b/db/migrate/20210702214539_add_file_urls_to_media.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class AddFileUrlsToMedia < ActiveRecord::Migration[6.0] def change - add_column :media, :mobile, :string - add_column :media, :tablet, :string - add_column :media, :desktop, :string + add_column(:media, :mobile, :string) + add_column(:media, :tablet, :string) + add_column(:media, :desktop, :string) end end diff --git a/db/migrate/20210706150527_add_filename_to_media.rb b/db/migrate/20210706150527_add_filename_to_media.rb index 125fe154..cf026159 100644 --- a/db/migrate/20210706150527_add_filename_to_media.rb +++ b/db/migrate/20210706150527_add_filename_to_media.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddFilenameToMedia < ActiveRecord::Migration[6.0] def change - add_column :media, :filename, :string + add_column(:media, :filename, :string) end end diff --git a/db/migrate/20210707161803_change_title_to_filename.rb b/db/migrate/20210707161803_change_title_to_filename.rb index a2eaa124..ffb754ab 100644 --- a/db/migrate/20210707161803_change_title_to_filename.rb +++ b/db/migrate/20210707161803_change_title_to_filename.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class ChangeTitleToFilename < ActiveRecord::Migration[6.0] def change - rename_column :map_icons, :title, :filename - rename_column :map_overlays, :title, :filename + rename_column(:map_icons, :title, :filename) + rename_column(:map_overlays, :title, :filename) end end diff --git a/db/migrate/20210712234206_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20210712234206_add_service_name_to_active_storage_blobs.active_storage.rb index 9967a132..72dc90fd 100644 --- a/db/migrate/20210712234206_add_service_name_to_active_storage_blobs.active_storage.rb +++ b/db/migrate/20210712234206_add_service_name_to_active_storage_blobs.active_storage.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20190112182829) class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up - unless column_exists?(:active_storage_blobs, :service_name) - add_column :active_storage_blobs, :service_name, :string + return if column_exists?(:active_storage_blobs, :service_name) - if configured_service = ActiveStorage::Blob.service.name - ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) - end + add_column(:active_storage_blobs, :service_name, :string) - change_column :active_storage_blobs, :service_name, :string, null: false + if (configured_service = ActiveStorage::Blob.service.name) + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end + + change_column(:active_storage_blobs, :service_name, :string, null: false) end def down - remove_column :active_storage_blobs, :service_name + remove_column(:active_storage_blobs, :service_name) end end diff --git a/db/migrate/20210712234207_create_active_storage_variant_records.active_storage.rb b/db/migrate/20210712234207_create_active_storage_variant_records.active_storage.rb index a2862695..602cb1e2 100644 --- a/db/migrate/20210712234207_create_active_storage_variant_records.active_storage.rb +++ b/db/migrate/20210712234207_create_active_storage_variant_records.active_storage.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20191206030411) class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] def change - create_table :active_storage_variant_records do |t| - t.belongs_to :blob, null: false, index: false - t.string :variation_digest, null: false + create_table(:active_storage_variant_records) do |t| + t.belongs_to(:blob, null: false, index: false) + t.string(:variation_digest, null: false) - t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true - t.foreign_key :active_storage_blobs, column: :blob_id + t.index([:blob_id, :variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true) + t.foreign_key(:active_storage_blobs, column: :blob_id) end end end diff --git a/db/migrate/20210816124019_add_link_to_tour.rb b/db/migrate/20210816124019_add_link_to_tour.rb index f1b74eb9..a1172c0c 100644 --- a/db/migrate/20210816124019_add_link_to_tour.rb +++ b/db/migrate/20210816124019_add_link_to_tour.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class AddLinkToTour < ActiveRecord::Migration[6.1] def change - add_column :tours, :link_address, :string - add_column :tours, :link_text, :string + add_column(:tours, :link_address, :string) + add_column(:tours, :link_text, :string) end end diff --git a/db/migrate/20210831190245_change_id_types.rb b/db/migrate/20210831190245_change_id_types.rb index 8ad84f5f..c984b9fc 100644 --- a/db/migrate/20210831190245_change_id_types.rb +++ b/db/migrate/20210831190245_change_id_types.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class ChangeIdTypes < ActiveRecord::Migration[6.1] def change - change_column :tours, :mode_id, :bigint - change_column :tours, :splash_image_medium_id, :bigint + change_column(:tours, :mode_id, :bigint) + change_column(:tours, :splash_image_medium_id, :bigint) end end diff --git a/db/migrate/20210831200851_change_body_type_for_flat_pages.rb b/db/migrate/20210831200851_change_body_type_for_flat_pages.rb index d26feaa4..988308a6 100644 --- a/db/migrate/20210831200851_change_body_type_for_flat_pages.rb +++ b/db/migrate/20210831200851_change_body_type_for_flat_pages.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class ChangeBodyTypeForFlatPages < ActiveRecord::Migration[6.1] def change - change_column :flat_pages, :body, :text + change_column(:flat_pages, :body, :text) end end diff --git a/db/migrate/20210831202533_drop_tag_tables.rb b/db/migrate/20210831202533_drop_tag_tables.rb index 7ab5a076..446948ba 100644 --- a/db/migrate/20210831202533_drop_tag_tables.rb +++ b/db/migrate/20210831202533_drop_tag_tables.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class DropTagTables < ActiveRecord::Migration[6.1] def change - drop_table :tour_tags - drop_table :stop_tags - drop_table :tags + drop_table(:tour_tags) + drop_table(:stop_tags) + drop_table(:tags) end end diff --git a/db/migrate/20210902145305_add_widths_to_medium.rb b/db/migrate/20210902145305_add_widths_to_medium.rb index f37112ea..5ab550c3 100644 --- a/db/migrate/20210902145305_add_widths_to_medium.rb +++ b/db/migrate/20210902145305_add_widths_to_medium.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddWidthsToMedium < ActiveRecord::Migration[6.1] def change - add_column :media, :lqip_width, :integer + add_column(:media, :lqip_width, :integer) end end diff --git a/db/migrate/20210902164843_fix_slug_id.rb b/db/migrate/20210902164843_fix_slug_id.rb index 8e117d92..6e39b5da 100644 --- a/db/migrate/20210902164843_fix_slug_id.rb +++ b/db/migrate/20210902164843_fix_slug_id.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class FixSlugId < ActiveRecord::Migration[6.1] def change - remove_column :slugs, :id - add_column :slugs, :id, :primary_key + remove_column(:slugs, :id) + add_column(:slugs, :id, :primary_key) end end diff --git a/db/migrate/20210916170901_change_lat_lng_type.rb b/db/migrate/20210916170901_change_lat_lng_type.rb index 2982d935..2508c572 100644 --- a/db/migrate/20210916170901_change_lat_lng_type.rb +++ b/db/migrate/20210916170901_change_lat_lng_type.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + class ChangeLatLngType < ActiveRecord::Migration[6.1] def change - change_column :map_overlays, :south, :string - change_column :map_overlays, :north, :string - change_column :map_overlays, :east, :string - change_column :map_overlays, :west, :string - change_column :stops, :lat, :string - change_column :stops, :lng, :string - change_column :stops, :parking_lat, :string - change_column :stops, :parking_lng, :string + change_column(:map_overlays, :south, :string) + change_column(:map_overlays, :north, :string) + change_column(:map_overlays, :east, :string) + change_column(:map_overlays, :west, :string) + change_column(:stops, :lat, :string) + change_column(:stops, :lng, :string) + change_column(:stops, :parking_lat, :string) + change_column(:stops, :parking_lng, :string) end end diff --git a/db/migrate/20211011120714_add_duration.rb b/db/migrate/20211011120714_add_duration.rb index 89f8f8dc..256e6fa3 100644 --- a/db/migrate/20211011120714_add_duration.rb +++ b/db/migrate/20211011120714_add_duration.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class AddDuration < ActiveRecord::Migration[6.1] def change - add_column :tours, :duration, :integer - add_column :tours, :saved_stop_order, :integer, array: true + add_column(:tours, :duration, :integer) + add_column(:tours, :saved_stop_order, :integer, array: true) end end diff --git a/db/migrate/20220207133352_add_over_bounds_restrition_to_tour.rb b/db/migrate/20220207133352_add_over_bounds_restrition_to_tour.rb index 3c5d38dc..32a9e897 100644 --- a/db/migrate/20220207133352_add_over_bounds_restrition_to_tour.rb +++ b/db/migrate/20220207133352_add_over_bounds_restrition_to_tour.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddOverBoundsRestritionToTour < ActiveRecord::Migration[6.1] def change - add_column :tours, :restrict_bounds_to_overlay, :boolean, default: false + add_column(:tours, :restrict_bounds_to_overlay, :boolean, default: false) end end diff --git a/db/migrate/20220208172935_add_blank_map_to_tour.rb b/db/migrate/20220208172935_add_blank_map_to_tour.rb index a3932223..30fabb34 100644 --- a/db/migrate/20220208172935_add_blank_map_to_tour.rb +++ b/db/migrate/20220208172935_add_blank_map_to_tour.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddBlankMapToTour < ActiveRecord::Migration[6.1] def change - add_column :tours, :blank_map, :boolean, default: false + add_column(:tours, :blank_map, :boolean, default: false) end end diff --git a/db/migrate/20220210160507_set_geo_default.rb b/db/migrate/20220210160507_set_geo_default.rb index 6b30c1ed..c0705b25 100644 --- a/db/migrate/20220210160507_set_geo_default.rb +++ b/db/migrate/20220210160507_set_geo_default.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class SetGeoDefault < ActiveRecord::Migration[6.1] def change - change_column :tours, :is_geo, :boolean, default: true + change_column(:tours, :is_geo, :boolean, default: true) end end diff --git a/db/migrate/20220211142554_add_restrict_bounds_to_tour.rb b/db/migrate/20220211142554_add_restrict_bounds_to_tour.rb index 2e6eecb9..68224299 100644 --- a/db/migrate/20220211142554_add_restrict_bounds_to_tour.rb +++ b/db/migrate/20220211142554_add_restrict_bounds_to_tour.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddRestrictBoundsToTour < ActiveRecord::Migration[6.1] def change - add_column :tours, :restrict_bounds, :boolean, default: true + add_column(:tours, :restrict_bounds, :boolean, default: true) end end diff --git a/db/migrate/20220728131300_add_term_accepted_to_user.rb.rb b/db/migrate/20220728131300_add_term_accepted_to_user.rb.rb index 314a1205..d25347c4 100644 --- a/db/migrate/20220728131300_add_term_accepted_to_user.rb.rb +++ b/db/migrate/20220728131300_add_term_accepted_to_user.rb.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddTermsAcceptedToUsers < ActiveRecord::Migration[6.1] def change - add_column :users, :terms_accepted, :boolean, default: false + add_column(:users, :terms_accepted, :boolean, default: false) end end diff --git a/db/migrate/20260217142949_add_column_rotation_to_map_overlay.rb b/db/migrate/20260217142949_add_column_rotation_to_map_overlay.rb new file mode 100644 index 00000000..89724b8d --- /dev/null +++ b/db/migrate/20260217142949_add_column_rotation_to_map_overlay.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddColumnRotationToMapOverlay < ActiveRecord::Migration[8.0] + def change + add_column(:map_overlays, :rotation, :integer, default: 0) + end +end diff --git a/db/migrate/20260305190704_create_access_requests.rb b/db/migrate/20260305190704_create_access_requests.rb new file mode 100644 index 00000000..ad57a203 --- /dev/null +++ b/db/migrate/20260305190704_create_access_requests.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateAccessRequests < ActiveRecord::Migration[8.0] + def change + create_table(:access_requests) do |t| + t.string(:tour_set, null: false) + t.integer(:tour, null: true) + t.belongs_to(:user, foreign_key: true) + t.boolean(:approved, default: false) + t.timestamps + end + end +end diff --git a/db/migrate/20260316232850_remove_tour_from_stop_slugs.rb b/db/migrate/20260316232850_remove_tour_from_stop_slugs.rb new file mode 100644 index 00000000..33e97017 --- /dev/null +++ b/db/migrate/20260316232850_remove_tour_from_stop_slugs.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveTourFromStopSlugs < ActiveRecord::Migration[8.0] + def change + remove_reference(:stop_slugs, :tour, null: false, foreign_key: true) + end +end diff --git a/db/migrate/20260318162153_add_description_to_tour_set.rb b/db/migrate/20260318162153_add_description_to_tour_set.rb new file mode 100644 index 00000000..90e87697 --- /dev/null +++ b/db/migrate/20260318162153_add_description_to_tour_set.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDescriptionToTourSet < ActiveRecord::Migration[8.0] + def change + add_column(:tour_sets, :description, :text) + end +end diff --git a/db/migrate/20260324165917_update_access_requests_tour_set_to_reference.rb b/db/migrate/20260324165917_update_access_requests_tour_set_to_reference.rb new file mode 100644 index 00000000..aa005f9e --- /dev/null +++ b/db/migrate/20260324165917_update_access_requests_tour_set_to_reference.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class UpdateAccessRequestsTourSetToReference < ActiveRecord::Migration[8.0] + def change + remove_column(:access_requests, :tour_set, :string) + add_reference(:access_requests, :tour_set, foreign_key: true, null: false) + end +end diff --git a/db/migrate/20260325165951_add_tour_ids_to_access_request.rb b/db/migrate/20260325165951_add_tour_ids_to_access_request.rb new file mode 100644 index 00000000..e7b4642e --- /dev/null +++ b/db/migrate/20260325165951_add_tour_ids_to_access_request.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddTourIdsToAccessRequest < ActiveRecord::Migration[8.0] + def change + add_column(:access_requests, :tour_ids, :integer, array: true, default: []) + end +end diff --git a/db/migrate/20260521125403_add_fields_to_tour.rb b/db/migrate/20260521125403_add_fields_to_tour.rb new file mode 100644 index 00000000..a77df232 --- /dev/null +++ b/db/migrate/20260521125403_add_fields_to_tour.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddFieldsToTour < ActiveRecord::Migration[8.0] + def change + add_column(:tours, :open_geographies, :boolean) + add_column(:tours, :open_geographies_endpoint, :string) + end +end diff --git a/db/migrate/20260624170941_add_published_on_to_tour.rb b/db/migrate/20260624170941_add_published_on_to_tour.rb new file mode 100644 index 00000000..22df8312 --- /dev/null +++ b/db/migrate/20260624170941_add_published_on_to_tour.rb @@ -0,0 +1,5 @@ +class AddPublishedOnToTour < ActiveRecord::Migration[8.0] + def change + add_column :tours, :published_on, :date + end +end diff --git a/db/migrate/20260624172750_add_unique_index_to_articles_tour.rb b/db/migrate/20260624172750_add_unique_index_to_articles_tour.rb new file mode 100644 index 00000000..712b1095 --- /dev/null +++ b/db/migrate/20260624172750_add_unique_index_to_articles_tour.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddUniqueIndexToArticlesTour < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + add_index( + :tours, + 'LOWER(title)', + name: 'index_articles_on_lower_title', + unique: true, + algorithm: :concurrently, + ) + end +end diff --git a/db/migrate/20260626141653_add_embed_id_to_medium.rb b/db/migrate/20260626141653_add_embed_id_to_medium.rb new file mode 100644 index 00000000..c44cb92e --- /dev/null +++ b/db/migrate/20260626141653_add_embed_id_to_medium.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddEmbedIdToMedium < ActiveRecord::Migration[8.0] + def change + add_column(:media, :embed_id, :string) + # rubocop:disable Rails/ReversibleMigration + # The reverse is just to delete the column + execute(<<-SQL) + UPDATE media#{" "} + SET embed_id = video + SQL + # rubocop:enable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20260627000000_add_map_icon_id_to_tours.rb b/db/migrate/20260627000000_add_map_icon_id_to_tours.rb new file mode 100644 index 00000000..be2bac3f --- /dev/null +++ b/db/migrate/20260627000000_add_map_icon_id_to_tours.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMapIconIdToTours < ActiveRecord::Migration[8.0] + def change + add_reference(:tours, :map_icon, foreign_key: true, index: true) + end +end diff --git a/db/migrate/20260721190202_create_tour_set_subdir_histories.rb b/db/migrate/20260721190202_create_tour_set_subdir_histories.rb new file mode 100644 index 00000000..bd79d080 --- /dev/null +++ b/db/migrate/20260721190202_create_tour_set_subdir_histories.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateTourSetSubdirHistories < ActiveRecord::Migration[8.0] + def change + create_table(:tour_set_subdir_histories) do |t| + t.references(:tour_set, null: false, foreign_key: true) + t.string(:subdir, null: false) + + t.timestamps + end + add_index(:tour_set_subdir_histories, :subdir, unique: true) + add_index(:tour_sets, :subdir, unique: true) + end +end diff --git a/db/migrate/20260721191705_add_unique_index_to_tour_set_name.rb b/db/migrate/20260721191705_add_unique_index_to_tour_set_name.rb new file mode 100644 index 00000000..795b82bc --- /dev/null +++ b/db/migrate/20260721191705_add_unique_index_to_tour_set_name.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUniqueIndexToTourSetName < ActiveRecord::Migration[8.0] + def change + add_index(:tour_sets, :name, unique: true) + end +end diff --git a/db/migrate/20260724120000_create_voice_overs.rb b/db/migrate/20260724120000_create_voice_overs.rb new file mode 100644 index 00000000..52831339 --- /dev/null +++ b/db/migrate/20260724120000_create_voice_overs.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateVoiceOvers < ActiveRecord::Migration[8.0] + def change + create_table(:voice_overs) do |t| + t.references(:tour, foreign_key: true) + t.references(:stop, foreign_key: true) + t.string(:language, null: false, default: 'en') + + t.timestamps + end + end +end diff --git a/db/migrate/20260727130000_add_unique_index_to_voice_overs_language.rb b/db/migrate/20260727130000_add_unique_index_to_voice_overs_language.rb new file mode 100644 index 00000000..f7f8df6a --- /dev/null +++ b/db/migrate/20260727130000_add_unique_index_to_voice_overs_language.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddUniqueIndexToVoiceOversLanguage < ActiveRecord::Migration[8.0] + def change + # A plain composite unique index on (tour_id, stop_id, language) would not work here: + # Postgres treats NULL as distinct from NULL, and stop_id/tour_id is always NULL on + # one side of the tour-or-stop split, so it would never actually block a duplicate. + add_index( + :voice_overs, + [:tour_id, :language], + unique: true, + where: 'stop_id IS NULL', + name: 'index_voice_overs_on_tour_and_language', + ) + + add_index( + :voice_overs, + [:stop_id, :language], + unique: true, + where: 'tour_id IS NULL', + name: 'index_voice_overs_on_stop_and_language', + ) + end +end diff --git a/db/migrate/20260817160809_add_unique_index_to_tour_authors.rb b/db/migrate/20260817160809_add_unique_index_to_tour_authors.rb new file mode 100644 index 00000000..960cb8fc --- /dev/null +++ b/db/migrate/20260817160809_add_unique_index_to_tour_authors.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddUniqueIndexToTourAuthors < ActiveRecord::Migration[8.0] + def up + # Remove any pre-existing duplicate (tour_id, user_id) pairs before + # adding the unique index below, keeping the earliest record for each + # pair. Without this, the index creation would fail outright on any + # tenant schema that already has duplicates. + execute(<<~SQL.squish) + DELETE FROM tour_authors + WHERE id NOT IN ( + SELECT MIN(id) + FROM tour_authors + GROUP BY tour_id, user_id + ) + SQL + + add_index(:tour_authors, [:tour_id, :user_id], unique: true) + end + + def down + remove_index(:tour_authors, [:tour_id, :user_id]) + end +end diff --git a/db/migrate/20260824123820_add_travel_and_read_duration_to_tour.rb b/db/migrate/20260824123820_add_travel_and_read_duration_to_tour.rb new file mode 100644 index 00000000..393568a9 --- /dev/null +++ b/db/migrate/20260824123820_add_travel_and_read_duration_to_tour.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddTravelAndReadDurationToTour < ActiveRecord::Migration[8.0] + def change + add_column(:tours, :travel_duration, :integer) + add_column(:tours, :read_duration, :integer) + end +end diff --git a/db/migrate/20260903000000_create_open_geographies_media.rb b/db/migrate/20260903000000_create_open_geographies_media.rb new file mode 100644 index 00000000..680065b5 --- /dev/null +++ b/db/migrate/20260903000000_create_open_geographies_media.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateOpenGeographiesMedia < ActiveRecord::Migration[8.0] + def change + create_table(:open_geographies_media) do |t| + t.string(:uuid, null: false, index: { unique: true }) + t.string(:provider) + t.string(:embed_url) + t.string(:thumbnail_url) + t.timestamps + end + end +end diff --git a/db/migrate/20261001134924_add_icon_color_to_tours.rb b/db/migrate/20261001134924_add_icon_color_to_tours.rb new file mode 100644 index 00000000..07f4f810 --- /dev/null +++ b/db/migrate/20261001134924_add_icon_color_to_tours.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIconColorToTours < ActiveRecord::Migration[8.0] + def change + add_column(:tours, :icon_color, :string, default: '#D32F2F') + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b66af81..6e569455 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,19 +10,30 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_08_01_155837) do - +ActiveRecord::Schema[8.0].define(version: 2026_10_01_134924) do # These are extensions that must be enabled in order to support this database - enable_extension "pgcrypto" - enable_extension "plpgsql" - enable_extension "uuid-ossp" + enable_extension "pg_catalog.plpgsql" + enable_extension "shared_extensions.pgcrypto" + enable_extension "shared_extensions.uuid-ossp" + + create_table "access_requests", force: :cascade do |t| + t.integer "tour" + t.bigint "user_id" + t.boolean "approved", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "tour_set_id", null: false + t.integer "tour_ids", default: [], array: true + t.index ["tour_set_id"], name: "index_access_requests_on_tour_set_id" + t.index ["user_id"], name: "index_access_requests_on_user_id" + end create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end @@ -34,7 +45,7 @@ t.text "metadata" t.bigint "byte_size", null: false t.string "checksum", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end @@ -49,23 +60,25 @@ t.string "who" t.string "provider" t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.bigint "{}_id" + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["user_id"], name: "index_ecds_rails_auth_engine_logins_on_user_id" + t.index ["{}_id"], name: "index_ecds_rails_auth_engine_logins_on_{}_id" end create_table "ecds_rails_auth_engine_tokens", force: :cascade do |t| t.string "token" t.bigint "login_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end create_table "flat_pages", force: :cascade do |t| t.string "title" t.text "body" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "position" end @@ -76,8 +89,8 @@ t.string "uid" t.string "single_use_oauth2_token" t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "provider" t.string "confirm_token" t.index ["user_id"], name: "index_logins_on_user_id" @@ -85,8 +98,8 @@ create_table "map_icons", force: :cascade do |t| t.text "base_sixty_four" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "filename" end @@ -97,10 +110,11 @@ t.string "west" t.bigint "tour_id" t.bigint "stop_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "base_sixty_four" t.text "filename" + t.integer "rotation", default: 0 t.index ["stop_id"], name: "index_map_overlays_on_stop_id" t.index ["tour_id"], name: "index_map_overlays_on_tour_id" end @@ -109,8 +123,8 @@ t.string "title" t.text "caption" t.string "original_image" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "video" t.string "provider" t.string "embed" @@ -127,13 +141,24 @@ t.string "desktop" t.string "filename" t.integer "lqip_width" + t.string "embed_id" end create_table "modes", force: :cascade do |t| t.string "title" + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false + t.string "icon" + end + + create_table "open_geographies_media", force: :cascade do |t| + t.string "uuid", null: false + t.string "provider" + t.string "embed_url" + t.string "thumbnail_url" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "icon" + t.index ["uuid"], name: "index_open_geographies_media_on_uuid", unique: true end create_table "roles", force: :cascade do |t| @@ -143,16 +168,16 @@ create_table "slugs", force: :cascade do |t| t.string "slug" t.bigint "tour_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["tour_id"], name: "index_slugs_on_tour_id" end create_table "stop_media", force: :cascade do |t| t.bigint "stop_id" t.bigint "medium_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "position" t.index ["medium_id"], name: "index_stop_media_on_medium_id" t.index ["stop_id"], name: "index_stop_media_on_stop_id" @@ -161,11 +186,9 @@ create_table "stop_slugs", force: :cascade do |t| t.string "slug" t.bigint "stop_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "tour_id" + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["stop_id"], name: "index_stop_slugs_on_stop_id" - t.index ["tour_id"], name: "index_stop_slugs_on_tour_id" end create_table "stops", force: :cascade do |t| @@ -181,8 +204,8 @@ t.string "parking_lng" t.text "direction_intro" t.text "direction_notes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "address" t.bigint "medium_id" t.string "parking_address" @@ -199,7 +222,7 @@ t.string "tagger_type" t.integer "tagger_id" t.string "context", limit: 128 - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["context"], name: "index_taggings_on_context" t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true t.index ["tag_id"], name: "index_taggings_on_tag_id" @@ -213,29 +236,30 @@ create_table "themes", force: :cascade do |t| t.string "title" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end create_table "tour_authors", force: :cascade do |t| t.bigint "tour_id" t.bigint "user_id" + t.index ["tour_id", "user_id"], name: "index_tour_authors_on_tour_id_and_user_id", unique: true t.index ["tour_id"], name: "index_tour_authors_on_tour_id" t.index ["user_id"], name: "index_tour_authors_on_user_id" end create_table "tour_collections", force: :cascade do |t| t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end create_table "tour_flat_pages", force: :cascade do |t| t.bigint "tour_id" t.bigint "flat_page_id" t.integer "position" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["flat_page_id"], name: "index_tour_flat_pages_on_flat_page_id" t.index ["tour_id"], name: "index_tour_flat_pages_on_tour_id" end @@ -243,8 +267,8 @@ create_table "tour_media", force: :cascade do |t| t.bigint "tour_id" t.bigint "medium_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "position" t.index ["medium_id"], name: "index_tour_media_on_medium_id" t.index ["tour_id"], name: "index_tour_media_on_tour_id" @@ -253,8 +277,8 @@ create_table "tour_modes", force: :cascade do |t| t.bigint "tour_id" t.bigint "mode_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["mode_id"], name: "index_tour_modes_on_mode_id" t.index ["tour_id"], name: "index_tour_modes_on_tour_id" end @@ -269,10 +293,19 @@ t.index ["user_id"], name: "index_tour_set_admins_on_user_id" end - create_table "tour_sets", force: :cascade do |t| - t.string "name" + create_table "tour_set_subdir_histories", force: :cascade do |t| + t.bigint "tour_set_id", null: false + t.string "subdir", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["subdir"], name: "index_tour_set_subdir_histories_on_subdir", unique: true + t.index ["tour_set_id"], name: "index_tour_set_subdir_histories_on_tour_set_id" + end + + create_table "tour_sets", force: :cascade do |t| + t.string "name" + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "subdir" t.bigint "tour_id" t.string "external_url" @@ -280,6 +313,9 @@ t.string "footer_logo" t.text "base_sixty_four" t.string "logo_title" + t.text "description" + t.index ["name"], name: "index_tour_sets_on_name", unique: true + t.index ["subdir"], name: "index_tour_sets_on_subdir", unique: true t.index ["tour_id"], name: "index_tour_sets_on_tours_id" end @@ -287,8 +323,8 @@ t.bigint "tour_id" t.bigint "stop_id" t.integer "position" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["stop_id"], name: "index_tour_stops_on_stop_id" t.index ["tour_id"], name: "index_tour_stops_on_tour_id" end @@ -301,8 +337,8 @@ t.boolean "is_geo", default: true t.boolean "published" t.bigint "theme_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.bigint "mode_id" t.integer "position" t.bigint "splash_image_medium_id" @@ -318,6 +354,15 @@ t.boolean "restrict_bounds_to_overlay", default: false t.boolean "blank_map", default: false t.boolean "restrict_bounds", default: true + t.boolean "open_geographies" + t.string "open_geographies_endpoint" + t.date "published_on" + t.bigint "map_icon_id" + t.integer "travel_duration" + t.integer "read_duration" + t.string "icon_color", default: "#D32F2F" + t.index "lower((title)::text)", name: "index_articles_on_lower_title", unique: true + t.index ["map_icon_id"], name: "index_tours_on_map_icon_id" t.index ["medium_id"], name: "index_tours_on_medium_id" t.index ["mode_id"], name: "index_tours_on_mode_id" t.index ["theme_id"], name: "index_tours_on_theme_id" @@ -326,23 +371,40 @@ create_table "users", force: :cascade do |t| t.string "display_name" t.bigint "login_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "super", default: false t.string "email" t.boolean "terms_accepted", default: false t.index ["login_id"], name: "index_users_on_login_id", unique: true end + create_table "voice_overs", force: :cascade do |t| + t.bigint "tour_id" + t.bigint "stop_id" + t.string "language", default: "en", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["stop_id", "language"], name: "index_voice_overs_on_stop_and_language", unique: true, where: "(tour_id IS NULL)" + t.index ["stop_id"], name: "index_voice_overs_on_stop_id" + t.index ["tour_id", "language"], name: "index_voice_overs_on_tour_and_language", unique: true, where: "(stop_id IS NULL)" + t.index ["tour_id"], name: "index_voice_overs_on_tour_id" + end + + add_foreign_key "access_requests", "tour_sets" + add_foreign_key "access_requests", "users" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" - add_foreign_key "stop_slugs", "tours" add_foreign_key "stops", "map_icons" add_foreign_key "stops", "media" add_foreign_key "tour_set_admins", "roles" + add_foreign_key "tour_set_subdir_histories", "tour_sets" add_foreign_key "tour_sets", "tours" + add_foreign_key "tours", "map_icons" add_foreign_key "tours", "media" add_foreign_key "tours", "media", column: "splash_image_medium_id" add_foreign_key "tours", "modes" add_foreign_key "users", "logins" + add_foreign_key "voice_overs", "stops" + add_foreign_key "voice_overs", "tours" end diff --git a/db/seeds.rb b/db/seeds.rb index ac21c9ef..67465765 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,21 +10,21 @@ Mode.create!( [ { - title: 'Walk' + title: 'Walk', }, { - title: 'Bike' + title: 'Bike', }, { - title: 'Transit' + title: 'Transit', }, { - title: 'Drive' + title: 'Drive', }, { - title: 'None' - } - ] + title: 'None', + }, + ], ) # Role.destroy_all @@ -32,15 +32,18 @@ Role.create!( [ { - title: 'Super' + title: 'Super', + }, + { + title: 'Tour Admin', }, { - title: 'Tour Admin' + title: 'Author', }, { - title: 'Author' - } - ] + title: 'Tour Creator', + }, + ], ) 3.times { FactoryBot.create(:tour_set) } @@ -51,11 +54,11 @@ # Login.last.user.update_attribute(:display_name, Faker::Movies::Lebowski) end -TourSet.all.each do |ts| +TourSet.all.find_each do |ts| Apartment::Tenant.switch!(ts.subdir) Random.new.rand(2..3).times do FactoryBot.create(:tour) - Tour.all.each do |tour| + Tour.all.find_each do |tour| FactoryBot.create_list(:stop, Random.new.rand(3..4)) tour.stops << Stop.all end @@ -72,14 +75,18 @@ u.save end -User.all.each do |u| +User.all.find_each do |u| FactoryBot.create(:login, who: u.email, user_id: u.id, provider: 'earth') next if u.super next if u.tours.present? + # u.tours = Tour.all.order(Arel.sql('random()')).limit(Random.new.rand(2..3)) Random.new.rand(2..3).times do - Apartment::Tenant.switch! TourSet.find(TourSet.pluck(:id).sample).subdir - u.tours << Tour.find(Tour.pluck(:id).sample) + Apartment::Tenant.switch!(TourSet.find(TourSet.pluck(:id).sample).subdir) + tour = Tour.find(Tour.pluck(:id).sample) + # The same tour can be sampled more than once across iterations; skip it + # if this user is already an author (TourAuthor now enforces this). + u.tours << tour unless TourAuthor.exists?(tour_id: tour.id, user_id: u.id) end u.save end diff --git a/entrypoint.sh b/entrypoint.sh index 71261527..0a46b873 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,5 @@ - -/usr/local/bin/bundle exec rake db:migrate - -/usr/local/bin/bundle exec rails server -b 0.0.0.0 \ No newline at end of file +#!/bin/bash +bundle exec rake db:migrate +bundle exec sidekiq& +bundle exec rake searchkick:reindex CLASS=User +bundle exec puma -C config/puma.rb diff --git a/lib/snippets.rb b/lib/snippets.rb index 4cef4160..f1dbc534 100644 --- a/lib/snippets.rb +++ b/lib/snippets.rb @@ -155,7 +155,7 @@ # next unless m.file.attached? - next unless File.exists? ActiveStorage::Blob.service.send(:path_for, m.file.key) + next unless File.exist? ActiveStorage::Blob.service.send(:path_for, m.file.key) Apartment::Tenant.switch! 'july-22nd' m.file.attach( io: File.open(ActiveStorage::Blob.service.send(:path_for, m.file.key)), @@ -172,7 +172,7 @@ m = MapIcon.find(id) next unless m.file.attached? - next unless File.exists? ActiveStorage::Blob.service.send(:path_for, m.file.key) + next unless File.exist? ActiveStorage::Blob.service.send(:path_for, m.file.key) m.file.attach( io: File.open(ActiveStorage::Blob.service.send(:path_for, m.file.key)), @@ -270,4 +270,12 @@ sites.each do |ts| Apartment::Tenant.switch! ts Tour.all.each { |t| t.update(restrict_bounds: false) } +end + +no_medium = [] +tenants.each do |t| + Apartment::Tenant.switch!(t) + TourMedium.all.each do |tm| + no_medium.push(tm) if tm.medium.nil? + end end \ No newline at end of file diff --git a/lib/tasks/check_duplicates.rake b/lib/tasks/check_duplicates.rake new file mode 100644 index 00000000..27980e58 --- /dev/null +++ b/lib/tasks/check_duplicates.rake @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +namespace :tenants do + desc 'Check for duplicate titles across all tenant schemas' + task check_duplicate_titles: :environment do + duplicates = {} + + Apartment::Tenant.each do |tenant| + Apartment::Tenant.switch!(tenant) + + dupes = Tour + .group('LOWER(title)') + .having('COUNT(*) > 1') + .count + + duplicates[tenant] = dupes if dupes.any? + ensure + Apartment::Tenant.reset + end + + if duplicates.empty? + puts '✓ No duplicates found' + else + puts "Duplicates found in #{duplicates.keys.length} tenant(s):" + duplicates.each do |tenant, dupes| + puts "\n #{tenant}:" + dupes.each { |title, count| puts " '#{title}' × #{count}" } + end + exit 1 + end + end +end diff --git a/lib/tasks/db_enhancements.rake b/lib/tasks/db_enhancements.rake index e3ac5d84..0fc9b3aa 100644 --- a/lib/tasks/db_enhancements.rake +++ b/lib/tasks/db_enhancements.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # lib/tasks/db_enhancements.rake ####### Important information #################### @@ -12,15 +14,15 @@ namespace :db do desc 'Also create shared_extensions Schema' - task extensions: :environment do + task extensions: :environment do # # Create Schema - # ActiveRecord::Base.connection.execute 'CREATE SCHEMA IF NOT EXISTS shared_extensions;' + ActiveRecord::Base.connection.execute('CREATE SCHEMA IF NOT EXISTS shared_extensions;') # # Enable pgcrypto - # ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA shared_extensions;' + ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA shared_extensions;') # # Enable UUID-OSSP - # ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;' + ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;') # # Grant usage to public - # ActiveRecord::Base.connection.execute 'GRANT usage ON SCHEMA shared_extensions to public;' + ActiveRecord::Base.connection.execute('GRANT usage ON SCHEMA shared_extensions to public;') end end diff --git a/lib/tasks/import_otb1.rake b/lib/tasks/import_otb1.rake index 69effabd..d620b21a 100644 --- a/lib/tasks/import_otb1.rake +++ b/lib/tasks/import_otb1.rake @@ -8,7 +8,7 @@ namespace :ImportOTB1 do task import: :environment do options = { dump: nil, - set: nil + set: nil, } opts = OptionParser.new @@ -31,7 +31,7 @@ namespace :ImportOTB1 do tour_set = TourSet.find_by(name: options[:set]) p "Switching to #{tour_set.subdir}" # Switch to TourSet - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) p "Current tenant: #{Apartment::Tenant.current}" @@ -76,18 +76,18 @@ namespace :ImportOTB1 do p 'Creating stops' d.select { |d1| d1['model'] === 'tour.tourstop' }.each do |s| if s['fields']['position'] == 0 - p "#{s['fields']['name']} is inro stop" - tour = Tour.where(title: d.select { |d1| d1['model'] === 'tour.tour' && d1['pk'] === s['fields']['tour'] }.first['fields']['name']).first + p "#{s["fields"]["name"]} is inro stop" + tour = Tour.where(title: d.select do |d1| + d1['model'] === 'tour.tour' && d1['pk'] === s['fields']['tour'] + end.first['fields']['name']).first tour.description = s['fields']['description'] tour.article_link = s['fields']['article_link'] tour.save - if !s['fields']['embed'].nil? - medium = Medium.create(video: s['fields']['embed'], tours: [tour]) - end + Medium.create(video: s['fields']['embed'], tours: [tour]) unless s['fields']['embed'].nil? else - p s['fields']['name'] + p s['fields']['name'] stop = Stop.find_or_create_by(title: s['fields']['name']) stop.title = s['fields']['name'] stop.description = s['fields']['description'] @@ -114,13 +114,13 @@ namespace :ImportOTB1 do # Set Stop Position p 'Setting stop positions' d.select { |d1| d1['model'] === 'tour.tourstop' }.each do |s| - if s['fields']['position'] != 0 - stop = Stop.where(title: s['fields']['name']).first - tour = stop.tours.first - tour_stop = TourStop.where(stop_id: stop.id).where(tour_id: tour.id).first - tour_stop.position = s['fields']['position'] - tour_stop.save - end + next if s['fields']['position'] == 0 + + stop = Stop.where(title: s['fields']['name']).first + tour = stop.tours.first + tour_stop = TourStop.where(stop_id: stop.id).where(tour_id: tour.id).first + tour_stop.position = s['fields']['position'] + tour_stop.save end # Create the Flat Pages @@ -146,22 +146,22 @@ namespace :ImportOTB1 do # Create Tour Media p 'Creating tour media' d.select { |d1| d1['model'] === 'tour.tourstop' }.each do |s| - if s['fields']['position'] == 0 - intro_id = s['pk'] - tour_id = s['fields']['tour'] - - d.select { |d1| d1['model'] === 'tour.tourstopmedia' && d1['fields']['tour_stop'] == intro_id }.each do |m| - p "http://api-campustour.ecdsweb.org/media/#{m['fields']['image']}" - if m['fields']['tour_stop'] == intro_id - medium = Medium.new - medium.title = m['fields']['title'] - medium.caption = m['fields']['caption'] - medium.remote_original_image_url = "http://api-campustour.ecdsweb.org/media/#{m['fields']['image']}" - medium.tours = [tour] - medium.save - d.delete(m) - end - end + next unless s['fields']['position'] == 0 + + intro_id = s['pk'] + s['fields']['tour'] + + d.select { |d1| d1['model'] === 'tour.tourstopmedia' && d1['fields']['tour_stop'] == intro_id }.each do |m| + p "http://api-campustour.ecdsweb.org/media/#{m["fields"]["image"]}" + next unless m['fields']['tour_stop'] == intro_id + + medium = Medium.new + medium.title = m['fields']['title'] + medium.caption = m['fields']['caption'] + medium.remote_original_image_url = "http://api-campustour.ecdsweb.org/media/#{m["fields"]["image"]}" + medium.tours = [tour] + medium.save + d.delete(m) end end @@ -180,12 +180,14 @@ namespace :ImportOTB1 do # Create Stop Media p 'Creating stop media' d.select { |d1| d1['model'] === 'tour.tourstopmedia' }.each do |m| - p "http://api-campustour.ecdsweb.org/media/#{m['fields']['image']}" + p "http://api-campustour.ecdsweb.org/media/#{m["fields"]["image"]}" medium = Medium.new medium.title = m['fields']['title'] medium.caption = m['fields']['caption'] - medium.remote_original_image_url = "http://api-campustour.ecdsweb.org/media/#{m['fields']['image']}" - medium.stops = [Stop.where(title: d.select { |d1| d1['model'] === 'tour.tourstop' && d1['pk'] === m['fields']['tour_stop'] }.first['fields']['name']).first] + medium.remote_original_image_url = "http://api-campustour.ecdsweb.org/media/#{m["fields"]["image"]}" + medium.stops = [Stop.where(title: d.select do |d1| + d1['model'] === 'tour.tourstop' && d1['pk'] === m['fields']['tour_stop'] + end.first['fields']['name']).first] medium.save end @@ -205,7 +207,7 @@ namespace :ImportOTB1 do task import_jsonapi: :environment do options = { dump: nil, - set: nil + set: nil, } opts = OptionParser.new @@ -228,7 +230,7 @@ namespace :ImportOTB1 do p "Switching to #{tour_set.subdir}" # Switch to TourSet - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) p "Current tenant: #{Apartment::Tenant.current}" @@ -280,7 +282,7 @@ namespace :ImportOTB1 do medium = Medium.new medium.title = m['attributes']['title'] medium.caption = m['attributes']['caption'] - medium.remote_original_image_url = "https://otp-api.ecdsdev.org#{m['attributes']['original_image']['ulr']}" + medium.remote_original_image_url = "https://otp-api.ecdsdev.org#{m["attributes"]["original_image"]["ulr"]}" medium.save end p 'DONE!!!!' @@ -290,7 +292,7 @@ namespace :ImportOTB1 do options = { dump: nil, set: nil, - media: nil + media: nil, } opts = OptionParser.new @@ -314,12 +316,13 @@ namespace :ImportOTB1 do p "Switching to #{tour_set.subdir}" # Switch to TourSet - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) p "Current tenant: #{Apartment::Tenant.current}" d.select { |stops| stops['model'] === 'tour.tourstop' }.each do |v2_stop| next if v2_stop['fields']['position'] == 0 + v3_stop = Stop.find_by(title: v2_stop['fields']['name']) v2_tour = d.select { |t| t['model'] === 'tour.tour' && t['pk'] === v2_stop['fields']['tour'] }.first v3_tour = Tour.find_by(title: v2_tour['fields']['name']) @@ -330,13 +333,15 @@ namespace :ImportOTB1 do v3_stop.save # Stop Media - d.select { |m| m['model'] === 'tour.tourstopmedia' && m['fields']['tour_stop'] == v2_stop['pk'] }.each do |v2_medium| + d.select do |m| + m['model'] === 'tour.tourstopmedia' && m['fields']['tour_stop'] == v2_stop['pk'] + end.each do |v2_medium| v3_medium = Medium.new v3_medium.title = v2_medium['fields']['title'] v3_medium.caption = v2_medium['fields']['caption'] - v3_medium.embed = "#{v2_medium['fields']['metadata']} ||| #{v2_medium['fields']['source_link']}" - p "Fetching #{options[:media]}/media/#{v2_medium['fields']['image']}" - v3_medium.remote_original_image_url = "#{options[:media]}/media/#{v2_medium['fields']['image']}" + v3_medium.embed = "#{v2_medium["fields"]["metadata"]} ||| #{v2_medium["fields"]["source_link"]}" + p "Fetching #{options[:media]}/media/#{v2_medium["fields"]["image"]}" + v3_medium.remote_original_image_url = "#{options[:media]}/media/#{v2_medium["fields"]["image"]}" v3_medium.save v3_stop.media << v3_medium end @@ -397,7 +402,6 @@ end # end # end - # file = File.read('vienna.json') # d = JSON.parse(file) diff --git a/old_spcs/requests/map_icons_spec.rb b/old_spcs/requests/map_icons_spec.rb index 9acc699b..99ba3911 100644 --- a/old_spcs/requests/map_icons_spec.rb +++ b/old_spcs/requests/map_icons_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' # This spec was generated by rspec-rails when you ran the scaffold generator. @@ -12,39 +14,39 @@ # of tools you can use to make these specs even more expressive, but we're # sticking to rails and rspec-rails APIs to keep things simple and stable. -RSpec.describe "/map_icons", type: :request do +RSpec.describe('/map_icons', type: :request) do # This should return the minimal set of attributes required to create a valid # MapIcon. As you add validations to MapIcon, be sure to # adjust the attributes here as well. - let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") - } + let(:valid_attributes) do + skip('Add a hash of attributes valid for your model') + end - let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") - } + let(:invalid_attributes) do + skip('Add a hash of attributes invalid for your model') + end # This should return the minimal set of values that should be in the headers # in order to pass any filters (e.g. authentication) defined in # MapIconsController, or in your router and rack # middleware. Be sure to keep this updated too. - let(:valid_headers) { + let(:valid_headers) do {} - } + end - describe "GET /index" do - it "renders a successful response" do - MapIcon.create! valid_attributes + describe 'GET /index' do + it 'renders a successful response' do + MapIcon.create!(valid_attributes) get map_icons_url, headers: valid_headers, as: :json - expect(response).to be_successful + expect(response).to(be_successful) end end - describe "GET /show" do - it "renders a successful response" do - map_icon = MapIcon.create! valid_attributes + describe 'GET /show' do + it 'renders a successful response' do + map_icon = MapIcon.create!(valid_attributes) get map_icon_url(map_icon), as: :json - expect(response).to be_successful + expect(response).to(be_successful) end end @@ -82,46 +84,52 @@ # end # end - describe "PATCH /update" do - context "with valid parameters" do - let(:new_attributes) { - skip("Add a hash of attributes valid for your model") - } + describe 'PATCH /update' do + context 'with valid parameters' do + let(:new_attributes) do + skip('Add a hash of attributes valid for your model') + end - it "updates the requested map_icon" do - map_icon = MapIcon.create! valid_attributes + it 'updates the requested map_icon' do + map_icon = MapIcon.create!(valid_attributes) patch map_icon_url(map_icon), - params: { map_icon: new_attributes }, headers: valid_headers, as: :json + params: { map_icon: new_attributes }, + headers: valid_headers, + as: :json map_icon.reload - skip("Add assertions for updated state") + skip('Add assertions for updated state') end - it "renders a JSON response with the map_icon" do - map_icon = MapIcon.create! valid_attributes + it 'renders a JSON response with the map_icon' do + map_icon = MapIcon.create!(valid_attributes) patch map_icon_url(map_icon), - params: { map_icon: new_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:ok) - expect(response.content_type).to match(a_string_including("application/json")) + params: { map_icon: new_attributes }, + headers: valid_headers, + as: :json + expect(response).to(have_http_status(:ok)) + expect(response.content_type).to(match(a_string_including('application/json'))) end end - context "with invalid parameters" do - it "renders a JSON response with errors for the map_icon" do - map_icon = MapIcon.create! valid_attributes + context 'with invalid parameters' do + it 'renders a JSON response with errors for the map_icon' do + map_icon = MapIcon.create!(valid_attributes) patch map_icon_url(map_icon), - params: { map_icon: invalid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to eq("application/json") + params: { map_icon: invalid_attributes }, + headers: valid_headers, + as: :json + expect(response).to(have_http_status(:unprocessable_entity)) + expect(response.content_type).to(eq('application/json')) end end end - describe "DELETE /destroy" do - it "destroys the requested map_icon" do - map_icon = MapIcon.create! valid_attributes - expect { - delete map_icon_url(map_icon), headers: valid_headers, as: :json - }.to change(MapIcon, :count).by(-1) + describe 'DELETE /destroy' do + it 'destroys the requested map_icon' do + map_icon = MapIcon.create!(valid_attributes) + expect do + delete(map_icon_url(map_icon), headers: valid_headers, as: :json) + end.to(change(MapIcon, :count).by(-1)) end end end diff --git a/old_spcs/requests/v3/flat_pages_spec.rb b/old_spcs/requests/v3/flat_pages_spec.rb index a7e7b2cf..c6540257 100644 --- a/old_spcs/requests/v3/flat_pages_spec.rb +++ b/old_spcs/requests/v3/flat_pages_spec.rb @@ -3,14 +3,19 @@ require 'rails_helper' require 'ecds_rails_auth_engine' -RSpec.describe 'V3::FlatPages', type: :request do +RSpec.describe('V3::FlatPages', type: :request) do let!(:theme) { create(:theme) } let!(:tours) { create_list(:tour_with_flat_pages, 1, theme: theme) } let!(:flat_page) { tours.first.flat_pages.first } let(:tour_id) { tours.first.id } + # valid payload + let(:valid_attributes) do + factory_to_json_api(FactoryBot.build(:flat_page)) + end + context 'create tour with flat pages' do - before { + before do user = create(:user) user.update(super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) @@ -18,11 +23,11 @@ signed_cookie(user) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token get "/#{Apartment::Tenant.current}/flat-pages" - } + end it 'associates flat_page with tour' do - expect(response).to have_http_status(200) - expect(json.size).to eq(FlatPage.count) + expect(response).to(have_http_status(:ok)) + expect(json.size).to(eq(FlatPage.count)) end end @@ -40,50 +45,47 @@ # end context 'get specific flat page by id' do - before { + before do Tour.first.update(published: true) Tour.first.flat_pages << FlatPage.first get "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.first.id}" - } + end it 'returns requested flat page' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end - # valid payload - let(:valid_attributes) do - factory_to_json_api(FactoryBot.build(:flat_page)) - end - describe 'POST /tenant/flat-pages' do - context 'create page not authenticated' do before { post "/#{Apartment::Tenant.current}/flat-pages", params: valid_attributes } + it 'returns status code 401' do - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end context 'when created by tour set admin' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token post "/#{Apartment::Tenant.current}/flat-pages", params: valid_attributes - } + end + it 'creates a tour' do - expect(response).to have_http_status(201) + expect(response).to(have_http_status(:created)) end end context 'create without valid params' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token post "/#{Apartment::Tenant.current}/flat-pages", params: { foo: 'bar' } - } + end + it 'returns unprocessable entity' do - expect(response).to have_http_status(422) + expect(response).to(have_http_status(:unprocessable_content)) end end end @@ -91,54 +93,58 @@ describe 'PUT /tenant/flat-pages/' do context 'update page not authenticated' do before { put "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.first.id}", params: valid_attributes } + it 'returns status code 401' do - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end context 'when updated by tour set admin' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token put "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.first.id}", params: valid_attributes - } + end + it 'creates a tour' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end context 'update without valid params' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) invalid_attributes = { data: { type: 'flat_pages', attributes: { title: nil } } } cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token put "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.last.id}", params: invalid_attributes - } + end + it 'returns unprocessable entity' do - expect(response).to have_http_status(422) + expect(response).to(have_http_status(:unprocessable_content)) end end - end describe 'DELETE /tenant/flat-pages/' do context 'delete page not authenticated' do - before { + before do delete "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.first.id}", params: valid_attributes - } + end + it 'returns status code 401' do - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end context 'when deletes by tour set admin' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token delete "/#{Apartment::Tenant.current}/flat-pages/#{FlatPage.first.id}", params: valid_attributes - } + end + it 'creates a tour' do - expect(response).to have_http_status(204) + expect(response).to(have_http_status(:no_content)) end end end diff --git a/old_spcs/requests/v3/map_overlays_request_spec.rb b/old_spcs/requests/v3/map_overlays_request_spec.rb index b183bcd8..ccfa9ee8 100644 --- a/old_spcs/requests/v3/map_overlays_request_spec.rb +++ b/old_spcs/requests/v3/map_overlays_request_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' - -RSpec.describe "V3::MapOverlays", type: :request do +# frozen_string_literal: true -end +require 'rails_helper' diff --git a/old_spcs/requests/v3/modes_spec.rb b/old_spcs/requests/v3/modes_spec.rb index 829200d4..a18b7216 100644 --- a/old_spcs/requests/v3/modes_spec.rb +++ b/old_spcs/requests/v3/modes_spec.rb @@ -2,11 +2,12 @@ require 'rails_helper' -RSpec.describe 'V3::Modes', type: :request do +RSpec.describe('V3::Modes', type: :request) do describe 'GET /modes' do before { get "/#{Apartment::Tenant.current}/modes" } + it 'works! (now write some real specs)' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end end diff --git a/old_spcs/requests/v3/stops_spec.rb b/old_spcs/requests/v3/stops_spec.rb index 339f6723..f17137f6 100644 --- a/old_spcs/requests/v3/stops_spec.rb +++ b/old_spcs/requests/v3/stops_spec.rb @@ -3,15 +3,14 @@ # app/requests/stops_spec.rb require 'rails_helper' -RSpec.describe 'V3::Stops API' do +RSpec.describe('V3::Stops API') do # Initialize the test data let!(:user) { User.find_by(super: true) } # Test suite for GET /stops describe 'GET /stops' do context 'when stops exist' do - - before { + before do user = create(:user) user.update(super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) @@ -19,14 +18,14 @@ signed_cookie(user) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token get "/#{Apartment::Tenant.current}/stops" - } + end it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'returns all tour stops' do - expect(json.size).to eq(Stop.count) + expect(json.size).to(eq(Stop.count)) end end @@ -41,30 +40,30 @@ # end context 'get stop by slug and tour' do - before { + before do Tour.first.update(published: true) Tour.first.stops << Stop.first get "/#{Apartment::Tenant.current}/tour-stops?slug=#{Stop.first.slug}&tour=#{Tour.first.id}" - } + end it 'returns stop in tour with slug' do - expect(json['id']).to eq(Stop.first.id.to_s) - expect(relationships['tour']['data']['id']).to eq(Stop.first.tours.first.id.to_s) + expect(json['id']).to(eq(Stop.first.id.to_s)) + expect(relationships['tour']['data']['id']).to(eq(Stop.first.tours.first.id.to_s)) end end end # Test suite for GET /stops/:id describe 'GET /stops/:id' do - before { + before do Tour.first.update(published: true) Tour.first.stops << Stop.first get "/#{Apartment::Tenant.current}/stops/#{Stop.first.id}" - } + end context 'when tour stop exists' do it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end # For now, access to stop is through /tour-stop?slug=XX&tour=Y @@ -81,12 +80,12 @@ before { get "/#{Apartment::Tenant.current}/stops/0" } it 'returns status code 404' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'returns dummy stop' do - expect(json[:id]).to match('0') - expect(attributes[:title]).to be_nil + expect(json[:id]).to(match('0')) + expect(attributes[:title]).to(be_nil) end end end @@ -95,20 +94,19 @@ let!(:stop) { Stop.second } let!(:tour) { stop.tours.first } let!(:original_slug) { stop.slug } - let!(:new_title) { "#{Faker::Movies::HitchhikersGuideToTheGalaxy.starship}" } + let!(:new_title) { Faker::Movies::HitchhikersGuideToTheGalaxy.starship.to_s } context 'get stop after title change' do - - before { + before do tour.update(published: true) stop.update(title: new_title) - } - before { get "/#{Apartment::Tenant.current}/tour-stops?slug=#{new_title.parameterize}&tour=#{tour.id}" } + get "/#{Apartment::Tenant.current}/tour-stops?slug=#{new_title.parameterize}&tour=#{tour.id}" + end it 'gets same stop with new slug' do - expect(response).to have_http_status(200) - expect(attributes['slug']).to eq(new_title.parameterize) - expect(json['id']).to eq(stop.id.to_s) + expect(response).to(have_http_status(:ok)) + expect(attributes['slug']).to(eq(new_title.parameterize)) + expect(json['id']).to(eq(stop.id.to_s)) end end @@ -130,14 +128,14 @@ end context 'when request attributes are valid' do - before { + before do User.first.update_attribute(:super, true) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token post "/#{Apartment::Tenant.current}/stops", params: valid_attributes - } + end it 'returns status code 201' do - expect(response).to have_http_status(201) + expect(response).to(have_http_status(:created)) end end @@ -160,47 +158,47 @@ factory_to_json_api(FactoryBot.build(:stop, title: '3 Stacks')) end - before { + before do cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token put "/#{Apartment::Tenant.current}/stops/#{Stop.second.id}", params: valid_attributes - } + end context 'when stop exists' do it 'returns status code 204' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'updates the stop' do updated_stop = Stop.second - expect(updated_stop.title).to match(/3 Stacks/) + expect(updated_stop.title).to(match(/3 Stacks/)) end end context 'when the stop does not exist' do - before { + before do cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token put "/#{Apartment::Tenant.current}/stops/0", params: valid_attributes - } + end it 'returns status code 404' do - expect(response).to have_http_status(404) + expect(response).to(have_http_status(:not_found)) end it 'returns a not found message' do - expect(response.body).to match(/Record not found/) + expect(response.body).to(match(/Record not found/)) end end end # Test suite for DELETE /stops/:id describe 'DELETE /stops/:id' do - before { + before do cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token delete "/#{Apartment::Tenant.current}/stops/#{Stop.last.id}" - } + end it 'returns status code 204' do - expect(response).to have_http_status(204) + expect(response).to(have_http_status(:no_content)) end end end diff --git a/old_spcs/requests/v3/themes_spec.rb b/old_spcs/requests/v3/themes_spec.rb index cb21d10d..f4b4bf9b 100644 --- a/old_spcs/requests/v3/themes_spec.rb +++ b/old_spcs/requests/v3/themes_spec.rb @@ -2,12 +2,13 @@ require 'rails_helper' -RSpec.describe 'V3::Themes', type: :request do +RSpec.describe('V3::Themes', type: :request) do describe 'GET /themes' do before { get "/#{Apartment::Tenant.current}/themes" } + it 'works! (now write some real specs)' do get themes_path - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end end diff --git a/old_spcs/requests/v3/tour_set_users_spec.rb b/old_spcs/requests/v3/tour_set_users_spec.rb index 340223d6..4e5342b2 100644 --- a/old_spcs/requests/v3/tour_set_users_spec.rb +++ b/old_spcs/requests/v3/tour_set_users_spec.rb @@ -1,10 +1,13 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe 'V3::TourSetAdmins', type: :request do +RSpec.describe('V3::TourSetAdmins', type: :request) do describe 'GET /tour_set_admins' do before { get "/#{Apartment::Tenant.current}/tour-set-users" } + it 'works! (now write some real specs)' do - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end end diff --git a/old_spcs/requests/v3/tour_stops_spec.rb b/old_spcs/requests/v3/tour_stops_spec.rb index 01d39771..25ba9ec6 100644 --- a/old_spcs/requests/v3/tour_stops_spec.rb +++ b/old_spcs/requests/v3/tour_stops_spec.rb @@ -3,84 +3,85 @@ # app/requests/stops_spec.rb require 'rails_helper' -RSpec.describe 'V3::Stops API' do +RSpec.describe('V3::Stops API') do # Initialize the test data # Test suite for GET /stops describe 'GET /tour-stops' do - before { - Apartment::Tenant.switch! TourSet.second.subdir - Tour.all.each { |tour| tour.update(published: true) } + before do + Apartment::Tenant.switch!(TourSet.second.subdir) + Tour.all.find_each { |tour| tour.update(published: true) } get "/#{Apartment::Tenant.current}/tour-stops" - } + end context 'when stops exist' do it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'returns all tour stops' do - expect(json.size).to eq(TourStop.count) + expect(json.size).to(eq(TourStop.count)) end end context 'previous and next are correct' do it 'previous is 3' do - expect(json[3]['attributes']['previous']['id']).to eq(json[3]['id'].to_i - 1) + expect(json[3]['attributes']['previous']['id']).to(eq(json[3]['id'].to_i - 1)) end it 'next is 5' do - expect(json[3]['attributes']['next']['id']).to eq(json[3]['id'].to_i + 1) + expect(json[3]['attributes']['next']['id']).to(eq(json[3]['id'].to_i + 1)) end end end describe 'GET /tour-stops?slug=slug&tour=X' do - before { - Apartment::Tenant.switch! TourSet.second.subdir + before do + Apartment::Tenant.switch!(TourSet.second.subdir) Tour.first.update(published: true) get "/#{Apartment::Tenant.current}/tour-stops?slug=#{Tour.first.stops.first.stop_slugs.first.slug}&tour=#{Tour.first.id}" - } + end context 'get tour stop by slug and tour' do it 'responds with the tour stop' do - expect(json['id'].to_i).to eq(Tour.first.stops.first.id) + expect(json['id'].to_i).to(eq(Tour.first.stops.first.id)) end end end describe 'GET /tour-stops?slug=duplicated_slug&tour=X' do - before { Apartment::Tenant.switch! TourSet.last.subdir } - # before { create_list(:tour_with_stops, 5, theme: create(:theme), mode: create(:mode)) } - let!(:tour1) { Tour.first } - let!(:stop1) { tour1.stops.second } - let!(:tour2) { Tour.last } - let!(:stop2) { tour2.stops.last } - let!(:new_title) { "#{Faker::Movies::Lebowski.character}" } - - before { + before do + Apartment::Tenant.switch!(TourSet.last.subdir) tour1.stops = [Stop.create(title: new_title)] tour1.update(published: true) tour1.save tour2.stops = [Stop.create(title: new_title)] tour2.update(published: true) tour2.save - } + end + # before { create_list(:tour_with_stops, 5, theme: create(:theme), mode: create(:mode)) } + let!(:tour1) { Tour.first } + let!(:stop1) { tour1.stops.second } + let!(:tour2) { Tour.last } + let!(:stop2) { tour2.stops.last } + let!(:new_title) { Faker::Movies::Lebowski.character.to_s } context 'get stop with duplicate title/slug in correct tour' do before { get "/#{Apartment::Tenant.current}/tour-stops?slug=#{new_title.parameterize}&tour=#{tour1.id}" } + it 'is true' do - expect(json['relationships']['stop']['data']['id'].to_i).to eq(tour1.stops.order(created_at: :desc).first.id) - expect(json['relationships']['tour']['data']['id'].to_i).to eq(tour1.id) + expect(json['relationships']['stop']['data']['id'].to_i).to(eq(tour1.stops.order(created_at: :desc).first.id)) + expect(json['relationships']['tour']['data']['id'].to_i).to(eq(tour1.id)) end end context 'get stop with duplicate title/slug in correct tour' do before { get "/#{Apartment::Tenant.current}/tour-stops?slug=#{new_title.parameterize}&tour=#{tour2.id}" } + it 'is true' do - expect(json['relationships']['stop']['data']['id'].to_i).to eq(tour2.stops.order(created_at: :desc).first.id) - expect(json['relationships']['tour']['data']['id'].to_i).to eq(tour2.id) + expect(json['relationships']['stop']['data']['id'].to_i).to(eq(tour2.stops.order(created_at: :desc).first.id)) + expect(json['relationships']['tour']['data']['id'].to_i).to(eq(tour2.id)) end end end diff --git a/old_spcs/requests/v3/tours_spec.rb b/old_spcs/requests/v3/tours_spec.rb index ac13555d..90034f73 100644 --- a/old_spcs/requests/v3/tours_spec.rb +++ b/old_spcs/requests/v3/tours_spec.rb @@ -2,24 +2,24 @@ require 'rails_helper' -RSpec.describe 'V3::Tours', type: :request do - before { +RSpec.describe('V3::Tours', type: :request) do + before do set = TourSet.find(TourSet.pluck(:id).sample).subdir - Apartment::Tenant.switch! set + Apartment::Tenant.switch!(set) Tour.first.update_attribute(:published, true) - } + end describe 'GET /atlanta/tours unauthenticated' do - before { + before do get "/#{Apartment::Tenant.current}/tours" - } + end it 'returns only published tours' do - expect(json.size).to eq(Tour.published.count) + expect(json.size).to(eq(Tour.published.count)) end it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end @@ -29,22 +29,22 @@ context 'when the record exists' do it 'returns the tour' do - expect(json).not_to be_empty - expect(json['id']).to eq(Tour.published.last.id.to_s) + expect(json).not_to(be_empty) + expect(json['id']).to(eq(Tour.published.last.id.to_s)) end it 'has five stops' do - expect(relationships['tour_stops']['data'].size).to eq(Tour.published.last.tour_stops.length) - expect(relationships['stops']['data'].size).to eq(Tour.published.last.stops.length) + expect(relationships['tour_stops']['data'].size).to(eq(Tour.published.last.tour_stops.length)) + expect(relationships['stops']['data'].size).to(eq(Tour.published.last.stops.length)) end it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'returns description without html tags and space between sentences' do - expect(attributes['sanitized_description']).not_to include('

') - expect(attributes['sanitized_description']).not_to match(/.*[A-z]\.[A-z].*/) + expect(attributes['sanitized_description']).not_to(include('

')) + expect(attributes['sanitized_description']).not_to(match(/.*[A-Za-z]\.[A-Za-z].*/)) end end @@ -52,11 +52,11 @@ before { get "/#{Apartment::Tenant.current}/tours/0" } it 'returns status code 404' do - expect(response).to have_http_status(404) + expect(response).to(have_http_status(:not_found)) end it 'returns a not found message' do - expect(response.body).to match(/Couldn't find Tour/) + expect(response.body).to(match(/Couldn't find Tour/)) end end end @@ -68,47 +68,46 @@ let!(:new_title) { Faker::TvShows::RickAndMorty.character } context 'get tour after title change' do - before { + before do tour.title = new_title tour.published = true tour.save - } - - before { get "/#{Apartment::Tenant.current}/tours?slug=#{new_title.parameterize}" } + get "/#{Apartment::Tenant.current}/tours?slug=#{new_title.parameterize}" + end it 'gets same tour with new slug' do - expect(response).to have_http_status(200) - expect(attributes['slug']).to eq(new_title.parameterize) - expect(json['id']).to eq(tour.id.to_s) + expect(response).to(have_http_status(:ok)) + expect(attributes['slug']).to(eq(new_title.parameterize)) + expect(json['id']).to(eq(tour.id.to_s)) end end context 'get tour by old slug' do - before { + before do tour.title = original_title tour.published = true tour.save tour.title = new_title tour.save - } - before { get "/#{Apartment::Tenant.current}/tours?slug=#{original_slug}" } + get "/#{Apartment::Tenant.current}/tours?slug=#{original_slug}" + end it 'returns the tour by the original slug' do - expect(response).to have_http_status(200) - expect(attributes['slug']).to eq(new_title.parameterize) - expect(json['id']).to eq(tour.id.to_s) + expect(response).to(have_http_status(:ok)) + expect(attributes['slug']).to(eq(new_title.parameterize)) + expect(json['id']).to(eq(tour.id.to_s)) end end context 'get nothing if tour is unpublished and no user' do - before { + before do tour.published = false tour.save get "/#{Apartment::Tenant.current}/tours?slug=#{tour.slug}" - } + end it 'returns nothing' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end end @@ -119,33 +118,35 @@ let(:valid_attributes) do factory_to_json_api(FactoryBot.build(:tour, title: 'Learn Elm', published: true)) end - before { Apartment::Tenant.switch! TourSet.find(TourSet.pluck(:id).sample).subdir } + + before { Apartment::Tenant.switch!(TourSet.find(TourSet.pluck(:id).sample).subdir) } context 'when the post is valid and authenticated as non-tour set admin' do - before { + before do User.last.update_attribute(:super, false) User.last.update_attribute(:tour_sets, []) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.last.id).token post "/#{Apartment::Tenant.current}/tours", params: valid_attributes - } + end it 'returns status code 401' do - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end context 'when created by tour set admin' do - before { + before do User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token post "/#{Apartment::Tenant.current}/tours", params: valid_attributes - } + end + it 'creates a tour' do - expect(attributes['title']).to eq('Learn Elm') + expect(attributes['title']).to(eq('Learn Elm')) end it 'returns status code 201' do - expect(response).to have_http_status(201) + expect(response).to(have_http_status(:created)) end end @@ -153,19 +154,20 @@ let(:invalid_attributes) do hash_to_json_api('tours', invalid: 'Foobar') end - before { + + before do # Tour.create!(published: true) User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token post "/#{Apartment::Tenant.current}/tours", params: invalid_attributes - } + end it 'returns status code 201' do - expect(response).to have_http_status(201) + expect(response).to(have_http_status(:created)) end it 'returns new tour titled `untitled`' do - expect(attributes['title']).to eq('untitled') + expect(attributes['title']).to(eq('untitled')) end end end @@ -177,55 +179,55 @@ end context 'when the record exists' do - before { + before do user = create(:user) user.update(super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: user.id).token put "/#{Apartment::Tenant.current}/tours/#{Tour.last.id}", params: valid_attributes - } + end it 'updates the record' do - expect(json).not_to be_empty - expect(attributes['title']).to eq('Shopping') + expect(json).not_to(be_empty) + expect(attributes['title']).to(eq('Shopping')) end it 'returns status code 200' do - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end end end # Test suite for DELETE /atlanta/tours/:id describe 'DELETE /atlanta/tours/:id' do - before { + before do Tour.create! User.first.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.first.id).token delete "/#{Apartment::Tenant.current}/tours/#{Tour.last.id}" - } + end it 'returns status code 204' do - expect(response).to have_http_status(204) + expect(response).to(have_http_status(:no_content)) end end describe 'Get //tours authenticated' do context 'tour set adim gets all the tours for that set' do - before { + before do User.last.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) cookies['auth'] = EcdsRailsAuthEngine::Login.find_by(user_id: User.last.id).token get "/#{Apartment::Tenant.current}/tours" - } + end it 'returns all the tours in the set' do - expect(json.size).to eq(Tour.count) + expect(json.size).to(eq(Tour.count)) end end context 'get tours as tour author' do - before { + before do user = User.first login = EcdsRailsAuthEngine::Login.find_by(user_id: user.id) user.update(super: false, tour_sets: [], tours: []) @@ -234,17 +236,17 @@ # user.tours = [] user.save user.tours << Tour.first - Tour.all.each do |t| + Tour.all.find_each do |t| t.published = false t.save end cookies['auth'] = login.token get "/#{Apartment::Tenant.current}/tours" - } + end it 'only returns tours user can edit' do - expect(json.size).to eq(1) - expect(json.size).not_to eq(Tour.count) + expect(json.size).to(eq(1)) + expect(json.size).not_to(eq(Tour.count)) end end end diff --git a/public/sketchfab.png b/public/sketchfab.png new file mode 100644 index 00000000..9c91443e Binary files /dev/null and b/public/sketchfab.png differ diff --git a/spec/controllers/v3/flat_pages_controller_spec.rb b/spec/controllers/v3/flat_pages_controller_spec.rb index d425c993..1575e9fb 100644 --- a/spec/controllers/v3/flat_pages_controller_spec.rb +++ b/spec/controllers/v3/flat_pages_controller_spec.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe V3::FlatPagesController, type: :controller do +RSpec.describe(V3::FlatPagesController, type: :controller) do describe 'GET #index' do it 'returns a 200 response with flat_pages connected to published tours' do create_list(:tour_with_flat_pages, 5, theme: create(:theme), mode: create(:mode)) Tour.first.update(published: true) if Tour.published.empty? Tour.last.update(published: false) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.count).to be > Tour.published.count + expect(response.status).to(eq(200)) + expect(Tour.count).to(be > Tour.published.count) json.each do |flat_page| - expect(FlatPage.find(flat_page[:id]).tours.any? { |tour| tour.published }) + expect(FlatPage.find(flat_page[:id]).tours.any?(&:published)).to(be(true)) end - expect(json.count).to be < FlatPage.count + expect(json.count).to(be < FlatPage.count) end it 'returns a 200 response with no flat_pages when request is authenticated by person with no access' do @@ -24,12 +26,12 @@ user.tours = [] signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.count).to be > Tour.published.count + expect(response.status).to(eq(200)) + expect(Tour.count).to(be > Tour.published.count) json.each do |flat_page| - expect(FlatPage.find(flat_page[:id]).tours.any? { |tour| tour.published }) + expect(FlatPage.find(flat_page[:id]).tours.any?(&:published)).to(be(true)) end - expect(json.count).to be == FlatPage.all.reject {|fp| !fp.published}.count + expect(json.count).to(eq(FlatPage.all.reject { |fp| !fp.published }.count)) end it 'returns a 200 response with flat_pages when request is authenticated by tenant admin and tour is unpublished' do @@ -39,9 +41,10 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: tour.tenant } - expect(FlatPage.count).to be > 1 - expect(response.status).to eq(200) - expect(json.count).to eq(FlatPage.count) + Apartment::Tenant.switch!(tour.tenant) + expect(FlatPage.count).to(be > 1) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(FlatPage.count)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -53,9 +56,9 @@ user.tours << tour signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(tour.flat_pages.count) - expect(json.count).to be < FlatPage.count + expect(response.status).to(eq(200)) + expect(json.count).to(eq(tour.flat_pages.count)) + expect(json.count).to(be < FlatPage.count) end end @@ -64,31 +67,31 @@ tour = create(:tour) tour.update(published: false) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } # Make sure the flat page is only associated with the newly created tour tour.flat_pages.last.update(tours: [tour]) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.last.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(tour.flat_pages.last.id.to_s) - expect(attributes[:title]).to be_nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(tour.flat_pages.last.id.to_s)) + expect(attributes[:title]).to(be_nil) end it 'returns a 200 response and stop when stop is part of published tour' do tour = create(:tour) tour.update(published: true) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } tour.flat_pages.last.update(tours: [tour]) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.last.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.flat_pages.last.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.flat_pages.last.title)) end it 'returns a 200 response that is empty stop when request is authenticated by someone w/o permission' do tour = create(:tour) tour.update(published: false) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } tour.flat_pages.last.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -96,16 +99,16 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.last.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(tour.flat_pages.last.id.to_s) - expect(attributes[:title]).to be_nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(tour.flat_pages.last.id.to_s)) + expect(attributes[:title]).to(be_nil) end it 'returns a 200 response that is a stop when request is authenticated by a tour author' do tour = create(:tour) tour.update(published: false) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } tour.flat_pages.first.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -113,15 +116,15 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.flat_pages.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.flat_pages.first.title)) end it 'returns a 200 response that is a stop when request is authenticated by a tenant admin' do tour = create(:tour) tour.update(published: false) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } tour.flat_pages.first.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -129,15 +132,15 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.flat_pages.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.flat_pages.first.title)) end it 'returns a 200 response that is a stop when request is authenticated by a super user' do tour = create(:tour) tour.update(published: false) create_list(:flat_page, 3) - FlatPage.all.each { |flat_page| tour.flat_pages << flat_page } + FlatPage.all.find_each { |flat_page| tour.flat_pages << flat_page } tour.flat_pages.first.update(tours: [tour]) user = create(:user) user.update(super: true) @@ -145,17 +148,17 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.flat_pages.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.flat_pages.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.flat_pages.first.title)) end end describe 'POST #create' do context 'with valid params' do it 'return 401 when unauthenciated' do - post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) - end + post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end it 'return 401 when authenciated but not an admin for current tenant' do user = create(:user) @@ -164,7 +167,7 @@ user.tours = [] signed_cookie(user) post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 201 when authenciated but an admin for current tenant' do @@ -174,9 +177,9 @@ signed_cookie(user) original_flat_page_count = FlatPage.count post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Burrito FlatPage') - expect(FlatPage.count).to eq(original_flat_page_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Burrito FlatPage')) + expect(FlatPage.count).to(eq(original_flat_page_count + 1)) end it 'return 201 when authenciated by super' do @@ -186,9 +189,9 @@ signed_cookie(user) original_flat_page_count = FlatPage.count post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Taco FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Taco FlatPage') - expect(FlatPage.count).to eq(original_flat_page_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Taco FlatPage')) + expect(FlatPage.count).to(eq(original_flat_page_count + 1)) end it 'return 201 when authenciated by a tour author' do @@ -199,9 +202,9 @@ signed_cookie(user) original_flat_page_count = FlatPage.count post :create, params: { data: { type: 'flat_pages', attributes: { title: 'Elmyr' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Elmyr') - expect(FlatPage.count).to eq(original_flat_page_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Elmyr')) + expect(FlatPage.count).to(eq(original_flat_page_count + 1)) end it 'return 422 when missing title' do @@ -209,9 +212,9 @@ signed_cookie(user) original_flat_page_count = FlatPage.count post :create, params: { data: { type: 'flat_pages', attributes: {} }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(422) - expect(errors).to include('Title can\'t be blank') - expect(FlatPage.count).to eq(original_flat_page_count) + expect(response.status).to(eq(422)) + expect(errors).to(include('Title can\'t be blank')) + expect(FlatPage.count).to(eq(original_flat_page_count)) end end end @@ -220,8 +223,13 @@ context 'with valid params' do it 'return 401 when unauthenciated' do tour = create(:tour_with_flat_pages) - post :update, params: { id: tour.flat_pages.last.id, data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :update, + params: { + id: tour.flat_pages.last.id, + data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, + tenant: Apartment::Tenant.current, + } + expect(response.status).to(eq(401)) end it 'return 401 when authenciated but not an admin for current tenant' do @@ -231,8 +239,13 @@ user.tour_sets = [] user.tours = [] signed_cookie(user) - post :update, params: { id: tour.flat_pages.first.id, data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :update, + params: { + id: tour.flat_pages.first.id, + data: { type: 'flat_pages', attributes: { title: 'Burrito FlatPage' } }, + tenant: Apartment::Tenant.current, + } + expect(response.status).to(eq(401)) end it 'return 200 and updated tour when authenciated but an admin for current tenant' do @@ -243,11 +256,16 @@ signed_cookie(user) original_flat_page_title = FlatPage.find(tour.flat_pages.last.id).title new_title = Faker::Name.unique.name - post :update, params: { id: tour.flat_pages.first.id, data: { type: 'flat_pages', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_flat_page_title) - expect(attributes[:title]).to eq(new_title) - expect(FlatPage.find(tour.flat_pages.first.id).title).to eq(new_title) + post :update, + params: { + id: tour.flat_pages.first.id, + data: { type: 'flat_pages', attributes: { title: new_title } }, + tenant: Apartment::Tenant.current, + } + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_flat_page_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(FlatPage.find(tour.flat_pages.first.id).title).to(eq(new_title)) end it 'return 200 and updated tour when authenciated by super' do @@ -258,11 +276,16 @@ signed_cookie(user) original_flat_page_title = FlatPage.find(tour.flat_pages.last.id).title new_title = Faker::Name.unique.name - post :update, params: { id: tour.flat_pages.last.id, data: { type: 'flat_pages', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_flat_page_title) - expect(attributes[:title]).to eq(new_title) - expect(FlatPage.find(tour.flat_pages.last.id).title).to eq(new_title) + post :update, + params: { + id: tour.flat_pages.last.id, + data: { type: 'flat_pages', attributes: { title: new_title } }, + tenant: Apartment::Tenant.current, + } + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_flat_page_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(FlatPage.find(tour.flat_pages.last.id).title).to(eq(new_title)) end it 'return 200 and updated tour when authenciated by tour author' do @@ -274,11 +297,16 @@ signed_cookie(user) original_flat_page_title = FlatPage.find(tour.flat_pages.last.id).title new_title = Faker::Name.unique.name - post :update, params: { id: tour.flat_pages.first.id, data: { type: 'flat_pages', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_flat_page_title) - expect(attributes[:title]).to eq(new_title) - expect(FlatPage.find(tour.flat_pages.first.id).title).to eq(new_title) + post :update, + params: { + id: tour.flat_pages.first.id, + data: { type: 'flat_pages', attributes: { title: new_title } }, + tenant: Apartment::Tenant.current, + } + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_flat_page_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(FlatPage.find(tour.flat_pages.first.id).title).to(eq(new_title)) end it 'returns 422 when title in nil' do @@ -286,8 +314,8 @@ user = create(:user, super: true) signed_cookie(user) post :update, params: { id: flat_page.id, data: { type: 'flat_pages', attributes: { title: nil } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(422) - expect(errors).to include('Title can\'t be blank') + expect(response.status).to(eq(422)) + expect(errors).to(include('Title can\'t be blank')) end end @@ -306,7 +334,7 @@ it 'return 401 when unauthenciated' do tour = create(:tour_with_flat_pages) post :destroy, params: { id: Tour.find(tour.id).flat_pages.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 401 when authenciated but not an admin for current tenant' do @@ -316,7 +344,7 @@ user.tour_sets = [] signed_cookie(user) post :destroy, params: { id: tour.flat_pages.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 204 and one less tour when authenciated but an admin for current tenant' do @@ -327,8 +355,8 @@ signed_cookie(user) flat_page_count = FlatPage.count post :destroy, params: { id: tour.flat_pages.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(FlatPage.count).to eq(flat_page_count - 1) + expect(response.status).to(eq(204)) + expect(FlatPage.count).to(eq(flat_page_count - 1)) end it 'return 204 and one less tour when authenciated by super' do @@ -339,8 +367,8 @@ signed_cookie(user) flat_page_count = FlatPage.count post :destroy, params: { id: tour.flat_pages.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(FlatPage.count).to eq(flat_page_count - 1) + expect(response.status).to(eq(204)) + expect(FlatPage.count).to(eq(flat_page_count - 1)) end it 'return 204 and one less tour when authenciated by tour author' do @@ -350,11 +378,11 @@ user.tour_sets = [] user.tours << tour signed_cookie(user) - new_title = Faker::Name.unique.name + Faker::Name.unique.name flat_page_count = FlatPage.count post :destroy, params: { id: tour.flat_pages.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(FlatPage.count).to eq(flat_page_count - 1) + expect(response.status).to(eq(204)) + expect(FlatPage.count).to(eq(flat_page_count - 1)) end end end diff --git a/spec/controllers/v3/map_icons_controller_spec.rb b/spec/controllers/v3/map_icons_controller_spec.rb index 35d607a5..8a96ab4f 100644 --- a/spec/controllers/v3/map_icons_controller_spec.rb +++ b/spec/controllers/v3/map_icons_controller_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' -RSpec.describe V3::MapIconsController, type: :controller do +RSpec.describe(V3::MapIconsController, type: :controller) do describe 'GET #index' do it 'returns all map icons' do create_list(:map_icon, rand(2..6)) get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq(MapIcon.count) + expect(json.count).to(eq(MapIcon.count)) end end @@ -17,8 +17,8 @@ tour = create(:tour, published: false, stops: create_list(:stop, 3)) map_icon = create(:map_icon, stop: tour.stops.last) get :show, params: { tenant: TourSet.first.subdir, id: map_icon.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end it 'returns empty MapIcon when not unauthenticated but unauthorized' do @@ -27,12 +27,12 @@ map_icon = create(:map_icon, stop: tour.stops.last) user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include TourSet.find_by(subdir: initial_tenant) + expect(user.tour_sets).not_to(include(TourSet.find_by(subdir: initial_tenant))) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) get :show, params: { tenant: initial_tenant, id: map_icon } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end end @@ -41,8 +41,8 @@ tour = create(:tour, published: true, stops: create_list(:stop, 3)) map_icon = create(:map_icon, stop: tour.stops.last) get :show, params: { tenant: TourSet.first.subdir, id: map_icon.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end it 'responds with 200 and a MapIcon when requested by a tour author' do @@ -52,8 +52,8 @@ user.tours << tour signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_icon } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end it 'responds with 200 and a list of MapIcon when requested by tenant admin' do @@ -63,8 +63,8 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_icon } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end it 'responds with 200 and a MapIcon when requested by super' do @@ -73,34 +73,34 @@ user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_icon } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_icon.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_icon.id.to_s)) end end end describe 'POST #create' do let(:tour) { create(:tour, stops: create_list(:stop, 2)) } - let(:valid_params) { + let(:valid_params) do { data: { type: 'map_icon', attributes: { filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), base_sixty_four: File.read(Rails.root.join('spec/factories/images/icon_base64.txt')), - tour_id: tour.id - } + tour_id: tour.id, + }, }, - tenant: Apartment::Tenant.current + tenant: Apartment::Tenant.current, } - } + end context 'unauthorized' do it 'returns 401 when unauthenticated' do initial_map_icon_count = MapIcon.count post :create, params: valid_params - expect(response.status).to eq(401) - expect(MapIcon.count).to eq(initial_map_icon_count) + expect(response.status).to(eq(401)) + expect(MapIcon.count).to(eq(initial_map_icon_count)) end it 'returns 401 when authenticated but unauthorized tenant admin' do @@ -108,13 +108,13 @@ initial_tenant = Apartment::Tenant.current user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include TourSet.find_by(subdir: initial_tenant) + expect(user.tour_sets).not_to(include(TourSet.find_by(subdir: initial_tenant))) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) valid_params[:tenant] = initial_tenant post :create, params: valid_params - expect(response.status).to eq(401) - expect(MapIcon.count).to eq(initial_map_icon_count) + expect(response.status).to(eq(401)) + expect(MapIcon.count).to(eq(initial_map_icon_count)) end end @@ -122,53 +122,53 @@ it 'creates when request by super' do user = create(:user, super: true) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapIcon, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapIcon, :count).by(1)) end it 'creates when request by current tenant admin' do user = create(:user, super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapIcon, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapIcon, :count).by(1)) end it 'creates when request by tour author' do user = create(:user, super: false) user.tours << tour signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapIcon, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapIcon, :count).by(1)) end end context 'invalid params' do let(:tour) { create(:tour, stops: create_list(:stop, 2)) } - let(:invalid_params) { + let(:invalid_params) do { data: { type: 'map_icon', attributes: { filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), base_sixty_four: File.read(Rails.root.join('spec/factories/images/atl_base64.txt')), - tour_id: tour.id - } + tour_id: tour.id, + }, }, - tenant: Apartment::Tenant.current + tenant: Apartment::Tenant.current, } - } + end it 'returns 422 with invalid params' do initial_map_icon_count = MapIcon.count user = create(:user, super: true) signed_cookie(user) post :create, params: { tenant: Apartment::Tenant.current, data: {} } - expect(response).to have_http_status(:unprocessable_entity) - expect(MapIcon.count).to eq(initial_map_icon_count) + expect(response).to(have_http_status(:unprocessable_entity)) + expect(MapIcon.count).to(eq(initial_map_icon_count)) end it 'returns 422 and size error message' do @@ -176,9 +176,9 @@ user = create(:user, super: true) signed_cookie(user) post :create, params: invalid_params - expect(response).to have_http_status(:unprocessable_entity) - expect(MapIcon.count).to eq(initial_map_icon_count) - expect(errors).to include('Icons should be no bigger that 80 by 80 pixels') + expect(response).to(have_http_status(:unprocessable_entity)) + expect(MapIcon.count).to(eq(initial_map_icon_count)) + expect(errors).to(include('Icons should be no bigger that 80 by 80 pixels')) end end end diff --git a/spec/controllers/v3/map_overlays_controller_spec.rb b/spec/controllers/v3/map_overlays_controller_spec.rb index e8175c3c..914c7450 100755 --- a/spec/controllers/v3/map_overlays_controller_spec.rb +++ b/spec/controllers/v3/map_overlays_controller_spec.rb @@ -2,17 +2,17 @@ require 'rails_helper' -RSpec.describe V3::MapOverlaysController, type: :controller do +RSpec.describe(V3::MapOverlaysController, type: :controller) do describe 'GET #index' do context 'unauthenticated and unauthorized' do it 'returns empty MapOverlay when not unauthenticated' do tour = create(:tour, published: false, stops: create_list(:stop, 3)) map_overlay = create(:map_overlay, tour: tour) get :show, params: { tenant: TourSet.first.subdir, id: map_overlay.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:south]).not_to eq(map_overlay.south.to_f.to_s) - expect(attributes[:east]).to be nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:south]).not_to(eq(map_overlay.south.to_f.to_s)) + expect(attributes[:east]).to(be_nil) end it 'returns empty MapOverlay when not unauthenticated but unauthorized' do @@ -21,14 +21,14 @@ map_overlay = create(:map_overlay, tour: tour) user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include TourSet.find_by(subdir: initial_tenant) + expect(user.tour_sets).not_to(include(TourSet.find_by(subdir: initial_tenant))) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) get :show, params: { tenant: initial_tenant, id: map_overlay } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:north]).not_to eq(map_overlay.north.to_f.to_s) - expect(attributes[:west]).to be nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:north]).not_to(eq(map_overlay.north.to_f.to_s)) + expect(attributes[:west]).to(be_nil) end end @@ -37,9 +37,9 @@ tour = create(:tour, published: true, stops: create_list(:stop, 3)) map_overlay = create(:map_overlay, tour: tour) get :show, params: { tenant: TourSet.first.subdir, id: map_overlay.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:south]).to eq(map_overlay.south.to_f.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:south]).to(eq(map_overlay.south.to_f.to_s)) end it 'responds with 200 and a MapOverlay when requested by a tour author' do @@ -49,9 +49,9 @@ user.tours << tour signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_overlay } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:north]).to eq(map_overlay.north.to_f.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:north]).to(eq(map_overlay.north.to_f.to_s)) end it 'responds with 200 and a list of MapOverlay when requested by tenant admin' do @@ -61,9 +61,9 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_overlay } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:north]).to eq(map_overlay.north.to_f.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:north]).to(eq(map_overlay.north.to_f.to_s)) end it 'responds with 200 and a MapOverlay when requested by super' do @@ -72,35 +72,35 @@ user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: map_overlay } - expect(response.status).to eq(200) - expect(json[:id]).to eq(map_overlay.id.to_s) - expect(attributes[:north]).to eq(map_overlay.north.to_f.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(map_overlay.id.to_s)) + expect(attributes[:north]).to(eq(map_overlay.north.to_f.to_s)) end end end describe 'POST #create' do let(:tour) { create(:tour, stops: create_list(:stop, 2)) } - let(:valid_params) { + let(:valid_params) do { data: { type: 'map_overlay', attributes: { filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), base_sixty_four: File.read(Rails.root.join('spec/factories/base64_image.txt')), - tour_id: tour.id - } + tour_id: tour.id, + }, }, - tenant: Apartment::Tenant.current + tenant: Apartment::Tenant.current, } - } + end context 'unauthorized' do it 'returns 401 when unauthenticated' do initial_map_overlay_count = MapOverlay.count post :create, params: valid_params - expect(response.status).to eq(401) - expect(MapOverlay.count).to eq(initial_map_overlay_count) + expect(response.status).to(eq(401)) + expect(MapOverlay.count).to(eq(initial_map_overlay_count)) end it 'returns 401 when authenticated but unauthorized tenant admin' do @@ -108,13 +108,13 @@ initial_tenant = Apartment::Tenant.current user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include TourSet.find_by(subdir: initial_tenant) + expect(user.tour_sets).not_to(include(TourSet.find_by(subdir: initial_tenant))) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) valid_params[:tenant] = initial_tenant post :create, params: valid_params - expect(response.status).to eq(401) - expect(MapOverlay.count).to eq(initial_map_overlay_count) + expect(response.status).to(eq(401)) + expect(MapOverlay.count).to(eq(initial_map_overlay_count)) end end @@ -122,27 +122,27 @@ it 'creates when request by super' do user = create(:user, super: true) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapOverlay, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapOverlay, :count).by(1)) end it 'creates when request by current tenant admin' do user = create(:user, super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapOverlay, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapOverlay, :count).by(1)) end it 'creates when request by tour author' do user = create(:user, super: false) user.tours << tour signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(MapOverlay, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(MapOverlay, :count).by(1)) end end @@ -152,8 +152,8 @@ user = create(:user, super: true) signed_cookie(user) post :create, params: { tenant: Apartment::Tenant.current, data: {} } - expect(response).to have_http_status(:unprocessable_entity) - expect(MapOverlay.count).to eq(initial_map_overlay_count) + expect(response).to(have_http_status(:unprocessable_entity)) + expect(MapOverlay.count).to(eq(initial_map_overlay_count)) end end end diff --git a/spec/controllers/v3/media_controller_spec.rb b/spec/controllers/v3/media_controller_spec.rb index 0e2116af..84da8856 100644 --- a/spec/controllers/v3/media_controller_spec.rb +++ b/spec/controllers/v3/media_controller_spec.rb @@ -2,33 +2,31 @@ require 'rails_helper' -RSpec.describe V3::MediaController, type: :controller do - - let(:valid_params) { +RSpec.describe(V3::MediaController, type: :controller) do + let(:valid_params) do { data: { type: 'media', attributes: { base_sixty_four: File.read(Rails.root.join('spec/factories/base64_image.txt')), - filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: '') - } + filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), + }, }, - tenant: Apartment::Tenant.current + tenant: Apartment::Tenant.current, } - } + end if ENV['DB_ADAPTER'] == 'mysql2' skip('Fix this spec for MySQL. Something to do with it being transactional') else describe 'GET #index' do - it 'returns a success response' do create_list(:medium, 5) tour = create(:tour, published: true) - Medium.all.each { |m| tour.media << m } + Medium.all.find_each { |m| tour.media << m } get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(tour.media.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(tour.media.count)) end it 'returns only media associated with a public tour' do @@ -37,8 +35,8 @@ unpublished_tour = create(:tour, published: false) create_list(:medium, rand(1..8)).each { |m| unpublished_tour.media << m } get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq(Tour.published.map { |t| t.media.count }.sum) - expect(json.count).to be < Medium.count + expect(json.count).to(eq(Tour.published.map { |t| t.media.count }.sum)) + expect(json.count).to(be < Medium.count) end it 'returns all media when requested by tenant admin' do @@ -50,18 +48,18 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(Medium.count) - expect(Medium.count).to be > Tour.published.map { |t| t.media.count }.sum + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Medium.count)) + expect(Medium.count).to(be > Tour.published.map { |t| t.media.count }.sum) end - it 'returns a paginated list when page parameter is persent' do + it 'returns a paginated list when page parameter is present' do user = create(:user, super: true) signed_cookie(user) published_tour = create(:tour, published: true) create_list(:medium, 35).each { |m| published_tour.media << m } get :index, params: { tenant: Apartment::Tenant.current, page: '2' } - expect(json.count).to eq(10) + expect(json.count).to(eq(10)) end end @@ -69,9 +67,9 @@ it 'returns 401 when medium is not published by a tour or stop' do medium = create(:medium) get :show, params: { tenant: Apartment::Tenant.current, id: medium.id } - expect(response.status).to eq(200) - expect(medium.published).to be false - expect(attributes[:title]).to eq('....') + expect(response.status).to(eq(200)) + expect(medium.published).to(be(false)) + expect(attributes[:title]).to(eq('....')) end it 'returns the medium when associated with published stop' do @@ -81,33 +79,37 @@ tour.stops << stop stop.media << medium get :show, params: { tenant: Apartment::Tenant.current, id: medium.id } - expect(medium.published).to be true - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(medium.title) + expect(medium.published).to(be(true)) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(medium.title)) end it 'returns the medium when unpublished but requested is authorized' do medium = create(:medium) - medium.save - expect(medium.file.attached?).to be true + # medium.save + expect(medium.file.attached?).to(be(true)) user = create(:user) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: medium.id } - expect(medium.published).to be false - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(medium.title) + expect(medium.published).to(be(false)) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(medium.title)) end it 'returns the medium that if a gif and requested is authorized' do - medium = create(:medium, base_sixty_four: File.read(Rails.root.join('spec/factories/images/gif_base64.txt')), filename: Faker::File.file_name(dir: '', ext: 'gif', directory_separator: '')) + medium = create( + :medium, + base_sixty_four: File.read(Rails.root.join('spec/factories/images/gif_base64.txt')), + filename: Faker::File.file_name(dir: '', ext: 'gif', directory_separator: ''), + ) medium.save - expect(medium.file.attached?).to be true + expect(medium.file.attached?).to(be(true)) user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: medium.id } - expect(response.status).to eq(200) - expect(attributes[:files][:mobile]).to end_with 'gif' + expect(response.status).to(eq(200)) + expect(attributes[:files][:mobile]).to(end_with('gif')) end end @@ -117,18 +119,18 @@ user = create(:user) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(Medium, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(Medium, :count).by(1)) end it 'renders a JSON response with the new medium when super' do user = create(:user, super: true) signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(json[:id]).to eq(Medium.last.id.to_s) + expect(response).to(have_http_status(:created)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) + expect(json[:id]).to(eq(Medium.last.id.to_s)) end it 'renders a JSON response with the new medium when tenant admin' do @@ -136,9 +138,9 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(json[:id]).to eq(Medium.last.id.to_s) + expect(response).to(have_http_status(:created)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) + expect(json[:id]).to(eq(Medium.last.id.to_s)) end it 'renders a JSON response with the new medium when tour author' do @@ -148,9 +150,9 @@ user.tours << tour signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(json[:id]).to eq(Medium.last.id.to_s) + expect(response).to(have_http_status(:created)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) + expect(json[:id]).to(eq(Medium.last.id.to_s)) end end @@ -160,8 +162,8 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) post :create, params: { medium: 'invalid_params', tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to eq('application/json; charset=utf-8') + expect(response).to(have_http_status(:unprocessable_entity)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) end it 'renders a JSON response with errors for the new medium when file is jp2' do @@ -170,18 +172,18 @@ jp2_params = valid_params.clone jp2_params[:data][:attributes] = { base_sixty_four: File.read(Rails.root.join('spec/factories/images/jp2_base64.txt')), - filename: Faker::File.file_name(dir: '', ext: 'jp2', directory_separator: '') + filename: Faker::File.file_name(dir: '', ext: 'jp2', directory_separator: ''), } post :create, params: jp2_params - expect(response).to have_http_status(:unprocessable_entity) - expect(errors).to include('JPEG 2000 fils are not supported. Plese convert the image to a reqular JPEG or WebP format.') + expect(response).to(have_http_status(:unprocessable_entity)) + expect(errors).to(include('JPEG 2000 fils are not supported. Please convert the image to a regular JPEG or WebP format.')) end end context 'with unauthenticated request' do it 'responds unauthorized when unauthenticated' do post :create, params: valid_params - expect(response).to have_http_status(:unauthorized) + expect(response).to(have_http_status(:unauthorized)) end it 'responds unauthorized when authenticated but not authorized' do @@ -189,16 +191,15 @@ user = create(:user) user.tour_sets << create(:tour_set) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) post :create, params: valid_params - expect(response).to have_http_status(:unauthorized) + expect(response).to(have_http_status(:unauthorized)) end end end describe 'PUT #update' do context 'with valid params and request is authorized' do - it 'renders a JSON response with the new medium when super' do medium = create(:medium) update_params = JSON.parse(ActiveModelSerializers::Adapter::JsonApi.new(V3::MediumSerializer.new(medium)).to_json).with_indifferent_access @@ -209,8 +210,8 @@ update_params[:data][:attributes][:title] = Faker::Movies::HitchhikersGuideToTheGalaxy.location signed_cookie(user) post :update, params: update_params - expect(response).to have_http_status(:ok) - expect(attributes[:title]).not_to eq(initial_title) + expect(response).to(have_http_status(:ok)) + expect(attributes[:title]).not_to(eq(initial_title)) end it 'renders a JSON response with the new medium when tenant admin' do @@ -225,8 +226,8 @@ update_params[:data][:attributes][:title] = Faker::Movies::HitchhikersGuideToTheGalaxy.location signed_cookie(user) post :update, params: update_params - expect(response).to have_http_status(:ok) - expect(attributes[:title]).not_to eq(initial_title) + expect(response).to(have_http_status(:ok)) + expect(attributes[:title]).not_to(eq(initial_title)) end it 'renders a JSON response with the new medium when tour author' do @@ -241,8 +242,8 @@ update_params[:data][:attributes][:title] = Faker::Movies::HitchhikersGuideToTheGalaxy.location signed_cookie(user) post :update, params: update_params - expect(response).to have_http_status(:ok) - expect(attributes[:title]).not_to eq(initial_title) + expect(response).to(have_http_status(:ok)) + expect(attributes[:title]).not_to(eq(initial_title)) end end @@ -250,9 +251,10 @@ it 'renders a JSON response with errors for the new medium' do user = create(:user, super: true) signed_cookie(user) - post :update, params: { id: create(:medium).id, data: { type: 'medium', attributes: { filename: nil } }, tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to eq('application/json; charset=utf-8') + post :update, + params: { id: create(:medium).id, data: { type: 'medium', attributes: { filename: nil } }, tenant: Apartment::Tenant.current } + expect(response).to(have_http_status(:unprocessable_entity)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) end end @@ -263,7 +265,7 @@ update_params[:tenant] = Apartment::Tenant.current update_params[:id] = update_params[:data][:id] post :update, params: update_params - expect(response).to have_http_status(:unauthorized) + expect(response).to(have_http_status(:unauthorized)) end it 'responds unauthorized when authenticated but not authorized' do @@ -275,9 +277,9 @@ user = create(:user, super: false) user.tour_sets << create(:tour_set) signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) post :update, params: update_params - expect(response).to have_http_status(:unauthorized) + expect(response).to(have_http_status(:unauthorized)) end end end @@ -288,9 +290,9 @@ medium = create(:medium) user = create(:user, super: true) signed_cookie(user) - expect { - delete :destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current } - }.to change(Medium, :count).by(-1) + expect do + delete(:destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current }) + end.to(change(Medium, :count).by(-1)) end it 'destroys the requested medium when request from tenant admin' do @@ -298,9 +300,9 @@ user = create(:user, super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - expect { - delete :destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current } - }.to change(Medium, :count).by(-1) + expect do + delete(:destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current }) + end.to(change(Medium, :count).by(-1)) end it 'destroys the requested medium when request from tour author' do @@ -309,9 +311,9 @@ user.tour_sets = [] user.tours << create(:tour) signed_cookie(user) - expect { - delete :destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current } - }.to change(Medium, :count).by(-1) + expect do + delete(:destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current }) + end.to(change(Medium, :count).by(-1)) end end @@ -324,11 +326,11 @@ tour_set = create(:tour_set) user.tour_sets << tour_set signed_cookie(user) - Apartment::Tenant.switch! initial_tenant + Apartment::Tenant.switch!(initial_tenant) initial_media_count = Medium.count delete :destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:unauthorized) - expect(Medium.count).to eq(initial_media_count) + expect(response).to(have_http_status(:unauthorized)) + expect(Medium.count).to(eq(initial_media_count)) end end @@ -337,8 +339,8 @@ medium = create(:medium) initial_media_count = Medium.count delete :destroy, params: { id: medium.to_param, tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:unauthorized) - expect(Medium.count).to eq(initial_media_count) + expect(response).to(have_http_status(:unauthorized)) + expect(Medium.count).to(eq(initial_media_count)) end end end diff --git a/spec/controllers/v3/modes_controller_spec.rb b/spec/controllers/v3/modes_controller_spec.rb index 6a103de3..39fe6528 100644 --- a/spec/controllers/v3/modes_controller_spec.rb +++ b/spec/controllers/v3/modes_controller_spec.rb @@ -2,25 +2,24 @@ require 'rails_helper' -RSpec.describe V3::ModesController, type: :controller do - - +RSpec.describe(V3::ModesController, type: :controller) do describe 'GET #index' do it 'returns a success response' do tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) get :index, params: { tenant: tour_set.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(4) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(4)) end end describe 'GET #show' do it 'returns a success response' do tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) get :show, params: { id: Mode.first.to_param, tenant: tour_set.subdir } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(Mode.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(Mode.first.title)) end end end diff --git a/spec/controllers/v3/stops_controller_spec.rb b/spec/controllers/v3/stops_controller_spec.rb index 929cd1f9..21a7fc5a 100644 --- a/spec/controllers/v3/stops_controller_spec.rb +++ b/spec/controllers/v3/stops_controller_spec.rb @@ -2,19 +2,19 @@ require 'rails_helper' -RSpec.describe V3::StopsController, type: :controller do +RSpec.describe(V3::StopsController, type: :controller) do describe 'GET #index' do it 'returns a 200 response with stops connected to published tours' do create_list(:tour_with_stops, 5, theme: create(:theme), mode: create(:mode)) Tour.first.update(published: true) Tour.last.update(published: false) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.count).to be > Tour.published.count + expect(response.status).to(eq(200)) + expect(Tour.count).to(be > Tour.published.count) json.each do |stop| - expect(Stop.find(stop[:id]).tours.any? { |tour| tour.published }) + expect(Stop.find(stop[:id]).tours.any?(&:published)) end - expect(json.count).to be < Stop.count + expect(json.count).to(be < Stop.count) end it 'returns a 200 response with no stops when request is authenticated by person with no access' do @@ -22,20 +22,18 @@ Tour.first.update(published: true) if Tour.published.empty? Tour.last.update(published: false) if Tour.published.count == Tour.count Tour.last.stops.tours = [] if Tour.last.stops.count > 1 - if Stop.all.all? { |s| s.published } - Stop.last.update(tours: []) - end + Stop.last.update(tours: []) if Stop.all.all?(&:published) user = create(:user) user.tour_sets = [] user.tours = [] signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.count).to be > Tour.published.count + expect(response.status).to(eq(200)) + expect(Tour.count).to(be > Tour.published.count) json.each do |stop| - expect(Stop.find(stop[:id]).tours.any? { |tour| tour.published }) + expect(Stop.find(stop[:id]).tours.any?(&:published)) end - expect(json.count).to be < Stop.count + expect(json.count).to(be < Stop.count) end it 'returns a 200 response with stops when request is authenticated by tenant admin and tour is unpublished' do @@ -45,9 +43,9 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: tour.tenant } - expect(Stop.count).to be > 1 - expect(response.status).to eq(200) - expect(json.count).to eq(Stop.count) + expect(Stop.count).to(be > 1) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Stop.count)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -57,9 +55,9 @@ user.tours << Tour.first signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(Tour.first.stops.count) - expect(json.count).to be < Stop.count + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Tour.first.stops.count)) + expect(json.count).to(be < Stop.count) end end @@ -67,29 +65,29 @@ it 'returns a 200 response that is empty stop' do tour = create(:tour) tour.update(published: false) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } # Make sure the stop is only associated with the newly created tour tour.stops.last.update(tours: [tour]) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.last.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(tour.stops.last.id.to_s) - expect(attributes[:title]).to be_nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(tour.stops.last.id.to_s)) + expect(attributes[:title]).to(be_nil) end it 'returns a 200 response and stop when stop is part of published tour' do tour = create(:tour) tour.update(published: true) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } tour.stops.last.update(tours: [tour]) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.last.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.stops.last.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.stops.last.title)) end it 'returns a 200 response that is empty stop when request is authenticated by someone w/o permission' do tour = create(:tour) tour.update(published: false) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } tour.stops.last.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -97,15 +95,15 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.last.id } - expect(response.status).to eq(200) - expect(json[:id]).to eq(tour.stops.last.id.to_s) - expect(attributes[:title]).to be_nil + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(tour.stops.last.id.to_s)) + expect(attributes[:title]).to(be_nil) end it 'returns a 200 response that is a stop when request is authenticated by a tour author' do tour = create(:tour) tour.update(published: false) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } tour.stops.first.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -113,14 +111,14 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.stops.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.stops.first.title)) end it 'returns a 200 response that is a stop when request is authenticated by a tenant admin' do tour = create(:tour) tour.update(published: false) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } tour.stops.first.update(tours: [tour]) user = create(:user) user.update(super: false) @@ -128,14 +126,14 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.stops.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.stops.first.title)) end it 'returns a 200 response that is a stop when request is authenticated by a super user' do tour = create(:tour) tour.update(published: false) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } tour.stops.first.update(tours: [tour]) user = create(:user) user.update(super: true) @@ -143,17 +141,17 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.stops.first.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.stops.first.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.stops.first.title)) end end describe 'POST #create' do context 'with valid params' do it 'return 401 when unauthenciated' do - post :create, params: { data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) - end + post :create, params: { data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end it 'return 401 when authenciated but not an admin for current tenant' do user = create(:user) @@ -162,7 +160,7 @@ user.tours = [] signed_cookie(user) post :create, params: { data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 201 when authenciated but an admin for current tenant' do @@ -172,9 +170,9 @@ signed_cookie(user) original_stop_count = Stop.count post :create, params: { data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Burrito Stop') - expect(Stop.count).to eq(original_stop_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Burrito Stop')) + expect(Stop.count).to(eq(original_stop_count + 1)) end it 'return 201 when authenciated by super' do @@ -184,9 +182,9 @@ signed_cookie(user) original_stop_count = Stop.count post :create, params: { data: { type: 'stops', attributes: { title: 'Taco Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Taco Stop') - expect(Stop.count).to eq(original_stop_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Taco Stop')) + expect(Stop.count).to(eq(original_stop_count + 1)) end it 'return 201 when authenciated by a tour author' do @@ -197,9 +195,9 @@ signed_cookie(user) original_stop_count = Stop.count post :create, params: { data: { type: 'stops', attributes: { title: 'Elmyr' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(attributes[:title]).to eq('Elmyr') - expect(Stop.count).to eq(original_stop_count + 1) + expect(response.status).to(eq(201)) + expect(attributes[:title]).to(eq('Elmyr')) + expect(Stop.count).to(eq(original_stop_count + 1)) end end end @@ -208,8 +206,9 @@ context 'with valid params' do it 'return 401 when unauthenciated' do create(:tour) - post :update, params: { id: Stop.last.id, data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :update, + params: { id: Stop.last.id, data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) end it 'return 401 when authenciated but not an admin for current tenant' do @@ -219,8 +218,9 @@ user.tour_sets = [] user.tours = [] signed_cookie(user) - post :update, params: { id: Stop.first.id, data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :update, + params: { id: Stop.first.id, data: { type: 'stops', attributes: { title: 'Burrito Stop' } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) end it 'return 200 and updated tour when authenciated but an admin for current tenant' do @@ -232,10 +232,10 @@ original_stop_title = Stop.last.title new_title = Faker::Name.unique.name post :update, params: { id: Stop.first.id, data: { type: 'stops', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_stop_title) - expect(attributes[:title]).to eq(new_title) - expect(Stop.first.title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_stop_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Stop.first.title).to(eq(new_title)) end it 'return 200 and updated tour when authenciated by super' do @@ -248,10 +248,10 @@ original_stop_title = Stop.last.title new_title = Faker::Name.unique.name post :update, params: { id: Stop.last.id, data: { type: 'stops', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_stop_title) - expect(attributes[:title]).to eq(new_title) - expect(Stop.last.title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_stop_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Stop.last.title).to(eq(new_title)) end it 'return 200 and updated tour when authenciated by tour author' do @@ -264,10 +264,10 @@ original_stop_title = Stop.last.title new_title = Faker::Name.unique.name post :update, params: { id: Stop.first.id, data: { type: 'stops', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(original_stop_title) - expect(attributes[:title]).to eq(new_title) - expect(Stop.first.title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(original_stop_title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Stop.first.title).to(eq(new_title)) end end @@ -286,21 +286,21 @@ it 'return 401 when unauthenciated' do create(:tour) post :destroy, params: { id: Stop.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 401 when authenciated but not an admin for current tenant' do - tour = create(:tour) + create(:tour) user = create(:user) user.update(super: false) user.tour_sets = [] signed_cookie(user) post :destroy, params: { id: Stop.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'return 204 and one less tour when authenciated but an admin for current tenant' do - tour = create(:tour) + create(:tour) user = create(:user) user.update(super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) @@ -309,12 +309,12 @@ Stop.last.update(tours: []) post :destroy, params: { id: Stop.last.id, tenant: Apartment::Tenant.current } Stop.last.update(tours: []) - expect(response.status).to eq(204) - expect(Stop.count).to eq(stop_count - 1) + expect(response.status).to(eq(204)) + expect(Stop.count).to(eq(stop_count - 1)) end it 'return 204 and one less tour when authenciated by super' do - tour = create(:tour) + create(:tour) user = create(:user) user.tour_sets = [] user.update(super: true) @@ -322,8 +322,8 @@ Stop.first.update(tours: []) stop_count = Stop.count post :destroy, params: { id: Stop.first.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(Stop.count).to eq(stop_count - 1) + expect(response.status).to(eq(204)) + expect(Stop.count).to(eq(stop_count - 1)) end it 'return 204 and one less stop when authenciated by tour author and Stop does not belong to a Tour' do @@ -336,8 +336,8 @@ Stop.last.update(tours: []) stop_count = Stop.count post :destroy, params: { id: Stop.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(Stop.count).to eq(stop_count - 1) + expect(response.status).to(eq(204)) + expect(Stop.count).to(eq(stop_count - 1)) end it 'return 405 and does not delete Stop when Stop belongs to a Tour and requested by super' do @@ -348,8 +348,8 @@ signed_cookie(user) stop_count = Stop.count post :destroy, params: { id: Stop.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Stop.count).to eq(stop_count) + expect(response.status).to(eq(405)) + expect(Stop.count).to(eq(stop_count)) end end end diff --git a/spec/controllers/v3/stops_media_controller_spec.rb b/spec/controllers/v3/stops_media_controller_spec.rb index 932c344d..c029b805 100644 --- a/spec/controllers/v3/stops_media_controller_spec.rb +++ b/spec/controllers/v3/stops_media_controller_spec.rb @@ -1,48 +1,49 @@ -require 'rails_helper' +# frozen_string_literal: true -RSpec.describe V3::StopMediaController, type: :controller do +require 'rails_helper' +RSpec.describe(V3::StopMediaController, type: :controller) do describe 'GET #index' do - before(:each) { Tour.all.each { |tour| tour.update(published: false) } } + before { Tour.all.find_each { |tour| tour.update(published: false) } } context 'unauthenticated' do it 'returns a success response but zero StopMedium objects' do create(:stop_medium, stop: create(:stop)) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(StopMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(StopMedium.count).to(be > 0) end end context 'authenticated unauthorized' do it 'returns zero StopMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current - stop_medium = create(:stop_medium, stop: create(:stop)) + create(:stop_medium, stop: create(:stop)) tour_set = create(:tour_set) user = create(:user, super: false) user.tour_sets << tour_set signed_cookie(user) get :index, params: { tenant: original_tenant } - Apartment::Tenant.switch! original_tenant - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(StopMedium.count).to be > 0 + Apartment::Tenant.switch!(original_tenant) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(StopMedium.count).to(be > 0) end it 'returns zero StopMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current - stop_medium = create(:stop_medium, stop: create(:stop)) + create(:stop_medium, stop: create(:stop)) tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) user = create(:user, super: false) user.tours << create(:tour) signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :index, params: { tenant: original_tenant } - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(StopMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(StopMedium.count).to(be > 0) end end @@ -52,8 +53,8 @@ user = create(:user, super: true) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(StopMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(StopMedium.count)) end it 'returns all StopMedium objects to tenant admin' do @@ -62,8 +63,8 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(StopMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(StopMedium.count)) end end end @@ -71,15 +72,14 @@ describe 'GET #show' do context 'unauthenticated' do it 'returns a success response but empty StopMedium objects' do - stop_medium = create(:stop_medium, stop: create(:stop)) get :show, params: { id: stop_medium.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:stop][:data]).to be nil - expect(stop_medium.stop).not_to be nil - expect(stop_medium.medium).not_to be nil - expect(StopMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:stop][:data]).to(be_nil) + expect(stop_medium.stop).not_to(be_nil) + expect(stop_medium.medium).not_to(be_nil) + expect(StopMedium.count).to(be > 0) end # it 'returns a success response but empty StopMedium objects' do @@ -91,7 +91,6 @@ end context 'authenticated unauthorized' do - it 'returns empty StopMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current stop_medium = create(:stop_medium, stop: create(:stop)) @@ -99,30 +98,30 @@ user = create(:user, super: false) user.tour_sets << tour_set signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :show, params: { id: stop_medium.id, tenant: original_tenant } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:stop][:data]).to be nil - expect(stop_medium.stop).not_to be nil - expect(stop_medium.medium).not_to be nil - expect(StopMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:stop][:data]).to(be_nil) + expect(stop_medium.stop).not_to(be_nil) + expect(stop_medium.medium).not_to(be_nil) + expect(StopMedium.count).to(be > 0) end it 'returns empty StopMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current stop_medium = create(:stop_medium, stop: create(:stop)) tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) user = create(:user, super: false) user.tours << create(:tour) signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :show, params: { id: stop_medium.id, tenant: original_tenant } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:stop][:data]).to be nil - expect(StopMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:stop][:data]).to(be_nil) + expect(StopMedium.count).to(be > 0) end end @@ -132,24 +131,25 @@ user = create(:user, super: true) signed_cookie(user) get :show, params: { id: StopMedium.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(StopMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(StopMedium.count)) end end end describe 'POST #create' do it 'returns does not create a new StopMedium' do - expect { - post :create, params: { tenant: Apartment::Tenant.current } - }.to change(StopMedium, :count).by(0) + expect do + post(:create, params: { tenant: Apartment::Tenant.current }) + end.not_to(change(StopMedium, :count)) end it 'returns 401' do user = create(:user, super: true) signed_cookie(user) - post :create, params: { data: { type: 'stop_media', attributes: { stop_id: 1, medium_id: 1, position: 1 } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :create, + params: { data: { type: 'stop_media', attributes: { stop_id: 1, medium_id: 1, position: 1 } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) end end @@ -157,13 +157,18 @@ context 'with valid params' do it 'renders a JSON response with the v3_stop_medium' do stop_medium = create(:stop_medium, position: 1) - expect(stop_medium.position).not_to eq(100) + expect(stop_medium.position).not_to(eq(100)) user = create(:user, super: true) signed_cookie(user) - put :update, params: { id: stop_medium.id, data: { type: 'stop_media', attributes: { stop_id: stop_medium.stop.id, medium_id: stop_medium.medium.id, position: 100 } }, tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:ok) - expect(attributes[:position]).to eq(100) - expect(StopMedium.find(stop_medium.id).position).to eq(100) + put :update, + params: { + id: stop_medium.id, + data: { type: 'stop_media', attributes: { stop_id: stop_medium.stop.id, medium_id: stop_medium.medium.id, position: 100 } }, + tenant: Apartment::Tenant.current, + } + expect(response).to(have_http_status(:ok)) + expect(attributes[:position]).to(eq(100)) + expect(StopMedium.find(stop_medium.id).position).to(eq(100)) end end end @@ -173,9 +178,9 @@ stop_medium = create(:stop_medium) user = create(:user, super: true) signed_cookie(user) - expect { - delete :destroy, params: { id: stop_medium.to_param, tenant: Apartment::Tenant.current } - }.to change(StopMedium, :count).by(0) + expect do + delete(:destroy, params: { id: stop_medium.to_param, tenant: Apartment::Tenant.current }) + end.not_to(change(StopMedium, :count)) end it 'responds with 405' do @@ -183,7 +188,7 @@ user = create(:user, super: true) signed_cookie(user) delete :destroy, params: { id: stop_medium.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end end diff --git a/spec/controllers/v3/themes_controller_spec.rb b/spec/controllers/v3/themes_controller_spec.rb index 80557559..ad2d219b 100644 --- a/spec/controllers/v3/themes_controller_spec.rb +++ b/spec/controllers/v3/themes_controller_spec.rb @@ -2,43 +2,41 @@ require 'rails_helper' -RSpec.describe V3::ThemesController, type: :controller do - - before(:each) { create_list(:theme, rand(3..6)) } +RSpec.describe(V3::ThemesController, type: :controller) do + before { create_list(:theme, rand(3..6)) } describe 'GET #index' do it 'returns a success response' do get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) + expect(response.status).to(eq(200)) end end describe 'GET #show' do it 'returns a success response' do get :show, params: { id: Theme.last.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) + expect(response.status).to(eq(200)) end end describe 'POST #create' do it 'returns 405' do post :create, params: { tenant: Apartment::Tenant.current, data: { type: 'theme', attributes: {} } } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end describe 'PUT #update' do it 'renders a JSON response with errors for the theme' do put :update, params: { id: Theme.first.to_param, tenant: Apartment::Tenant.current, data: { type: 'theme', attributes: {} } } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end describe 'DELETE #destroy' do it 'destroys the requested theme' do delete :destroy, params: { id: Theme.first.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end - end diff --git a/spec/controllers/v3/tour_authors_controller_spec.rb b/spec/controllers/v3/tour_authors_controller_spec.rb index c0931e9f..1dadc8f5 100755 --- a/spec/controllers/v3/tour_authors_controller_spec.rb +++ b/spec/controllers/v3/tour_authors_controller_spec.rb @@ -2,42 +2,43 @@ require 'rails_helper' -RSpec.describe V3::TourAuthorsController, type: :controller do - before(:each) { +RSpec.describe(V3::TourAuthorsController, type: :controller) do + before do create_list(:tour_set, rand(2..5)) - TourSet.all.each { |tour_set| tour_set.update(admins: create_list(:user, rand(2..5))) } - } + TourSet.all.find_each { |tour_set| tour_set.update(admins: create_list(:user, rand(2..5))) } + end describe 'GET #index' do context 'unauthenticated and unauthorized' do it 'returns 401 when not unauthenticated' do get :index, params: { tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'returns 401 when authenticated but unauthorized' do - initial_tour_set = TourSet.find_by(subdir: Apartment::Tenant.current) + initial_tour_set = TourSet.find_by(subdir: TourSet.first.subdir) user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include initial_tour_set + expect(user.tour_sets).not_to(include(initial_tour_set)) signed_cookie(user) - Apartment::Tenant.switch! initial_tour_set.subdir + # Apartment::Tenant.switch! initial_tour_set.subdir get :index, params: { tenant: initial_tour_set.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end end context 'authorized' do it 'responds with 200 and a list of TourAuthors when requested by tenant admin' do + Apartment::Tenant.switch!(TourSet.last.subdir) create_list(:tour_author, rand(3..4)) user = create(:user, super: false) - user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) + user.tour_sets << TourSet.last signed_cookie(user) - get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.first[:type]).to eq('tour_authors') - expect(TourAuthor.count).to be > 1 - expect(json.count).to eq(TourAuthor.count) + get :index, params: { tenant: TourSet.last.subdir } + expect(response.status).to(eq(200)) + expect(json.first[:type]).to(eq('tour_authors')) + expect(TourAuthor.count).to(be > 1) + expect(json.count).to(eq(TourAuthor.count)) end it 'responds with 200 and a list of TourAuthors when requested by super' do @@ -45,10 +46,10 @@ user = create(:user, super: true) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.first[:type]).to eq('tour_authors') - expect(TourAuthor.count).to be > 1 - expect(json.count).to eq(TourAuthor.count) + expect(response.status).to(eq(200)) + expect(json.first[:type]).to(eq('tour_authors')) + expect(TourAuthor.count).to(be > 1) + expect(json.count).to(eq(TourAuthor.count)) end end end @@ -58,32 +59,34 @@ it 'returns 401 when unauthenticated' do tour_author = create(:tour_author) get :show, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'returns 401 when authenticated but not authorized' do + Apartment::Tenant.switch!(TourSet.second.subdir) tour_author = create(:tour_author) - initial_tour_set = TourSet.find_by(subdir: Apartment::Tenant.current) + initial_tour_set = TourSet.second user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include initial_tour_set + expect(user.tour_sets).not_to(include(initial_tour_set)) signed_cookie(user) - Apartment::Tenant.switch! initial_tour_set.subdir + Apartment::Tenant.switch!(initial_tour_set.subdir) get :show, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end end context 'authorized' do it 'responds with 200 and a list of TourAuthors when requested by tenant admin' do - tour_author = create(:tour_author) user = create(:user, super: false) - user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) + Apartment::Tenant.switch!(TourSet.first.subdir) + tour_author = create(:tour_author) + user.tour_sets << TourSet.first signed_cookie(user) - get :show, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(200) - expect(json[:type]).to eq('tour_authors') - expect(json[:id]).to eq(tour_author.id.to_s) + get :show, params: { tenant: TourSet.first.subdir, id: tour_author.id } + expect(response.status).to(eq(200)) + expect(json[:type]).to(eq('tour_authors')) + expect(json[:id]).to(eq(tour_author.id.to_s)) end it 'responds with 200 and a list of TourAuthors when requested by super' do @@ -91,9 +94,9 @@ user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(200) - expect(json[:type]).to eq('tour_authors') - expect(json[:id]).to eq(tour_author.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:type]).to(eq('tour_authors')) + expect(json[:id]).to(eq(tour_author.id.to_s)) end end end @@ -101,7 +104,7 @@ describe 'POST #create' do it 'returns 405' do post :create, params: { tenant: Apartment::Tenant.current, data: {} } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end @@ -109,7 +112,7 @@ it 'returns 405' do tour_author = create(:tour_author) put :update, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end @@ -117,7 +120,7 @@ it 'returns 405' do tour_author = create(:tour_author) delete :destroy, params: { tenant: Apartment::Tenant.current, id: tour_author.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end end diff --git a/spec/controllers/v3/tour_flat_pages_controller_spec.rb b/spec/controllers/v3/tour_flat_pages_controller_spec.rb index 5043ef0d..4f3ff31a 100755 --- a/spec/controllers/v3/tour_flat_pages_controller_spec.rb +++ b/spec/controllers/v3/tour_flat_pages_controller_spec.rb @@ -2,24 +2,24 @@ require 'rails_helper' -RSpec.describe V3::TourFlatPagesController, type: :controller do +RSpec.describe(V3::TourFlatPagesController, type: :controller) do def data(tour, flat_page, position = 1) { type: 'tour_flat_pages', attributes: { position: position }, relationships: { tour: { data: { type: 'tours', id: tour.id } }, - flat_page: { data: { type: 'flat_pages', id: flat_page.id } } - } + flat_page: { data: { type: 'flat_pages', id: flat_page.id } }, + }, } end describe 'GET #index' do it 'returns a 200 response and empty tour when none are part of a published tour' do - Tour.all.each { |tour| tour.update(published: false) } + Tour.all.find_each { |tour| tour.update(published: false) } get :index, params: { tenant: Apartment::Tenant.current } - expect(json).to be_empty - expect(response.status).to eq(200) + expect(json).to(be_empty) + expect(response.status).to(eq(200)) end it 'returns a 200 response and only tour flat_pages that are part of a published tour' do @@ -27,16 +27,16 @@ def data(tour, flat_page, position = 1) Tour.first.update(published: true) if Tour.published.empty? Tour.last.update(published: false) if Tour.published.count == Tour.count get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(Tour.published.map { |tour| tour.tour_flat_pages.count }.sum) - end + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Tour.published.map { |tour| tour.tour_flat_pages.count }.sum)) + end it 'returns a 200 response when requeted by slug' do tour = create(:tour_with_flat_pages) tour.update(published: true) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(FlatPage.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(FlatPage.count)) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -46,8 +46,8 @@ def data(tour, flat_page, position = 1) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(FlatPage.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(FlatPage.count)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -58,8 +58,8 @@ def data(tour, flat_page, position = 1) user.tours << tour signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(FlatPage.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(FlatPage.count)) end end @@ -68,8 +68,8 @@ def data(tour, flat_page, position = 1) tour = create(:tour_with_flat_pages) tour.update(published: true) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_flat_pages.first.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -80,8 +80,8 @@ def data(tour, flat_page, position = 1) user.tours << tour signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_flat_pages.first.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -91,16 +91,16 @@ def data(tour, flat_page, position = 1) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_flat_pages.first.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response and empty json when tour is unpublished and request is not authenticated' do tour = create(:tour_with_flat_pages) tour.update(published: false) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_flat_pages.first.id } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end it 'returns a 200 response and empty json when tour is unpublished and request is authenticated by someone who is nither a tenant admin or tour author' do @@ -111,22 +111,22 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_flat_pages.first.id } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end end # TourFlatPage objects are NOT created via tha API. Every test should return 401 describe 'POST #create' do context 'with valid params' do - it 'return 405 when unauthenciated' do + it 'return 405 when unauthenticated' do tour = create(:tour) flat_page = create(:flat_page) post :create, params: { data: data(tour, flat_page), tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 when authenciated but not an admin for current tenant' do + it 'return 405 when authenticated but not an admin for current tenant' do tour = create(:tour) flat_page = create(:flat_page) original_tour_flat_page_count = TourFlatPage.count @@ -135,12 +135,12 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] user.tours = [] signed_cookie(user) - post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_flat_page_count).to eq(TourFlatPage.count) + post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_flat_page_count).to(eq(TourFlatPage.count)) end - it 'return 405 when authenciated but an admin for current tenant' do + it 'return 405 when authenticated but an admin for current tenant' do tour = create(:tour) flat_page = create(:flat_page) original_tour_flat_page_count = TourFlatPage.count @@ -149,12 +149,12 @@ def data(tour, flat_page, position = 1) user.tours = [] user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_flat_page_count).to eq(TourFlatPage.count) + post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_flat_page_count).to(eq(TourFlatPage.count)) end - it 'return 405 when authenciated by super' do + it 'return 405 when authenticated by super' do tour = create(:tour) flat_page = create(:flat_page) original_tour_flat_page_count = TourFlatPage.count @@ -163,12 +163,12 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] user.update(super: true) signed_cookie(user) - post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_flat_page_count).to eq(TourFlatPage.count) + post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_flat_page_count).to(eq(TourFlatPage.count)) end - it 'return 405 when authenciated by tour author' do + it 'return 405 when authenticated by tour author' do tour = create(:tour) flat_page = create(:flat_page) original_tour_flat_page_count = TourFlatPage.count @@ -177,26 +177,26 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] user.update(super: false) signed_cookie(user) - post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_flat_page_count).to eq(TourFlatPage.count) + post :create, params: { data: data(tour, flat_page), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_flat_page_count).to(eq(TourFlatPage.count)) end end end describe 'PUT #update' do context 'with valid params' do - it 'return 401 when unauthenciated' do + it 'return 401 when unauthenticated' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page request_data = data(tour, flat_page, 4) request_data[:id] = TourFlatPage.find_by(tour: tour, flat_page: flat_page).id post :update, params: { id: request_data[:id], data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 401 when authenciated but not an admin for current tenant' do + it 'return 401 when authenticated but not an admin for current tenant' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page @@ -208,10 +208,10 @@ def data(tour, flat_page, position = 1) request_data = data(tour, flat_page, 5) request_data[:id] = TourFlatPage.find_by(tour: tour, flat_page: flat_page).id post :update, params: { id: request_data[:id], data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 200 and updated tour when authenciated but an admin for current tenant' do + it 'return 200 and updated tour when authenticated but an admin for current tenant' do tour = create(:tour) flat_pages = create_list(:flat_page, 5) flat_pages.each { |flat_page| tour.flat_pages << flat_page } @@ -225,16 +225,16 @@ def data(tour, flat_page, position = 1) signed_cookie(user) tour_flat_page = TourFlatPage.find_by(tour: tour, flat_page: flat_page) tour_flat_page.update(position: 2) - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(2) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(2)) request_data = data(tour, flat_page, 5) request_data[:id] = tour_flat_page.id post :update, params: { id: tour_flat_page.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('5') - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(5) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('5')) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(5)) end - it 'return 200 and updated tour when authenciated by super' do + it 'return 200 and updated tour when authenticated by super' do tour = create(:tour) flat_pages = create_list(:flat_page, 5) flat_pages.each { |flat_page| tour.flat_pages << flat_page } @@ -248,16 +248,16 @@ def data(tour, flat_page, position = 1) signed_cookie(user) tour_flat_page = TourFlatPage.find_by(tour: tour, flat_page: flat_page) tour_flat_page.update(position: 3) - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(3) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(3)) request_data = data(tour, flat_page, 4) request_data[:id] = tour_flat_page.id post :update, params: { id: tour_flat_page.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('4') - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(4) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('4')) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(4)) end - it 'return 200 and updated tour when authenciated by tour author' do + it 'return 200 and updated tour when authenticated by tour author' do tour = create(:tour) flat_pages = create_list(:flat_page, 5) flat_pages.each { |flat_page| tour.flat_pages << flat_page } @@ -271,38 +271,42 @@ def data(tour, flat_page, position = 1) signed_cookie(user) tour_flat_page = TourFlatPage.find_by(tour: tour, flat_page: flat_page) tour_flat_page.update(position: 6) - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(6) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(6)) request_data = data(tour, flat_page, 1) request_data[:id] = tour_flat_page.id post :update, params: { id: tour_flat_page.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('1') - expect(TourFlatPage.find(tour_flat_page.id).position).to eq(1) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('1')) + expect(TourFlatPage.find(tour_flat_page.id).position).to(eq(1)) end it 'returns 422 when params are invalid' do tour_flat_page = create(:tour_flat_page) user = create(:user, super: true) - invalid_params = { type: 'tour_flat_pages', attributes: {}, relationships: { tour: { data: nil }, flat_page: { data: nil } } } + invalid_params = { + type: 'tour_flat_pages', + attributes: {}, + relationships: { tour: { data: nil }, flat_page: { data: nil } }, + } signed_cookie(user) post :update, params: { id: tour_flat_page.id, data: invalid_params, tenant: TourSet.first.subdir } - expect(response.status).to eq(422) - expect(errors).to include('Tour must exist') + expect(response.status).to(eq(422)) + expect(errors).to(include('Tour must exist')) end end end describe 'DELETE #destroy' do - it 'return 405 when unauthenciated' do + it 'return 405 when unauthenticated' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page tour_flat_page = TourFlatPage.find_by(tour: tour, flat_page: flat_page) post :destroy, params: { id: tour_flat_page.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 when authenciated but not an admin for current tenant' do + it 'return 405 when authenticated but not an admin for current tenant' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page @@ -312,10 +316,10 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] signed_cookie(user) post :destroy, params: { id: tour_flat_page.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 and one less tour when authenciated but an admin for current tenant' do + it 'return 405 and one less tour when authenticated but an admin for current tenant' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page @@ -326,11 +330,11 @@ def data(tour, flat_page, position = 1) signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour_flat_page.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end - it 'return 405 and one less tour when authenciated by super' do + it 'return 405 and one less tour when authenticated by super' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page @@ -341,11 +345,11 @@ def data(tour, flat_page, position = 1) signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour_flat_page.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end - it 'return 405 and one less tour when authenciated by tour author' do + it 'return 405 and one less tour when authenticated by tour author' do tour = create(:tour) flat_page = create(:flat_page) tour.flat_pages << flat_page @@ -355,11 +359,10 @@ def data(tour, flat_page, position = 1) user.tour_sets = [] user.tours << tour signed_cookie(user) - new_title = Faker::Name.unique.name tour_count = Tour.count post :destroy, params: { id: tour_flat_page.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end end end diff --git a/spec/controllers/v3/tour_geojson_controller_spec.rb b/spec/controllers/v3/tour_geojson_controller_spec.rb index b3193615..795f399d 100644 --- a/spec/controllers/v3/tour_geojson_controller_spec.rb +++ b/spec/controllers/v3/tour_geojson_controller_spec.rb @@ -1,18 +1,24 @@ -require 'rails_helper' +# frozen_string_literal: true -RSpec.describe V3::GeojsonToursController, type: :controller do +require 'rails_helper' +RSpec.describe(V3::GeojsonToursController, type: :controller) do describe 'GET #show' do it 'returns a geojosn representation of a tour when tour published' do - tour = create(:tour, published: true, media: create_list(:medium, rand(1..3)), stops: create_list(:stop, rand(4..7))) + tour = create( + :tour, + published: true, + media: create_list(:medium, rand(1..3)), + stops: create_list(:stop, rand(4..7)), + ) tour.stops.each { |stop| stop.media << create_list(:medium, rand(1..3)) } get :show, params: { id: tour.to_param, tenant: Apartment::Tenant.current } geojson = JSON.parse(response.body).with_indifferent_access first_stop = Stop.find_by(title: geojson[:features].first[:properties][:title]) - expect(geojson[:type]).to eq('FeatureCollection') - expect(geojson[:features].count).to eq(tour.stops.count) - expect(geojson[:features].first[:geometry][:coordinates]).to eq([first_stop.lng.to_f, first_stop.lat.to_f]) - expect(first_stop.media.map(&:caption)).to include geojson[:features].first[:properties][:images].first[:caption] + expect(geojson[:type]).to(eq('FeatureCollection')) + expect(geojson[:features].count).to(eq(tour.stops.count)) + expect(geojson[:features].first[:geometry][:coordinates]).to(eq([first_stop.lng.to_f, first_stop.lat.to_f])) + expect(first_stop.media.map(&:caption)).to(include(geojson[:features].first[:properties][:images].first[:caption])) end # TODO: Renable after OpenWorld stuff @@ -22,5 +28,4 @@ # expect(response.status).to eq(401) # end end - end diff --git a/spec/controllers/v3/tour_media_controller_spec.rb b/spec/controllers/v3/tour_media_controller_spec.rb index ac53ccde..908a712a 100644 --- a/spec/controllers/v3/tour_media_controller_spec.rb +++ b/spec/controllers/v3/tour_media_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' # This spec was generated by rspec-rails when you ran the scaffold generator. @@ -23,58 +25,55 @@ # removed from Rails core in Rails 5, but can be added back in via the # `rails-controller-testing` gem. -RSpec.describe V3::TourMediaController, type: :controller do - +RSpec.describe(V3::TourMediaController, type: :controller) do describe 'GET #index' do - before(:each) { Tour.all.each { |tour| tour.update(published: false) } } + before { Tour.all.find_each { |tour| tour.update(published: false) } } context 'unauthenticated' do it 'returns a success response but zero TourMedium objects' do - - tour_medium = create(:tour_medium, tour: create(:tour, published: false)) + create(:tour_medium, tour: create(:tour, published: false)) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(TourMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(TourMedium.count).to(be > 0) end it 'returns a success response but zero TourMedium objects' do - tour_medium = create(:tour_medium, tour: create(:tour, published: true)) + create(:tour_medium, tour: create(:tour, published: true)) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(1) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) end end context 'authenticated unauthorized' do - it 'returns zero TourMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current - tour_medium = create(:tour_medium, tour: create(:tour, published: false)) + create(:tour_medium, tour: create(:tour, published: false)) tour_set = create(:tour_set) user = create(:user, super: false) user.tour_sets << tour_set signed_cookie(user) get :index, params: { tenant: original_tenant } - Apartment::Tenant.switch! original_tenant - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(TourMedium.count).to be > 0 + Apartment::Tenant.switch!(original_tenant) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(TourMedium.count).to(be > 0) end it 'returns zero TourMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current - tour_medium = create(:tour_medium, tour: create(:tour, published: false)) + create(:tour_medium, tour: create(:tour, published: false)) tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) user = create(:user, super: false) user.tours << create(:tour) signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :index, params: { tenant: original_tenant } - expect(response.status).to eq(200) - expect(json.count).to eq(0) - expect(TourMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) + expect(TourMedium.count).to(be > 0) end end @@ -84,8 +83,8 @@ user = create(:user, super: true) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(TourMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(TourMedium.count)) end it 'returns all TourMedium objects to tenant admin' do @@ -94,8 +93,8 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(TourMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(TourMedium.count)) end end end @@ -103,15 +102,14 @@ describe 'GET #show' do context 'unauthenticated' do it 'returns a success response but empty TourMedium objects' do - tour_medium = create(:tour_medium, tour: create(:tour, published: false)) get :show, params: { id: tour_medium.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:tour][:data]).to be nil - expect(tour_medium.tour).not_to be nil - expect(tour_medium.medium).not_to be nil - expect(TourMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:tour][:data]).to(be_nil) + expect(tour_medium.tour).not_to(be_nil) + expect(tour_medium.medium).not_to(be_nil) + expect(TourMedium.count).to(be > 0) end # it 'returns a success response but empty TourMedium objects' do @@ -123,7 +121,6 @@ end context 'authenticated unauthorized' do - it 'returns empty TourMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current tour_medium = create(:tour_medium, tour: create(:tour, published: false)) @@ -131,30 +128,30 @@ user = create(:user, super: false) user.tour_sets << tour_set signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :show, params: { id: tour_medium.id, tenant: original_tenant } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:tour][:data]).to be nil - expect(tour_medium.tour).not_to be nil - expect(tour_medium.medium).not_to be nil - expect(TourMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:tour][:data]).to(be_nil) + expect(tour_medium.tour).not_to(be_nil) + expect(tour_medium.medium).not_to(be_nil) + expect(TourMedium.count).to(be > 0) end it 'returns empty TourMedium objects, not current tenant admin, non tour author' do original_tenant = Apartment::Tenant.current tour_medium = create(:tour_medium, tour: create(:tour, published: false)) tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir + Apartment::Tenant.switch!(tour_set.subdir) user = create(:user, super: false) user.tours << create(:tour) signed_cookie(user) - Apartment::Tenant.switch! original_tenant + Apartment::Tenant.switch!(original_tenant) get :show, params: { id: tour_medium.id, tenant: original_tenant } - expect(response.status).to eq(200) - expect(relationships[:medium][:data]).to be nil - expect(relationships[:tour][:data]).to be nil - expect(TourMedium.count).to be > 0 + expect(response.status).to(eq(200)) + expect(relationships[:medium][:data]).to(be_nil) + expect(relationships[:tour][:data]).to(be_nil) + expect(TourMedium.count).to(be > 0) end end @@ -164,61 +161,67 @@ user = create(:user, super: true) signed_cookie(user) get :show, params: { id: TourMedium.last.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(TourMedium.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(TourMedium.count)) end end end describe 'POST #create' do it 'returns does not create a new TourMedium' do - expect { - post :create, params: { tenant: Apartment::Tenant.current } - }.to change(TourMedium, :count).by(0) + expect do + post(:create, params: { tenant: Apartment::Tenant.current }) + end.not_to(change(TourMedium, :count)) end it 'returns 401' do user = create(:user, super: true) signed_cookie(user) - post :create, params: { data: { type: 'tour_media', attributes: { tour_id: 1, medium_id: 1, position: 1 } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + post :create, + params: { data: { type: 'tour_media', attributes: { tour_id: 1, medium_id: 1, position: 1 } }, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) end end describe 'PUT #update' do context 'with valid params' do - # let(:new_attributes) { - # skip('Add a hash of attributes valid for your model') - # } + # let(:new_attributes) { + # skip('Add a hash of attributes valid for your model') + # } - # it 'updates the requested v3_tour_medium' do - # tour_medium = TourMedium.create! valid_attributes - # put :update, params: {id: tour_medium.to_param, v3_tour_medium: new_attributes}, session: valid_session - # tour_medium.reload - # skip('Add assertions for updated state') - # end + # it 'updates the requested v3_tour_medium' do + # tour_medium = TourMedium.create! valid_attributes + # put :update, params: {id: tour_medium.to_param, v3_tour_medium: new_attributes}, session: valid_session + # tour_medium.reload + # skip('Add assertions for updated state') + # end it 'renders a JSON response with the v3_tour_medium' do tour_medium = create(:tour_medium, position: 1) - expect(tour_medium.position).not_to eq(100) + expect(tour_medium.position).not_to(eq(100)) user = create(:user, super: true) signed_cookie(user) - put :update, params: { id: tour_medium.id, data: { type: 'tour_media', attributes: { tour_id: tour_medium.tour.id, medium_id: tour_medium.medium.id, position: 100 } }, tenant: Apartment::Tenant.current } - expect(response).to have_http_status(:ok) - expect(attributes[:position]).to eq(100) - expect(TourMedium.find(tour_medium.id).position).to eq(100) + put :update, + params: { + id: tour_medium.id, + data: { type: 'tour_media', attributes: { tour_id: tour_medium.tour.id, medium_id: tour_medium.medium.id, position: 100 } }, + tenant: Apartment::Tenant.current, + } + expect(response).to(have_http_status(:ok)) + expect(attributes[:position]).to(eq(100)) + expect(TourMedium.find(tour_medium.id).position).to(eq(100)) end end - # context 'with invalid params' do - # it 'renders a JSON response with errors for the v3_tour_medium' do - # tour_medium = TourMedium.create! valid_attributes + # context 'with invalid params' do + # it 'renders a JSON response with errors for the v3_tour_medium' do + # tour_medium = TourMedium.create! valid_attributes - # put :update, params: {id: tour_medium.to_param, v3_tour_medium: invalid_attributes}, session: valid_session - # expect(response).to have_http_status(:unprocessable_entity) - # expect(response.content_type).to eq('application/json') - # end - # end + # put :update, params: {id: tour_medium.to_param, v3_tour_medium: invalid_attributes}, session: valid_session + # expect(response).to have_http_status(:unprocessable_entity) + # expect(response.content_type).to eq('application/json') + # end + # end end describe 'DELETE #destroy' do @@ -226,9 +229,9 @@ tour_medium = create(:tour_medium) user = create(:user, super: true) signed_cookie(user) - expect { - delete :destroy, params: { id: tour_medium.to_param, tenant: Apartment::Tenant.current } - }.to change(TourMedium, :count).by(0) + expect do + delete(:destroy, params: { id: tour_medium.to_param, tenant: Apartment::Tenant.current }) + end.not_to(change(TourMedium, :count)) end it 'responds with 405' do @@ -236,7 +239,7 @@ user = create(:user, super: true) signed_cookie(user) delete :destroy, params: { id: tour_medium.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end end diff --git a/spec/controllers/v3/tour_set_admins_controller_spec.rb b/spec/controllers/v3/tour_set_admins_controller_spec.rb index 196290ee..dfe8b7cc 100644 --- a/spec/controllers/v3/tour_set_admins_controller_spec.rb +++ b/spec/controllers/v3/tour_set_admins_controller_spec.rb @@ -2,28 +2,28 @@ require 'rails_helper' -RSpec.describe V3::TourSetAdminsController, type: :controller do - before(:each) { +RSpec.describe(V3::TourSetAdminsController, type: :controller) do + before do create_list(:tour_set, rand(2..5)) - TourSet.all.each { |tour_set| tour_set.update(admins: create_list(:user, rand(2..5))) } - } + TourSet.all.find_each { |tour_set| tour_set.update(admins: create_list(:user, rand(2..5))) } + end describe 'GET #index' do context 'unauthenticated and unauthorized' do it 'returns 401 when not unauthenticated' do get :index, params: { tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'returns 401 when not unauthenticated but unauthorized' do initial_tour_set = TourSet.first user = create(:user, super: false) user.tour_sets << create(:tour_set) - expect(user.tour_sets).not_to include initial_tour_set + expect(user.tour_sets).not_to(include(initial_tour_set)) signed_cookie(user) - Apartment::Tenant.switch! initial_tour_set.subdir + Apartment::Tenant.switch!(initial_tour_set.subdir) get :index, params: { tenant: initial_tour_set.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end end @@ -32,21 +32,21 @@ user = create(:user, super: false) user.tour_sets << TourSet.last signed_cookie(user) - Apartment::Tenant.switch! TourSet.last.subdir + Apartment::Tenant.switch!(TourSet.last.subdir) get :index, params: { tenant: TourSet.last.subdir } - expect(response.status).to eq(200) - expect(json.first[:type]).to eq('tour_set_admins') - expect(json.count).to eq(TourSetAdmin.count) + expect(response.status).to(eq(200)) + expect(json.first[:type]).to(eq('tour_set_admins')) + expect(json.count).to(eq(TourSetAdmin.count)) end it 'responds with 200 and a list of TourSetAdmins when requested by super' do user = create(:user, super: true) signed_cookie(user) - Apartment::Tenant.switch! TourSet.first.subdir + Apartment::Tenant.switch!(TourSet.first.subdir) get :index, params: { tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(json.first[:type]).to eq('tour_set_admins') - expect(json.count).to eq(TourSetAdmin.count) + expect(response.status).to(eq(200)) + expect(json.first[:type]).to(eq('tour_set_admins')) + expect(json.count).to(eq(TourSetAdmin.count)) end end end @@ -54,28 +54,28 @@ describe 'GET #show' do it 'returns 405' do get :show, params: { tenant: Apartment::Tenant.current, id: TourSetAdmin.first.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end describe 'POST #create' do it 'returns 405' do post :create, params: { tenant: Apartment::Tenant.current, id: TourSetAdmin.first.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end describe 'PUT #update' do it 'returns 405' do put :update, params: { tenant: Apartment::Tenant.current, id: TourSetAdmin.first.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end describe 'DELETE #destroy' do it 'returns 405' do delete :destroy, params: { tenant: Apartment::Tenant.current, id: TourSetAdmin.first.id } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end end end diff --git a/spec/controllers/v3/tour_sets_controller_spec.rb b/spec/controllers/v3/tour_sets_controller_spec.rb index 5c383f5a..e6c0d9b8 100644 --- a/spec/controllers/v3/tour_sets_controller_spec.rb +++ b/spec/controllers/v3/tour_sets_controller_spec.rb @@ -2,16 +2,15 @@ require 'rails_helper' -RSpec.describe V3::TourSetsController, type: :controller do - +RSpec.describe(V3::TourSetsController, type: :controller) do # This should return the minimal set of attributes required to create a valid # TourSet. As you add validations to TourSet, be sure to # adjust the attributes here as well. - let(:valid_attributes) { - } + let(:valid_attributes) do + end - let(:invalid_attributes) { - } + let(:invalid_attributes) do + end # This should return the minimal set of values that should be in the session # in order to pass any filters (e.g. authentication) defined in @@ -22,63 +21,63 @@ let(:valid_params) { { data: { type: 'tour_sets', attributes: { name: Faker::Music::Hiphop.artist } }, tenant: 'public' } } let(:invalid_params) { { data: { type: 'tour_sets', attributes: { name: nil } }, tenant: 'public' } } - before(:each) do + before do Apartment::Tenant.reset - TourSet.all.each { |ts| ts.delete } + TourSet.all.find_each(&:delete) create_list(:tour_set, rand(3..5)) end describe 'GET #index' do it 'returns a success response but return no TourSet objects' do get :index, params: { tenant: 'public' } - expect(response.status).to eq(200) - expect(json.count).to eq(0) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) end it 'returns a success response and returns TourSet objects with tours that are published and have stops' do - Apartment::Tenant.switch! TourSet.second.subdir + Apartment::Tenant.switch!(TourSet.second.subdir) tour = create(:tour, published: true) tour.stops << create(:stop) get :index, params: { tenant: 'public' } - expect(response.status).to eq(200) - expect(json.count).to eq(1) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) end it 'returns a success response by subdir but returns no TourSet objects when no published tours and not authorized' do get :index, params: { tenant: 'public', subdir: TourSet.last.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(0) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) end it 'returns a success response and only TourSet object by subdir when tour set has a published tour and not authorized' do - TourSet.all.each do |ts| - Apartment::Tenant.switch! ts.subdir + TourSet.all.find_each do |ts| + Apartment::Tenant.switch!(ts.subdir) tour = create(:tour, published: true) tour.stops << create(:stop) end Apartment::Tenant.reset - expect(TourSet.all.reject { |ts| ts.published_tours.empty? }.count).to be > 1 + expect(TourSet.all.reject { |ts| ts.published_tours.empty? }.count).to(be > 1) get :index, params: { tenant: 'public', subdir: TourSet.second.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(1) - expect(attributes.first[:name]).to eq(TourSet.second.name) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) + expect(attributes.first[:name]).to(eq(TourSet.second.name)) end it 'returns a success response and only one TourSet object by subdir when tour set has a published tour and not authorized' do - Apartment::Tenant.switch! TourSet.second.subdir + Apartment::Tenant.switch!(TourSet.second.subdir) tour = create(:tour, published: true) tour.stops << create(:stop) get :index, params: { tenant: 'public', subdir: TourSet.second.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(1) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) end it 'returns all TourSet objects when requested by super' do user = create(:user, super: true) signed_cookie(user) get :index, params: { tenant: 'public' } - expect(response.status).to eq(200) - expect(json.count).to eq(TourSet.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(TourSet.count)) end it 'returns TourSet objects when requested by admin' do @@ -87,20 +86,20 @@ # Make sure a set doesn't slip in because of published tours. [TourSet.first.subdir, TourSet.last.subdir].each do |ts| - Apartment::Tenant.switch! ts + Apartment::Tenant.switch!(ts) Tour.all.update(published: false) end # Make a new set with published tour to make sure it's included. published_set = create(:tour_set) - Apartment::Tenant.switch! published_set.subdir + Apartment::Tenant.switch!(published_set.subdir) create(:tour, published: true, stops: create_list(:stop, 2)) Apartment::Tenant.reset signed_cookie(user) get :index, params: { tenant: 'public' } - expect(response.status).to eq(200) - expect(json.count).to be > 2 + expect(response.status).to(eq(200)) + expect(json.count).to(be > 2) end it 'returns no TourSet objects when requested by non admin' do @@ -108,33 +107,33 @@ user.tour_sets = [] signed_cookie(user) get :index, params: { tenant: 'public' } - expect(response.status).to eq(200) - expect(json.count).to eq(0) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(0)) end it 'returns one unpublished TourSet object when requested by subdir and by super' do user = create(:user, super: true) signed_cookie(user) - Apartment::Tenant.switch! TourSet.second.subdir - Tour.all.each { |t| t.update(published: false) } + Apartment::Tenant.switch!(TourSet.second.subdir) + Tour.all.find_each { |t| t.update(published: false) } Apartment::Tenant.reset get :index, params: { tenant: 'public', subdir: TourSet.second.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(1) - expect(attributes.first[:name]).to eq(TourSet.second.name) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) + expect(attributes.first[:name]).to(eq(TourSet.second.name)) end it 'returns one unpublished TourSet object when requested by subdir and by super' do user = create(:user, super: false) user.tour_sets << TourSet.last signed_cookie(user) - Apartment::Tenant.switch! TourSet.last.subdir - Tour.all.each { |t| t.update(published: false) } + Apartment::Tenant.switch!(TourSet.last.subdir) + Tour.all.find_each { |t| t.update(published: false) } Apartment::Tenant.reset get :index, params: { tenant: 'public', subdir: TourSet.last.subdir } - expect(response.status).to eq(200) - expect(json.count).to eq(1) - expect(attributes.first[:name]).to eq(TourSet.last.name) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(1)) + expect(attributes.first[:name]).to(eq(TourSet.last.name)) end end @@ -142,18 +141,18 @@ context 'unauthenticated' do it 'returns a success response and dummy TourSet when no tour is published' do get :show, params: { tenant: 'public', id: TourSet.first.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq('....') + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq('....')) end it 'returns a success response and TourSet when TourSet has published tour' do - Apartment::Tenant.switch! TourSet.last.subdir + Apartment::Tenant.switch!(TourSet.last.subdir) tour = create(:tour, published: true) tour.stops << create(:stop) get :show, params: { tenant: 'public', id: TourSet.last.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq(TourSet.last.name) - expect(relationships[:admins][:data]).to be_empty + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq(TourSet.last.name)) + expect(relationships[:admins][:data]).to(be_empty) end end @@ -163,8 +162,8 @@ user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: 'public', id: TourSet.first.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq('....') + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq('....')) end it 'returns a success response and TourSet when TourSet has published tour' do @@ -172,8 +171,8 @@ user.tour_sets << TourSet.first signed_cookie(user) get :show, params: { tenant: 'public', id: TourSet.last.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq('....') + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq('....')) end end @@ -183,17 +182,17 @@ user.tour_sets << TourSet.last signed_cookie(user) get :show, params: { tenant: 'public', id: TourSet.last.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq(TourSet.last.name) - expect(relationships[:admins][:data].map { |admin| admin[:id] }).to include(user.id.to_s) + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq(TourSet.last.name)) + expect(relationships[:admins][:data].map { |admin| admin[:id] }).to(include(user.id.to_s)) end it 'returns a success response and TourSet when requested by super' do user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: 'public', id: TourSet.last.to_param } - expect(response.status).to eq(200) - expect(attributes[:name]).to eq(TourSet.last.name) + expect(response.status).to(eq(200)) + expect(attributes[:name]).to(eq(TourSet.last.name)) end end end @@ -201,14 +200,14 @@ describe 'POST #create' do context 'when unauthenticated and unauthorized' do it 'does not create a new TourSet when not super' do - expect { - post :create, params: valid_params - }.to change(TourSet, :count).by(0) + expect do + post(:create, params: valid_params) + end.not_to(change(TourSet, :count)) end it 'returns 401' do post :create, params: valid_params - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end @@ -216,25 +215,25 @@ it 'does not create a new TourSet when not super' do user = create(:user, super: false) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(TourSet, :count).by(0) + expect do + post(:create, params: valid_params) + end.not_to(change(TourSet, :count)) end it 'returns 401 when not super' do user = create(:user, super: false) signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end it 'does not create a new TourSet when not super but is a tenant admin' do user = create(:user, super: false) user.tour_sets << TourSet.second signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(TourSet, :count).by(0) + expect do + post(:create, params: valid_params) + end.not_to(change(TourSet, :count)) end it 'returns 401 when not super but is a tenant admin' do @@ -242,7 +241,7 @@ user.tour_sets << TourSet.first signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end @@ -251,17 +250,17 @@ it 'creates a new TourSet' do user = create(:user, super: true) signed_cookie(user) - expect { - post :create, params: valid_params - }.to change(TourSet, :count).by(1) + expect do + post(:create, params: valid_params) + end.to(change(TourSet, :count).by(1)) end it 'renders a JSON response with the new tour_set' do user = create(:user, super: true) signed_cookie(user) post :create, params: valid_params - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') + expect(response).to(have_http_status(:created)) + expect(response.content_type).to(eq('application/json; charset=utf-8')) end end end @@ -271,7 +270,7 @@ user = create(:user, super: true) signed_cookie(user) post :create, params: invalid_params - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to(have_http_status(:unprocessable_entity)) end end end @@ -280,7 +279,7 @@ context 'when unauthenticated and unauthorized' do it 'returns 401' do put :update, params: valid_params.merge({ id: TourSet.last.to_param }) - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end end @@ -289,7 +288,7 @@ user = create(:user, super: false) signed_cookie(user) put :update, params: valid_params.merge({ id: TourSet.first.to_param }) - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end it 'allows update TourSet when not super but is a tenant admin' do @@ -297,20 +296,19 @@ user.tour_sets << TourSet.second signed_cookie(user) put :update, params: valid_params.merge({ id: TourSet.second.to_param }) - expect(response).to have_http_status(200) + expect(response).to(have_http_status(:ok)) end it 'does not update TourSet when not super not a tenant admin but is a tour author' do - Apartment::Tenant.switch! TourSet.second.subdir + Apartment::Tenant.switch!(TourSet.second.subdir) tour = create(:tour) user = create(:user, super: false) user.tour_sets = [] user.tours << tour signed_cookie(user) put :update, params: valid_params.merge({ id: TourSet.second.to_param }) - expect(response).to have_http_status(401) + expect(response).to(have_http_status(:unauthorized)) end - end context 'when authenticated and authorized' do @@ -321,8 +319,8 @@ user = create(:user, super: true) signed_cookie(user) put :update, params: valid_params.merge({ id: TourSet.last.to_param }) - expect(response).to have_http_status(200) - expect(attributes[:name]).to eq(new_name) + expect(response).to(have_http_status(:ok)) + expect(attributes[:name]).to(eq(new_name)) end end @@ -331,17 +329,17 @@ tour_set = create(:tour_set) tour_set.update( logo_title: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), - base_sixty_four: File.read(Rails.root.join('spec/factories/base64_image.txt')) + base_sixty_four: File.read(Rails.root.join('spec/factories/base64_image.txt')), ) - expect(TourSet.find(tour_set.id).logo.attached?).to be true + expect(TourSet.find(tour_set.id).logo.attached?).to(be(true)) serialized_tour_set = JSON.parse(ActiveModelSerializers::Adapter::JsonApi.new(V3::TourSetSerializer.new(tour_set)).to_json).with_indifferent_access serialized_tour_set[:data][:attributes][:base_sixty_four] = nil serialized_tour_set[:data][:attributes][:logo] = nil user = create(:user, super: true) signed_cookie(user) put :update, params: { data: serialized_tour_set[:data], id: tour_set.id, tenant: 'public' } - expect(response).to have_http_status(200) - expect(TourSet.find(tour_set.id).logo.attached?).to be false + expect(response).to(have_http_status(:ok)) + expect(TourSet.find(tour_set.id).logo.attached?).to(be(false)) end end end @@ -351,7 +349,7 @@ user = create(:user, super: true) signed_cookie(user) put :update, params: invalid_params.merge({ id: TourSet.first.to_param }) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to(have_http_status(:unprocessable_entity)) end end end @@ -362,9 +360,9 @@ user = create(:user, super: true) signed_cookie(user) Apartment::Tenant.reset - expect { - delete :destroy, params: { id: tour_set.to_param, tenant: Apartment::Tenant.current } - }.to change(TourSet, :count).by(-1) + expect do + delete(:destroy, params: { id: tour_set.to_param, tenant: Apartment::Tenant.current }) + end.to(change(TourSet, :count).by(-1)) end end end diff --git a/spec/controllers/v3/tour_stops_controller_spec.rb b/spec/controllers/v3/tour_stops_controller_spec.rb index 4d26035c..ba5ae295 100644 --- a/spec/controllers/v3/tour_stops_controller_spec.rb +++ b/spec/controllers/v3/tour_stops_controller_spec.rb @@ -2,24 +2,24 @@ require 'rails_helper' -RSpec.describe V3::TourStopsController, type: :controller do +RSpec.describe(V3::TourStopsController, type: :controller) do def data(tour, stop, position = 1) { type: 'tour_stops', attributes: { position: position }, relationships: { tour: { data: { type: 'tours', id: tour.id } }, - stop: { data: { type: 'stops', id: stop.id } } - } + stop: { data: { type: 'stops', id: stop.id } }, + }, } end describe 'GET #index' do it 'returns a 200 response and empty tour when none are part of a published tour' do - Tour.all.each { |tour| tour.update(published: false) } + Tour.all.find_each { |tour| tour.update(published: false) } get :index, params: { tenant: Apartment::Tenant.current } - expect(json).to be_empty - expect(response.status).to eq(200) + expect(json).to(be_empty) + expect(response.status).to(eq(200)) end it 'returns a 200 response and only tour stops that are part of a published tour' do @@ -27,16 +27,16 @@ def data(tour, stop, position = 1) Tour.first.update(published: true) if Tour.published.empty? Tour.last.update(published: false) if Tour.published.count == Tour.count get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json.count).to eq(Tour.published.map { |tour| tour.tour_stops.count }.sum) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Tour.published.map { |tour| tour.tour_stops.count }.sum)) end - it 'returns a 200 response when requeted by slug' do + it 'returns a 200 response when requested by slug' do tour = create(:tour_with_stops) tour.update(published: true) get :index, params: { tenant: Apartment::Tenant.current, slug: tour.tour_stops.first.stop.slug, tour: tour.id } - expect(response.status).to eq(200) - expect(included.first[:attributes][:title]).to eq(tour.tour_stops.first.stop.title) + expect(response.status).to(eq(200)) + expect(included.first[:attributes][:title]).to(eq(tour.tour_stops.first.stop.title)) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -46,8 +46,8 @@ def data(tour, stop, position = 1) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current, slug: tour.tour_stops.first.stop.slug, tour: tour.id } - expect(response.status).to eq(200) - expect(included.first[:attributes][:title]).to eq(tour.tour_stops.first.stop.title) + expect(response.status).to(eq(200)) + expect(included.first[:attributes][:title]).to(eq(tour.tour_stops.first.stop.title)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -58,21 +58,21 @@ def data(tour, stop, position = 1) user.tours << tour signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current, slug: tour.tour_stops.first.stop.slug, tour: tour.id } - expect(response.status).to eq(200) - expect(included.first[:attributes][:title]).to eq(tour.tour_stops.first.stop.title) + expect(response.status).to(eq(200)) + expect(included.first[:attributes][:title]).to(eq(tour.tour_stops.first.stop.title)) end it 'returns empty TourStop when `fastboo` in params' do create(:tour_with_stops, published: true) get :index, params: { tenant: Apartment::Tenant.current, fastboot: 'true' } - expect(response.status).to eq(200) - expect(json[:id]).to eq(0) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(0)) end it 'returns empty json when unauthenticated but tour is not published' do tour = create(:tour_with_stops, published: false) get :index, params: { tenant: Apartment::Tenant.current, slug: tour.tour_stops.first.stop.slug, tour: tour.id } - expect(json).to be nil + expect(json).to(be_nil) end it 'returns all TourStops when requested by tenant admin' do @@ -81,18 +81,17 @@ def data(tour, stop, position = 1) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq(TourStop.count) + expect(json.count).to(eq(TourStop.count)) end end describe 'GET #show' do it 'returns a 200 response' do - tours = create(:tour_with_stops) tour = Tour.last tour.update(published: true) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_stops.last.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -103,8 +102,8 @@ def data(tour, stop, position = 1) user.tours << tour signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_stops.last.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -114,16 +113,16 @@ def data(tour, stop, position = 1) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_stops.last.id } - expect(response.status).to eq(200) - expect(relationships[:tour][:data][:id]).to eq(tour.id.to_s) + expect(response.status).to(eq(200)) + expect(relationships[:tour][:data][:id]).to(eq(tour.id.to_s)) end it 'returns a 200 response and empty json when tour is unpublished and request is not authenticated' do tour = create(:tour_with_stops) tour.update(published: false) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_stops.last.id } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end it 'returns a 200 response and empty json when tour is unpublished and request is authenticated by someone who is nither a tenant admin or tour author' do @@ -134,22 +133,22 @@ def data(tour, stop, position = 1) user.tour_sets = [] signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.tour_stops.last.id } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end end # TourStop objects are NOT created via tha API. Every test should return 401 describe 'POST #create' do context 'with valid params' do - it 'return 405 when unauthenciated' do + it 'return 405 when unauthenticated' do tour = create(:tour) stop = create(:stop) post :create, params: { data: data(tour, stop), tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 when authenciated but not an admin for current tenant' do + it 'return 405 when authenticated but not an admin for current tenant' do tour = create(:tour) stop = create(:stop) original_tour_stop_count = TourStop.count @@ -158,12 +157,12 @@ def data(tour, stop, position = 1) user.tour_sets = [] user.tours = [] signed_cookie(user) - post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_stop_count).to eq(TourStop.count) + post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_stop_count).to(eq(TourStop.count)) end - it 'return 405 when authenciated but an admin for current tenant' do + it 'return 405 when authenticated but an admin for current tenant' do tour = create(:tour) stop = create(:stop) original_tour_stop_count = TourStop.count @@ -172,12 +171,12 @@ def data(tour, stop, position = 1) user.tours = [] user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) - post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_stop_count).to eq(TourStop.count) + post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_stop_count).to(eq(TourStop.count)) end - it 'return 405 when authenciated by super' do + it 'return 405 when authenticated by super' do tour = create(:tour) stop = create(:stop) original_tour_stop_count = TourStop.count @@ -186,12 +185,12 @@ def data(tour, stop, position = 1) user.tour_sets = [] user.update(super: true) signed_cookie(user) - post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_stop_count).to eq(TourStop.count) + post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_stop_count).to(eq(TourStop.count)) end - it 'return 405 when authenciated by tour author' do + it 'return 405 when authenticated by tour author' do tour = create(:tour) stop = create(:stop) original_tour_stop_count = TourStop.count @@ -200,26 +199,26 @@ def data(tour, stop, position = 1) user.tour_sets = [] user.update(super: false) signed_cookie(user) - post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(original_tour_stop_count).to eq(TourStop.count) + post :create, params: { data: data(tour, stop), tenant: Apartment::Tenant.current } + expect(response.status).to(eq(405)) + expect(original_tour_stop_count).to(eq(TourStop.count)) end end end describe 'PUT #update' do context 'with valid params' do - it 'return 401 when unauthenciated' do + it 'return 401 when unauthenticated' do tour = create(:tour) stop = create(:stop) tour.stops << stop request_data = data(tour, stop, 4) request_data[:id] = TourStop.find_by(tour: tour, stop: stop).id post :update, params: { id: request_data[:id], data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 401 when authenciated but not an admin for current tenant' do + it 'return 401 when authenticated but not an admin for current tenant' do tour = create(:tour) stop = create(:stop) tour.stops << stop @@ -231,10 +230,10 @@ def data(tour, stop, position = 1) request_data = data(tour, stop, 5) request_data[:id] = TourStop.find_by(tour: tour, stop: stop).id post :update, params: { id: request_data[:id], data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 200 and updated tour when authenciated but an admin for current tenant' do + it 'return 200 and updated tour when authenticated but an admin for current tenant' do tour = create(:tour) stops = create_list(:stop, 5) stops.each { |stop| tour.stops << stop } @@ -248,16 +247,16 @@ def data(tour, stop, position = 1) signed_cookie(user) tour_stop = TourStop.find_by(tour: tour, stop: stop) tour_stop.update(position: 2) - expect(TourStop.find(tour_stop.id).position).to eq(2) + expect(TourStop.find(tour_stop.id).position).to(eq(2)) request_data = data(tour, stop, 5) request_data[:id] = tour_stop.id post :update, params: { id: tour_stop.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('5') - expect(TourStop.find(tour_stop.id).position).to eq(5) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('5')) + expect(TourStop.find(tour_stop.id).position).to(eq(5)) end - it 'return 200 and updated tour when authenciated by super' do + it 'return 200 and updated tour when authenticated by super' do tour = create(:tour) stops = create_list(:stop, 5) stops.each { |stop| tour.stops << stop } @@ -271,16 +270,16 @@ def data(tour, stop, position = 1) signed_cookie(user) tour_stop = TourStop.find_by(tour: tour, stop: stop) tour_stop.update(position: 3) - expect(TourStop.find(tour_stop.id).position).to eq(3) + expect(TourStop.find(tour_stop.id).position).to(eq(3)) request_data = data(tour, stop, 4) request_data[:id] = tour_stop.id post :update, params: { id: tour_stop.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('4') - expect(TourStop.find(tour_stop.id).position).to eq(4) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('4')) + expect(TourStop.find(tour_stop.id).position).to(eq(4)) end - it 'return 200 and updated tour when authenciated by tour author' do + it 'return 200 and updated tour when authenticated by tour author' do tour = create(:tour) stops = create_list(:stop, 5) stops.each { |stop| tour.stops << stop } @@ -294,16 +293,16 @@ def data(tour, stop, position = 1) signed_cookie(user) tour_stop = TourStop.find_by(tour: tour, stop: stop) tour_stop.update(position: 6) - expect(TourStop.find(tour_stop.id).position).to eq(6) + expect(TourStop.find(tour_stop.id).position).to(eq(6)) request_data = data(tour, stop, 1) request_data[:id] = tour_stop.id post :update, params: { id: tour_stop.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(200) - expect(attributes[:position]).not_to eq('1') - expect(TourStop.find(tour_stop.id).position).to eq(1) + expect(response.status).to(eq(200)) + expect(attributes[:position]).not_to(eq('1')) + expect(TourStop.find(tour_stop.id).position).to(eq(1)) end - it 'return 422 authenciated by super but tour stop and position are nil' do + it 'return 422 authenticated by super but tour stop and position are nil' do tour = create(:tour, stops: create_list(:stop, rand(4..6))) user = create(:user, super: true) tour_stop = TourStop.find_by(tour: tour, stop: tour.stops.first) @@ -313,25 +312,25 @@ def data(tour, stop, position = 1) request_data[:attributes][:position] = nil signed_cookie(user) post :update, params: { id: tour_stop.id, data: request_data, tenant: TourSet.first.subdir } - expect(response.status).to eq(422) - expect(errors).to include('Tour must exist') - expect(errors).to include('Stop must exist') - expect(errors).to include('Position can\'t be blank') + expect(response.status).to(eq(422)) + expect(errors).to(include('Tour must exist')) + expect(errors).to(include('Stop must exist')) + expect(errors).to(include('Position can\'t be blank')) end end end describe 'DELETE #destroy' do - it 'return 405 when unauthenciated' do + it 'return 405 when unauthenticated' do tour = create(:tour) stop = create(:stop) tour.stops << stop tour_stop = TourStop.find_by(tour: tour, stop: stop) post :destroy, params: { id: tour_stop.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 when authenciated but not an admin for current tenant' do + it 'return 405 when authenticated but not an admin for current tenant' do tour = create(:tour) stop = create(:stop) tour.stops << stop @@ -340,10 +339,10 @@ def data(tour, stop, position = 1) user.tour_sets = [] signed_cookie(user) post :destroy, params: { id: tour_stop.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(405) + expect(response.status).to(eq(405)) end - it 'return 405 and one less tour when authenciated but an admin for current tenant' do + it 'return 405 and one less tour when authenticated but an admin for current tenant' do tour = create(:tour) stop = create(:stop) tour.stops << stop @@ -353,11 +352,11 @@ def data(tour, stop, position = 1) signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour_stop.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end - it 'return 405 and one less tour when authenciated by super' do + it 'return 405 and one less tour when authenticated by super' do tour = create(:tour) stop = create(:stop) tour.stops << stop @@ -366,11 +365,11 @@ def data(tour, stop, position = 1) signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour_stop.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end - it 'return 405 and one less tour when authenciated by tour author' do + it 'return 405 and one less tour when authenticated by tour author' do tour = create(:tour) stop = create(:stop) tour.stops << stop @@ -379,11 +378,10 @@ def data(tour, stop, position = 1) user.tour_sets = [] user.tours << tour signed_cookie(user) - new_title = Faker::Name.unique.name tour_count = Tour.count post :destroy, params: { id: tour_stop.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(405) - expect(Tour.count).to eq(tour_count) + expect(response.status).to(eq(405)) + expect(Tour.count).to(eq(tour_count)) end end end diff --git a/spec/controllers/v3/tours_controller_spec.rb b/spec/controllers/v3/tours_controller_spec.rb index 635f7cca..60677255 100644 --- a/spec/controllers/v3/tours_controller_spec.rb +++ b/spec/controllers/v3/tours_controller_spec.rb @@ -2,31 +2,31 @@ require 'rails_helper' -RSpec.describe V3::ToursController, type: :controller do +RSpec.describe(V3::ToursController, type: :controller) do describe 'GET #index' do it 'returns a 200 response and empty tour when none found' do - StopSlug.all.each { |t| t.delete } - Stop.all.each { |t| t.delete } - Tour.all.each { |t| t.delete } + StopSlug.all.find_each(&:delete) + Stop.all.find_each(&:delete) + Tour.all.find_each(&:delete) get :index, params: { tenant: 'public' } - expect(json).to be_empty - expect(response.status).to eq(200) + expect(json).to(be_empty) + expect(response.status).to(eq(200)) end it 'returns a 200 response' do tour = create(:tour) tour.update(published: true) get :index, params: { tenant: tour.tenant } - expect(response.status).to eq(200) - expect(json.count).to eq(Tour.published.count) + expect(response.status).to(eq(200)) + expect(json.count).to(eq(Tour.published.count)) end - it 'returns a 200 response when requeted by slug' do + it 'returns a 200 response when requested by slug' do tour = create(:tour) tour.update(published: true) get :index, params: { tenant: tour.tenant, slug: tour.slug } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) end # This is for when an authenticated person is viewing an unpublished tour. @@ -37,9 +37,9 @@ tour = create(:tour) tour.update(published: false) get :index, params: { tenant: tour.tenant, slug: tour.slug } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(tour.title) - expect(attributes[:title]).to eq('....') + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(tour.title)) + expect(attributes[:title]).to(eq('....')) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -49,8 +49,8 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: tour.tenant, slug: tour.slug } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do @@ -61,8 +61,8 @@ user.tours << tour signed_cookie(user) get :index, params: { tenant: tour.tenant, slug: tour.slug } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) end it 'returns all Tour objects when requested by tenant admin' do @@ -71,7 +71,7 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq(Tour.count) + expect(json.count).to(eq(Tour.count)) end it 'returns only tours where requester is an author' do @@ -82,20 +82,26 @@ user.tours << [Tour.published.first, new_tours.first, new_tours.last] signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq((user.tours + Tour.published).uniq.count) - expect(json.count).to be < Tour.count + expect(json.count).to(eq((user.tours + Tour.published).uniq.count)) + expect(json.count).to(be < Tour.count) end end describe 'GET #show' do it 'returns a 200 response' do - tour = create(:tour, published: false, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, rand(5..7))) + tour = create( + :tour, + published: false, + mode: Mode.find_by(title: 'BICYCLING'), + stops: create_list(:stop, 5), + ) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 400).join(' ')) } tour.update(published: true) tour.save get :show, params: { tenant: tour.tenant, id: tour.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) - expect(attributes[:est_time]).to eq('About 2 hours bicycling') + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) + expect(attributes[:est_time]).to(eq('About 4 hours bicycling')) end # This is for when an authenticated person is viewing an unpublished tour. @@ -107,21 +113,22 @@ tour.update(published: false) cookies[:auth] = nil get :show, params: { tenant: tour.tenant, id: tour.id } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(tour.title) - expect(attributes[:title]).to eq('....') + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(tour.title)) + expect(attributes[:title]).to(eq('....')) end it 'returns a 200 response when request is authenticated by tour author and tour is unpublished' do tour = create(:tour, published: false) tour.update(published: false, media: create_list(:medium, 3)) + tour.save user = create(:user) user.tour_sets = [] user.tours << tour signed_cookie(user) get :show, params: { tenant: tour.tenant, id: tour.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) end it 'returns a 200 response when request is authenticated by tenant admin and tour is unpublished' do @@ -131,57 +138,59 @@ user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) get :show, params: { tenant: tour.tenant, id: tour.id } - expect(response.status).to eq(200) - expect(attributes[:title]).to eq(tour.title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).to(eq(tour.title)) end - it 'retuns a tour with center lat/lng based on request' do + it 'returns a tour with center lat/lng based on request' do request.env['ipinfo'] = MockIpinfo.new tour = create(:tour, stops: []) user = create(:user, super: true) signed_cookie(user) get :show, params: { tenant: Apartment::Tenant.current, id: tour.id } - expect(attributes[:bounds][:centerLat]).not_to eq(33.75432) - expect(attributes[:bounds][:centerLng]).not_to eq(-84.38979) + expect(attributes[:bounds][:centerLat]).not_to(eq(33.75432)) + expect(attributes[:bounds][:centerLng]).not_to(eq(-84.38979)) end end describe 'POST #create' do context 'with valid params' do - it 'return 401 when unauthenciated' do - post :create, params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) - end + it 'return 401 when unauthenticated' do + post :create, + params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } + expect(response.status).to(eq(401)) + end - it 'return 401 when authenciated but not an admin for current tenant' do + it 'return 401 when authenticated but not an admin for current tenant' do user = create(:user) user.update(super: false) user.tour_sets = [] signed_cookie(user) - post :create, params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + post :create, + params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } + expect(response.status).to(eq(401)) end - it 'return 201 when authenciated but an admin for current tenant' do + it 'return 201 when authenticated but an admin for current tenant' do user = create(:user) user.update(super: false) user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) original_tour_count = Tour.count post :create, params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(Tour.count).to eq(original_tour_count + 1) + expect(response.status).to(eq(201)) + expect(Tour.count).to(eq(original_tour_count + 1)) end - it 'return 201 when authenciated by super' do + it 'return 201 when authenticated by super' do user = create(:user) user.tour_sets = [] user.update(super: true) signed_cookie(user) original_tour_count = Tour.count post :create, params: { data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(201) - expect(Tour.count).to eq(original_tour_count + 1) + expect(response.status).to(eq(201)) + expect(Tour.count).to(eq(original_tour_count + 1)) end end @@ -191,9 +200,9 @@ signed_cookie(user) original_tour_count = Tour.count post :create, params: { data: { type: 'tours', attributes: { title: nil } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(422) - expect(Tour.count).to eq(original_tour_count) - expect(errors).to include('Title can\'t be blank') + expect(response.status).to(eq(422)) + expect(Tour.count).to(eq(original_tour_count)) + expect(errors).to(include('Title can\'t be blank')) end it 'returns 422 when title already used' do @@ -202,31 +211,41 @@ title = Faker::Movies::HitchhikersGuideToTheGalaxy.location create(:tour, title: title) post :create, params: { data: { type: 'tours', attributes: { title: title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(422) - expect(Tour.where(title: title).count). to eq(1) + expect(response.status).to(eq(422)) + expect(Tour.where(title: title).count).to(eq(1)) end end end describe 'PUT #update' do context 'with valid params' do - it 'return 401 when unauthenciated' do + it 'return 401 when unauthenticated' do tour = create(:tour, published: false) - post :update, params: { id: tour.id, data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + post :update, + params: { + id: tour.id, + data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, + tenant: TourSet.first.subdir, + } + expect(response.status).to(eq(401)) end - it 'return 401 when authenciated but not an admin for current tenant' do + it 'return 401 when authenticated but not an admin for current tenant' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) user.tour_sets = [] signed_cookie(user) - post :update, params: { id: tour.id, data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + post :update, + params: { + id: tour.id, + data: { type: 'tours', attributes: { title: 'Burrito Tour' } }, + tenant: TourSet.first.subdir, + } + expect(response.status).to(eq(401)) end - it 'return 200 and updated tour when authenciated but an admin for current tenant' do + it 'returns 200 and updated tour when authenticated but an admin for current tenant' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) @@ -234,13 +253,13 @@ signed_cookie(user) new_title = Faker::Name.unique.name post :update, params: { id: tour.id, data: { type: 'tours', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(tour.title) - expect(attributes[:title]).to eq(new_title) - expect(Tour.find(tour.id).title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(tour.title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Tour.find(tour.id).title).to(eq(new_title)) end - it 'return 200 and updated tour when authenciated by super' do + it 'return 200 and updated tour when authenticated by super' do tour = create(:tour, published: false) user = create(:user) user.tour_sets = [] @@ -248,13 +267,13 @@ signed_cookie(user) new_title = Faker::Name.unique.name post :update, params: { id: tour.id, data: { type: 'tours', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(tour.title) - expect(attributes[:title]).to eq(new_title) - expect(Tour.find(tour.id).title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(tour.title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Tour.find(tour.id).title).to(eq(new_title)) end - it 'return 200 and updated tour when authenciated by tour author' do + it 'return 200 and updated tour when authenticated by tour author' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) @@ -263,69 +282,69 @@ signed_cookie(user) new_title = Faker::Name.unique.name post :update, params: { id: tour.id, data: { type: 'tours', attributes: { title: new_title } }, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(attributes[:title]).not_to eq(tour.title) - expect(attributes[:title]).to eq(new_title) - expect(Tour.find(tour.id).title).to eq(new_title) + expect(response.status).to(eq(200)) + expect(attributes[:title]).not_to(eq(tour.title)) + expect(attributes[:title]).to(eq(new_title)) + expect(Tour.find(tour.id).title).to(eq(new_title)) end it 'returns 200 and adds stop to a tour' do tour = create(:tour) create_list(:stop, 5) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } serialized_tour = JSON.parse(ActiveModelSerializers::Adapter::JsonApi.new(V3::TourSerializer.new(tour)).to_json).with_indifferent_access original_stop_count = serialized_tour[:data][:relationships][:stops][:data].count original_tour_stop_count = serialized_tour[:data][:relationships][:tour_stops][:data].count - expect(original_stop_count).to be >= 5 - expect(original_tour_stop_count).to be >= 5 - expect(original_stop_count).to eq(original_tour_stop_count) - expect(original_stop_count).to eq(tour.stops.count) - expect(original_tour_stop_count).to eq(tour.tour_stops.count) + expect(original_stop_count).to(be >= 5) + expect(original_tour_stop_count).to(be >= 5) + expect(original_stop_count).to(eq(original_tour_stop_count)) + expect(original_stop_count).to(eq(tour.stops.count)) + expect(original_tour_stop_count).to(eq(tour.tour_stops.count)) stop = create(:stop) - expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).not_to include(stop.id.to_s) + expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).not_to(include(stop.id.to_s)) serialized_tour[:data][:relationships][:stops][:data].push(JSON.parse("{\"id\":\"#{stop.id}\",\"type\":\"stops\"}")) - expect(serialized_tour[:data][:relationships][:stops][:data].count).to eq(original_stop_count + 1) - expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).to include(stop.id.to_s) - expect(tour.stops).not_to include(stop) + expect(serialized_tour[:data][:relationships][:stops][:data].count).to(eq(original_stop_count + 1)) + expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).to(include(stop.id.to_s)) + expect(tour.stops).not_to(include(stop)) user = create(:user) user.update(super: false) user.tour_sets = [] user.tours << tour signed_cookie(user) post :update, params: { id: tour.id, data: serialized_tour[:data], tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.find(tour.id).stops).to include(stop) - expect(relationships[:stops][:data].count).to eq(original_stop_count + 1) - expect(relationships[:tour_stops][:data].count).to eq(original_tour_stop_count + 1) + expect(response.status).to(eq(200)) + expect(Tour.find(tour.id).stops).to(include(stop)) + expect(relationships[:stops][:data].count).to(eq(original_stop_count + 1)) + expect(relationships[:tour_stops][:data].count).to(eq(original_tour_stop_count + 1)) end it 'returns 200 and removes a stop from a tour' do tour = create(:tour) create_list(:stop, 5) - Stop.all.each { |stop| tour.stops << stop } + Stop.all.find_each { |stop| tour.stops << stop } serialized_tour = JSON.parse(ActiveModelSerializers::Adapter::JsonApi.new(V3::TourSerializer.new(tour)).to_json).with_indifferent_access original_stop_count = serialized_tour[:data][:relationships][:stops][:data].count original_tour_stop_count = serialized_tour[:data][:relationships][:tour_stops][:data].count - expect(original_stop_count).to be >= 5 - expect(original_tour_stop_count).to be >= 5 - expect(original_stop_count).to eq(original_tour_stop_count) - expect(original_stop_count).to eq(tour.stops.count) - expect(original_tour_stop_count).to eq(tour.tour_stops.count) - expect(serialized_tour[:data][:relationships][:stops][:data].count).to eq(original_stop_count) + expect(original_stop_count).to(be >= 5) + expect(original_tour_stop_count).to(be >= 5) + expect(original_stop_count).to(eq(original_tour_stop_count)) + expect(original_stop_count).to(eq(tour.stops.count)) + expect(original_tour_stop_count).to(eq(tour.tour_stops.count)) + expect(serialized_tour[:data][:relationships][:stops][:data].count).to(eq(original_stop_count)) stop = serialized_tour[:data][:relationships][:stops][:data].pop - expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).not_to include(stop[:id]) - expect(serialized_tour[:data][:relationships][:stops][:data].count).to eq(original_stop_count - 1) - expect(tour.stops).to include(Stop.find(stop[:id])) + expect(serialized_tour[:data][:relationships][:stops][:data].map { |s| s['id'] }).not_to(include(stop[:id])) + expect(serialized_tour[:data][:relationships][:stops][:data].count).to(eq(original_stop_count - 1)) + expect(tour.stops).to(include(Stop.find(stop[:id]))) user = create(:user) user.update(super: false) user.tour_sets = [] user.tours << tour signed_cookie(user) post :update, params: { id: tour.id, data: serialized_tour[:data], tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(Tour.find(tour.id).stops).not_to include(Stop.find(stop[:id])) - expect(relationships[:stops][:data].count).to eq(original_stop_count - 1) - expect(relationships[:tour_stops][:data].count).to eq(original_tour_stop_count - 1) + expect(response.status).to(eq(200)) + expect(Tour.find(tour.id).stops).not_to(include(Stop.find(stop[:id]))) + expect(relationships[:stops][:data].count).to(eq(original_stop_count - 1)) + expect(relationships[:tour_stops][:data].count).to(eq(original_tour_stop_count - 1)) end it 'returns 422 when title in nil' do @@ -335,30 +354,30 @@ user = create(:user, super: true) signed_cookie(user) post :update, params: { id: tour.id, data: serialized_tour[:data], tenant: Apartment::Tenant.current } - expect(response.status).to eq(422) - expect(errors).to include('Title can\'t be blank') + expect(response.status).to(eq(422)) + expect(errors).to(include('Title can\'t be blank')) end end end describe 'DELETE #destroy' do - it 'return 401 when unauthenciated' do + it 'return 401 when unauthenticated' do tour = create(:tour, published: false) post :destroy, params: { id: tour.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 401 when authenciated but not an admin for current tenant' do + it 'return 401 when authenticated but not an admin for current tenant' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) user.tour_sets = [] signed_cookie(user) post :destroy, params: { id: tour.id, tenant: TourSet.first.subdir } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end - it 'return 204 and one less tour when authenciated but an admin for current tenant' do + it 'return 204 and one less tour when authenticated but an admin for current tenant' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) @@ -366,11 +385,11 @@ signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(Tour.count).to eq(tour_count - 1) + expect(response.status).to(eq(204)) + expect(Tour.count).to(eq(tour_count - 1)) end - it 'return 204 and one less tour when authenciated by super' do + it 'return 204 and one less tour when authenticated by super' do tour = create(:tour, published: false) user = create(:user) user.tour_sets = [] @@ -378,11 +397,11 @@ signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(Tour.count).to eq(tour_count - 1) + expect(response.status).to(eq(204)) + expect(Tour.count).to(eq(tour_count - 1)) end - it 'return 204 and one less tour when authenciated by tour author' do + it 'return 204 and one less tour when authenticated by tour author' do tour = create(:tour, published: false) user = create(:user) user.update(super: false) @@ -391,8 +410,8 @@ signed_cookie(user) tour_count = Tour.count post :destroy, params: { id: tour.id, tenant: Apartment::Tenant.current } - expect(response.status).to eq(204) - expect(Tour.count).to eq(tour_count - 1) + expect(response.status).to(eq(204)) + expect(Tour.count).to(eq(tour_count - 1)) end end end diff --git a/spec/controllers/v3/users_controller_spec.rb b/spec/controllers/v3/users_controller_spec.rb index 872f7425..0f3ada7a 100644 --- a/spec/controllers/v3/users_controller_spec.rb +++ b/spec/controllers/v3/users_controller_spec.rb @@ -2,14 +2,14 @@ require 'rails_helper' -RSpec.describe V3::UsersController, type: :controller do +RSpec.describe(V3::UsersController, type: :controller) do describe 'GET #index' do context 'unauthorized' do it 'returns a success response but empty json when request is unauthenticated' do create_list(:user, rand(4..5)) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end it 'returns a success response but empty json when request is unauthenticated' do @@ -18,8 +18,8 @@ user.update(super: false) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json).to be_empty + expect(response.status).to(eq(200)) + expect(json).to(be_empty) end end @@ -28,20 +28,20 @@ user = create(:user) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current, me: true } - expect(response.status).to eq(200) - expect(json[:id]).to eq(user.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(user.id.to_s)) end it 'returns list of users when requested by super' do create_list(:user, rand(4..7)) - User.all.each {|user| user.tours << create_list(:tour, rand(0..3))} + User.all.find_each { |user| user.tours << create_list(:tour, rand(0..3)) } user = User.last user.update(super: true) signed_cookie(user) get :index, params: { tenant: Apartment::Tenant.current } - expect(json.count).to eq(User.count) - expect(User.all.map(&:all_tours).all? { |tours| tours.empty? }).not_to be true - expect(json.map { |user| user[:attributes][:all_tours].empty? }).to all(be true) + expect(json.count).to(eq(User.count)) + expect(User.all.map(&:all_tours).all?(&:empty?)).not_to(be(true)) + expect(json.map { |user| user[:attributes][:all_tours].empty? }).to(all(be(true))) end end end @@ -54,21 +54,21 @@ it 'returns 401 when unauthenticated' do signed_cookie(user) get :show, params: { id: other_user.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'returns 401 when unauthorized tenant admin' do user.tour_sets << create(:tour_set) signed_cookie(user) get :show, params: { id: other_user.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end it 'returns 401 when unauthorized tour author' do user.tours << create(:tour) signed_cookie(user) get :show, params: { id: other_user.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) end end @@ -76,8 +76,8 @@ it 'returns user when requested by self' do signed_cookie(user) get :show, params: { id: user.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json[:id]).to eq(user.id.to_s) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(user.id.to_s)) end it 'returns user when requested by super' do @@ -85,11 +85,10 @@ other_user.tours << create_list(:tour, 3) signed_cookie(user) get :show, params: { id: other_user.to_param, tenant: Apartment::Tenant.current } - expect(response.status).to eq(200) - expect(json[:id]).to eq(other_user.id.to_s) - expect(json[:attributes][:all_tours].count).to eq(3) + expect(response.status).to(eq(200)) + expect(json[:id]).to(eq(other_user.id.to_s)) + expect(json[:attributes][:all_tours].count).to(eq(3)) end - end end @@ -100,24 +99,24 @@ context 'unauthorized' do it 'does not create a new User when unauthenticated' do expect do - post :create, params: valid_params - end.to change(User, :count).by(0) + post(:create, params: valid_params) + end.not_to(change(User, :count)) end it 'does not create a new User for tenant admin' do user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) signed_cookie(user) expect do - post :create, params: valid_params - end.to change(User, :count).by(0) + post(:create, params: valid_params) + end.not_to(change(User, :count)) end it 'does not create a new User for tour author' do user.tours << create(:tour) signed_cookie(user) expect do - post :create, params: valid_params - end.to change(User, :count).by(0) + post(:create, params: valid_params) + end.not_to(change(User, :count)) end end @@ -126,8 +125,8 @@ user.update(super: true) signed_cookie(user) expect do - post :create, params: valid_params - end.to change(User, :count).by(1) + post(:create, params: valid_params) + end.to(change(User, :count).by(1)) end it 'does not create a new User for super with invalid_params' do @@ -135,8 +134,8 @@ valid_params[:data][:attributes].delete(:email) signed_cookie(user) expect do - post :create, params: valid_params - end.to change(User, :count).by(0) + post(:create, params: valid_params) + end.not_to(change(User, :count)) end it 'responds with errors when creating a new User for super with invalid_params' do @@ -144,8 +143,8 @@ valid_params[:data][:attributes].delete(:email) signed_cookie(user) post :create, params: valid_params - expect(response.status).to eq(422) - expect(errors).to include('Email can\'t be blank') + expect(response.status).to(eq(422)) + expect(errors).to(include('Email can\'t be blank')) end end end @@ -154,41 +153,50 @@ context 'unauthorized' do it 'does not update when unauthenticated' do user = create(:user, super: false) - initial_display_name = user.display_name new_display_name = Faker::Music::Hiphop.artist - update_params = { id: user.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } put :update, params: update_params - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) user.reload - expect(user.display_name).not_to eq(new_display_name) + expect(user.display_name).not_to(eq(new_display_name)) end it 'does not update when authenticated as user not the one being updated' do user = create(:user, super: false) user_to_update = create(:user) user.tour_sets << create(:tour_set) - initial_display_name = user_to_update.display_name new_display_name = Faker::Music::Hiphop.artist - update_params = { id: user_to_update.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user_to_update.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) user_to_update.reload - expect(user_to_update.display_name).not_to eq(new_display_name) + expect(user_to_update.display_name).not_to(eq(new_display_name)) end it 'does not update when authenticated by tenant admin' do user = create(:user) user_to_update = create(:user) user.tour_sets << create(:tour_set) - initial_display_name = user_to_update.display_name new_display_name = "#{Faker::Music::Hiphop.artist}!" - update_params = { id: user_to_update.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user_to_update.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) user_to_update.reload - expect(user_to_update.display_name).not_to eq(new_display_name) + expect(user_to_update.display_name).not_to(eq(new_display_name)) end it 'does not update when authenticated by tour author' do @@ -197,52 +205,66 @@ user.tours << create(:tour) initial_display_name = user_to_update.display_name new_display_name = Faker::Music::Hiphop.artist - update_params = { id: user_to_update.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user_to_update.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(401) + expect(response.status).to(eq(401)) user_to_update.reload - expect(user_to_update.display_name).not_to eq(new_display_name) + expect(initial_display_name).not_to(eq(new_display_name)) end end context 'authorized' do it 'updates user when requested by self' do user = create(:user) - initial_display_name = Faker::Music::Hiphop.artist new_display_name = Faker::Music::Hiphop.artist - update_params = { id: user.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(200) + expect(response.status).to(eq(200)) user.reload - expect(user.display_name).to eq(new_display_name) + expect(user.display_name).to(eq(new_display_name)) end it 'updates user when requested by super' do user = create(:user, super: true) user_to_update = create(:user) - initial_display_name = user_to_update.display_name new_display_name = Faker::Music::Hiphop.artist - update_params = { id: user_to_update.to_param, tenant: 'public', data: { type: 'users', attributes: { display_name: new_display_name } } } + update_params = { + id: user_to_update.to_param, + tenant: 'public', + data: { type: 'users', attributes: { display_name: new_display_name } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(200) + expect(response.status).to(eq(200)) user_to_update.reload - expect(user_to_update.display_name).to eq(new_display_name) + expect(user_to_update.display_name).to(eq(new_display_name)) end it 'returns 422 when email in nil' do user = create(:user, super: true) user_to_update = create(:user) initial_email = user_to_update.email - update_params = { id: user_to_update.to_param, tenant: 'public', data: { type: 'users', attributes: { email: nil } } } + update_params = { + id: user_to_update.to_param, + tenant: 'public', + data: { type: 'users', attributes: { email: nil } }, + } signed_cookie(user) put :update, params: update_params - expect(response.status).to eq(422) - expect(errors).to include('Email can\'t be blank') + expect(response.status).to(eq(422)) + expect(errors).to(include('Email can\'t be blank')) user_to_update.reload - expect(user_to_update.email).to eq(initial_email) + expect(user_to_update.email).to(eq(initial_email)) end end end @@ -252,16 +274,16 @@ it 'does not destroy the requested user when unauthenticated' do user = create(:user) expect do - delete :destroy, params: { id: user.to_param, tenant: 'public' } - end.to change(User, :count).by(0) + delete(:destroy, params: { id: user.to_param, tenant: 'public' }) + end.not_to(change(User, :count)) end it 'does not destroy the requested user when authenticated' do user = create(:user) signed_cookie(user) expect do - delete :destroy, params: { id: user.to_param, tenant: 'public' } - end.to change(User, :count).by(0) + delete(:destroy, params: { id: user.to_param, tenant: 'public' }) + end.not_to(change(User, :count)) end end @@ -271,9 +293,9 @@ user = create(:user) signed_cookie(super_user) expect do - delete :destroy, params: { id: user.to_param, tenant: 'public' } - end.to change(User, :count).by(-1) + delete(:destroy, params: { id: user.to_param, tenant: 'public' }) + end.to(change(User, :count).by(-1)) end end end -end \ No newline at end of file +end diff --git a/spec/controllers/v3_controller_spec.rb b/spec/controllers/v3_controller_spec.rb index 91b4d0f8..ccfa9ee8 100644 --- a/spec/controllers/v3_controller_spec.rb +++ b/spec/controllers/v3_controller_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' - -RSpec.describe V3Controller, type: :controller do +# frozen_string_literal: true -end +require 'rails_helper' diff --git a/spec/controllers/v4/admin/access_requests_controller_spec.rb b/spec/controllers/v4/admin/access_requests_controller_spec.rb new file mode 100644 index 00000000..0f7fff3f --- /dev/null +++ b/spec/controllers/v4/admin/access_requests_controller_spec.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::AccessRequestsController, type: :controller) do + include ActiveJob::TestHelper + + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 401 when authenticated if not current tenant admin' do + user = create(:user, super: false) + signed_cookie(user) + tour_set = create(:tour_set) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and list of requests when authenticated as current tenant admin' do + user = create(:user, super: false) + signed_cookie(user) + tour_set = create(:tour_set) + user.tour_sets << tour_set + create_list(:access_request, 4, tour_set:) + Apartment::Tenant.switch!(tour_set.subdir) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(AccessRequest.count).to(eq(4)) + expect(v4_json.count).to(eq(AccessRequest.count)) + end + + it 'returns 200 and list of requests when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + create_list(:access_request, 4, tour_set:) + Apartment::Tenant.switch!(tour_set.subdir) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(AccessRequest.count).to(eq(4)) + expect(v4_json.count).to(eq(AccessRequest.count)) + end + + it 'returns 401 when bad credentials' do + tour_set = create(:tour_set) + invalid_signed_cooke + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 401 when bad credentials and me parameter is present' do + tour_set = create(:tour_set) + invalid_signed_cooke + get :index, params: { tenant: tour_set.subdir, me: true } + expect(response.status).to(eq(401)) + end + + it 'returns empty array when no requests' do + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + expect(AccessRequest.where(tour_set:)).to(be_empty) + get :index, params: { tenant: tour_set.subdir } + expect(v4_json).to(be_empty) + end + end + + describe 'POST #create' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + post :create, params: { tenant: tour_set.subdir, user: create(:user).id } + expect(response.status).to(eq(401)) + end + + it 'creates new access request' do + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + expect do + post(:create, params: { tenant: tour_set.subdir, user: user.id }) + end.to(change(AccessRequest, :count).by(1)) + end + + it 'creates new access request for tours' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tours = create_list(:tour, 2) + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + post :create, params: { tenant: tour_set.subdir, user: user.id, tour_ids: [tours.first.id, tours.last.id] } + expect(v4_json[:tour_ids]).to(include(tours.first.id)) + expect(v4_json[:tour_ids]).to(include(tours.last.id)) + end + + it 'sends an email when access request is created' do + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + expect do + post(:create, params: { tenant: tour_set.subdir, user: user.id }) + end.to(have_enqueued_mail(AccessRequestMailer, :access_request_email)) + end + + it 'sends an email when access request to tour is created' do + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + expect do + post(:create, params: { tenant: tour_set.subdir, user: user.id, tour_ids: [tour.id] }) + end.to(have_enqueued_mail(AccessRequestMailer, :access_request_tour_email)) + end + end + + describe 'PUT #update' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + access_request = create(:access_request, tour_set:) + put :update, params: { tenant: tour_set.subdir, id: access_request.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'approves request to join site' do + tour_set = create(:tour_set) + requester = create(:user, super: false, tour_sets: []) + access_request = create(:access_request, tour_set:, user: requester) + expect(requester.tour_sets).to(be_empty) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + put :update, params: { tenant: tour_set.subdir, id: access_request.id, access_request: { approved: true } } + expect(response).to(have_http_status(:ok)) + requester.reload + expect(requester.tour_sets).to(include(tour_set)) + end + + it 'denies request to join site' do + tour_set = create(:tour_set) + access_request = create(:access_request, tour_set:) + expect(access_request.user.tour_sets).to(be_empty) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + put :update, params: { tenant: tour_set.subdir, id: access_request.id, access_request: { approved: false } } + expect(response).to(have_http_status(:ok)) + expect(access_request.user.tour_sets).to(be_empty) + end + + it 'approves request to join tour' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tours = create_list(:tour, 2) + access_request = create(:access_request, tour_set:, tour_ids: [tours.first.id, tours.last.id]) + expect(access_request.user.tour_sets).to(be_empty) + expect(access_request.user.tours).to(be_empty) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + put :update, params: { tenant: tour_set.subdir, id: access_request.id, access_request: { approved: true } } + expect(response).to(have_http_status(:ok)) + expect(access_request.user.tour_sets).to(be_empty) + expect(access_request.user.tours).to(include(tours.first)) + expect(access_request.user.tours).to(include(tours.last)) + end + + it 'denies request to join tour' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + access_request = create(:access_request, tour_set:, tour_ids: [tour.id]) + expect(access_request.user.tour_sets).to(be_empty) + expect(access_request.user.tours).to(be_empty) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + put :update, params: { tenant: tour_set.subdir, id: access_request.id, access_request: { approved: false } } + expect(response).to(have_http_status(:ok)) + expect(access_request.user.tour_sets).to(be_empty) + expect(access_request.user.tours).to(be_empty) + end + + it 'approves request but limits to specific tour when request was for whole site' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + requester = create(:user, super: false) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + access_request = create(:access_request, tour_set:, user: requester) + put :update, + params: { + tenant: tour_set.subdir, + id: access_request.id, + access_request: { tour_ids: [tour.id.to_s], approved: true }, + } + Apartment::Tenant.switch!(tour_set.subdir) + requester.reload + expect(requester.tours).to(include(tour)) + end + end + + describe 'DELETE #destroy' do + it 'allows requester to delete request' do + tour_set = create(:tour_set) + user = create(:user, super: false) + signed_cookie(user) + access_request = create(:access_request, tour_set:, user:) + expect do + delete(:destroy, params: { tenant: tour_set.subdir, id: access_request.id }) + end.to(change(AccessRequest, :count).by(-1)) + end + + it 'disallows non-requester to delete request' do + tour_set = create(:tour_set) + user = create(:user, super: false) + access_request = create(:access_request, tour_set:, user:) + other_user = create(:user) + signed_cookie(other_user) + expect do + delete(:destroy, params: { tenant: tour_set.subdir, id: access_request.id }) + end.not_to(change(AccessRequest, :count)) + end + end +end diff --git a/spec/controllers/v4/admin/crud_controller_spec.rb b/spec/controllers/v4/admin/crud_controller_spec.rb new file mode 100644 index 00000000..189049aa --- /dev/null +++ b/spec/controllers/v4/admin/crud_controller_spec.rb @@ -0,0 +1,305 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::CrudController, type: :controller) do + describe 'POST #create' do + it 'returns 401 when unauthenticated' do + post :create, params: { tenant: TourSet.last.subdir, data: { model: 'tour', title: Faker::Book.title } } + expect(response.status).to(eq(401)) + end + + it 'returns 422 when creating tour via CrudController' do + user = create(:user, super: true) + signed_cookie(user) + post :create, params: { tenant: TourSet.first.subdir, model: 'tour', tour: { title: 'Not Here' } } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'uploads a file' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/0.jpg'), + 'image/jpeg', + ) + post :create, params: { tenant: TourSet.first.subdir, model: 'medium', medium: { file: file, filename: '0.jpg' } } + medium = Medium.find(v4_json[:id]) + expect(medium.file).to(be_attached) + expect(response).to(have_http_status(:created)) + end + + it 'adds a medium to a tour' do + user = create(:user, super: true) + signed_cookie(user) + tour = create(:tour) + medium = create(:medium) + expect(medium.file.attached?).to(be(true)) + post :create, params: { tenant: Apartment::Tenant.current, model: 'tour_medium', tour_medium: { tour_id: tour.id, medium_id: medium.id, position: 2 } } + expect(response).to(have_http_status(:created)) + end + + it 'uploads a map icon' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/map_icon.jpg'), 'image/jpeg' + ) + post :create, + params: { tenant: TourSet.second.subdir, model: 'map_icon', map_icon: { file:, filename: 'map_icon.jpg' } } + expect(response).to(have_http_status(:created)) + end + + it 'rejects a map icon that is to big' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/map_icon_too_big.jpg'), 'image/jpeg' + ) + post :create, + params: { tenant: TourSet.second.subdir, model: 'map_icon', map_icon: { file:, filename: 'map_icon.jpg' } } + expect(response).to(have_http_status(:unprocessable_entity)) + expect(v4_json[:errors].first[:detail]).to(eq('File Icons should be no bigger that 80 by 80 pixels')) + end + + it 'creates a stop with default lat/lon based on request location' do + user = create(:user, super: true) + signed_cookie(user) + request.remote_ip = Faker::Internet.ip_v4_address + post :create, params: { tenant: TourSet.last.subdir, model: 'stop', stop: { title: Faker::Movies::HitchhikersGuideToTheGalaxy.planet } } + expect(v4_json[:lat]).not_to(be_nil) + expect(v4_json[:lng]).not_to(be_nil) + end + + it 'allows super to create record in public tenant' do + user = create(:user, super: true) + new_user = create(:user, super: false) + new_tenant = create(:tour_set) + signed_cookie(user) + post :create, + params: { + tenant: 'public', + model: 'tour_set_admin', + tour_set_admin: { user_id: new_user.id, tour_set_id: new_tenant.id }, + } + expect(response).to(have_http_status(:created)) + expect(new_user.tour_sets).to(include(new_tenant)) + end + + it 'uploads a voice over mp3' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/fixtures/audio/sample.mp3'), + 'audio/mpeg', + ) + Apartment::Tenant.switch!(TourSet.first.subdir) + tour = create(:tour) + post :create, params: { tenant: TourSet.first.subdir, model: 'voice_over', voice_over: { file: file, tour_id: tour.id } } + voice_over = VoiceOver.find(v4_json[:id]) + expect(voice_over.file).to(be_attached) + expect(v4_json[:filename]).to(eq('sample.mp3')) + expect(response).to(have_http_status(:created)) + end + + it 'uploads a voice over m4a' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/fixtures/audio/sample.m4a'), + 'audio/mp4', + ) + Apartment::Tenant.switch!(TourSet.first.subdir) + stop = create(:stop) + post :create, params: { tenant: TourSet.first.subdir, model: 'voice_over', voice_over: { file: file, stop_id: stop.id } } + voice_over = VoiceOver.find(v4_json[:id]) + expect(voice_over.file).to(be_attached) + expect(response).to(have_http_status(:created)) + end + + it 'uploads a voice over audio/x-m4a' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/fixtures/audio/sample.m4a'), + 'audio/x-m4a', + ) + Apartment::Tenant.switch!(TourSet.first.subdir) + stop = create(:stop) + post :create, params: { tenant: TourSet.first.subdir, model: 'voice_over', voice_over: { file: file, stop_id: stop.id } } + voice_over = VoiceOver.find(v4_json[:id]) + expect(voice_over.file).to(be_attached) + expect(response).to(have_http_status(:created)) + end + + it 'rejects a voice over wav' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/fixtures/audio/sample.wav'), + 'audio/wave', + ) + Apartment::Tenant.switch!(TourSet.first.subdir) + tour = create(:tour) + post :create, params: { tenant: TourSet.first.subdir, model: 'voice_over', voice_over: { file: file, tour_id: tour.id } } + expect(v4_json[:errors].first[:detail]).to(eq('File must be an MP3 or M4A audio file')) + expect(response).to(have_http_status(:unprocessable_entity)) + end + + it 'rejects a voice over with no tour or stop' do + user = create(:user, super: true) + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/fixtures/audio/sample.mp3'), + 'audio/mpeg', + ) + Apartment::Tenant.switch!(TourSet.first.subdir) + post :create, params: { tenant: TourSet.first.subdir, model: 'voice_over', voice_over: { file: file } } + expect(v4_json[:errors].first[:detail]).to(eq('must belong to exactly one of tour or stop')) + expect(response).to(have_http_status(:unprocessable_entity)) + end + end + + describe 'PUT #update' do + it 'returns 401 when unauthenticated' do + post :create, params: { tenant: TourSet.last.subdir, data: { model: 'tour', title: Faker::Book.title } } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and updates tour title when authenticated' do + tour = create(:tour, published: false) + user = create(:user) + user.update(super: false) + user.tour_sets << TourSet.find_by(subdir: Apartment::Tenant.current) + signed_cookie(user) + new_title = Faker::Name.unique.name + request_body = { + model: 'tour', + tour: { title: new_title }, + } + expect(Tour.find(tour.id).title).not_to(eq(new_title)) + put :update, params: { id: tour.id, **request_body, tenant: Apartment::Tenant.current } + expect(response.status).to(eq(200)) + expect(Tour.find(tour.id).title).to(eq(new_title)) + end + + it 'adds a belongs to association' do + tour = create(:tour) + new_theme = create(:theme) + user = create(:user, super: true) + signed_cookie(user) + expect(tour.theme).not_to(eq(new_theme)) + tour.update(theme: new_theme) + request_body = { + model: 'tour', + attribute: 'theme', + value: new_theme.id.to_s, + related_model: 'theme', + relation_type: 'belongs_to', + } + put :update, params: { id: tour.id, **request_body, tenant: Apartment::Tenant.current } + expect(tour.theme).to(eq(new_theme)) + end + + it 'adds icon to stop' do + icon = create(:map_icon) + stop = create(:stop, map_icon: nil) + user = create(:user, super: true) + signed_cookie(user) + request_body = { + id: stop.id, + tenant: Apartment::Tenant.current, + model: 'stop', + attribute: 'map_icon', + related_model: 'map_icon', + related_type: 'belongs_to', + value: icon.id, + } + put :update, params: { id: stop.id, tenant: Apartment::Tenant.current, **request_body } + expect(v4_json[:map_icon]).to(include(icon.filename)) + end + + it 'uploads and replaces file' do + user = create(:user, super: true) + signed_cookie(user) + medium = create( + :medium, + base_sixty_four: nil, + file: fixture_file_upload(Rails.root.join('spec/factories/images/atl.png'), 'image/png'), + ) + expect(medium.file.attached?) + original_files = medium.search_data[:files] + original_checksum = medium.file.blob.checksum + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/0.jpg'), + 'image/jpeg', + ) + request_body = { + id: medium.id, + tenant: Apartment::Tenant.current, + model: 'medium', + medium: { file: }, + } + put :update, params: request_body + expect(v4_json[:files]).not_to(eq(original_files)) + medium.reload + expect(medium.file).to(be_attached) + expect(medium.file.blob.checksum).not_to(eq(original_checksum)) + end + + it 'adds a logo to a tour set' do + user = create(:user, super: true) + tour_set = TourSet.second + user.tour_sets << tour_set + expect(tour_set.logo_url).to(be_nil) + Apartment::Tenant.switch!('public') + signed_cookie(user) + file = Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/0.jpg'), + 'image/jpeg', + ) + request_body = { + id: tour_set.id, + tenant: 'public', + model: 'tour_set', + tour_set: { logo: file }, + } + put :update, params: request_body + tour_set.reload + expect(v4_json[:logo_url]).not_to(be_nil) + expect(tour_set.logo).to(be_attached) + end + + it 'cannot rename a tour with a name that already exists' do + user = create(:user, super: true) + tour1 = create(:tour) + tour2 = create(:tour) + signed_cookie(user) + request_body = { + id: tour2.id, + tenant: 'public', + model: 'tour', + tour: { title: tour1.title }, + } + put :update, params: request_body + expect(v4_json[:errors].first[:detail]).to(eq('Title has already been taken')) + expect(response.status).to(eq(422)) + end + end + + describe 'DELETE #destroy' do + it 'destroys the requested record' do + tour = create(:tour_with_stops) + user = create(:user, super: true) + signed_cookie(user) + expect do + delete(:destroy, params: { id: tour.tour_stops.first.id, model: 'tour_stop', tenant: Apartment::Tenant.current }) + end.to(change(TourStop, :count).by(-1)) + end + + it 'responds unauthorized when unauthenticated' do + delete :destroy, params: { id: Tour.first.id, model: 'tour', tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end + end +end diff --git a/spec/controllers/v4/admin/flat_pages_controller_spec.rb b/spec/controllers/v4/admin/flat_pages_controller_spec.rb new file mode 100644 index 00000000..cee44e56 --- /dev/null +++ b/spec/controllers/v4/admin/flat_pages_controller_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::FlatPagesController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and list of flat pages when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + create_list(:flat_page, 4) + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(FlatPage.count)) + end + + it 'returns 200 and list of flat pages when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:flat_page, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(FlatPage.count)) + end + + it 'returns 200 and list of flat pages when authenticated as tour author' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + create_list(:flat_page, 2) + user.tours << tour + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(FlatPage.count)) + end + + it 'returns 401 when authenticated as site owner but requesting flat pages for different site' do + user = create(:user, super: false) + tour_sets = create_list(:tour_set, 4) + Apartment::Tenant.switch!(tour_sets.first.subdir) + user.tour_sets << tour_sets.last + signed_cookie(user) + get :index, params: { tenant: tour_sets.first.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and paginated list of flat pages when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:flat_page, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir, page: 2, per: 1 } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(1)) + end + + it 'returns 200 with specified flat pages excluded' do + user = create(:user, super: true) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + flat_pages = create_list(:flat_page, 4) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir, exclude: "#{flat_pages.first.id}, #{flat_pages.second.id}" } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(FlatPage.count - 2)) + expect(v4_json.map { |m| m[:id] }).not_to(include(flat_pages.first.id)) + expect(v4_json.map { |m| m[:id] }).not_to(include(flat_pages.second.id)) + end + end +end diff --git a/spec/controllers/v4/admin/media_controller_spec.rb b/spec/controllers/v4/admin/media_controller_spec.rb new file mode 100644 index 00000000..01311160 --- /dev/null +++ b/spec/controllers/v4/admin/media_controller_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::MediaController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and list of medium when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + create_list(:medium, 4) + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Medium.count)) + end + + it 'returns 200 and list of medium when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:medium, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Medium.count)) + end + + it 'returns 200 and list of medium when authenticated as tour author' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + create_list(:medium, 2) + user.tours << tour + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Medium.count)) + end + + it 'returns 401 when authenticated as site owner but requesting medium for different site' do + user = create(:user, super: false) + tour_sets = create_list(:tour_set, 4) + Apartment::Tenant.switch!(tour_sets.first.subdir) + user.tour_sets << tour_sets.last + signed_cookie(user) + get :index, params: { tenant: tour_sets.first.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and paginated list of medium when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:medium, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir, page: 2, per: 1 } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(1)) + end + + it 'returns 200 with specified media excluded' do + user = create(:user, super: true) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + media = create_list(:medium, 4) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir, exclude: "#{media.first.id}, #{media.second.id}" } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Medium.count - 2)) + expect(v4_json.map { |m| m[:id] }).not_to(include(media.first.id)) + expect(v4_json.map { |m| m[:id] }).not_to(include(media.second.id)) + end + end +end diff --git a/spec/controllers/v4/admin/stops_controller_spec.rb b/spec/controllers/v4/admin/stops_controller_spec.rb new file mode 100644 index 00000000..99f9f59e --- /dev/null +++ b/spec/controllers/v4/admin/stops_controller_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::StopsController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and list of stops when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + create_list(:stop, 4) + get :index, params: { tenant: Apartment::Tenant.current } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Stop.count)) + end + + it 'returns 200 and list of stops when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:stop, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Stop.count)) + end + + it 'returns 200 and list of stops when authenticated as tour author' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + create_list(:stop, 2) + user.tours << tour + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(Stop.count)) + end + + it 'returns 401 when authenticated as site owner but requesting stops for different site' do + user = create(:user, super: false) + tour_sets = create_list(:tour_set, 4) + Apartment::Tenant.switch!(tour_sets.first.subdir) + user.tour_sets << tour_sets.last + signed_cookie(user) + get :index, params: { tenant: tour_sets.first.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and paginated list of stops when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:stop, 3) + user.tour_sets << tour_set + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir, page: 2, per: 1 } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(1)) + end + end +end diff --git a/spec/controllers/v4/admin/tour_authors_controller_spec.rb b/spec/controllers/v4/admin/tour_authors_controller_spec.rb new file mode 100644 index 00000000..cc0308a1 --- /dev/null +++ b/spec/controllers/v4/admin/tour_authors_controller_spec.rb @@ -0,0 +1,370 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::TourAuthorsController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 when authenticated but not a tenant admin or super' do + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 for a mere tour author (not an admin)' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + author = create(:user, super: false, tour_sets: []) + create(:tour_author, tour:, user: author) + signed_cookie(author) + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns the users for a tour when authenticated as current tenant admin' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + authors = create_list(:user, 3) + authors.each { |author| create(:tour_author, tour:, user: author) } + + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + + expect(response).to(have_http_status(:ok)) + expect(v4_json.length).to(eq(3)) + expect(v4_json.map { |u| u[:id] }).to(match_array(authors.map(&:id))) + end + + it 'returns the users for a tour when authenticated as super' do + tour_set = create(:tour_set) + super_user = create(:user, super: true) + signed_cookie(super_user) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + author = create(:user) + create(:tour_author, tour:, user: author) + + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + + expect(response).to(have_http_status(:ok)) + expect(v4_json.length).to(eq(1)) + expect(v4_json.first[:id]).to(eq(author.id)) + end + + it 'returns an empty array when the tour has no authors' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + + expect(response).to(have_http_status(:ok)) + expect(v4_json).to(eq([])) + end + + it 'only returns authors for the requested tour' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + other_tour = create(:tour) + author = create(:user) + create(:tour_author, tour:, user: author) + create(:tour_author, tour: other_tour, user: create(:user)) + + get :index, params: { tenant: tour_set.subdir, tour_id: tour.id } + + expect(v4_json.length).to(eq(1)) + expect(v4_json.first[:id]).to(eq(author.id)) + end + end + + describe 'POST #create' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user) + post :create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 when authenticated but not a tenant admin or super' do + requester = create(:user, super: false, tour_sets: []) + signed_cookie(requester) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user) + post :create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 for a mere tour author (not an admin)' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + author = create(:user, super: false, tour_sets: []) + create(:tour_author, tour:, user: author) + signed_cookie(author) + user = create(:user) + post :create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'creates a tour author when authenticated as current tenant admin' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id }) + end.to(change(TourAuthor, :count).by(1)) + expect(response).to(have_http_status(:created)) + expect(v4_json[:tour_id]).to(eq(tour.id)) + expect(v4_json[:user_id]).to(eq(user.id)) + end + + it 'creates a tour author when authenticated as super' do + tour_set = create(:tour_set) + super_user = create(:user, super: true) + signed_cookie(super_user) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id }) + end.to(change(TourAuthor, :count).by(1)) + expect(response).to(have_http_status(:created)) + end + + it 'returns 422 and does not create a record when user_id is missing' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id }) + end.not_to(change(TourAuthor, :count)) + expect(response).to(have_http_status(:unprocessable_entity)) + end + + it 'returns 422 and does not create a record when tour_id is missing' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + user = create(:user) + + expect do + post(:create, params: { tenant: tour_set.subdir, user_id: user.id }) + end.not_to(change(TourAuthor, :count)) + expect(response).to(have_http_status(:unprocessable_entity)) + end + + it 'returns 422 and does not create a duplicate when the user is already an author of the tour' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user) + create(:tour_author, tour:, user:) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, user_id: user.id }) + end.not_to(change(TourAuthor, :count)) + expect(response).to(have_http_status(:unprocessable_entity)) + end + + it 'creates a tour author by username when authenticated as current tenant admin' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user, display_name: 'Jane Doe') + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, username: 'Jane Doe' }) + end.to(change(TourAuthor, :count).by(1)) + expect(response).to(have_http_status(:created)) + expect(v4_json[:tour_id]).to(eq(tour.id)) + expect(v4_json[:user_id]).to(eq(user.id)) + end + + it 'creates a tour author by username when authenticated as super' do + tour_set = create(:tour_set) + super_user = create(:user, super: true) + signed_cookie(super_user) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user, display_name: 'Jane Doe') + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, username: 'Jane Doe' }) + end.to(change(TourAuthor, :count).by(1)) + expect(response).to(have_http_status(:created)) + expect(v4_json[:user_id]).to(eq(user.id)) + end + + it 'prefers username over user_id when both are given' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + by_username = create(:user, display_name: 'Jane Doe') + by_id = create(:user) + + post( + :create, + params: { tenant: tour_set.subdir, tour_id: tour.id, username: 'Jane Doe', user_id: by_id.id }, + ) + + expect(response).to(have_http_status(:created)) + expect(v4_json[:user_id]).to(eq(by_username.id)) + end + + it 'returns 422 and does not create a record when username does not match any user' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, username: 'Nobody Here' }) + end.not_to(change(TourAuthor, :count)) + expect(response).to(have_http_status(:unprocessable_entity)) + end + + it 'returns 422 and does not create a duplicate when the user found by username is already an author' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user, display_name: 'Jane Doe') + create(:tour_author, tour:, user:) + + expect do + post(:create, params: { tenant: tour_set.subdir, tour_id: tour.id, username: 'Jane Doe' }) + end.not_to(change(TourAuthor, :count)) + expect(response).to(have_http_status(:unprocessable_entity)) + end + end + + describe 'DELETE #destroy' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + tour_author = create(:tour_author, tour:) + delete :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: tour_author.user_id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 when authenticated but not a tenant admin or super' do + user = create(:user, super: false, tour_sets: []) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + tour_author = create(:tour_author, tour:) + delete :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: tour_author.user_id } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'removes the tour author when authenticated as current tenant admin' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + tour_author = create(:tour_author, tour:) + + expect do + delete( + :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: tour_author.user_id }, + ) + end.to(change(TourAuthor, :count).by(-1)) + expect(response).to(have_http_status(:no_content)) + end + + it 'removes the tour author when authenticated as super' do + tour_set = create(:tour_set) + super_user = create(:user, super: true) + signed_cookie(super_user) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + tour_author = create(:tour_author, tour:) + + expect do + delete( + :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: tour_author.user_id }, + ) + end.to(change(TourAuthor, :count).by(-1)) + expect(response).to(have_http_status(:no_content)) + end + + it 'only removes the tour author matching both tour_id and user_id' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + other_tour = create(:tour) + author = create(:user) + create(:tour_author, tour: other_tour, user: author) + matching_tour_author = create(:tour_author, tour:, user: author) + + delete :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: author.id } + + expect(response).to(have_http_status(:no_content)) + expect(TourAuthor.exists?(matching_tour_author.id)).to(be(false)) + expect(TourAuthor.exists?(tour_id: other_tour.id, user_id: author.id)).to(be(true)) + end + + it 'returns 404 without crashing when tour_id/user_id do not match a tour author' do + tour_set = create(:tour_set) + admin = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(admin) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + + delete :destroy, + params: { tenant: tour_set.subdir, id: 1, tour_id: tour.id, user_id: 999_999 } + + expect(response).to(have_http_status(:not_found)) + end + end +end diff --git a/spec/controllers/v4/admin/tour_sets_controller_spec.rb b/spec/controllers/v4/admin/tour_sets_controller_spec.rb new file mode 100644 index 00000000..d9597e50 --- /dev/null +++ b/spec/controllers/v4/admin/tour_sets_controller_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::TourSetsController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: 'public' } + expect(response.status).to(eq(401)) + expect(v4_json[:error]).to(eq('unauthorized')) + end + + it 'returns all TourSets when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + get :index, params: { tenant: 'public' } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(TourSet.count)) + end + + it 'returns all TourSets (not just assigned) when authenticated as non-super' do + # Any authenticated user can see all TourSets so they can request access + user = create(:user, super: false) + user.tour_sets << create_list(:tour_set, 3) + signed_cookie(user) + get :index, params: { tenant: 'public' } + expect(response.status).to(eq(200)) + expect(v4_json.count).to(eq(TourSet.count)) + expect(v4_json.count).to(be > user.tour_sets.count) + end + end + + describe 'GET #show' do + it 'returns 401 when unauthenticated' do + tour_set = create(:tour_set) + get :show, params: { tenant: 'public', slug: tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and a tour set when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + get :show, params: { tenant: 'public', slug: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json[:name]).to(eq(tour_set.name)) + end + + it 'returns 200 a tour set when authenticated as site owner' do + user = create(:user, super: false) + tour_set = create(:tour_set) + user.tour_sets << tour_set + signed_cookie(user) + get :show, params: { tenant: 'public', slug: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json[:name]).to(eq(tour_set.name)) + end + + it 'returns 401 when authenticated as site owner but requesting a different site' do + user = create(:user, super: false) + owned_tour_set = create(:tour_set) + other_tour_set = create(:tour_set) + user.tour_sets << owned_tour_set + signed_cookie(user) + get :show, params: { tenant: 'public', slug: other_tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'lists all tours in the set' do + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:tour, 3, published: true) + create_list(:tour_with_stops, 2, published: false) + Apartment::Tenant.reset + get :show, params: { tenant: 'public', slug: tour_set.subdir } + Apartment::Tenant.switch!(tour_set.subdir) + expect(v4_json[:tours].count).to(eq(Tour.count)) + end + + it 'includes tour authors in response' do + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour1 = create(:tour) + tour2 = create(:tour) + user1 = create(:user, tours: [tour1, tour2]) + user2 = create(:user, display_name: nil, email: 'rwoodruf@emory.edu', tours: [tour2]) + user2.tour_set_admins << create(:tour_set_admin, user:, tour_set:, role: Role.find_by(title: 'Tour Creator')) + Apartment::Tenant.reset + get :show, params: { tenant: 'public', slug: tour_set.subdir } + expect(v4_json[:tour_authors].select { |ta| ta[:user] == user1.display_name }.count).to(eq(2)) + expect(v4_json[:tour_authors].select { |ta| ta[:user] == user2.email }.count).to(eq(1)) + expect(v4_json[:tour_authors].find { |ta| ta[:user] == user2.email }[:may_create_tour]).to(be(true)) + expect(v4_json[:tour_authors].find { |ta| ta[:user] == user1.display_name }[:may_create_tour]).to(be(false)) + end + + it 'returns no content when user is a tour editor within the site (not a site admin)' do + tour_set = create(:tour_set) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user = create(:user, super: false, tours: [tour]) + signed_cookie(user) + Apartment::Tenant.reset + get :show, params: { tenant: 'public', slug: tour_set.subdir } + expect(response).to(have_http_status(:no_content)) + end + end +end diff --git a/spec/controllers/v4/admin/tours_controller_spec.rb b/spec/controllers/v4/admin/tours_controller_spec.rb new file mode 100644 index 00000000..21df74b3 --- /dev/null +++ b/spec/controllers/v4/admin/tours_controller_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::ToursController, type: :controller) do + let(:tour_set) { create(:tour_set) } + + def clean_reindex + begin + Tour.search_index.delete + rescue StandardError + nil + end + Tour.reindex + end + + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(401)) + end + + it 'returns all tours when super user' do + user = create(:user, super: true) + signed_cookie(user) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:tour, 4) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json.count).to(eq(Tour.count)) + end + + it 'returns all tours when tour set admin' do + user = create(:user, super: false) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:tour, 4) + user.tour_sets << tour_set + signed_cookie(user) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json.count).to(eq(Tour.count)) + end + + it 'returns only tours assigned to user' do + user = create(:user, super: false) + signed_cookie(user) + Apartment::Tenant.switch!(tour_set.subdir) + tours = create_list(:tour, 4) + user.tours << [tours.first, tours.last] + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json.count).to(eq(2)) + expect(v4_json.count).not_to(eq(Tour.count)) + end + + it 'returns empty list when no tours' do + user = create(:user, super: true) + signed_cookie(user) + Apartment::Tenant.switch!(tour_set.subdir) + expect(Tour.count).to(be_zero) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json.count).to(be_zero) + end + + it 'returns list of all tours if all param is present' do + user = create(:user, super: false) + signed_cookie(user) + Apartment::Tenant.switch!(tour_set.subdir) + create_list(:tour, 3) + clean_reindex + get :index, params: { tenant: tour_set.subdir, all: true } + expect(v4_json.count).to(eq(Tour.count)) + expect(v4_json.count).to(be > 0) + end + end + + describe 'GET #show' do + it 'returns 401 when unauthenticated' do + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + clean_reindex + get :show, params: { tenant: tour_set.subdir, id: tour.id } + expect(response.status).to(eq(401)) + end + + it 'returns 200 and tour when authenticated as super' do + user = create(:user, super: true) + signed_cookie(user) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + clean_reindex + get :show, params: { tenant: tour_set.subdir, id: tour.id } + expect(response.status).to(eq(200)) + expect(v4_json[:title]).to(eq(tour.title)) + end + + it 'returns 200 a tour when authenticated as site owner' do + user = create(:user, super: false) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user.tour_sets << tour_set + signed_cookie(user) + clean_reindex + get :show, params: { tenant: tour_set.subdir, id: tour.id } + expect(response.status).to(eq(200)) + expect(v4_json[:title]).to(eq(tour.title)) + end + + it 'returns 200 a tour when authenticated as tour author' do + user = create(:user, super: false) + Apartment::Tenant.switch!(tour_set.subdir) + tour = create(:tour) + user.tours << tour + signed_cookie(user) + clean_reindex + get :show, params: { tenant: tour_set.subdir, id: tour.id } + expect(response.status).to(eq(200)) + expect(v4_json[:id]).to(eq(tour.id)) + end + + it 'returns 401 when authenticated as site owner but requesting tour for different site' do + other_tour_set = create(:tour_set) + user = create(:user, super: false) + Apartment::Tenant.switch!(other_tour_set.subdir) + tour = create(:tour, published: false) + user.tour_sets << tour_set + signed_cookie(user) + clean_reindex + get :show, params: { tenant: other_tour_set.subdir, id: tour.id } + expect(response.status).to(eq(401)) + end + end + + describe 'POST #create' do + it 'responds with 401 a tour when user is super' do + user = create(:user, super: false) + signed_cookie(user) + post :create, params: { tenant: TourSet.first.subdir, tour: { title: 'A New Tour' } } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'creates a tour when user is super' do + user = create(:user, super: true) + signed_cookie(user) + post :create, params: { tenant: TourSet.first.subdir, tour: { title: 'A New Tour' } } + expect(response).to(have_http_status(:created)) + end + + it 'creates a tour when user is tour set admin' do + tour_set = TourSet.last + user = create(:user, super: false, tour_sets: [tour_set]) + signed_cookie(user) + post :create, params: { tenant: tour_set.subdir, tour: { title: 'A New Tour' } } + expect(response).to(have_http_status(:created)) + end + + it 'creates a tour when user is can create tours' do + tour_set = TourSet.first + user = create(:user, super: false) + create(:tour_set_admin, user:, tour_set:, role: Role.find_by(title: 'Tour Creator')) + signed_cookie(user) + post :create, params: { tenant: TourSet.first.subdir, tour: { title: 'A New Tour' } } + expect(response).to(have_http_status(:created)) + Apartment::Tenant.switch!(tour_set.subdir) + expect(user.tours).to(include(Tour.find(v4_json[:id]))) + end + + it 'returns 401 when user is not tour creator for current tenant' do + tour_set = TourSet.first + user = create(:user, super: false) + create(:tour_set_admin, user:, tour_set:, role: Role.find_by(title: 'Tour Creator')) + signed_cookie(user) + post :create, params: { tenant: TourSet.last.subdir, tour: { title: 'A New Tour' } } + expect(response).to(have_http_status(:unauthorized)) + end + end +end diff --git a/spec/controllers/v4/admin/users_controller_spec.rb b/spec/controllers/v4/admin/users_controller_spec.rb new file mode 100644 index 00000000..e25c2815 --- /dev/null +++ b/spec/controllers/v4/admin/users_controller_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Admin::UsersController, type: :controller) do + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: Apartment::Tenant.current } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 when authenticated but not super' do + user = create(:user, super: false) + signed_cookie(user) + tour_set = create(:tour_set) + get :index, params: { tenant: tour_set.subdir } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 401 and list of users when authenticated as current tenant admin' do + user = create(:user, super: false) + signed_cookie(user) + tour_set = create(:tour_set) + user.tour_sets << tour_set + Apartment::Tenant.switch!(tour_set.subdir) + get :index, params: { tenant: tour_set.subdir } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns 200 and list of users when authenticated as super' do + create(:user, display_name: nil) # Ensures the sort_by works + user = create(:user, super: true) + signed_cookie(user) + tour_set = create(:tour_set) + get :index, params: { tenant: tour_set.subdir } + expect(response).to(have_http_status(:ok)) + expect(v4_json.count).to(eq(User.count)) + end + + it 'returns 401 when bad credentials' do + tour_set = create(:tour_set) + invalid_signed_cooke + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(401)) + end + end + + describe 'GET #show' do + it 'returns 200 and current user when authenticated and me parameter is present' do + user = create(:user, super: false) + User.reindex + signed_cookie(user) + get :show, params: { tenant: 'public' } + expect(response).to(have_http_status(:ok)) + expect(v4_json[:email]).to(eq(user.email)) + end + + it 'returns 401 when unauthenticated and me parameter is present' do + get :show, params: { tenant: 'public' } + expect(response.status).to(eq(401)) + end + + it 'returns 401 when bad credentials and me parameter is present' do + invalid_signed_cooke + get :show, params: { tenant: 'public' } + expect(response.status).to(eq(401)) + end + end +end diff --git a/spec/controllers/v4/public/media_controller_spec.rb b/spec/controllers/v4/public/media_controller_spec.rb new file mode 100644 index 00000000..043da5f4 --- /dev/null +++ b/spec/controllers/v4/public/media_controller_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Public::MediaController, type: :controller) do + let(:tour_set) { create(:tour_set) } + + before { Apartment::Tenant.switch!(tour_set.subdir) } + + describe 'GET #show' do + it 'returns 404 when key does not exist' do + get :show, params: { tenant: tour_set.subdir, key: 'nonexistent' } + expect(response).to(have_http_status(:not_found)) + end + + it 'redirects to blob url for a known key' do + medium = create(:medium) + get :show, params: { tenant: tour_set.subdir, key: medium.file.blob.key } + expect(response).to(have_http_status(:redirect)) + end + + it 'redirects to variant url when variant param is given' do + medium = create(:medium) + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(Medium).to(receive(:files).and_return({ + desktop: 'http://example.com/desktop.jpg', + tablet: 'http://example.com/tablet.jpg', + mobile: 'http://example.com/mobile.jpg', + lqip: 'http://example.com/lqip.jpg', + })) + # rubocop:enable RSpec/AnyInstance + get :show, params: { tenant: tour_set.subdir, key: medium.file.blob.key, variant: 'desktop' } + expect(response).to(have_http_status(:redirect)) + end + end +end diff --git a/spec/controllers/v4/public/modes_controller_spec.rb b/spec/controllers/v4/public/modes_controller_spec.rb new file mode 100644 index 00000000..17484413 --- /dev/null +++ b/spec/controllers/v4/public/modes_controller_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Public::ModesController, type: :controller) do + describe 'PUT #create' do + it 'gets the list of modes' do + get :index, params: { tenant: Apartment::Tenant.current } + expect(v4_json.count).to(eq(4)) + end + end +end diff --git a/spec/controllers/v4/public/stops_controller_spec.rb b/spec/controllers/v4/public/stops_controller_spec.rb new file mode 100644 index 00000000..b0713df8 --- /dev/null +++ b/spec/controllers/v4/public/stops_controller_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Public::StopsController, type: :controller) do + let(:tour_set) { create(:tour_set) } + let(:published_tour) { create(:tour, published: true) } + let(:unpublished_tour) { create(:tour, published: false) } + + before { Apartment::Tenant.switch!(tour_set.subdir) } + + describe 'GET #index' do + it 'returns 401 when unauthenticated' do + get :index, params: { tenant: tour_set.subdir } + expect(response).to(have_http_status(:unauthorized)) + end + + it 'returns stops when authenticated as super admin' do + user = create(:user, super: true) + create(:stop, tours: [published_tour]) + signed_cookie(user) + get :index, params: { tenant: tour_set.subdir } + expect(response).to(have_http_status(:ok)) + end + end + + describe 'GET #show' do + it 'returns 404 when slug does not exist' do + get :show, params: { tenant: tour_set.subdir, slug: 'nonexistent' } + expect(response).to(have_http_status(:not_found)) + end + + it 'returns 404 when stop exists but is unpublished' do + stop = create(:stop, tours: [unpublished_tour]) + get :show, params: { tenant: tour_set.subdir, slug: stop.slug } + expect(response).to(have_http_status(:not_found)) + end + + it 'returns stop data when stop is published' do + stop = create(:stop, tours: [published_tour]) + get :show, params: { tenant: tour_set.subdir, slug: stop.slug } + expect(response).to(have_http_status(:ok)) + expect(v4_json[:title]).to(eq(stop.title)) + end + end +end diff --git a/spec/controllers/v4/public/tour_sets_controller_spec.rb b/spec/controllers/v4/public/tour_sets_controller_spec.rb new file mode 100644 index 00000000..49d8a609 --- /dev/null +++ b/spec/controllers/v4/public/tour_sets_controller_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Public::TourSetsController, type: :controller) do + before do + create_list(:tour_set, Random.new.rand(2..4)) + TourSet.all.find_each do |ts| + Apartment::Tenant.switch!(ts.subdir) + create_list(:tour, Random.new.rand(2..4)) + end + Apartment::Tenant.switch!(TourSet.first.subdir) + Tour.all.find_each { |t| t.update(published: false) } + Apartment::Tenant.switch!(TourSet.last.subdir) + Tour.first.update(published: true) + Apartment::Tenant.reset + TourSet.reindex + TourSet.reindex + end + + describe 'GET #index' do + it 'returns a 200 and only TourSets with published tours when unauthenticated' do + Apartment::Tenant.switch!(TourSet.first.subdir) + create_list(:tour, 2, published: true) + published_tour_sets = TourSet.all.select(&:should_index?) + get :index, params: { tenant: 'public' } + expect(v4_json.count).not_to(be_zero) + expect(v4_json.count).to(eq(published_tour_sets.count)) + expect(TourSet.count).to(be > published_tour_sets.count) + expect(response.status).to(eq(200)) + end + + it 'returns a 200 and only TourSets with published and authenticated user is admin' do + Apartment::Tenant.switch!(TourSet.second.subdir) + Tour.all.find_each { |t| t.update(published: false) } + Apartment::Tenant.reset + published_tour_sets = TourSet.all.select(&:should_index?) + user = create(:user, super: false) + signed_cookie(user) + user.tour_sets << TourSet.second + get :index, params: { tenant: 'public' } + expect(v4_json.count).to(eq(published_tour_sets.count + 1)) + expect(v4_json.map { |ts| ts[:subdir] }).to(include(TourSet.second.subdir)) + expect(TourSet.count).to(be > published_tour_sets.count) + expect(response.status).to(eq(200)) + end + + it 'returns a 200 and all TourSets when authenticated as super' do + Apartment::Tenant.switch!(TourSet.second.subdir) + Tour.all.find_each { |t| t.update(published: false) } + Apartment::Tenant.reset + published_tour_sets = TourSet.all.select(&:should_index?) + user = create(:user, super: true) + signed_cookie(user) + get :index, params: { tenant: 'public' } + expect(v4_json.count).to(eq(TourSet.count)) + expect(TourSet.count).to(be > published_tour_sets.count) + expect(response.status).to(eq(200)) + end + end +end diff --git a/spec/controllers/v4/public/tours_controller_spec.rb b/spec/controllers/v4/public/tours_controller_spec.rb new file mode 100644 index 00000000..1530d789 --- /dev/null +++ b/spec/controllers/v4/public/tours_controller_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(V4::Public::ToursController, type: :controller) do + let(:tour_set) { create(:tour_set) } + + def clean_reindex + begin + Tour.search_index.delete + rescue StandardError + nil + end + Tour.reindex + end + + before do + Apartment::Tenant.switch!(tour_set.subdir) + clean_reindex + end + + describe 'GET #index' do + it 'returns 404 when no tours exist' do + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(404)) + end + + it 'returns 200 and only published tours when unauthenticated' do + create(:tour_with_stops, published: true) + create(:tour, published: false) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(response.status).to(eq(200)) + expect(v4_json[:tours].count).to(eq(Tour.published.count)) + end + + it 'returns all tours when requested by tenant admin' do + create_list(:tour, 3, published: false) + user = create(:user, super: false) + user.tour_sets << tour_set + signed_cookie(user) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json[:tours].count).to(eq(Tour.count)) + end + + it 'returns only published tours plus authored tours for a non-admin user' do + published_tour = create(:tour, published: true) + authored_unpublished = create_list(:tour, 3, published: false) + other_unpublished = create(:tour, published: false) + user = create(:user, super: false, tour_sets: []) + user.tours << [published_tour, authored_unpublished.first, authored_unpublished.last] + signed_cookie(user) + clean_reindex + get :index, params: { tenant: tour_set.subdir } + expect(v4_json[:tours].count).to(be < Tour.count) + expect(v4_json[:tours].count).to(eq([*user.tours, *Tour.published].uniq.count)) + expect(v4_json[:tours].map { |t| t[:id] }).not_to(include(other_unpublished.id)) + end + end + + describe 'GET #show' do + it 'returns 404 when tour is not published' do + tour = create(:tour, published: false) + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slug } + expect(response.status).to(eq(404)) + expect(v4_json[:errors].first).to(eq('Not found')) + end + + it 'returns 200 when requested by tenant admin and tour is unpublished' do + tour = create(:tour, published: false) + user = create(:user) + user.tour_sets << tour_set + signed_cookie(user) + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slug } + expect(response.status).to(eq(200)) + expect(v4_json[:tour][:title]).to(eq(tour.title)) + end + + it 'returns 200 when requested by tour author and tour is unpublished' do + tour = create(:tour, published: false) + user = create(:user, tour_sets: []) + user.tours << tour + signed_cookie(user) + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slug } + expect(response.status).to(eq(200)) + expect(v4_json[:tour][:title]).to(eq(tour.title)) + end + + it 'returns 200 when requested by slug' do + tour = create(:tour, published: true) + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slug } + expect(response.status).to(eq(200)) + expect(v4_json[:tour][:title]).to(eq(tour.title)) + end + + it 'returns tour when requested by an old slug after title change' do + tour = create(:tour, published: true) + original_slug = tour.slugs.first.slug + tour.update(title: Faker::Movies::HitchhikersGuideToTheGalaxy.location) + clean_reindex + expect(tour.slugs.count).to(eq(2)) + get :show, params: { tenant: tour_set.subdir, slug: original_slug } + expect(response.status).to(eq(200)) + expect(v4_json[:tour][:title]).to(eq(tour.title)) + expect(v4_json[:tour][:slug]).not_to(eq(original_slug)) + end + + it 'returns stops from an Open Geographies endpoint' do + tour = create(:tour, published: true, open_geographies: true, open_geographies_endpoint: 'http://og.ecds.io') + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slugs.first.slug } + # expect(v4_json[:tour][:stops]).to(eq(['Open Geographies'])) + expect(v4_json[:tour][:stop_count]).to(eq(4)) + expect(v4_json[:tour][:bounds]).to(eq({ + west: -83.8150232, + east: -82.88327699999999, + south: 32.6648851, + north: 33.8113142, + centerLat: 33.238099649999995, + centerLng: -83.3491501, + })) + expect(v4_json[:tour][:title]).to(eq(tour.title)) + end + + it 'includes durations for published tour' do + tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), published: false) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 400).join(' ')) } + tour.update(published: true) + clean_reindex + get :show, params: { tenant: tour_set.subdir, slug: tour.slugs.first.slug } + + expect(v4_json[:tour][:travel_duration]).to(eq('About 4 hours bicycling')) + expect(v4_json[:tour][:read_duration]).to(eq('10 minutes reading')) + end + end +end diff --git a/spec/factories/access_requests.rb b/spec/factories/access_requests.rb new file mode 100644 index 00000000..5af85c81 --- /dev/null +++ b/spec/factories/access_requests.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# spec/factories/users.rb +FactoryBot.define do + factory :access_request do + user { create(:user) } + end +end diff --git a/spec/factories/images/map_icon.jpg b/spec/factories/images/map_icon.jpg new file mode 100644 index 00000000..a7d6a4ee Binary files /dev/null and b/spec/factories/images/map_icon.jpg differ diff --git a/spec/factories/images/map_icon_too_big.jpg b/spec/factories/images/map_icon_too_big.jpg new file mode 100644 index 00000000..bf8c554a Binary files /dev/null and b/spec/factories/images/map_icon_too_big.jpg differ diff --git a/spec/factories/login.rb b/spec/factories/login.rb index 4d7ba13c..e246af7d 100644 --- a/spec/factories/login.rb +++ b/spec/factories/login.rb @@ -5,8 +5,12 @@ require 'jwt' FactoryBot.define do - factory :login, class: EcdsRailsAuthEngine::Login do + factory :login, class: 'EcdsRailsAuthEngine::Login' do provider { Faker::Internet.domain_name } user_id { nil } + + after(:create) do |login| + create_list(:token, 1, login:) + end end end diff --git a/spec/factories/map_icons.rb b/spec/factories/map_icons.rb index f65c076c..bd363701 100755 --- a/spec/factories/map_icons.rb +++ b/spec/factories/map_icons.rb @@ -5,6 +5,5 @@ factory :map_icon do filename { Faker::File.file_name(dir: '', ext: 'png', directory_separator: '') } base_sixty_four { File.read(Rails.root.join('spec/factories/images/icon_base64.txt')) } - created_at { Faker::Number.number(digits: 10) } end end diff --git a/spec/factories/media.rb b/spec/factories/media.rb index f85290f5..24557488 100644 --- a/spec/factories/media.rb +++ b/spec/factories/media.rb @@ -5,9 +5,9 @@ factory :medium do title { Faker::TvShows::RickAndMorty.character } caption { Faker::TvShows::RickAndMorty.quote } - filename { Faker::File.file_name(dir: '', ext: 'png', directory_separator: '') } + # file { Rack::Test::UploadedFile.new(Rails.root.join("spec", "factories", "images", "0.jpg"), "image/jpeg") } + filename { Faker::File.file_name(dir: '', ext: 'jpg', directory_separator: '') } base_sixty_four { File.read(Rails.root.join('spec/factories/base64_image.txt')) } - created_at { Faker::Number.number(digits: 10) } video_provider { 'keiner' } video { nil } end diff --git a/spec/factories/stops.rb b/spec/factories/stops.rb index 677a4713..62900bdc 100644 --- a/spec/factories/stops.rb +++ b/spec/factories/stops.rb @@ -10,7 +10,6 @@ description { Faker::Hipster.paragraph(sentence_count: 2, supplemental: true, random_sentences_to_add: 4) } lat { Faker::Address.latitude } lng { Faker::Address.longitude } - created_at { Faker::Number.number(digits: 10) } factory :stop_with_media do transient do @@ -19,7 +18,7 @@ # https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#transient-attributes after(:create) do |stop, evaluator| - # create_list(:medium, evaluator.media_count, stops: [stop]) + create_list(:medium, evaluator.media_count, stops: [stop]) end end end diff --git a/spec/factories/themes.rb b/spec/factories/themes.rb index c7b4720b..6172e742 100644 --- a/spec/factories/themes.rb +++ b/spec/factories/themes.rb @@ -4,6 +4,5 @@ FactoryBot.define do factory :theme do title { Faker::Color.name } - created_at { Faker::Number.number(digits: 10) } end end diff --git a/spec/factories/token.rb b/spec/factories/token.rb index a3093281..4ab0f525 100755 --- a/spec/factories/token.rb +++ b/spec/factories/token.rb @@ -5,7 +5,7 @@ require 'jwt' FactoryBot.define do - factory :token, class: EcdsRailsAuthEngine::Token do + factory :token, class: 'EcdsRailsAuthEngine::Token' do token { JWT.encode(Faker::Beer.style, Faker::Address.zip, 'HS256') } end end diff --git a/spec/factories/tour_stops.rb b/spec/factories/tour_stops.rb index 5f9a5bf3..46537642 100644 --- a/spec/factories/tour_stops.rb +++ b/spec/factories/tour_stops.rb @@ -3,8 +3,7 @@ # spec/factories/tour_stops.rb FactoryBot.define do factory :tour_stop do - tour_id { nil } - stop_id { nil } - position { nil } + association :tour + association :stop end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 97e4cb7a..50c08322 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -5,5 +5,10 @@ factory :user do email { Faker::Internet.email } display_name { Faker::Music::Hiphop.artist } + terms_accepted { false } + + after(:create) do |user| + create(:login, user_id: user.id) if user.login.blank? + end end end diff --git a/spec/factories/voice_overs.rb b/spec/factories/voice_overs.rb new file mode 100644 index 00000000..326c44b0 --- /dev/null +++ b/spec/factories/voice_overs.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# spec/factories/media.rb +FactoryBot.define do + factory :voice_over do + association :tour + end +end diff --git a/spec/fixtures/access_request/access_request_email b/spec/fixtures/access_request/access_request_email new file mode 100644 index 00000000..8743b1d8 --- /dev/null +++ b/spec/fixtures/access_request/access_request_email @@ -0,0 +1,3 @@ +AccessRequest#access_request_email + +Hi, find me in app/views/access_request/access_request_email diff --git a/spec/fixtures/audio/sample.m4a b/spec/fixtures/audio/sample.m4a new file mode 100644 index 00000000..56926e5b Binary files /dev/null and b/spec/fixtures/audio/sample.m4a differ diff --git a/spec/fixtures/audio/sample.mp3 b/spec/fixtures/audio/sample.mp3 new file mode 100644 index 00000000..fbdcc289 Binary files /dev/null and b/spec/fixtures/audio/sample.mp3 differ diff --git a/spec/fixtures/audio/sample.wav b/spec/fixtures/audio/sample.wav new file mode 100644 index 00000000..d71512a2 Binary files /dev/null and b/spec/fixtures/audio/sample.wav differ diff --git a/spec/fixtures/files/open_geographies.json b/spec/fixtures/files/open_geographies.json new file mode 100644 index 00000000..3cb077f0 --- /dev/null +++ b/spec/fixtures/files/open_geographies.json @@ -0,0 +1,1990 @@ +{ + "_index": "open_geographies_v1_20260831093211268", + "_id": "13", + "_score": 1, + "uuid": "e0d9d15c-06f7-40c2-86a1-bf8b1df0b0d0", + "slug": "test-tour", + "slugs": ["test-tour"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "tour", + "model_id": "62", + "model_name": "Tours", + "name": "Test Tour", + "visibility": "published", + "date_modified": "2026-05-21T12:43:21Z", + "identifiers": [], + "stops": [ + { + "@context": "https://opengeographies.org/api/v1/context.json", + "uuid": "46149c73-1234-4362-bfea-b72a0481880e", + "slug": "antioch-baptist", + "slugs": ["antioch-baptist"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "place", + "model_id": "20", + "model_name": "Churches", + "name": "Antioch Baptist", + "visibility": "published", + "date_modified": "2026-04-21T20:43:26Z", + "identifiers": [], + "geo": { + "point": { + "lat": 33.4680484, + "lon": -82.88327699999999 + } + }, + "administrative_area": [ + { + "name": "Taliaferro", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ], + "legacy_id": { + "label": "Legacy ID", + "value": 8977 + }, + "slug_2": { + "label": "Slug", + "value": "antioch-baptist" + }, + "short_description": "In November 1886, a group of formerly enslaved men and their children from Powelton New Hope Baptist Church, led by Deacons Willie Peak, Abe Frazier, and Philic Jones, founded Antioch Baptist Church. The deacons purchased two acres from the Veazey family estate, with an additional two acres donated for a cemetery. Soon after the church’s…", + "wordpress_link": { + "label": "WordPress Link", + "value": "https://www.hrcga.org/church/antioch-baptist-3/" + }, + "address": "4136 Sparta Rd SE, Crawfordville, GA 30631", + "description": "

In November 1886, a group of formerly enslaved men and their children from Powelton New Hope Baptist Church, led by Deacons Willie Peak, Abe Frazier, and Philic Jones, founded Antioch Baptist Church. The deacons purchased two acres from the Veazey family estate, with an additional two acres donated for a cemetery. Soon after the church’s dedication, a one-room school was built on the grounds.

Before integration, schools like these were often the only educational opportunity for rural African Americans. Until the mid-1950s, Black children in the area were not permitted to ride county school buses. Church-affiliated schools served these communities until the “equalization schools” of the 1950s, and it wasn’t until after the Civil Rights Act of 1964 that African American students began to gain access to equal facilities. Even then, the transition was far from smooth—nearby Crawfordville saw significant unrest in 1965, widely covered in national news. Sadly, the original Antioch school, once standing to the left of the church, is now gone.

Though the sanctuary may appear abandoned, a dedicated group of descendants still gathers for an annual reunion every August. As seen in the gallery below, one such reunion brought together four generations from multiple states, touching their roots, sharing stories, and passing history to the young. For many years, these efforts were led by George Turner, whose father was Antioch’s last active deacon. Under his leadership, repairs were made and the grounds kept, but more support is needed to ensure Antioch’s survival.

The cemetery holds at least 75 recorded burials. Before his passing, Deacon Turner documented 71 additional unmarked graves, common in early African American cemeteries, where headstones were often financially out of reach for farming families. His careful record-keeping preserves a vital piece of this community’s history.

With renewed commitment, Antioch can once again serve its community, just as it has since 1886. Preserving this site honors the resilience of those who built it and ensures future generations can learn from its story.

Please click the audio tour link to hear fascinating stories from Antioch.

", + "year": { + "label": "Year", + "value": 1886 + }, + "denomination": "Baptist", + "denomination_facet": "Baptist", + "types": ["Baptist Church"], + "works": [ + { + "uuid": "d70323ef-1d17-4652-8e48-a5f3b0c3223f", + "slug": "antioch-baptist-cemetery", + "slugs": ["antioch-baptist-cemetery"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "work", + "model_id": "45", + "model_name": "Works", + "name": "Antioch Baptist - Cemetery", + "visibility": "published", + "date_modified": "2025-12-15T15:32:27Z", + "identifiers": [], + "link": { + "label": "Link", + "value": "https://www.findagrave.com/cemetery/32571/antioch-cemetery" + }, + "url": "https://www.findagrave.com/cemetery/32571/antioch-cemetery", + "order": null + } + ], + "contained_in": { + "uuid": "918b3ac6-a265-4b5d-9a18-e0d3f360469a", + "slug": "taliaferro-county", + "slugs": ["taliaferro-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Taliaferro County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:22Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4225621" + } + ], + "geo": { + "point": { + "lat": 33.56609, + "lon": -82.87876 + } + }, + "administrative_area": [ + { + "level": "PCLI", + "name": "United States", + "geonames_id": "6252001", + "geonames_url": "https://www.geonames.org/6252001" + }, + { + "level": "ADM1", + "name": "Georgia", + "geonames_id": "4197000", + "geonames_url": "https://www.geonames.org/4197000" + }, + { + "level": "ADM2", + "name": "Taliaferro", + "geonames_id": "4225621", + "geonames_url": "https://www.geonames.org/4225621" + } + ] + }, + "contained_in_place": { + "uuid": "918b3ac6-a265-4b5d-9a18-e0d3f360469a", + "slug": "taliaferro-county", + "slugs": ["taliaferro-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Taliaferro County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:22Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4225621" + } + ], + "geo": { + "point": { + "lat": 33.56609, + "lon": -82.87876 + } + }, + "administrative_area": [ + { + "level": "PCLI", + "name": "United States", + "geonames_id": "6252001", + "geonames_url": "https://www.geonames.org/6252001" + }, + { + "level": "ADM1", + "name": "Georgia", + "geonames_id": "4197000", + "geonames_url": "https://www.geonames.org/4197000" + }, + { + "level": "ADM2", + "name": "Taliaferro", + "geonames_id": "4225621", + "geonames_url": "https://www.geonames.org/4225621" + } + ] + }, + "media": [ + { + "uuid": "7f3828f0-121b-4689-8e8c-b320bfa7add9", + "slug": "antioch-baptist-3d-tour", + "slugs": ["antioch-baptist-3d-tour"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch Baptist - 3D Tour", + "visibility": "published", + "date_modified": "2026-08-28T17:01:21Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/eba2636f-01e6-405a-af8b-4cb432c51930/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/eba2636f-01e6-405a-af8b-4cb432c51930/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/eba2636f-01e6-405a-af8b-4cb432c51930/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/eba2636f-01e6-405a-af8b-4cb432c51930/manifest", + "embed_url": "https://my.matterport.com/show/?m=8QfqqRUffe1", + "media_type": "model3d", + "order": null + }, + { + "uuid": "e7315a7a-1b9b-4b2b-a1e0-c5d18af01fdd", + "slug": "antioch-1-memories", + "slugs": ["antioch-1-memories"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch 1- Memories", + "visibility": "published", + "date_modified": "2026-08-28T17:20:21Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/0168359c-89c1-4c82-99a2-b85bdca25bb4/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/0168359c-89c1-4c82-99a2-b85bdca25bb4/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/0168359c-89c1-4c82-99a2-b85bdca25bb4/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/0168359c-89c1-4c82-99a2-b85bdca25bb4/manifest", + "media_type": "audio", + "order": null + }, + { + "uuid": "8d135ab9-bb9b-424d-82f0-31eed4d60d2e", + "slug": "antioch-2-the-church", + "slugs": ["antioch-2-the-church"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch 2- The Church", + "visibility": "published", + "date_modified": "2026-08-28T17:20:22Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/8f1f581e-8bac-452f-a061-9a5529380ddf/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/8f1f581e-8bac-452f-a061-9a5529380ddf/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/8f1f581e-8bac-452f-a061-9a5529380ddf/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/8f1f581e-8bac-452f-a061-9a5529380ddf/manifest", + "media_type": "audio", + "order": null + }, + { + "uuid": "325c5c16-f518-43c7-982e-7556b2e546f1", + "slug": "antioch-3-the-revival", + "slugs": ["antioch-3-the-revival"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch 3- The Revival", + "visibility": "published", + "date_modified": "2026-08-28T17:20:23Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/e5de15a1-3239-43ba-a5fd-788a52a2a253/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/e5de15a1-3239-43ba-a5fd-788a52a2a253/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/e5de15a1-3239-43ba-a5fd-788a52a2a253/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/e5de15a1-3239-43ba-a5fd-788a52a2a253/manifest", + "media_type": "audio", + "order": null + }, + { + "uuid": "80505d8d-db3f-4d4c-ba10-f59b3946ecff", + "slug": "antioch-4-the-cemetery", + "slugs": ["antioch-4-the-cemetery"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch 4- The Cemetery", + "visibility": "published", + "date_modified": "2026-08-28T17:20:24Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/09ae3b90-d749-46bd-99e2-76416491b1d3/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/09ae3b90-d749-46bd-99e2-76416491b1d3/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/09ae3b90-d749-46bd-99e2-76416491b1d3/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/09ae3b90-d749-46bd-99e2-76416491b1d3/manifest", + "media_type": "audio", + "order": null + }, + { + "uuid": "15f70a85-2123-4154-90c0-8c8f8089f11b", + "slug": "antiochehmqoy-1-scaled", + "slugs": ["antiochehmqoy-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochEhMqoY-1-scaled", + "visibility": "published", + "date_modified": "2025-12-11T23:21:06Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/1b23305a-9993-45eb-819a-640d8016f77c/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/1b23305a-9993-45eb-819a-640d8016f77c/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/1b23305a-9993-45eb-819a-640d8016f77c/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/1b23305a-9993-45eb-819a-640d8016f77c/manifest", + "order": null + }, + { + "uuid": "e9c0e1bc-d5c2-4d17-a53b-87f8d99f1dd1", + "slug": "antioch8caolbec-3-scaled", + "slugs": ["antioch8caolbec-3-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch8cAoLBEc-3-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:55Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/2c5e719e-b47d-49d0-ac51-8104b89fb5eb/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/2c5e719e-b47d-49d0-ac51-8104b89fb5eb/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/2c5e719e-b47d-49d0-ac51-8104b89fb5eb/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/2c5e719e-b47d-49d0-ac51-8104b89fb5eb/manifest", + "caption": "

Here lies Dock Peek, born in 1852 and died in 1925 at the age of 72. At the end of the Civil War, he would have been about thirteen. We don’t know how many freed slaves are buried in the graveyard but we suspect there are a lot. Many of the early unmarked graves would have been formally enslaved people.

", + "order": null + }, + { + "uuid": "446d493a-47ae-45b8-bf30-d669b480a176", + "slug": "antioch-solar-box1-scaled", + "slugs": ["antioch-solar-box1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch-solar-box1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:55Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/93233727-a2f5-488c-adff-53dca4dac848/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/93233727-a2f5-488c-adff-53dca4dac848/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/93233727-a2f5-488c-adff-53dca4dac848/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/93233727-a2f5-488c-adff-53dca4dac848/manifest", + "caption": "

In front of the church is a solar box containing very interesting aspects of Antioch history. It is well worth a visit and a listen. The four audio files can be accessed on the video virtual tour. THE CHURCH – THE CEMETERY – A REVIVAL – MEMORIES OF ANTIOCH

", + "order": null + }, + { + "uuid": "01e8d257-0f3a-4ce2-a8b8-c324a3e1c590", + "slug": "antioch3r53htzt-scaled", + "slugs": ["antioch3r53htzt-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch3R53htzt-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:56Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/37b7b798-3884-44b4-8dec-7e85fb0387de/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/37b7b798-3884-44b4-8dec-7e85fb0387de/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/37b7b798-3884-44b4-8dec-7e85fb0387de/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/37b7b798-3884-44b4-8dec-7e85fb0387de/manifest", + "caption": "

As stated earlier, the Antioch Baptist church building is suffering significant damage from top to bottom. But, engineering analysis reveals that with money, time and local support it can be successfully restored. Though the damage to the brick corner foundation shown looks daunting, a local contractor declares that it can be repaired at a reasonable…

", + "order": null + }, + { + "uuid": "19c732fd-cf49-49ba-85bb-3a5640fb25b9", + "slug": "antiochnxrd3r9w-scaled", + "slugs": ["antiochnxrd3r9w-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochnXRd3r9W-scaled", + "visibility": "published", + "date_modified": "2025-12-11T23:21:09Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/0fe0e950-13b6-400a-8e9e-6ae828273126/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/0fe0e950-13b6-400a-8e9e-6ae828273126/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/0fe0e950-13b6-400a-8e9e-6ae828273126/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/0fe0e950-13b6-400a-8e9e-6ae828273126/manifest", + "order": null + }, + { + "uuid": "cc377ce1-7fdf-4d46-a973-1143b5bdda43", + "slug": "antioch6vmzkvxa-scaled", + "slugs": ["antioch6vmzkvxa-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch6VmZKvXa-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:56Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/16419836-2c0c-4958-b748-83befbe06825/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/16419836-2c0c-4958-b748-83befbe06825/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/16419836-2c0c-4958-b748-83befbe06825/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/16419836-2c0c-4958-b748-83befbe06825/manifest", + "caption": "This photo presents that significant repairs will be needed to stabilize and restore the building framing. However, most of the timbers remain sound and the building remains plumb and square after 120 years. Note the fieldstone footings. All the locals had to work with was wood, rocks and sometimes brick. They made great use of what was at hand.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "2a01c8af-911e-47ea-ad78-bae99f663aaa", + "slug": "antiochm4klx4ak-scaled", + "slugs": ["antiochm4klx4ak-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochM4KLX4AK-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:57Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/b95b6e7e-5ac1-4c77-9c8e-9fd158448b27/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/b95b6e7e-5ac1-4c77-9c8e-9fd158448b27/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/b95b6e7e-5ac1-4c77-9c8e-9fd158448b27/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/b95b6e7e-5ac1-4c77-9c8e-9fd158448b27/manifest", + "caption": "Though definitely compromised, the front steps and open vestibule entry area and its large support framing remains level and repairable. The siding, though rough and worn will, in most cases, not need to be replaced and can be repaired and painted along with the curved vestibule ceiling.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "dd7a5e01-4096-430b-826a-d3bc1df8334a", + "slug": "antiochwmrzjqx4-1-scaled", + "slugs": ["antiochwmrzjqx4-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochWmrZJqx4-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:57Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/6cab7e37-0e27-40f5-82e0-b7c7f41cc002/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/6cab7e37-0e27-40f5-82e0-b7c7f41cc002/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/6cab7e37-0e27-40f5-82e0-b7c7f41cc002/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/6cab7e37-0e27-40f5-82e0-b7c7f41cc002/manifest", + "caption": "Here we get a peek through the open front door into the sanctuary. Remarkably, the original heart pine floor boards, though neglected for decades, remain intact and can be brought to life with minimum effort. Isn’t the glimpse of the pulpit area and apse windows intriguing?", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "c8957c8b-c15e-497e-928b-25d6f22795aa", + "slug": "antiochl2bxugmq-scaled", + "slugs": ["antiochl2bxugmq-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochL2BxuGmq-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:58Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/67b78ce4-d39a-474c-af13-6788b0dd1056/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/67b78ce4-d39a-474c-af13-6788b0dd1056/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/67b78ce4-d39a-474c-af13-6788b0dd1056/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/67b78ce4-d39a-474c-af13-6788b0dd1056/manifest", + "caption": "This charming photo presents the Antioch sanctuary as it is today. We see over twenty of the original hand hewn, heart pine pews resting on the pine floor boards just as they have since the church’s 19th century beginning. The weight of each, solid wood pew is remarkable. Throughout the sanctuary, we see the signs of many restoration efforts. The original heart pine ceiling and walls has been covered and sheet rocked. Thankfully, all of the original elements of this period interior can be restored.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "2eb494d8-647d-42a2-8bcc-da3ed5823421", + "slug": "antioch7be8omn1-scaled", + "slugs": ["antioch7be8omn1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch7Be8oMN1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:58Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/672551ef-d2c1-47c0-87ad-58310d877b0b/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/672551ef-d2c1-47c0-87ad-58310d877b0b/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/672551ef-d2c1-47c0-87ad-58310d877b0b/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/672551ef-d2c1-47c0-87ad-58310d877b0b/manifest", + "caption": "Here we see a close up photograph of the chancel, pulpit area and apse. This church was built by and for former slaves and without the input of professional contractors and architects. We marvel at the creativity and cleverness exhibited in the design of gothic window frames created by placing triangular elements atop rectangular frames. We love these three windows and what they represent. All elements like these can be repaired and restored and remain available for all to see and enjoy for decades to come.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "c561b89f-a453-40dd-95ee-4c18355b8e2a", + "slug": "antioch2ewfzsdh-scaled", + "slugs": ["antioch2ewfzsdh-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch2EWFZSDh-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:59Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/d294afcd-4ce4-49e0-904d-647f35b13ab0/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/d294afcd-4ce4-49e0-904d-647f35b13ab0/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/d294afcd-4ce4-49e0-904d-647f35b13ab0/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/d294afcd-4ce4-49e0-904d-647f35b13ab0/manifest", + "caption": "In this photo we see how the builders created a charming apse at this simple church by kicking out its back wall to create a bay. We also see a decorative wainscot along the walls. These are just another example of the creative design talent and capability of these former slaves that is reflected in the construction of this old church.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "c4128f37-f898-40e9-8321-070acb850f6c", + "slug": "antiochu3qpkzfd-scaled", + "slugs": ["antiochu3qpkzfd-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochU3qpKzFD-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:59Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/4c9cc771-33b2-44bf-84c8-c645d0c96558/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/4c9cc771-33b2-44bf-84c8-c645d0c96558/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/4c9cc771-33b2-44bf-84c8-c645d0c96558/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/4c9cc771-33b2-44bf-84c8-c645d0c96558/manifest", + "caption": "According to George Turner, son of the last Antioch Baptist Bishop, congregational singing was a critical element of the worship services at the church. He also tells us that sometimes, the congregation would not sing but simply tap their feet in rhythm expressing elements of unity and community.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "d84c10b6-7ecb-43b1-9914-12ff536cdb17", + "slug": "antiochwokoxyen-scaled", + "slugs": ["antiochwokoxyen-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochwOkOxyen-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:55:59Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/039a6e2c-f309-49d2-8f10-f69b342beb34/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/039a6e2c-f309-49d2-8f10-f69b342beb34/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/039a6e2c-f309-49d2-8f10-f69b342beb34/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/039a6e2c-f309-49d2-8f10-f69b342beb34/manifest", + "caption": "We chose to present this photo of a hand hewn, un-planed heart pine pew back to provide an example of the authenticity of almost every element present at Antioch today. We believe that preservation of such treasures as these for the edification and enjoyment of future generations is critical.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "db49df4f-80d2-4415-8d21-6c44fcc22c6a", + "slug": "antiochxi7h9xma-scaled", + "slugs": ["antiochxi7h9xma-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antiochxi7H9XMa-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:00Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/c58b1ebd-a1ec-45e5-a2b2-1475eb6912d0/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/c58b1ebd-a1ec-45e5-a2b2-1475eb6912d0/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/c58b1ebd-a1ec-45e5-a2b2-1475eb6912d0/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/c58b1ebd-a1ec-45e5-a2b2-1475eb6912d0/manifest", + "caption": "This is a close up of one of the gothic window frames. Here we point out that these are handmade, sashed, 6 over 6 windows. None are from a factory. All were hand made, another example of the talents of the builders. We also want to point out again that the walls, ceilings etc. were sheet rocked in the 20th century. The plan is to see that all the significant, original sanctuary elements will be restored.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "e5dc5a26-3550-41fb-b930-b810ffeef945", + "slug": "antiochkercqhjl-scaled", + "slugs": ["antiochkercqhjl-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochKErcqHJl-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:00Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/8b4e9810-61a3-49e6-af56-ad9c8e12a5f1/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/8b4e9810-61a3-49e6-af56-ad9c8e12a5f1/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/8b4e9810-61a3-49e6-af56-ad9c8e12a5f1/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/8b4e9810-61a3-49e6-af56-ad9c8e12a5f1/manifest", + "caption": "In this view, we display some of the problems that must be addressed to breathe life back into Antioch. The roof is leaking as evidenced by the ceiling stains near the chancel and patched, damaged areas seen nearby. More serious leaks exist and will need to be addressed. On the other hand, isn’t the scene depicted charming and tranquil. We hope to bring you in the future pictures of the repaired church and the back story of its successful rejuvenation.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "93576c80-8684-45b6-b2e9-633744fddd3f", + "slug": "antioch-homecoming-scaled", + "slugs": ["antioch-homecoming-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antioch-homecoming-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:01Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/08f879f4-4ead-4fd0-959c-8d8357d5746a/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/08f879f4-4ead-4fd0-959c-8d8357d5746a/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/08f879f4-4ead-4fd0-959c-8d8357d5746a/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/08f879f4-4ead-4fd0-959c-8d8357d5746a/manifest", + "caption": "The tendency would be to think of Antioch as an abandoned church that has no support, but that is not the case. There is an annual homecoming that takes place each August, attended by family descendants from far and wide, who pay homage to their heritage and their roots. In this photo of a recent reunion are represented four generations of descendants from as far away as New York and Connecticut. They raise enough money to keep the grounds up and do some repairs but need help for the full renovation.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "e608421a-225f-4a0f-be5a-68152ee00dc6", + "slug": "antiochpxsxw77y-scaled", + "slugs": ["antiochpxsxw77y-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochpXsxW77Y-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:01Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/28d7eed3-57f6-4c01-811e-a92b7023f34c/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/28d7eed3-57f6-4c01-811e-a92b7023f34c/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/28d7eed3-57f6-4c01-811e-a92b7023f34c/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/28d7eed3-57f6-4c01-811e-a92b7023f34c/manifest", + "caption": "What a lovely shot from the church interior to the little graveyard outside. While there are 75 recorded interments, Deacon Turner, when he was still alive, identified 67 other graves that are unmarked. Gravestones and caskets were expensive for people making perhaps two dollars a day. They were buried with love and dignity but headstones were simply unaffordable.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "af3d6ab4-5393-40fc-8e5a-218780787d50", + "slug": "antiochoeg4l-3k-1-scaled", + "slugs": ["antiochoeg4l-3k-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochoeG4L-3K-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:02Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/1eeb0051-334c-4008-93c9-664b516f4160/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/1eeb0051-334c-4008-93c9-664b516f4160/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/1eeb0051-334c-4008-93c9-664b516f4160/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/1eeb0051-334c-4008-93c9-664b516f4160/manifest", + "caption": "The oldest interment in the cemetery is that of Rev. William H. Darden, who was born into slavery in 1856 and died in 1898, shortly after the congregation was formed. He lies at rest under the signature pine tree in front of the church.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "24e7a378-d64c-4d91-b708-37416c54fd79", + "slug": "antiochtoie3anh-scaled", + "slugs": ["antiochtoie3anh-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Antiochtoie3Anh-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:56:02Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/631c2152-23d9-4302-955e-fa210c34fcf1/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/631c2152-23d9-4302-955e-fa210c34fcf1/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/631c2152-23d9-4302-955e-fa210c34fcf1/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/631c2152-23d9-4302-955e-fa210c34fcf1/manifest", + "caption": "Here lies Deacon Turner, George Turner's father, his mother and her sister. Family roots run deep at Antioch and especially so for the Turner family.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "083e856b-dfb3-4fee-a28b-6dce2e3806de", + "slug": "antiochnxrd3r9w-2-scaled", + "slugs": ["antiochnxrd3r9w-2-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "AntiochnXRd3r9W-2-scaled", + "visibility": "published", + "date_modified": "2025-12-18T17:50:07Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/b0df9ca2-addd-4c03-b649-6e95a9f9e4c7/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/b0df9ca2-addd-4c03-b649-6e95a9f9e4c7/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/b0df9ca2-addd-4c03-b649-6e95a9f9e4c7/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/b0df9ca2-addd-4c03-b649-6e95a9f9e4c7/manifest", + "caption": "

Antioch is a very spiritual place that is difficult to describe. It is nestled in the trees beside the old graveyard and presents a very photogenic profile. There used to be a small one room schoolhouse to the left of the church but it was dismantled some years ago. Antioch can still be saved but time is running out.

", + "order": null + } + ], + "order": 50 + }, + { + "@context": "https://opengeographies.org/api/v1/context.json", + "uuid": "347ea1da-562c-4a67-8cf7-706a5ea882bf", + "slug": "hardison-baptist", + "slugs": ["hardison-baptist"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "place", + "model_id": "20", + "model_name": "Churches", + "name": "Hardison Baptist", + "visibility": "published", + "date_modified": "2026-04-21T20:42:36Z", + "identifiers": [], + "geo": { + "point": { + "lat": 32.6648851, + "lon": -83.8150232 + } + }, + "administrative_area": [ + { + "name": "Crawford", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ], + "legacy_id": { + "label": "Legacy ID", + "value": 18281 + }, + "slug_2": { + "label": "Slug", + "value": "hardison-baptist" + }, + "short_description": "Hardison Baptist Church, established in 1902, is located in rural Crawford County, about four miles west of Byron. The county itself was created on December 9, 1822, from Houston County, which had been formed from land ceded by the Creek Indians in the 1821 Treaty of Indian Springs. It was named for statesman William Harris…", + "wordpress_link": { + "label": "WordPress Link", + "value": "https://www.hrcga.org/church/hardison-baptist/" + }, + "address": "13845 GA-42, Byron, GA 31008", + "description": "

Hardison Baptist Church, established in 1902, is located in rural Crawford County, about four miles west of Byron. The county itself was created on December 9, 1822, from Houston County, which had been formed from land ceded by the Creek Indians in the 1821 Treaty of Indian Springs. It was named for statesman William Harris Crawford, who served as a U.S. senator, minister to France, and secretary of the treasury.

For the first two years, the Hardison congregation met in a tent while their permanent church was under construction. The completed sanctuary was dedicated in 1904 on land donated by Jeff Hardison, for whom the church is named. Unlike many rural congregations that have declined over time, Hardison Baptist has continued to grow, with several additions made to the original sanctuary. In its early years, baptisms were conducted in a pond at a nearby Boy Scout camp. That tradition continued until the 1960s, when the church built its own baptismal pool. Around the same time, the church’s front entrance was extended, and the ornate steeple seen today was added. Interestingly, the steeple was designed and built by a local company that normally constructed crates for the military. The bell was donated by Lester Almaroad with the stipulation that it would revert to the original donor if the building was ever used for anything other than a Baptist church.

Today, Hardison Baptist Church is lovingly maintained by its dedicated congregation, who continue to preserve both its physical structure and its spiritual mission. Special thanks to Rev. Rick Odum, who served as pastor for twenty-two years, for sharing this history.

", + "year": { + "label": "Year", + "value": 1902 + }, + "denomination": "Baptist", + "denomination_facet": "Baptist", + "types": ["Baptist Church"], + "contained_in": { + "uuid": "65221348-aeb7-4812-95ec-3c0451941a98", + "slug": "crawford-county", + "slugs": ["crawford-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Crawford County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:14Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4189924" + } + ], + "geo": { + "point": { + "lat": 32.7145, + "lon": -83.98634 + } + }, + "administrative_area": [ + { + "name": "United States", + "level": "PCLI", + "geonames_id": "6252001", + "geonames_url": "https://www.geonames.org/6252001" + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": "4197000", + "geonames_url": "https://www.geonames.org/4197000" + }, + { + "name": "Crawford", + "level": "ADM2", + "geonames_id": "4189924", + "geonames_url": "https://www.geonames.org/4189924" + } + ] + }, + "contained_in_place": { + "uuid": "65221348-aeb7-4812-95ec-3c0451941a98", + "slug": "crawford-county", + "slugs": ["crawford-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Crawford County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:14Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4189924" + } + ], + "geo": { + "point": { + "lat": 32.7145, + "lon": -83.98634 + } + }, + "administrative_area": [ + { + "name": "United States", + "level": "PCLI", + "geonames_id": "6252001", + "geonames_url": "https://www.geonames.org/6252001" + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": "4197000", + "geonames_url": "https://www.geonames.org/4197000" + }, + { + "name": "Crawford", + "level": "ADM2", + "geonames_id": "4189924", + "geonames_url": "https://www.geonames.org/4189924" + } + ] + }, + "media": [ + { + "uuid": "a7102171-f321-473c-9fe7-021b6844a4b3", + "slug": "img_5304", + "slugs": ["img_5304"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5304", + "visibility": "published", + "date_modified": "2026-04-20T19:23:41Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/27e9dab0-2f3b-42e5-9d68-1cd531be6a42/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/27e9dab0-2f3b-42e5-9d68-1cd531be6a42/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/27e9dab0-2f3b-42e5-9d68-1cd531be6a42/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/27e9dab0-2f3b-42e5-9d68-1cd531be6a42/manifest", + "order": null + }, + { + "uuid": "321bddf8-9899-4df7-9b19-cf2babc61367", + "slug": "img_5304-scaled", + "slugs": ["img_5304-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5304-scaled", + "visibility": "published", + "date_modified": "2026-06-25T17:31:47Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/18001fb0-6b92-4f91-bb48-3d45bebce657/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/18001fb0-6b92-4f91-bb48-3d45bebce657/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/18001fb0-6b92-4f91-bb48-3d45bebce657/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/18001fb0-6b92-4f91-bb48-3d45bebce657/manifest", + "order": null + }, + { + "uuid": "940f0bca-9682-4e51-aaa0-1220530378a2", + "slug": "img_5314", + "slugs": ["img_5314"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5314", + "visibility": "published", + "date_modified": "2025-12-12T22:41:37Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/0801995f-3560-484c-910e-434a1357edac/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/0801995f-3560-484c-910e-434a1357edac/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/0801995f-3560-484c-910e-434a1357edac/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/0801995f-3560-484c-910e-434a1357edac/manifest", + "caption": "An old church bell tower with intricate woodwork, showing signs of weathering and age, standing tall under a clear blue sky. The steeple was designed and built locally.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "2410d04f-1d8b-4b3c-8f5d-9a7d12fc552c", + "slug": "img_5326-scaled", + "slugs": ["img_5326-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5326-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:37Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/b9a387dd-1407-4789-8ab2-7712e60ef879/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/b9a387dd-1407-4789-8ab2-7712e60ef879/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/b9a387dd-1407-4789-8ab2-7712e60ef879/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/b9a387dd-1407-4789-8ab2-7712e60ef879/manifest", + "caption": "A peaceful church sanctuary with neatly arranged pews, a central pulpit adorned with floral arrangements, and a serene backdrop painting, all illuminated by elegant chandeliers.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "3bc06dbe-7c85-408f-a8e3-764c1c7bb777", + "slug": "img_5323-1-scaled", + "slugs": ["img_5323-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5323-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:38Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/5c06d965-f76c-4b0c-a1fb-574ab4f24349/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/5c06d965-f76c-4b0c-a1fb-574ab4f24349/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/5c06d965-f76c-4b0c-a1fb-574ab4f24349/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/5c06d965-f76c-4b0c-a1fb-574ab4f24349/manifest", + "caption": "A close-up view of a church stage featuring a wooden pulpit, floral arrangement, comfortable chairs, and a piano, ready for a service or sermon.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "e35c2825-c653-47ec-9381-f22ab9462717", + "slug": "img_5319-scaled", + "slugs": ["img_5319-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5319-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:38Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/c4b3101a-6fb4-4f22-ba75-ca0e66718527/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/c4b3101a-6fb4-4f22-ba75-ca0e66718527/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/c4b3101a-6fb4-4f22-ba75-ca0e66718527/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/c4b3101a-6fb4-4f22-ba75-ca0e66718527/manifest", + "caption": "A serene view of a church interior from the pulpit, showcasing rows of neatly arranged pews, warm lighting from chandeliers, and a welcoming atmosphere.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "ef18eb0d-bd33-4dc5-afe0-b2caf1c7bcf4", + "slug": "img_5318-1-scaled", + "slugs": ["img_5318-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5318-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:39Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/7fea7928-84a6-424c-bb64-b8fdeee961cc/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/7fea7928-84a6-424c-bb64-b8fdeee961cc/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/7fea7928-84a6-424c-bb64-b8fdeee961cc/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/7fea7928-84a6-424c-bb64-b8fdeee961cc/manifest", + "caption": "Elegant leather chairs and a table with religious texts, set up on the church stage for clergy or guest speakers.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "2df63e56-5cac-4c48-ad1f-4f82f66d6892", + "slug": "img_5321-1-scaled", + "slugs": ["img_5321-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "IMG_5321-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:39Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/f95489aa-e01d-4630-982e-7fabde28fc82/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/f95489aa-e01d-4630-982e-7fabde28fc82/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/f95489aa-e01d-4630-982e-7fabde28fc82/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/f95489aa-e01d-4630-982e-7fabde28fc82/manifest", + "caption": "A baptismal pool with a serene mural of a flowing river and lush landscape in the background. Prior to the construction of this pool in the 1960s, the congregation used a pond nearby at a local Boy Scout camp.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + } + ], + "order": 20 + }, + { + "@context": "https://opengeographies.org/api/v1/context.json", + "uuid": "3c64edbd-54e4-4b97-a8ca-c2e1ed2bae28", + "slug": "oconee-baptist-2", + "slugs": ["oconee-baptist-2", "oconee-baptist"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "place", + "model_id": "20", + "model_name": "Churches", + "name": "Oconee Baptist", + "visibility": "published", + "date_modified": "2026-04-21T20:42:36Z", + "identifiers": [], + "geo": { + "point": { + "lat": 33.8113142, + "lon": -83.2818954 + } + }, + "administrative_area": [ + { + "name": "Oglethorpe", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ], + "legacy_id": { + "label": "Legacy ID", + "value": 18483 + }, + "slug_2": { + "label": "Slug", + "value": "oconee-baptist-2" + }, + "short_description": "The church you see here was built in the late 1890s and was bricked over in 1970. The congregation remains very active but relocated to a more accessible location when ten acres were purchased in 2003 closer to the highway. This historic African American church is well maintained by the present congregation, who deeply appreciates…", + "wordpress_link": { + "label": "WordPress Link", + "value": "https://www.hrcga.org/church/oconee-baptist-2/" + }, + "address": "wire bridge road oglethorpe county, georgia", + "description": "

The church you see here was built in the late 1890s and was bricked over in 1970. The congregation remains very active but relocated to a more accessible location when ten acres were purchased in 2003 closer to the highway. This historic African American church is well maintained by the present congregation, who deeply appreciates its heritage on this remote dirt road in rural Oglethorpe County. Such churches are scattered throughout rural Georgia—appearing modest yet holding profound stories about Georgia’s history. Oconee Baptist stands as one such example.

Among its notable congregants was Reverend Parker Poulain, the first minister of Oconee Baptist. Rev. Poulain served several churches in the area and, in 1884, converted and baptized A.D. (Adam) Williams at Bethabara Baptist church in nearby Greene County. A.D.’s father, Willis Williams, had been enslaved in Greene County and died there in 1874. A.D. moved to Atlanta in 1893, where he successfully built up the congregation of Ebenezer Baptist. His grandson was Martin Luther King, Jr., and Ebenezer later became Dr. King’s home church.

The church cemetery appears unassuming at first glance, but closer examination reveals significant historical narratives. According to Find a Grave, there are 216 recorded interments, twelve of them born into slavery. However, older congregation members estimate there are approximately 300 unmarked graves, many likely belonging to emancipated slaves.

Of the 12 documented graves of people born into slavery, three have death certificates posted on Find a Grave. These certificates provide insights into 19th-century Georgia history. State law mandated statewide death registration in 1919, and county officials implemented this requirement gradually over several years. The death certificate process requests the name and birthplace of both parents of the deceased. All three certificates of these formerly enslaved people answer these requests with the designation “Unknown”—a poignant but common occurrence prior to 1865.

James Sims’ death certificate is particularly noteworthy, and is posted below. Born around 1828, he died in 1930 at the remarkable age of 102. The Oglethorpe County 1860 slave schedule records a large slave owner named Dr. James Sims (owning 52 slaves with 10 slave houses). James was likely enslaved by Dr. Sims, as the 1860 slave schedule lists a Black male belonging to Dr. Sims that matches his age. When emancipation came in 1865, James would have begun his life as a free man in rural Oglethorpe County at age 37—unable to read or write, without knowledge of his parents, and without a last name. The census of 1930, taken just before he died, confirms that he remained illiterate for life.

Like many emancipated slaves, James adopted the surname of his former enslaver. From these humble beginnings in 1865, James and his contemporaries began to build new lives for themselves and their children. Adjacent to the church once stood a church-sponsored school, now gone. These small schools often provided the only educational opportunities available to many of the first generation of freedmen and their children in rural Georgia. The freedmen may have been illiterate but they carried deep educational aspirations for their children. These church sponsored schools emerged throughout the state in the early 1900s, though few remain today. Today, 23 of the documented interments in the cemetery carry the Sims surname. We honor Oconee Baptist’s history and its place in Georgia’s heritage, and we appreciate the congregation’s devoted stewardship of this important legacy.

", + "year": { + "label": "Year", + "value": 1891 + }, + "denomination": "Baptist", + "denomination_facet": "Baptist", + "types": ["Baptist Church"], + "works": [ + { + "uuid": "6b496688-6575-4879-a86c-a43e01c4b337", + "slug": "oconee-baptist-cemetery", + "slugs": ["oconee-baptist-cemetery"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "work", + "model_id": "45", + "model_name": "Works", + "name": "Oconee Baptist - Cemetery", + "visibility": "published", + "date_modified": "2025-12-15T15:32:11Z", + "identifiers": [], + "link": { + "label": "Link", + "value": "https://www.findagrave.com/cemetery/1965028/memorial-search" + }, + "url": "https://www.findagrave.com/cemetery/1965028/memorial-search", + "order": null + } + ], + "contained_in": { + "uuid": "c9e9ac65-6aa1-4ef8-a715-b68457427335", + "slug": "oglethorpe-county", + "slugs": ["oglethorpe-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Oglethorpe County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:20Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4213930" + } + ], + "geo": { + "point": { + "lat": 33.88065, + "lon": -83.0807 + } + }, + "administrative_area": [ + { + "name": "Oglethorpe", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ] + }, + "contained_in_place": { + "uuid": "c9e9ac65-6aa1-4ef8-a715-b68457427335", + "slug": "oglethorpe-county", + "slugs": ["oglethorpe-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Oglethorpe County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:20Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4213930" + } + ], + "geo": { + "point": { + "lat": 33.88065, + "lon": -83.0807 + } + }, + "administrative_area": [ + { + "name": "Oglethorpe", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ] + }, + "media": [ + { + "uuid": "1323a8fe-a5da-4e76-bec3-4538e9bb0180", + "slug": "p5226589_4-scaled", + "slugs": ["p5226589_4-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226589_4-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/536c58fc-5474-4a80-b9ae-b32e84a974ce/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/536c58fc-5474-4a80-b9ae-b32e84a974ce/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/536c58fc-5474-4a80-b9ae-b32e84a974ce/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/536c58fc-5474-4a80-b9ae-b32e84a974ce/manifest", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": 10 + }, + { + "uuid": "c901b02c-bc80-4a46-a048-fc66d998316a", + "slug": "p5226581_3-scaled", + "slugs": ["p5226581_3-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226581_3-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:26Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/5c6ad96f-48b0-43a5-aab7-ae2798cbffa4/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/5c6ad96f-48b0-43a5-aab7-ae2798cbffa4/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/5c6ad96f-48b0-43a5-aab7-ae2798cbffa4/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/5c6ad96f-48b0-43a5-aab7-ae2798cbffa4/manifest", + "caption": "

Although, the church no longer meets here, the congregation takes care of the maintenance required out of respect for the history she represents.

", + "order": null + }, + { + "uuid": "7a13c8f9-0ead-4ca9-9cbc-b5691661cfa5", + "slug": "p5226492_5-scaled", + "slugs": ["p5226492_5-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226492_5-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:26Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/f130d91c-54b9-4b2d-acad-c2f75d5f140a/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/f130d91c-54b9-4b2d-acad-c2f75d5f140a/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/f130d91c-54b9-4b2d-acad-c2f75d5f140a/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/f130d91c-54b9-4b2d-acad-c2f75d5f140a/manifest", + "caption": "

Here lies Lizzie Perry (1879 – 1920). She died at the young age of 40.

", + "order": null + }, + { + "uuid": "cb47582d-b3d6-4889-91ba-cd84e9cf16e7", + "slug": "p5226520_3-scaled", + "slugs": ["p5226520_3-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226520_3-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:27Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/28aad90e-d4d0-4e96-9642-6fb465c88247/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/28aad90e-d4d0-4e96-9642-6fb465c88247/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/28aad90e-d4d0-4e96-9642-6fb465c88247/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/28aad90e-d4d0-4e96-9642-6fb465c88247/manifest", + "caption": "

OLYMPUS DIGITAL CAMERA

", + "order": null + }, + { + "uuid": "1a02341f-7c8e-43e5-a1cc-18be87088f64", + "slug": "p5226538_5-scaled", + "slugs": ["p5226538_5-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226538_5-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:27Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/19684dc4-a830-4eb4-a2f8-36289b162c1c/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/19684dc4-a830-4eb4-a2f8-36289b162c1c/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/19684dc4-a830-4eb4-a2f8-36289b162c1c/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/19684dc4-a830-4eb4-a2f8-36289b162c1c/manifest", + "caption": "

Oconee is located in a very remote part of Oglethorpe County. She has been at the center of the local community since he founding in 1891. What a pleasant place for Dinner on the Grounds.

", + "order": null + }, + { + "uuid": "ebb182c3-d9fc-47b2-b963-6749b0350b38", + "slug": "p5226428_8-1-scaled", + "slugs": ["p5226428_8-1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226428_8-1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:28Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/392760a2-3285-46da-bedd-a07091412eac/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/392760a2-3285-46da-bedd-a07091412eac/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/392760a2-3285-46da-bedd-a07091412eac/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/392760a2-3285-46da-bedd-a07091412eac/manifest", + "caption": "

OLYMPUS DIGITAL CAMERA

", + "order": null + }, + { + "uuid": "03fff060-a7a7-4c5d-a0b2-2366b4cdd252", + "slug": "elbert_ruckersville_01", + "slugs": ["elbert_ruckersville_01"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Elbert_Ruckersville_01", + "visibility": "published", + "date_modified": "2025-12-11T23:11:52Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/1c3ab609-bd89-450d-9b30-309d82f60050/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/1c3ab609-bd89-450d-9b30-309d82f60050/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/1c3ab609-bd89-450d-9b30-309d82f60050/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/1c3ab609-bd89-450d-9b30-309d82f60050/manifest", + "order": null + }, + { + "uuid": "dec6de70-1f9e-4b52-a3c5-b506a8e0e882", + "slug": "fannin_tarpley_01-scaled", + "slugs": ["fannin_tarpley_01-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Fannin_Tarpley_01-scaled", + "visibility": "published", + "date_modified": "2025-12-11T23:11:56Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/c8bec286-6d60-4c0b-af93-d5b3cdabb845/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/c8bec286-6d60-4c0b-af93-d5b3cdabb845/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/c8bec286-6d60-4c0b-af93-d5b3cdabb845/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/c8bec286-6d60-4c0b-af93-d5b3cdabb845/manifest", + "order": null + }, + { + "uuid": "25280a8d-a654-446a-b051-d9b76675ae75", + "slug": "habersham_mt-airy_01", + "slugs": ["habersham_mt-airy_01"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Habersham_Mt.-Airy_01", + "visibility": "published", + "date_modified": "2025-12-12T22:41:29Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/03632f40-dbde-4de0-9338-ffeb3a9b9ec6/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/03632f40-dbde-4de0-9338-ffeb3a9b9ec6/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/03632f40-dbde-4de0-9338-ffeb3a9b9ec6/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/03632f40-dbde-4de0-9338-ffeb3a9b9ec6/manifest", + "caption": "

COPYRIGHT 2013 RANDY CLEGG

", + "order": null + }, + { + "uuid": "2b5bc8bb-1857-456a-abaf-93c57d331917", + "slug": "jefferson_ways-grove_01", + "slugs": ["jefferson_ways-grove_01"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jefferson_Ways-Grove_01", + "visibility": "published", + "date_modified": "2025-12-12T22:41:29Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/9ba33a16-5559-4128-9475-3850370d7bb0/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/9ba33a16-5559-4128-9475-3850370d7bb0/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/9ba33a16-5559-4128-9475-3850370d7bb0/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/9ba33a16-5559-4128-9475-3850370d7bb0/manifest", + "caption": "

Nikon D850

", + "order": null + }, + { + "uuid": "e05118d5-82f8-45f0-b59e-7ed6f07a3bf5", + "slug": "and", + "slugs": ["and"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "And", + "visibility": "published", + "date_modified": "2025-12-12T22:41:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/9ba8570d-fc9a-421e-ab1a-548e131c6ab5/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/9ba8570d-fc9a-421e-ab1a-548e131c6ab5/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/9ba8570d-fc9a-421e-ab1a-548e131c6ab5/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/9ba8570d-fc9a-421e-ab1a-548e131c6ab5/manifest", + "caption": "

And

", + "order": null + }, + { + "uuid": "e08796cc-88cd-404c-9065-8df81410a086", + "slug": "p5226559_7-scaled", + "slugs": ["p5226559_7-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226559_7-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/90d5b678-6d11-4e8f-b30e-d70bff7ccc2f/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/90d5b678-6d11-4e8f-b30e-d70bff7ccc2f/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/90d5b678-6d11-4e8f-b30e-d70bff7ccc2f/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/90d5b678-6d11-4e8f-b30e-d70bff7ccc2f/manifest", + "caption": "This is one of the most impressively constructed Baptismal pools we have seen in rural Georgia. Immersion is central to the Baptist faith.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "f70803e2-de7c-4db3-98a2-4158841995f8", + "slug": "p5226403_4-scaled", + "slugs": ["p5226403_4-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226403_4-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:31Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/56cf4c16-4e37-496b-9a72-750236b1a0d9/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/56cf4c16-4e37-496b-9a72-750236b1a0d9/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/56cf4c16-4e37-496b-9a72-750236b1a0d9/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/56cf4c16-4e37-496b-9a72-750236b1a0d9/manifest", + "caption": "The bones of the church are original but many improvements s were made over the years. This view from the old chancel shots shows a drop ceiling, paneled walls and replacement window.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "207328bb-86cf-4717-b82b-40c9a95758c5", + "slug": "p5226418_6-scaled", + "slugs": ["p5226418_6-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226418_6-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:31Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/22bfe25e-fb72-4bba-973f-1b0b600408bb/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/22bfe25e-fb72-4bba-973f-1b0b600408bb/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/22bfe25e-fb72-4bba-973f-1b0b600408bb/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/22bfe25e-fb72-4bba-973f-1b0b600408bb/manifest", + "caption": "This view forward also shows many changes. About the only original features appear the be the vertical wainscoting and chair rail.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "dd7fc20a-5bee-4734-b159-37a8fd8ba918", + "slug": "p5226532_4-scaled", + "slugs": ["p5226532_4-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226532_4-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:31Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/bef153a8-793e-44c1-8acf-6966f0a5b92a/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/bef153a8-793e-44c1-8acf-6966f0a5b92a/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/bef153a8-793e-44c1-8acf-6966f0a5b92a/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/bef153a8-793e-44c1-8acf-6966f0a5b92a/manifest", + "caption": "This the original cemetery and was used until very recently. However, it is well maintained. Some of the older church members say that there are approximately 300 unmarked graves locate here.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "633866c4-be8b-4c79-8970-a33cef64ec9e", + "slug": "p5226443_2-scaled", + "slugs": ["p5226443_2-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226443_2-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:32Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/87b5935c-1c7c-4273-8110-15af8e28286f/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/87b5935c-1c7c-4273-8110-15af8e28286f/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/87b5935c-1c7c-4273-8110-15af8e28286f/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/87b5935c-1c7c-4273-8110-15af8e28286f/manifest", + "caption": "In the late 19th century, field stone markers were very common. They were readily available and they were free. Proper headstones were just unaffordable for many of the members struggling to get by in a harsh environment.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "5fd77511-900e-46de-9caf-81630feccd9e", + "slug": "p5226454_1-scaled", + "slugs": ["p5226454_1-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "P5226454_1-scaled", + "visibility": "published", + "date_modified": "2025-12-12T22:41:32Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/887c19b1-6a26-412f-87bf-66187dc2357e/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/887c19b1-6a26-412f-87bf-66187dc2357e/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/887c19b1-6a26-412f-87bf-66187dc2357e/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/887c19b1-6a26-412f-87bf-66187dc2357e/manifest", + "caption": "More examples of unmarked graves among the more recent headstones.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + }, + { + "uuid": "0543c7f4-d429-4da0-b76b-78c5649faf21", + "slug": "james-sims-death-certificate", + "slugs": ["james-sims-death-certificate"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "James-Sims-death-certificate", + "visibility": "published", + "date_modified": "2025-12-12T22:41:33Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/e5384c2d-d96f-4e42-8fe8-c6633927174d/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/e5384c2d-d96f-4e42-8fe8-c6633927174d/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/e5384c2d-d96f-4e42-8fe8-c6633927174d/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/e5384c2d-d96f-4e42-8fe8-c6633927174d/manifest", + "caption": "Here is the death certificate for James Maxey. He is discussed more thoroughly in the general history at the beginning. Note that his father and mother are unknown. James is reported to be 102 years old, although his exact birthdate is unknown. We think he was born on the plantation of Dr. James Sims sometime around 1828 to 1830.", + "gallery": { + "label": "Gallery", + "value": true + }, + "order": null + } + ], + "order": 10 + }, + { + "@context": "https://opengeographies.org/api/v1/context.json", + "uuid": "54449ba8-e705-413a-8109-dffbddffcdd4", + "slug": "adgateville-baptist", + "slugs": ["adgateville-baptist"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "place", + "model_id": "20", + "model_name": "Churches", + "name": "Adgateville Baptist", + "visibility": "published", + "date_modified": "2026-04-21T20:43:14Z", + "identifiers": [], + "geo": { + "point": { + "lat": 33.2223937, + "lon": -83.67214969999999 + } + }, + "administrative_area": [ + { + "name": "Jasper", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ], + "legacy_id": { + "label": "Legacy ID", + "value": 1647 + }, + "slug_2": { + "label": "Slug", + "value": "adgateville-baptist" + }, + "short_description": "Adgateville Baptist began as Hebron Primitive Baptist in 1812, on two acres of land donated by William Cleveland. At that time, Jasper County was still in its infancy, having been created in 1807 and renamed Jasper in 1812 in honor of Revolutionary War hero Sergeant William Jasper. Much of the church’s earliest history has been…", + "wordpress_link": { + "label": "WordPress Link", + "value": "https://www.hrcga.org/church/adgateville-baptist/" + }, + "address": "37 Clay Tillman Rd, Hillsboro, GA 31038", + "description": "

Adgateville Baptist began as Hebron Primitive Baptist in 1812, on two acres of land donated by William Cleveland. At that time, Jasper County was still in its infancy, having been created in 1807 and renamed Jasper in 1812 in honor of Revolutionary War hero Sergeant William Jasper.

Much of the church’s earliest history has been lost, but we know it prospered during the 19th century, serving families in the surrounding community as well as those in nearby Hillsboro, a once-thriving town just down the road. Like many rural Georgia congregations, Hebron provided not only a place of worship but also a vital community center for spiritual life, social gatherings, and moral order.

In 1915, the congregation reorganized and took on a new name—Adgateville Baptist—reflecting the influence of the Adgate family, prominent agricultural leaders in the late 1800s. H. L. Adgate, patriarch of the family, became the namesake of the small settlement that grew around the church. That same year, the present sanctuary was constructed, replacing earlier structures that had served the congregation for more than a century.

Today, the building stands as a modest but important reminder of the area’s history. Its survival tells the story not only of a church but of the shifting communities of Jasper County and the resilience of rural congregations that continued to adapt through war, agricultural changes, and population shifts. Though small in size, Adgateville Baptist connects us to more than 200 years of Georgia’s religious and community history.

", + "year": { + "label": "Year", + "value": 1812 + }, + "denomination": "Baptist", + "denomination_facet": "Baptist", + "types": ["Baptist Church"], + "contained_in": { + "uuid": "0a269aa1-b886-4a33-aaf0-bc553bf61871", + "slug": "jasper-county", + "slugs": ["jasper-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Jasper County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:17Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4202638" + } + ], + "geo": { + "point": { + "lat": 33.31643, + "lon": -83.68809 + } + }, + "administrative_area": [ + { + "name": "Jasper", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ] + }, + "contained_in_place": { + "uuid": "0a269aa1-b886-4a33-aaf0-bc553bf61871", + "slug": "jasper-county", + "slugs": ["jasper-county"], + "project_id": "7", + "project": "administrative-districts", + "model_type": "place", + "model_id": "63", + "model_name": "Counties", + "name": "Jasper County", + "visibility": "published", + "date_modified": "2026-08-27T17:39:17Z", + "identifiers": [ + { + "authority": "geonames", + "identifier": "https://www.geonames.org/4202638" + } + ], + "geo": { + "point": { + "lat": 33.31643, + "lon": -83.68809 + } + }, + "administrative_area": [ + { + "name": "Jasper", + "level": "ADM2", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "Georgia", + "level": "ADM1", + "geonames_id": null, + "geonames_url": null + }, + { + "name": "US", + "level": "PCLI", + "geonames_id": null, + "geonames_url": null + } + ] + }, + "media": [ + { + "uuid": "bb4df2e7-9868-45f1-a831-9aa355fdf987", + "slug": "adgateville-baptist-3d-tour", + "slugs": ["adgateville-baptist-3d-tour"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Adgateville Baptist - 3D Tour", + "visibility": "published", + "date_modified": "2026-08-28T17:01:28Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/b0edaea6-4967-4564-bc50-4c621930a5cc/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/b0edaea6-4967-4564-bc50-4c621930a5cc/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/b0edaea6-4967-4564-bc50-4c621930a5cc/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/b0edaea6-4967-4564-bc50-4c621930a5cc/manifest", + "embed_url": "https://my.matterport.com/show/?m=eTtau15mrcK", + "media_type": "model3d", + "order": null + }, + { + "uuid": "8bba2424-33b4-45e0-bda9-ffea4bdc9e49", + "slug": "1adgateville-baptist-2-topaz-upscale-4x", + "slugs": ["1adgateville-baptist-2-topaz-upscale-4x"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "1Adgateville-Baptist-2-topaz-upscale-4x", + "visibility": "published", + "date_modified": "2026-04-20T19:33:34Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/db444e9f-3779-406c-804d-781bddecd581/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/db444e9f-3779-406c-804d-781bddecd581/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/db444e9f-3779-406c-804d-781bddecd581/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/db444e9f-3779-406c-804d-781bddecd581/manifest", + "order": null + }, + { + "uuid": "4a28160c-6f8b-4712-ac87-d86f4ab199d0", + "slug": "jasper-wall-photo-dsc05635", + "slugs": ["jasper-wall-photo-dsc05635"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jasper-Wall-photo-DSC05635", + "visibility": "published", + "date_modified": "2025-12-11T23:26:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/7c9c47bf-eb09-4816-b116-86b6d871d1b9/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/7c9c47bf-eb09-4816-b116-86b6d871d1b9/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/7c9c47bf-eb09-4816-b116-86b6d871d1b9/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/7c9c47bf-eb09-4816-b116-86b6d871d1b9/manifest", + "order": null + }, + { + "uuid": "91dc28a7-eb58-4137-bcb4-05ea9bb8b6db", + "slug": "jasper-holga-abc-1", + "slugs": ["jasper-holga-abc-1"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jasper-Holga-ABC-1", + "visibility": "published", + "date_modified": "2025-12-11T23:26:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/bf88c78e-9b08-4818-829a-2fca247db973/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/bf88c78e-9b08-4818-829a-2fca247db973/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/bf88c78e-9b08-4818-829a-2fca247db973/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/bf88c78e-9b08-4818-829a-2fca247db973/manifest", + "order": null + }, + { + "uuid": "b2ebf488-471b-4cff-b3cc-13d2041bcc15", + "slug": "jasper-1adgateville-baptist-4", + "slugs": ["jasper-1adgateville-baptist-4"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jasper-1Adgateville-Baptist-4", + "visibility": "published", + "date_modified": "2025-12-12T23:02:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/af9fdde0-be05-451a-ba59-b435654db61e/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/af9fdde0-be05-451a-ba59-b435654db61e/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/af9fdde0-be05-451a-ba59-b435654db61e/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/af9fdde0-be05-451a-ba59-b435654db61e/manifest", + "caption": "

The Hebron Primitive Baptist church was once located across the road by the cemetery. The present building was constructed about 100 years ago. Church records have all the pastors (21 of them) recorded back to the original founding in 1812.

", + "order": null + }, + { + "uuid": "f84e750c-29a1-4093-ba2e-a3e13a747c17", + "slug": "jasper-1adgateville-baptist-3", + "slugs": ["jasper-1adgateville-baptist-3"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jasper-1Adgateville-Baptist-3", + "visibility": "published", + "date_modified": "2025-12-11T23:26:32Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/29bfd7b2-7e6d-4ad8-be0f-b2491b7d1186/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/29bfd7b2-7e6d-4ad8-be0f-b2491b7d1186/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/29bfd7b2-7e6d-4ad8-be0f-b2491b7d1186/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/29bfd7b2-7e6d-4ad8-be0f-b2491b7d1186/manifest", + "order": null + }, + { + "uuid": "0f23ab4e-67d5-43db-aada-3b72e2028c0b", + "slug": "jasper-1adgateville-baptist-1", + "slugs": ["jasper-1adgateville-baptist-1"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "Jasper-1Adgateville-Baptist-1", + "visibility": "published", + "date_modified": "2025-12-12T23:02:30Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/c1b38031-769e-489c-bde2-23b2428d2192/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/c1b38031-769e-489c-bde2-23b2428d2192/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/c1b38031-769e-489c-bde2-23b2428d2192/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/c1b38031-769e-489c-bde2-23b2428d2192/manifest", + "caption": "

The village schoolhouse is located close to the church and served the community for many years. It is now used as a community center and is well maintained.

", + "order": null + }, + { + "uuid": "efd64045-2d04-4726-985d-b2924a4ec094", + "slug": "1adgateville-baptist-2", + "slugs": ["1adgateville-baptist-2"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "1Adgateville-Baptist-2", + "visibility": "published", + "date_modified": "2025-12-12T23:02:31Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/9172763c-7457-4aec-8fde-80d40f712113/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/9172763c-7457-4aec-8fde-80d40f712113/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/9172763c-7457-4aec-8fde-80d40f712113/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/9172763c-7457-4aec-8fde-80d40f712113/manifest", + "alt_text": "Adgateville Baptist", + "order": null + }, + { + "uuid": "5b671cca-4fd5-4171-8d38-062a71d3720d", + "slug": "1adgateville-baptist-2-topaz-upscale-4x-scaled", + "slugs": ["1adgateville-baptist-2-topaz-upscale-4x-scaled"], + "project_id": "2", + "project": "historic-rural-churches-of-georgia", + "model_type": "media", + "model_id": "22", + "model_name": "Media", + "name": "1Adgateville-Baptist-2-topaz-upscale-4x-scaled", + "visibility": "published", + "date_modified": "2025-12-18T17:51:09Z", + "identifiers": [], + "preview": "https://iiif-cloud.ecds.io/public/resources/cb7a71d2-6b00-454d-b0a6-7984e04ec22b/preview", + "thumbnail": "https://iiif-cloud.ecds.io/public/resources/cb7a71d2-6b00-454d-b0a6-7984e04ec22b/thumbnail", + "content_url": "https://iiif-cloud.ecds.io/public/resources/cb7a71d2-6b00-454d-b0a6-7984e04ec22b/iiif", + "manifest_url": "https://iiif-cloud.ecds.io/public/resources/cb7a71d2-6b00-454d-b0a6-7984e04ec22b/manifest", + "order": null + } + ], + "order": 40 + } + ], + "id": "13" +} diff --git a/spec/mailers/access_request_mailer_spec.rb b/spec/mailers/access_request_mailer_spec.rb new file mode 100644 index 00000000..98d7d215 --- /dev/null +++ b/spec/mailers/access_request_mailer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(AccessRequestMailer, type: :mailer) do + describe 'access_request_email' do + let(:tour_set) { create(:tour_set) } + let(:user) { create(:user, super: false) } + let(:access_request) { create(:access_request, user:, tour_set:) } + let(:super_admin) { create(:user, super: true) } + let(:admin) { create(:user, super: false, tour_sets: [tour_set]) } + let(:mail) { described_class.with(access_request:).access_request_email } + + it 'renders the headers' do + expect(super_admin.email).not_to(be_nil) + expect(admin.email).not_to(be_nil) + expect(mail.subject).to(eq("OpenTour Access Request #{tour_set.name}")) + expect(mail.to).to(include(super_admin.email)) + expect(mail.to).to(include(admin.email)) + expect(mail.from).to(eq(['noreply@opentour.site'])) + end + + it 'sends the email' do + expect { mail.deliver_now }.to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end + + describe 'access_request_tour_email' do + let(:tour_set) { create(:tour_set) } + let(:user) { create(:user, super: false) } + let(:access_request) { create(:access_request, user:, tour_set:) } + let!(:super_admin) { create(:user, super: true) } + let!(:admin) { create(:user, super: false, tour_sets: [tour_set]) } + let(:mail) { described_class.with(access_request:).access_request_tour_email } + + it 'renders the headers' do + expect(mail.subject).to(eq("OpenTour Access Request for #{tour_set.name}")) + expect(mail.to).to(include(super_admin.email)) + expect(mail.to).to(include(admin.email)) + expect(mail.from).to(eq(['noreply@opentour.site'])) + end + + it 'sends the email' do + expect { mail.deliver_now }.to(change { ActionMailer::Base.deliveries.count }.by(1)) + end + end +end diff --git a/spec/mailers/previews/access_request_mailer_preview.rb b/spec/mailers/previews/access_request_mailer_preview.rb new file mode 100644 index 00000000..dc47e451 --- /dev/null +++ b/spec/mailers/previews/access_request_mailer_preview.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Preview all emails at http://localhost:3000/rails/mailers/access_request_mailer +class AccessRequestMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/access_request_mailer/access_request_email + delegate :access_request_email, to: :AccessRequestMailer +end diff --git a/spec/models/access_request_spec.rb b/spec/models/access_request_spec.rb new file mode 100644 index 00000000..02a733ea --- /dev/null +++ b/spec/models/access_request_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(AccessRequest, type: :model) do + let(:tour_set) { create(:tour_set) } + let(:user) { create(:user, super: false, tour_sets: []) } + let(:access_request) { create(:access_request, tour_set:, user:) } + + describe 'validations' do + it 'is invalid when a user already has a pending request for the same tour set' do + create(:access_request, tour_set:, user:) + duplicate = build(:access_request, tour_set:, user:) + expect(duplicate).not_to(be_valid) + end + end + + describe '#requested_tours' do + it 'returns empty array when no tour ids are set' do + expect(access_request.requested_tours).to(be_empty) + end + + it 'returns the tours matching stored tour_ids' do + Apartment::Tenant.switch!(tour_set.subdir) + tours = create_list(:tour, 2) + request_with_tours = create(:access_request, tour_set:, tour_ids: [tours.first.id, tours.last.id]) + expect(request_with_tours.requested_tours).to(include(tours.first)) + expect(request_with_tours.requested_tours).to(include(tours.last)) + end + end + + describe '#search_data' do + it 'includes expected fields' do + data = access_request.search_data + expect(data[:id]).to(eq(access_request.id)) + expect(data[:email]).to(eq(user.email)) + expect(data[:site]).to(eq(tour_set.name)) + expect(data[:tours]).to(eq([])) + end + end + + describe 'approval via #update' do + it 'adds the tour set to the user and destroys the request when approved for whole site' do + access_request + expect(user.tour_sets).not_to(include(tour_set)) + access_request.update(approved: true) + user.reload + expect(user.tour_sets).to(include(tour_set)) + expect(described_class.find_by(id: access_request.id)).to(be_nil) + end + + it 'adds specific tours to the user and destroys the request when approved for tours' do + Apartment::Tenant.switch!(tour_set.subdir) + tours = create_list(:tour, 2) + request_with_tours = create(:access_request, tour_set:, user:, tour_ids: [tours.first.id, tours.last.id]) + request_with_tours.update(approved: true) + user.reload + expect(user.tours).to(include(tours.first)) + expect(user.tours).to(include(tours.last)) + expect(user.tour_sets).not_to(include(tour_set)) + expect(described_class.find_by(id: request_with_tours.id)).to(be_nil) + end + + it 'destroys the request without granting access when denied' do + access_request + access_request.update(approved: false) + user.reload + expect(user.tour_sets).not_to(include(tour_set)) + expect(described_class.find_by(id: access_request.id)).to(be_nil) + end + end +end diff --git a/spec/models/flat_page_spec.rb b/spec/models/flat_page_spec.rb new file mode 100644 index 00000000..5e9c81f2 --- /dev/null +++ b/spec/models/flat_page_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(FlatPage, type: :model) do + it { is_expected.to(have_many(:tour_flat_pages)) } + it { is_expected.to(have_many(:tours)) } + it { is_expected.to(validate_presence_of(:title)) } + + describe '#slug' do + it 'returns parameterized title' do + flat_page = build(:flat_page, title: 'Hello World') + expect(flat_page.slug).to(eq('hello-world')) + end + end + + describe '#orphaned' do + it 'is true when not associated with any tour' do + flat_page = create(:flat_page) + expect(flat_page.orphaned).to(be(true)) + end + + it 'is false when associated with a tour' do + tour = create(:tour) + flat_page = create(:flat_page, tours: [tour]) + expect(flat_page.orphaned).to(be(false)) + end + end + + describe '#published' do + it 'is false when not associated with any tour' do + flat_page = create(:flat_page) + expect(flat_page.published).to(be(false)) + end + + it 'is false when associated only with unpublished tours' do + tour = create(:tour, published: false) + flat_page = create(:flat_page, tours: [tour]) + expect(flat_page.published).to(be(false)) + end + + it 'is true when associated with a published tour' do + tour = create(:tour, published: true) + flat_page = create(:flat_page, tours: [tour]) + expect(flat_page.published).to(be(true)) + end + end + + describe '#search_data' do + it 'includes expected keys' do + flat_page = create(:flat_page) + data = flat_page.search_data + expect(data).to(include(:id, :title, :slug, :body, :orphaned, :tour_count)) + end + end +end diff --git a/spec/models/map_icon_spec.rb b/spec/models/map_icon_spec.rb index 5ab515b1..23919377 100644 --- a/spec/models/map_icon_spec.rb +++ b/spec/models/map_icon_spec.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe MapIcon, type: :model do +RSpec.describe(MapIcon, type: :model) do context 'size error' do it 'fails validation when image it too big' do - icon = MapIcon.create( + icon = described_class.create( base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt')), - filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: '') + filename: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), ) - expect(icon.errors.full_messages).to include 'Icons should be no bigger that 80 by 80 pixels' + expect(icon.errors.full_messages).to(include('Icons should be no bigger that 80 by 80 pixels')) end end end diff --git a/spec/models/map_overlay_spec.rb b/spec/models/map_overlay_spec.rb index bc869cd2..a76f7094 100644 --- a/spec/models/map_overlay_spec.rb +++ b/spec/models/map_overlay_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe MapOverlay, type: :model do +RSpec.describe(MapOverlay, type: :model) do it 'has nil values for south, east, north, and west' do mo = create(:map_overlay, tour: create(:tour, stops: [])) - expect([mo.south, mo.east, mo.north, mo.west]).to all(be nil) + expect([mo.south, mo.east, mo.north, mo.west]).to(all(be_nil)) end it 'has values for south, east, north, and west based on tour stops' do tour = create(:tour, stops: create_list(:stop, 3)) mo = create(:map_overlay, tour: tour) - expect([mo.south, mo.east, mo.north, mo.west]).to all(be_a String) + expect([mo.south, mo.east, mo.north, mo.west]).to(all(be_a(String))) end end diff --git a/spec/models/medium_base_record_spec.rb b/spec/models/medium_base_record_spec.rb new file mode 100644 index 00000000..539d4566 --- /dev/null +++ b/spec/models/medium_base_record_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(MediumBaseRecord, type: :model) do + describe 'JPEG 2000 rejection' do + it 'is invalid when base64 content type is JPEG 2000' do + jp2_base64 = "data:image/jp2;base64,#{Base64.encode64('fake jp2 data')}" + medium = build(:medium, base_sixty_four: jp2_base64, filename: 'test.jp2') + expect(medium).not_to(be_valid) + expect(medium.errors[:base]).to(include(match(/JPEG 2000/))) + end + + it 'is valid for standard jpeg content' do + medium = build(:medium) + expect(medium).to(be_valid) + end + end +end diff --git a/spec/models/medium_spec.rb b/spec/models/medium_spec.rb index e6df5afb..0ee39bca 100644 --- a/spec/models/medium_spec.rb +++ b/spec/models/medium_spec.rb @@ -2,97 +2,207 @@ require 'rails_helper' -RSpec.describe Medium, type: :model do - it { should have_many(:stop_media) } - it { should have_many(:stops) } +RSpec.describe(Medium, type: :model) do + it { is_expected.to(have_many(:stop_media)) } + it { is_expected.to(have_many(:stops)) } - context 'video' do + context 'when embed_id is present' do it 'gets image from youtube and sets embed' do - medium = create(:medium, video: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') - expect(medium.embed).to eq("//www.youtube.com/embed/#{medium.video}") - expect(medium.file.attached?).to be true - end - - it 'gets image from youtube when downloaded image is a StrinIO object and sets embed' do - file = File.open(Rails.root + 'spec/factories/images/atl.png') - string_io = StringIO.new(file.read) - base64 = VideoProps.encode_image(string_io) - medium = create(:medium, base_sixty_four: base64) - expect(medium.file.attached?).to be true + medium = create(:medium, video: 'F9ULbmCvmxY', embed_id: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') + expect(medium.embed).to(eq("//www.youtube.com/embed/#{medium.embed_id}")) + expect(medium.file.attached?).to(be(true)) end it 'gets nothing when YouTube video is not found' do - medium = create(:medium, video: 'CvmxYF9ULbm', base_sixty_four: nil, video_provider: 'youtube') - expect(medium.embed).to be nil - expect(medium.provider).to be nil - expect(medium.file.attached?).to be false + medium = create(:medium, video: 'CvmxYF9ULbm', embed_id: 'CvmxYF9ULbm', base_sixty_four: nil, video_provider: 'youtube') + expect(medium.embed).to(be_nil) + expect(medium.provider).to(be_nil) + expect(medium.file.attached?).to(be(false)) end it 'gets image from vimeo and sets embed' do - medium = create(:medium, video: '310645255', base_sixty_four: nil, video_provider: 'vimeo') - expect(medium.embed).to eq("//player.vimeo.com/video/#{medium.video}") - expect(medium.file.attached?).to be true + medium = create(:medium, video: '310645255', embed_id: '310645255', base_sixty_four: nil, video_provider: 'vimeo') + expect(medium.embed).to(eq("//player.vimeo.com/video/#{medium.embed_id}")) + expect(medium.file.attached?).to(be(true)) end it 'gets image from soundcloud and sets embed' do - iframe = '
FiendBassy · Boca Raton (with A$AP Ferg)
' - medium = create(:medium, video: iframe, base_sixty_four: nil, video_provider: 'soundcloud') - expect(medium.embed).to eq("//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{medium.video}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false") - expect(medium.file.attached?).to be true - expect(medium.title).to eq('FiendBassy: Boca Raton (with A$AP Ferg)') + medium = create(:medium, video: '457871163', embed_id: '457871163', base_sixty_four: nil, video_provider: 'soundcloud') + expect(medium.embed).to(eq('//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/457871163&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false')) + expect(medium.file.attached?).to(be(true)) + expect(medium.title).to(eq('A SoundCloud Track')) + end + + it 'gets title and embed from sketchfab model id' do + medium = create(:medium, video: '4b570878a3cc4ca786af824a03ada414', embed_id: '4b570878a3cc4ca786af824a03ada414', base_sixty_four: nil, video_provider: 'sketchfab') + expect(medium.title).to(eq('A Sketchfab Model')) + expect(medium.embed).to(eq('//sketchfab.com/models/4b570878a3cc4ca786af824a03ada414/embed')) + expect(medium.filename).to(eq('fd4348dc5c7f48eea5c0d0b8ef376f8b.jpeg')) + expect(medium.file.attached?).to(be(true)) + end + + it 'resolves unknown embed' do + medium = create(:medium, video: '//3d-api.si.edu/voyager/3d_package:a1651b35', embed_id: '//3d-api.si.edu/voyager/3d_package:a1651b35', base_sixty_four: nil, video_provider: 'unknown') + expect(medium.embed).to(eq('//3d-api.si.edu/voyager/3d_package:a1651b35')) + expect(medium.title).to(eq('A Model')) + expect(medium.embed_id).to(eq('//3d-api.si.edu/voyager/3d_package:a1651b35')) + expect(medium.file.attached?).to(be(true)) end - it 'gets default image from when no image found for soundcloud and sets embed' do - iframe = '
Emory Center for Digital Scholarship · Subsatellite Launch
' - medium = create(:medium, video: iframe, base_sixty_four: nil, video_provider: 'soundcloud') - expect(medium.embed).to eq("//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/#{medium.video}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false") - expect(medium.file.attached?).to be true - expect(medium.title).to eq('Emory Center for Digital Scholarship') + it 'gets embed from morphosource manifest uuid' do + uuid = '81902b5f-f5d6-4c59-9fcb-84cf7489819f' + medium = create(:medium, video: uuid, embed_id: uuid, base_sixty_four: nil, video_provider: 'morphosource') + expect(medium.embed).to(eq("//www.morphosource.org/uv.html#?manifest=/manifests/#{uuid}&c=0&m=0&cv=0")) + expect(medium.title).to(eq('Fragment [Mesh] [StrLight]')) + expect(medium.caption).to(eq('Italic terra sigillata fragment')) + expect(medium.file.attached?).to(be(true)) + end + + it 'gets title and embed from matterport link' do + medium = create(:medium, video: 'matterport_id', embed_id: 'matterport_id', base_sixty_four: nil, video_provider: 'matterport') + expect(medium.title).to(eq('A Matterport Model')) + expect(medium.embed).to(eq('//my.matterport.com/show/?m=matterport_id')) + expect(medium.filename).to(eq('matterport_id.jpg')) + expect(medium.embed_id).to(eq('matterport_id')) + expect(medium.file.attached?).to(be(true)) + end + + it 'gets title and embed from sketchfab model number' do + medium = create( + :medium, + base_sixty_four: nil, + video_provider: 'sketchfab', + filename: '4b570878a3cc4ca786af824a03ada414.jpg', + video: '4b570878a3cc4ca786af824a03ada414', + embed_id: '4b570878a3cc4ca786af824a03ada414', + ) + expect(medium.title).to(eq('A Sketchfab Model')) + expect(medium.embed).to(eq('//sketchfab.com/models/4b570878a3cc4ca786af824a03ada414/embed')) + expect(medium.filename).to(eq('fd4348dc5c7f48eea5c0d0b8ef376f8b.jpeg')) + expect(medium.file.attached?).to(be(true)) + expect(medium.search_data[:provider]).to(eq('sketchfab')) + end + + it 'gets default image from when no image found for soundcloud and sets embed full' do + stub_request(:get, %r{https://soundcloud\.com/oembed}) + .to_return(status: 200, body: '{"title": "Subsatellite Launch", "thumbnail_url":null}', headers: { 'Content-Type' => 'application/json' }) + medium = create(:medium, video: '431162745', embed_id: '431162745', base_sixty_four: nil, video_provider: 'soundcloud') + expect(medium.embed).to(eq('//w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/431162745&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false')) + expect(medium.file.attached?).to(be(true)) + expect(medium.title).to(eq('Subsatellite Launch')) end it 'replaces file for video' do - medium = create(:medium, video: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') + medium = create(:medium, video: 'F9ULbmCvmxY', embed_id: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') original_checksum = medium.file.blob.checksum - expect(original_checksum).to eq(Digest::MD5.file(Rails.root.join('spec/factories/images/0.jpg')).base64digest) + expect(original_checksum).to(eq(Digest::MD5.file(Rails.root.join('spec/factories/images/0.jpg')).base64digest)) medium.update(base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt'))) - expect(medium.file.blob.checksum).not_to eq(original_checksum) - expect(medium.file.blob.checksum).to eq(Digest::MD5.file(Rails.root.join('spec/factories/images/atl.png')).base64digest) + expect(medium.file.blob.checksum).not_to(eq(original_checksum)) + expect(medium.file.blob.checksum).to(eq(Digest::MD5.file(Rails.root.join('spec/factories/images/atl.png')).base64digest)) + expect(medium.search_data[:provider]).to(eq('youtube')) end it 'updates title and caption of video' do - medium = create(:medium, video: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') - original_checksum = medium.file.blob.checksum - expect(medium.title).to include('Goodie') - expect(medium.caption).to include('Goodie') + medium = create(:medium, video: 'F9ULbmCvmxY', embed_id: 'F9ULbmCvmxY', base_sixty_four: nil, video_provider: 'youtube') + expect(medium.title).to(include('Goodie')) + expect(medium.caption).to(include('Goodie')) medium.update(title: 'Outkast') medium.update(caption: 'GOATs') - expect(medium.title).not_to include('Goodie') - expect(medium.caption).not_to include('Goodie') - expect(medium.title).to include('Outkast') - expect(medium.caption).to include('GOATs') + expect(medium.title).not_to(include('Goodie')) + expect(medium.caption).not_to(include('Goodie')) + expect(medium.title).to(include('Outkast')) + expect(medium.caption).to(include('GOATs')) # medium.update(base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt'))) # expect(medium.file.blob.checksum).not_to eq(original_checksum) # expect(medium.file.blob.checksum).to eq(Digest::MD5.file(Rails.root.join('spec/factories/images/atl.png')).base64digest) end - it 'skips video_props when provider in nil' do - medium = create(:medium, video: 'ACod3', base_sixty_four: nil) - expect(medium.file.attached?).to be false + it 'skips embed_props when provider is nil' do + medium = create(:medium, video: 'ACod3', embed_id: 'ACod3', base_sixty_four: nil) + expect(medium.file.attached?).to(be(false)) + end + end + + describe '#orphaned' do + it 'is true when not associated with any tour or stop' do + medium = create(:medium) + expect(medium.orphaned).to(be(true)) + end + + it 'is false when associated with a stop' do + stop = create(:stop) + medium = create(:medium, stops: [stop]) + expect(medium.orphaned).to(be(false)) + end + + it 'is false when associated with a tour' do + tour = create(:tour) + medium = create(:medium, tours: [tour]) + expect(medium.orphaned).to(be(false)) end end + describe '#published' do + it 'is false when orphaned' do + medium = create(:medium) + expect(medium.published).to(be(false)) + end + + it 'is true when associated with a published tour' do + tour = create(:tour, published: true) + medium = create(:medium, tours: [tour]) + expect(medium.published).to(be(true)) + end + + it 'is true when associated with a stop on a published tour' do + tour = create(:tour, published: true) + stop = create(:stop, tours: [tour]) + medium = create(:medium, stops: [stop]) + expect(medium.published).to(be(true)) + end + end + + describe '#files' do + it 'returns same url for all variants when file is a gif' do + medium = create( + :medium, + filename: Faker::File.file_name(dir: '', ext: 'gif', directory_separator: ''), + base_sixty_four: File.read(Rails.root.join('spec/factories/images/gif_base64.txt')), + video: nil, + ) + files = medium.files + expect(files[:mobile]).to(include('/active_storage/disk/')) + expect(files[:desktop]).to(include('/active_storage/disk/')) + expect(files[:lqip]).to(include('/active_storage/disk/')) + end + + it 'returns nil when no file attached' do + medium = build(:medium, base_sixty_four: nil, video: nil, embed_id: nil) + expect(medium.files).to(be_nil) + end + end + + context 'when creating images' do + it 'creates a medium record with attachment' do + poo = nil + File.open(Rails.root.join('spec/factories/images/atl_base64.txt'), 'r') do |b64| + poo = b64.read + end + described_class.create(base_sixty_four: poo, filename: 'atl.png') + medium = described_class.find_by(filename: 'atl.png') + expect(medium.filename).to(eq('atl.png')) + end - context 'createing images' do it 'sets widths for variants' do medium = create( :medium, filename: Faker::File.file_name(dir: '', ext: 'jpg', directory_separator: ''), - base_sixty_four: File.read(Rails.root.join('spec/factories/images/atl_base64.txt')), - video: nil + video: nil, ) - + medium = described_class.find(medium.id) medium.save - expect(medium.lqip_width).not_to be nil + expect(medium).not_to(be_nil) + # expect(medium.lqip_width).not_to be nil end it 'saves a gif' do @@ -100,10 +210,10 @@ :medium, filename: Faker::File.file_name(dir: '', ext: 'gif', directory_separator: ''), base_sixty_four: File.read(Rails.root.join('spec/factories/images/gif_base64.txt')), - video: nil + video: nil, ) - expect(medium.file.blob.checksum).to eq('4fqkSXu+qjQuQWCms8xBBQ==') + expect(medium.file.blob.checksum).to(eq('4fqkSXu+qjQuQWCms8xBBQ==')) end end end diff --git a/spec/models/mode_spec.rb b/spec/models/mode_spec.rb index 6c8c65c3..ffd331d1 100644 --- a/spec/models/mode_spec.rb +++ b/spec/models/mode_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe Mode, type: :model do - it { should have_many(:tour_modes) } - it { should have_many(:tours) } +RSpec.describe(Mode, type: :model) do + it { is_expected.to(have_many(:tour_modes)) } + it { is_expected.to(have_many(:tours)) } end diff --git a/spec/models/slug_spec.rb b/spec/models/slug_spec.rb index 95b75790..fe6a9f2f 100644 --- a/spec/models/slug_spec.rb +++ b/spec/models/slug_spec.rb @@ -1,5 +1,19 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe Slug, type: :model do - it { should belong_to(:tour) } +RSpec.describe(Slug, type: :model) do + it { is_expected.to(belong_to(:tour)) } + + it 'gets reassigned' do + title = Faker::Movies::HitchhikersGuideToTheGalaxy.location + tour1 = create(:tour, title:) + slug = described_class.find_by(slug: title.parameterize_intl) + expect(slug.tour).to(eq(tour1)) + tour1.update(title: 'changed') + expect(tour1.slugs).to(include(slug)) + tour2 = create(:tour, title:) + expect(tour1.slugs).not_to(include(slug)) + expect(tour2.slugs).to(include(slug)) + end end diff --git a/spec/models/stop_medium_spec.rb b/spec/models/stop_medium_spec.rb index 3a9a3b75..6ecb7589 100644 --- a/spec/models/stop_medium_spec.rb +++ b/spec/models/stop_medium_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe StopMedium, type: :model do - it { should belong_to(:stop) } - it { should belong_to(:medium) } +RSpec.describe(StopMedium, type: :model) do + it { is_expected.to(belong_to(:stop)) } + it { is_expected.to(belong_to(:medium)) } end diff --git a/spec/models/stop_slug_spec.rb b/spec/models/stop_slug_spec.rb index 475ecc26..a35c0032 100644 --- a/spec/models/stop_slug_spec.rb +++ b/spec/models/stop_slug_spec.rb @@ -1,5 +1,19 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe StopSlug, type: :model do - it { should belong_to(:stop) } +RSpec.describe(StopSlug, type: :model) do + it { is_expected.to(belong_to(:stop)) } + + it 'gets reassigned' do + title = Faker::Movies::HitchhikersGuideToTheGalaxy.location + stop1 = create(:stop, title:) + slug = described_class.find_by(slug: title.parameterize_intl) + expect(slug.stop).to(eq(stop1)) + stop1.update(title: 'changed') + expect(stop1.stop_slugs).to(include(slug)) + stop2 = create(:stop, title:) + expect(stop1.stop_slugs).not_to(include(slug)) + expect(stop2.stop_slugs).to(include(slug)) + end end diff --git a/spec/models/stop_spec.rb b/spec/models/stop_spec.rb index fc93751f..ed97f89f 100644 --- a/spec/models/stop_spec.rb +++ b/spec/models/stop_spec.rb @@ -2,23 +2,114 @@ require 'rails_helper' -RSpec.describe Stop, type: :model do - it { should have_many(:tours) } - it { should have_many(:tour_stops) } +RSpec.describe(Stop, type: :model) do + it { is_expected.to(have_many(:tours)) } + it { is_expected.to(have_many(:tour_stops)) } # it { should validate_presence_of(:title) } - it { should have_many(:stop_media) } - it { should have_many(:media) } + it { is_expected.to(have_many(:stop_media)) } + it { is_expected.to(have_many(:media)) } it 'has specified splash' do stop = create(:stop, medium: create(:medium)) - expect(stop.splash).not_to be nil + expect(stop.splash).not_to(be_nil) end it 'has uses the first medium for splash' do stop = create(:stop) create_list(:medium, 3) - Medium.all.each { |medium| stop.media << medium } - expect(stop.splash).not_to be nil - expect(stop.splash[:title]).to eq(StopMedium.find_by(position: 1).medium.title) + Medium.all.find_each { |medium| stop.media << medium } + expect(stop.splash).not_to(be_nil) + expect(stop.splash[:title]).to(eq(StopMedium.find_by(position: 1).medium.title)) + end + + it 'allows two stops with the same title' do + title = Faker::Movies::HitchhikersGuideToTheGalaxy.location + create(:stop, title:) + expect(build(:stop, title:)).to(be_valid) + end + + describe '#orphaned' do + it 'is true when not associated with any tour' do + stop = create(:stop) + expect(stop.orphaned).to(be(true)) + end + + it 'is false when associated with a tour' do + tour = create(:tour) + stop = create(:stop, tours: [tour]) + expect(stop.orphaned).to(be(false)) + end + end + + describe '#published' do + it 'is false when stop has no tours' do + stop = create(:stop) + expect(stop.published).to(be(false)) + end + + it 'is false when associated only with unpublished tours' do + tour = create(:tour, published: false) + stop = create(:stop, tours: [tour]) + expect(stop.published).to(be(false)) + end + + it 'is true when associated with a published tour' do + tour = create(:tour, published: true) + stop = create(:stop, tours: [tour]) + expect(stop.published).to(be(true)) + end + end + + describe '#should_index?' do + it 'is false when orphaned' do + stop = create(:stop) + expect(stop.should_index?).to(be(false)) + end + + it 'is true when associated with a tour' do + stop = create(:stop, tours: [create(:tour)]) + expect(stop.should_index?).to(be(true)) + end + end + + describe '#slug' do + it 'returns parameterized title' do + stop = build(:stop, title: 'Hello World') + expect(stop.slug).to(eq('hello-world')) + end + end + + describe '#search_data' do + it 'includes expected keys' do + stop = create(:stop) + data = stop.search_data + expect(data).to(include(:id, :title, :slug, :description, :lat, :lng, :published, :orphaned)) + end + end + + context 'when html field contains _blank link with no rel or aria-label' do + it 'adds rel="noopener noreferrer" to link.' do + description = 'click here' + stop = create(:stop, description:) + expect(stop.description).to(include('rel="noopener noreferrer"')) + end + + it 'adds aria-label="here (opens in a new tab)" to link.' do + description = 'click here' + stop = create(:stop, description:) + expect(stop.description).to(include('aria-label="here (opens in a new tab)"')) + end + + it 'adds rel="noopener noreferrer" to link in direction notes.' do + direction_notes = 'click here' + stop = create(:stop, direction_notes:) + expect(stop.direction_notes).to(include('rel="noopener noreferrer"')) + end + + it 'adds aria-label="here (opens in a new tab)" to link in direction notes.' do + direction_notes = 'click here' + stop = create(:stop, direction_notes:) + expect(stop.direction_notes).to(include('aria-label="here (opens in a new tab)"')) + end end end diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 7c667f34..3af42db6 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -2,6 +2,6 @@ require 'rails_helper' -RSpec.describe Theme, type: :model do - it { should have_many(:tours) } +RSpec.describe(Theme, type: :model) do + it { is_expected.to(have_many(:tours)) } end diff --git a/spec/models/tour_author_spec.rb b/spec/models/tour_author_spec.rb index 379098d3..bcaf6d43 100644 --- a/spec/models/tour_author_spec.rb +++ b/spec/models/tour_author_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe TourAuthor, type: :model do - it { should belong_to(:tour) } - it { should belong_to(:user) } +RSpec.describe(TourAuthor, type: :model) do + it { is_expected.to(belong_to(:tour)) } + it { is_expected.to(belong_to(:user)) } end diff --git a/spec/models/tour_mode_spec.rb b/spec/models/tour_mode_spec.rb index f23c537c..7a57f22a 100644 --- a/spec/models/tour_mode_spec.rb +++ b/spec/models/tour_mode_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe TourMode, type: :model do - it { should belong_to(:tour) } - it { should belong_to(:mode) } +RSpec.describe(TourMode, type: :model) do + it { is_expected.to(belong_to(:tour)) } + it { is_expected.to(belong_to(:mode)) } end diff --git a/spec/models/tour_set_spec.rb b/spec/models/tour_set_spec.rb index e299b5a8..651021de 100644 --- a/spec/models/tour_set_spec.rb +++ b/spec/models/tour_set_spec.rb @@ -2,34 +2,71 @@ require 'rails_helper' -RSpec.describe TourSet, type: :model do - it { should validate_presence_of(:name) } +RSpec.describe(TourSet, type: :model) do + it { is_expected.to(validate_presence_of(:name)) } it 'creates four travel modes' do tour_set = create(:tour_set) - Apartment::Tenant.switch! tour_set.subdir - expect(Mode.count).to eq(4) + Apartment::Tenant.switch!(tour_set.subdir) + expect(Mode.count).to(eq(4)) end it 'attaches logo' do tour_set = create(:tour_set) - expect(tour_set.logo.attached?).to be false + expect(tour_set.logo.attached?).to(be(false)) tour_set.update( logo_title: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), - base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt')) + base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt')), ) - expect(tour_set.logo.attached?).to be true + expect(tour_set.logo.attached?).to(be(true)) end it 'removes logo' do tour_set = create( :tour_set, logo_title: Faker::File.file_name(dir: '', ext: 'png', directory_separator: ''), - base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt')) + base_sixty_four: File.read(Rails.root.join('spec/factories/images/png_base64.txt')), ) tour_set.save - expect(tour_set.logo.attached?).to be true + expect(tour_set.logo.attached?).to(be(true)) tour_set.update(base_sixty_four: nil) - expect(tour_set.logo.attached?).to be false + expect(tour_set.logo.attached?).to(be(false)) + end + + describe 'renaming' do + it 'renames the tenant schema and records the old subdir in history' do + tour_set = create(:tour_set) + old_subdir = tour_set.subdir + + tour_set.update!(name: 'A Brand New Name') + + expect(tour_set.subdir).to(eq('a-brand-new-name')) + expect(described_class.exists?(subdir: old_subdir)).to(be(false)) + expect(tour_set.subdir_histories.pluck(:subdir)).to(include(old_subdir)) + + Apartment::Tenant.switch!(tour_set.subdir) + expect(Mode.count).to(eq(4)) + Apartment::Tenant.switch!('public') + end + + it "rejects a rename that collides with another tenant's current subdir" do + create(:tour_set, name: 'Bobs Site') + tour_set = create(:tour_set) + + tour_set.name = 'BOBS SITE' # different name, same parameterized subdir + expect(tour_set.save).to(be(false)) + expect(tour_set.errors[:subdir]).to(be_present) + end + + it 'rejects reusing a subdir that used to belong to another tenant' do + renamed_away = create(:tour_set) + old_subdir = renamed_away.subdir + renamed_away.update!(name: 'Something Else Entirely') + + tour_set = create(:tour_set) + tour_set.name = old_subdir.titleize + expect(tour_set.save).to(be(false)) + expect(tour_set.errors[:subdir]).to(be_present) + end end end diff --git a/spec/models/tour_spec.rb b/spec/models/tour_spec.rb index 590687d0..a199e2a6 100644 --- a/spec/models/tour_spec.rb +++ b/spec/models/tour_spec.rb @@ -2,103 +2,110 @@ require 'rails_helper' -RSpec.describe Tour, type: :model do +RSpec.describe(Tour, type: :model) do + subject(:tour) { described_class.new } + # it { should validate_presence_of(:title) } # it { expect(subject).to validate_presence_of :title } - it { expect(subject).to have_many(:stops) } - it { expect(subject).to have_many(:tour_stops) } - it { expect(Tour.reflect_on_association(:theme).macro).to eq(:belongs_to) } - it { expect(Tour.reflect_on_association(:mode).macro).to eq(:belongs_to) } + it { expect(tour).to(have_many(:stops)) } + it { expect(tour).to(have_many(:tour_stops)) } + it { expect(described_class.reflect_on_association(:theme).macro).to(eq(:belongs_to)) } + it { expect(described_class.reflect_on_association(:mode).macro).to(eq(:belongs_to)) } it 'gets a duration' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), published: false) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 300).join(' ')) } tour.update(published: true) tour.save - expect(tour.duration).to eq(7336) + expect(tour.duration).to(eq(15394)) end it 'gets no duration when unpublished' do tour = create(:tour, mode: Mode.find_by(title: 'TRANSIT'), stops: create_list(:stop, 5), published: false) tour.update(published: false) tour.save - expect(tour.duration).to be nil + expect(tour.duration).to(be_nil) end it 'gets duration when tour is updated to published' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), published: false) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 400).join(' ')) } tour.update(published: false) - expect(tour.duration).to be nil + expect(tour.duration).to(be_nil) tour.update(published: true) - expect(tour.saved_change_to_attribute?(:published)).to be true - expect(tour.duration).to eq(7336) + expect(tour.saved_change_to_attribute?(:published)).to(be(true)) + expect(tour.duration).to(eq(15549)) end it 'updates duration when mode changes' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), published: false) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 222).join(' ')) } tour.update(published: true) tour.save - expect(tour.duration).to eq(7336) + expect(tour.duration).to(eq(15279)) tour.mode = Mode.find_by(title: 'TRANSIT') - expect(tour.will_save_change_to_mode_id?).to be true + expect(tour.will_save_change_to_mode_id?).to(be(true)) tour.save - expect(tour.duration).to eq(6336) - expect(tour.saved_change_to_attribute?(:duration)).to be true - expect(tour.saved_change_to_attribute?(:saved_stop_order)).to be false + expect(tour.duration).to(eq(11279)) + expect(tour.saved_change_to_attribute?(:duration)).to(be(true)) + expect(tour.saved_change_to_attribute?(:saved_stop_order)).to(be(false)) end - it 'updates duration when stop order chages' do + it 'updates duration when stop order changes' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), published: false) 5.times { |i| create(:tour_stop, tour: tour, stop: create(:stop), position: i + 1) } + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 400).join(' ')) } tour.update(published: true) tour.save - expect(tour.duration).to eq(7336) - # Trick the network stub to fetch different distance matrix but doesn't presist a + expect(tour.duration).to(eq(15549)) + # Trick the network stub to fetch different distance matrix but doesn't persist a # change to the tour's mode. tour.mode.title = 'TRANSIT' tour.tour_stops.order(:position).last.update(position: 0) tour.validate # Make sure duration isn't being updated because we changed the mode or published status. - expect(tour.will_save_change_to_mode_id?).to be false - expect(tour.will_save_change_to_published?).to be false - expect(tour.will_save_change_to_saved_stop_order?).to be true + expect(tour.will_save_change_to_mode_id?).to(be(false)) + expect(tour.will_save_change_to_published?).to(be(false)) + expect(tour.will_save_change_to_saved_stop_order?).to(be(true)) tour.save - expect(tour.duration).to eq(6336) - expect(tour.saved_change_to_attribute?(:duration)).to be true + expect(tour.duration).to(eq(11549)) + expect(tour.saved_change_to_attribute?(:duration)).to(be(true)) end it 'gets no duration whin invalid request is made to Google' do tour = create(:tour, mode: Mode.find_by(title: 'DRIVING'), stops: create_list(:stop, 5), published: false) tour.update(published: true) tour.save - expect(tour.duration).to be nil + expect(tour.travel_duration).to(be_zero) end it 'gets no duration whin response has ZERO_RESULTS' do tour = create(:tour, mode: Mode.find_by(title: 'WALKING'), stops: create_list(:stop, 4), published: false) tour.update(published: true) tour.save - expect(tour.duration).to be nil + expect(tour.travel_duration).to(be_zero) end - it 'does not update the duration when other attributes are updaeted' do + it 'does not update the duration when other attributes are updated' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), published: false) + tour.stops.each { |stop| stop.update(description: Faker::Lorem.words(number: 400).join(' ')) } tour.update(published: true) tour.save - expect(tour.duration).to eq(7336) - # Trick the network stub to fetch different distance matrix but doesn't presist a + expect(tour.duration).to(eq(15549)) + # Trick the network stub to fetch different distance matrix but doesn't persist a # change to the tour's mode. In this case, it should NOT fetch. This is just to - # test that it does not actually make teh request when we don't want it to. + # test that it does not actually make the request when we don't want it to. tour.mode.title = 'TRANSIT' tour.update( title: Faker::Music::Prince.band, - description: Faker::Music::Prince.lyric + description: Faker::Music::Prince.lyric, ) - expect(tour.saved_change_to_attribute?(:title)).to be true - expect(tour.saved_change_to_attribute?(:description)).to be true - expect(tour.saved_change_to_attribute?(:duration)).to be false - expect(tour.saved_change_to_attribute?(:published)).to be false - expect(tour.saved_change_to_attribute?(:saved_stop_order)).to be false - expect(tour.duration).to eq(7336) + expect(tour.saved_change_to_attribute?(:title)).to(be(true)) + expect(tour.saved_change_to_attribute?(:description)).to(be(true)) + expect(tour.saved_change_to_attribute?(:duration)).to(be(false)) + expect(tour.saved_change_to_attribute?(:published)).to(be(false)) + expect(tour.saved_change_to_attribute?(:saved_stop_order)).to(be(false)) + expect(tour.duration).to(eq(15549)) end it 'when restricted to overlay bounds, tour bounds mirror overlay' do @@ -108,55 +115,186 @@ south: '33.73324867399921', north: '33.81498938289962', east: '-84.25453244903566', - west: '-84.37135369046021' + west: '-84.37135369046021', ) tour.update(restrict_bounds_to_overlay: true) - expect(tour.bounds[:south]).to eq(33.723031085386665) + expect(tour.bounds[:south]).to(eq(33.723031085386665)) end it 'has no bounds when no stops' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), published: false) - expect(tour.bounds).to be nil + expect(tour.bounds).to(be_nil) end it 'does not restrict bounds to overlay when no overlay' do - tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), restrict_bounds_to_overlay: true) - expect(tour.restrict_bounds_to_overlay).to be false + tour = create( + :tour, + mode: Mode.find_by(title: 'BICYCLING'), + stops: create_list(:stop, 5), + restrict_bounds_to_overlay: true, + ) + expect(tour.restrict_bounds_to_overlay).to(be(false)) end - it 'sets restrict_bounds to false when restricted to overlay bounds' do - tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), restrict_bounds_to_overlay: true) - mo = create(:map_overlay, tour: tour) - expect(tour.restrict_bounds).to be true - tour.update(restrict_bounds_to_overlay: true) - expect(tour.restrict_bounds).to be false - expect(tour.restrict_bounds_to_overlay).to be true - end + # it 'sets restrict_bounds to false when restricted to overlay bounds' do + # tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), restrict_bounds_to_overlay: true) + # expect(tour.restrict_bounds).to be true + # tour.update(restrict_bounds_to_overlay: true) + # expect(tour.restrict_bounds).to be false + # expect(tour.restrict_bounds_to_overlay).to be true + # end - it 'sets restrict_to_overlay_bounds when updated to restrict_bounds' do - tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), restrict_bounds_to_overlay: true) - mo = create(:map_overlay, tour: tour) - expect(tour.restrict_bounds).to be true - tour.update(restrict_bounds_to_overlay: true) - expect(tour.restrict_bounds).to be false - expect(tour.restrict_bounds_to_overlay).to be true - tour.update(restrict_bounds: true) - expect(tour.restrict_bounds).to be true - expect(tour.restrict_bounds_to_overlay).to be false - end + # it 'sets restrict_to_overlay_bounds when updated to restrict_bounds' do + # tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5), restrict_bounds_to_overlay: true) + # expect(tour.restrict_bounds_to_overlay).to be false + # create(:map_overlay, tour:) + # tour.update(restrict_bounds_to_overlay: true) + # expect(tour.restrict_bounds).to be false + # expect(tour.restrict_bounds_to_overlay).to be true + # tour.update(restrict_bounds: true) + # expect(tour.restrict_bounds).to be true + # expect(tour.restrict_bounds_to_overlay).to be false + # end it 'allows both restrictions to be false' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5)) - mo = create(:map_overlay, tour: tour) - expect(tour.restrict_bounds).to be true + expect(tour.restrict_bounds).to(be(true)) tour.update(restrict_bounds: false) - expect(tour.restrict_bounds).to be false - expect(tour.restrict_bounds_to_overlay).to be false + expect(tour.restrict_bounds).to(be(false)) + expect(tour.restrict_bounds_to_overlay).to(be(false)) end - it 'will not allow restriction to overlay if no overlay' do + it 'does not allow restriction to overlay if no overlay' do tour = create(:tour, mode: Mode.find_by(title: 'BICYCLING'), stops: create_list(:stop, 5)) tour.update(restrict_bounds_to_overlay: true) - expect(tour.restrict_bounds_to_overlay).to be false + expect(tour.restrict_bounds_to_overlay).to(be(false)) + end + + it 'does not allow a title with a duplicate name' do + title = Faker::Movies::HitchhikersGuideToTheGalaxy.location + create(:tour, title:) + expect(build(:tour, title:)).not_to(be_valid) + end + + it 'takes the first stop medium for splash when tour has no media' do + tour = create(:tour, media: [], stops: []) + stop = create(:stop_with_media, tours: [tour]) + tour.reload + expect(tour.search_data[:splash][:url]).to(eq(stop.media.first.search_data[:files][:desktop])) + end + + describe '#check_for_overlay' do + subject(:tour) { build(:tour) } + + context 'when restrict_bounds_to_overlay is true but no overlay exists' do + before do + tour.map_overlay = nil + tour.restrict_bounds_to_overlay = true + end + + it 'resets restrict_bounds_to_overlay to false' do + tour.save + expect(tour.restrict_bounds_to_overlay).to(be(false)) + end + end + + context 'when restrict_bounds_to_overlay is changed to true and an overlay exists' do + before do + tour.map_overlay = build(:map_overlay) + tour.restrict_bounds = true + tour.restrict_bounds_to_overlay = true + end + + it 'disables restrict_bounds' do + tour.save + expect(tour.restrict_bounds).to(be(false)) + end + + it 'leaves restrict_bounds_to_overlay enabled' do + tour.save + expect(tour.restrict_bounds_to_overlay).to(be(true)) + end + end + + context 'when restrict_bounds is changed to true' do + before do + create(:map_overlay, tour:) + tour.update(restrict_bounds_to_overlay: true) + tour.restrict_bounds = true + end + + it 'disables restrict_bounds_to_overlay' do + tour.save + expect(tour.restrict_bounds_to_overlay).to(be(false)) + end + + it 'leaves restrict_bounds enabled' do + tour.update(restrict_bounds: true) + expect(tour.restrict_bounds).to(be(true)) + end + end + + context 'when neither flag is changed' do + before do + tour.map_overlay = build(:map_overlay) + tour.restrict_bounds = false + tour.restrict_bounds_to_overlay = false + end + + it 'does not change restrict_bounds' do + expect { tour.save }.not_to(change(tour, :restrict_bounds)) + end + + it 'does not change restrict_bounds_to_overlay' do + expect { tour.save }.not_to(change(tour, :restrict_bounds_to_overlay)) + end + end + end + + describe 'dependent associations' do + it 'destroys tour_flat_pages join records when tour is deleted but preserves flat pages' do + tour = create(:tour_with_flat_pages, flat_pages_count: 2) + flat_page_ids = tour.flat_pages.pluck(:id) + tour.reload.destroy + expect(TourFlatPage.where(tour_id: tour.id)).to(be_empty) + expect(FlatPage.where(id: flat_page_ids).count).to(eq(flat_page_ids.size)) + end + + it 'destroys tour_flat_pages join records when flat page is deleted but preserves tour' do + tour = create(:tour_with_flat_pages, flat_pages_count: 1) + flat_page = tour.flat_pages.first + expect { flat_page.destroy }.to(change(TourFlatPage, :count).by(-1)) + expect(described_class.exists?(tour.id)).to(be(true)) + end + end + + context 'when description contains _blank link with no rel or aria-label' do + it 'adds rel="noopener noreferrer" to link.' do + description = 'click here' + tour = create(:tour, description:) + expect(tour.description).to(include('rel="noopener noreferrer"')) + end + + it 'adds aria-label="here (opens in a new tab)" to link.' do + description = 'click here' + tour = create(:tour, description:) + expect(tour.description).to(include('aria-label="here (opens in a new tab)"')) + end + end + + context 'when the published status of a tour is changed' do + it 'sets published_on when published' do + tour = create(:tour, published: false) + expect(tour.published_on).to(be_nil) + tour.update(published: true) + expect(tour.published_on).to(be_present) + end + + it 'sets published_on to null when unpublished' do + tour = create(:tour, published: true) + expect(tour.published_on).to(be_present) + tour.update(published: false) + expect(tour.published_on).to(be_nil) + end end end diff --git a/spec/models/tour_stop_spec.rb b/spec/models/tour_stop_spec.rb index 3f7bab93..c638936b 100644 --- a/spec/models/tour_stop_spec.rb +++ b/spec/models/tour_stop_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe TourStop, type: :model do - it { should belong_to(:tour) } - it { should belong_to(:stop) } +RSpec.describe(TourStop, type: :model) do + it { is_expected.to(belong_to(:tour)) } + it { is_expected.to(belong_to(:stop)) } end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0f1a4be1..1f146df1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,46 +2,31 @@ require 'rails_helper' -RSpec.describe User, type: :model do - # it { should have_one(:login) } - # it { expect(User.reflect_on_association(:login).macro).to eq(:has_one) } - - context 'tour author across tenants' do +RSpec.describe(User, type: :model) do + context 'when tour author across tenants' do it 'lists all tours across tenants' do - # pw = Faker::Internet.password(min_length: 8) - # u = User.create!(displayname: Faker::Movies::HitchhikersGuideToTheGalaxy.character) - # Login.create!(identification: 'foo@bar.com', password: pw, password_confirmation: pw, user: u) - # # RailsApiAuth uses `has_secure_password` The `authenticate` method returns - # # the `Login` object. This just checks that the password authenticates - # expect(u.login.authenticate(pw).user).to eq(u) - TourSet.all.each { |tour_set| tour_set.delete } + TourSet.all.find_each(&:delete) user = create(:user) create_list(:tour_set, 4) - TourSet.all.each do |tour_set| - Apartment::Tenant.switch! tour_set.subdir + TourSet.all.find_each do |tour_set| + Apartment::Tenant.switch!(tour_set.subdir) user.tours << create_list(:tour, 2) end - expect(user.all_tours.count).to eq(8) + expect(user.all_tours.count).to(eq(8)) end end - context 'has login' do - it 'has no provider' do - user = create(:user) - expect(user.provider).to be nil - end - + context 'when has login' do it 'has no provider' do user = create(:user) - login = create(:login, user_id: user.id) - expect(user.provider).to eq(login.provider) + expect(user.provider).not_to(be_nil) end end - context 'has default' do + context 'when has default' do it 'terms accepted defaults to false' do user = create(:user) - expect(user.terms_accepted).to be(false) + expect(user.terms_accepted).to(be(false)) end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8484bcbb..2fcea279 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -24,21 +24,59 @@ # require only the support files necessary. # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } - # Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. ActiveRecord::Migration.maintain_test_schema! Shoulda::Matchers.configure do |config| config.integrate do |with| - with.test_framework :rspec - with.library :rails + with.test_framework(:rspec) + with.library(:rails) + end +end + +def delete_test_indices + Searchkick.client.indices.get(index: 'otb_*_test').keys.each_slice(10) do |batch| + Searchkick.client.indices.delete(index: batch.join(',')) + end +end + +def reset_test_db! + # Drop every Apartment tenant schema so factory-created schemas from a + # previous run (or a force-killed run) don't accumulate or conflict. + Apartment::Tenant.reset + TourSet.pluck(:subdir).each do |subdir| + Apartment::Tenant.drop(subdir) + rescue StandardError + nil + end + + # Wipe all public-schema tables. Order matters for FK constraints. + tables = [ + 'active_storage_attachments', + 'active_storage_blobs', + 'active_storage_variant_records', + 'tour_set_admins', + 'access_requests', + 'tour_authors', + 'logins', + 'tokens', + 'tour_sets', + 'users', + 'modes', + 'roles', + 'themes', + ] + tables.each do |t| + ActiveRecord::Base.connection.execute("TRUNCATE #{t} RESTART IDENTITY CASCADE") + rescue + nil end end RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_paths = [Rails.root.join('spec/fixtures').to_s] # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false @@ -47,208 +85,208 @@ # config.use_transactional_fixtures = true end - config.include RequestSpecHelper, type: :request + config.include(RequestSpecHelper, type: :request) config.include(RequestSpecHelper, type: :controller) config.include(SignedCookieHelper, type: :request) config.include(SignedCookieHelper, type: :controller) - config.include FactoryBot::Syntax::Methods + config.include(FactoryBot::Syntax::Methods) # start by truncating all the tables but then use the faster transaction strategy the rest of the time. config.before(:suite) do - # DatabaseCleaner.clean_with(:truncation, except: [:modes, :roles]) - # DatabaseCleaner.strategy = :transaction - # Truncating doesn't drop schemas, ensure we're clean here, app *may not* exist - # begin - # Apartment::Tenant.drop('atlanta') - # rescue - # nil - # end - # # Create the default tenant for our tests - # TourSet.create(name: 'Atlanta') + # Start from a clean slate every run so re-seeding is safe and + # schemas from a previous (possibly force-killed) run don't linger. + reset_test_db! + delete_test_indices + Rails.application.routes.default_url_options = { host: 'www.example.com', protocol: 'https' } + ActiveStorage::Current.url_options = { protocol: 'https', host: 'example.com', port: 443 } load Rails.root + 'db/seeds.rb' end # config.use_transactional_fixtures = true # start the transaction strategy as examples are run - config.around(:each) do |example| - example.run - end - config.before(:each) do - MiniMagick.configure do |config| - config.validate_on_create = false - end + config.before do + # MiniMagick.configure do |config| + # config.validate_on_create = false + # end # Start transaction for this test # DatabaseCleaner.start # Switch into the default tenant - Apartment::Tenant.switch! TourSet.find(TourSet.pluck(:id).sample).subdir - + Apartment::Tenant.switch!(TourSet.find(TourSet.pluck(:id).sample).subdir) # Set the host for ActiveStorage urls - ActiveStorage::Current.host = 'http://test.host' - # Switch to the below version for Rails 7 - # ActiveStorage::Current.url_options = { host: 'http://test.host' } - # host! 'atlanta.lvh.me' - # load Rails.root + 'db/seeds.rb' + ActiveStorage::Current.url_options = { host: 'http://test.host' } - # Stub a network requests + # request.env["ipinfo"] = { city: Faker::Address.city, county: Faker::Address.country_code } + + # Stub network requests stub_request(:get, 'https://placehold.it/300x300.png_1000x1000') - .to_return( - body: File.open(Rails.root + 'spec/factories/images/0.jpg'), - status: 200 - ) + .to_return(body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200) - stub_request(:get, 'https://vimeo.com/api/oembed.json?url=https://vimeo.com/310645255') - .to_return( - body: "{ title: 'CycloramaBattleSites.org Stop 2', thumbnail_url: 'https://placehold.it/300x300.png' }", - status: 200 - ) - - stub_request(:get, 'https://vimeo.com/api/oembed.json?url=https://vimeo.com/video/310645255') - .with( - headers: { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'User-Agent': 'Ruby' - } - ) + # Vimeo — matches any video ID + stub_request(:get, %r{https://vimeo\.com/api/oembed\.json\?url=https://vimeo\.com/video/\d+}) .to_return( status: 200, - body: '{ "title": "CycloramaBattleSites.org Stop 2", "thumbnail_url": "https://placehold.it/300x300.png", "thumbnail_width": 100, "thumbnail_height": 100 }', - headers: { 'content-type': 'application/json' } + body: '{"title": "CycloramaBattleSites.org Stop 2", "description": "A stub description", "thumbnail_url": "https://placehold.it/300x300.png", "thumbnail_width": 100, "thumbnail_height": 100}', + headers: { 'content-type' => 'application/json' }, ) - stub_request(:get, 'https://vimeo.com/310645255') + stub_request(:get, 'https://img.youtube.com/vi/F9ULbmCvmxY/0.jpg') + .to_return(body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200) + + stub_request(:get, %r{http://test\.host/rails/active_storage/.*}) + .to_return(body: File.open(Rails.root + 'spec/factories/images/atl.png'), status: 200) + + # YouTube — valid video + stub_request(:get, 'https://www.googleapis.com/youtube/v3/videos?id=F9ULbmCvmxY&key=AIzaSyAafrj3VvNLJNXeW5-NNCVwY5cdB06p1_s&part=snippet') .to_return( - status: 200 + status: 200, + body: '{"items": [{"id": "F9ULbmCvmxY", "snippet": {"title": "Goodie Mob - Black Ice (Sky High) ft. OutKast", "description": "Music video by Goodie Mob feat. OutKast performing Black Ice (Sky High). (C) 1998 LaFace Records LLC"}}]}', + headers: { 'content-type' => 'application/json' }, ) - stub_request(:get, 'https://youtu.be/F9ULbmCvmxY') + # YouTube — not found + stub_request(:get, 'https://www.googleapis.com/youtube/v3/videos?id=CvmxYF9ULbm&key=AIzaSyAafrj3VvNLJNXeW5-NNCVwY5cdB06p1_s&part=snippet') .to_return( - status: 200 + status: 200, + body: '{"kind": "youtube#videoListResponse", "items": [], "pageInfo": {"totalResults": 0, "resultsPerPage": 0}}', + headers: { 'content-type' => 'application/json' }, ) - stub_request(:get, 'https://img.youtube.com/vi/F9ULbmCvmxY/0.jpg') + # SoundCloud oembed — matches any track URL (HTTParty sorts params alphabetically) + stub_request(:get, %r{https://soundcloud\.com/oembed}) .to_return( - body: File.open(Rails.root + 'spec/factories/images/0.jpg'), - status: 200 + status: 200, + body: '{"title": "A SoundCloud Track", "thumbnail_url": "https://i1.sndcdn.com/artworks-stub-0-t500x500.jpg"}', + headers: { 'Content-Type' => 'application/json' }, ) - stub_request(:get, /http:\/\/test\.host\/rails\/active_storage\/.*/) - .to_return( - body: File.open(Rails.root + 'spec/factories/images/atl.png'), - status: 200 - ) - - stub_request( - :get, - 'https://www.googleapis.com/youtube/v3/videos?id=F9ULbmCvmxY&key=AIzaSyAafrj3VvNLJNXeW5-NNCVwY5cdB06p1_s&part=snippet' - ) - .to_return( - status: 200, - body: '{"items": [{ "id": "F9ULbmCvmxY", "snippet": { "title": "Goodie Mob - Black Ice (Sky High) ft. OutKast", "description": "Music video by Goodie Mob feat. OutKast performing Black Ice (Sky High). (C) 1998 LaFace Records LLC" }}] }', - headers: { 'content-type': 'application/json' } - ) - - stub_request(:get, 'https://www.youtube.com/watch?v=F9ULbmCvmxY') - .to_return(status: 200, body: '', headers: {}) - - stub_request(:get, 'https://vimeo.com/F9ULbmCvmxY') - .to_return(status: 404, body: '', headers: {}) - - stub_request(:get, 'https://vimeo.com/https://youtu.be/F9ULbmCvmxY') - .to_return(status: 404, body: '', headers: {}) + # SoundCloud thumbnail image + stub_request(:get, %r{https://i1\.sndcdn\.com/artworks-.*\.jpg}) + .to_return(body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200) - stub_request(:get, 'https://www.googleapis.com/youtube/v3/videos?id=CvmxYF9ULbm&key=AIzaSyAafrj3VvNLJNXeW5-NNCVwY5cdB06p1_s&part=snippet') + # Sketchfab embed page — matches any model ID + stub_request(:get, %r{https://sketchfab\.com/models/.+/embed}) .to_return( status: 200, - body: '{"kind": "youtube#videoListResponse", "etag": "YIUPVpqNjppyCWOZfL-19bLb7uk", "items": [ ], "pageInfo": { "totalResults": 0, "resultsPerPage": 0 } }', - headers: { 'content-type': 'application/json' } + body: '', + headers: { 'Content-Type' => 'text/html' }, ) - stub_request(:get, 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/431162745&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=true&sharing=false') + # Sketchfab thumbnail image + stub_request(:get, %r{https://media\.sketchfab\.com/.*\.jpe?g}) + .to_return(body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200) + + # Generic iframe from unknown origin. + stub_request(:get, 'https://3d-api.si.edu/voyager/3d_package:a1651b35') .to_return( status: 200, - body: '', - headers: {} + body: 'A Model', + headers: { 'Content-Type' => 'text/html' }, ) - stub_request(:get, 'https://w.soundcloud.com/player/?auto_play=false&color=%23ff5500&hide_related=true&sharing=false&show_comments=false&show_reposts=false&show_teaser=false&show_user=false&url=https://api.soundcloud.com/tracks/457871163&visual=true') + # Open Geographies media content_url HEAD check (iiif-cloud) + stub_request(:head, %r{https://iiif-cloud\.ecds\.io/.*}) + .to_return(status: 200) + + # MorphoSource IIIF manifest + stub_request(:get, %r{https://www\.morphosource\.org/manifests/.*\.json}) .to_return( status: 200, - body: '
', - headers: {} + body: '{"label":{"@none":["Fragment [Mesh] [StrLight]"]},"summary":{"@none":["Italic terra sigillata fragment"]}}', + headers: { 'Content-Type' => 'application/json' }, ) - stub_request(:get, /https:\/\/i1\.sndcdn.com\/artworks-.*\.jpg/) + # Matterport model + stub_request(:get, 'https://my.matterport.com/show/?m=matterport_id') .to_return( - body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200, - headers: {} + body: '', + headers: { 'Content-Type' => 'text/html' }, ) - stub_request(:get, /http:\/\/127\.0\.0\.1:.*\/json\/version/).to_return(body: '{}', status: 200) + # Matterport thumbnail image + stub_request(:get, %r{https://my\.matterport\.com/.*\/thumb}) + .to_return(body: File.open(Rails.root + 'spec/factories/images/0.jpg'), status: 200) + + stub_request(:get, %r{http://127\.0\.0\.1:.*/json/version}).to_return(body: '{}', status: 200) - stub_request(:get, /http.*:\/\/maps\.googleapis\.com\/maps\/api\/.*BICYCLING.*/) + stub_request(:get, %r{http.*://maps\.googleapis\.com/maps/api/.*bicycling.*}) .to_return(body: File.read(Rails.root + 'spec/factories/distance_matrix.json'), status: 200, headers: { 'Content-Type': 'application/json' }) - stub_request(:get, /http.*:\/\/maps\.googleapis\.com\/maps\/api\/.*TRANSIT.*/) + stub_request(:get, %r{http.*://maps\.googleapis\.com/maps/api/.*transit.*}) .to_return(body: File.read(Rails.root + 'spec/factories/distance_matrix2.json'), status: 200, headers: { 'Content-Type': 'application/json' }) - stub_request(:get, /http.*:\/\/maps\.googleapis\.com\/maps\/api\/.*WALKING.*/) + stub_request(:get, %r{http.*://maps\.googleapis\.com/maps/api/.*walking.*}) .to_return(body: File.read(Rails.root + 'spec/factories/distance_matrix_zero.json'), status: 200, headers: { 'Content-Type': 'application/json' }) - stub_request(:get, /http.*:\/\/maps\.googleapis\.com\/maps\/api\/.*DRIVING.*/) + stub_request(:get, %r{http.*://maps\.googleapis\.com/maps/api/.*driving.*}) .to_return(body: '{"status": "INVALID_REQUEST"}', status: 200, headers: { 'Content-Type': 'application/json' }) - end - config.after(:each) do - # Reset tentant back to `public` - # Apartment::Tenant.reset - # Rollback transaction - # DatabaseCleaner.clean + stub_request(:get, 'http://og.ecds.io') + .to_return( + status: 200, + body: file_fixture('open_geographies.json'), + headers: { 'Content-Type' => 'application/json' }, + ) + + stub_request(:get, %r{http.*://ipinfo\.io/.*/geo.*}) + .with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'User-Agent' => 'Ruby', + }, + ) + .to_return(status: 200, headers: {}, body: ip_info_body) + + stub_request(:get, %r{http.*://ipinfo\.io/.*\?token.*}) + .with( + headers: { + 'Accept' => 'application/json', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization' => 'Bearer d3bb06e9a6567d', + 'User-Agent' => 'IPinfoClient/Ruby/2.4.0', + }, + ) + .to_return(status: 200, body: ip_info_body, headers: {}) end - # RSpec Rails can automatically mix in different behaviours to your tests - # based on their file location, for example enabling you to call `get` and - # `post` in specs under `spec/controllers`. - # - # You can disable this behaviour by removing the line below, and instead - # explicitly tag your specs with their type, e.g.: - # - # RSpec.describe UsersController, :type => :controller do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! # Filter lines from Rails gems in backtraces. config.filter_rails_from_backtrace! - # arbitrary gems may also be filtered via: - # config.filter_gems_from_backtrace("gem name") # Clean up uploaded images config.after(:all) do # Get rid of the linked images if Rails.env.test? - FileUtils.rm_rf(Dir["#{Rails.root}/public/uploads/test/[^.]*"]) - FileUtils.rm_rf(Dir["#{Rails.root}/public/uploads/tmp/test/[^.]*"]) + FileUtils.rm_rf(Dir[Rails.root.join('public/uploads/test/[^.]*').to_s]) + FileUtils.rm_rf(Dir[Rails.root.join('public/uploads/tmp/test/[^.]*').to_s]) end end config.after(:suite) do - # TourSet.all.each { |ts| ts.destroy } + delete_test_indices end # Class to mock IPinfo class MockIpinfo - def longitude - Faker::Address.longitude - end + delegate :longitude, to: :'Faker::Address' - def latitude - Faker::Address.latitude - end + delegate :latitude, to: :'Faker::Address' + end + + def ip_info_body + { + ip: Faker::Internet.ip_v4_address, + hostname: Faker::Internet.domain_name, + city: Faker::Address.city, + region: Faker::Address.state, + country: Faker::Address.country_code, + loc: "#{Faker::Address.latitude},#{Faker::Address.longitude}", + org: Faker::Company.name, + postal: Faker::Address.zip, + timezone: Faker::Address.time_zone, + }.to_json end end diff --git a/spec/requests/health_request_spec.rb b/spec/requests/health_request_spec.rb new file mode 100755 index 00000000..31a91fb7 --- /dev/null +++ b/spec/requests/health_request_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('Rails::HealthController', type: :request) do + describe 'GET /health' do + before do + Apartment::Tenant.switch!(TourSet.last.subdir) + get '/health', headers: { 'HTTP_USER_AGENT': 'bot' } + end + + it 'returns status code 200' do + expect(response).to(have_http_status(:ok)) + end + end +end diff --git a/spec/requests/tenant_redirect_spec.rb b/spec/requests/tenant_redirect_spec.rb new file mode 100644 index 00000000..30bf0a08 --- /dev/null +++ b/spec/requests/tenant_redirect_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('Renamed tenant redirects', type: :request) do + it 'redirects a request using an old subdir to the current one' do + tour_set = create(:tour_set) + old_subdir = tour_set.subdir + tour_set.update!(name: 'A Totally Different Name') + + get "/#{old_subdir}/v4/public/modes" + + expect(response).to(have_http_status(:permanent_redirect)) + expect(response.headers['Location']).to(eq("/#{tour_set.subdir}/v4/public/modes")) + end + + it 'does not redirect a subdir that was never renamed' do + tour_set = create(:tour_set) + + get "/#{tour_set.subdir}/v4/public/modes" + + expect(response).not_to(have_http_status(:permanent_redirect)) + end +end diff --git a/spec/requests/tours_request_spec.rb b/spec/requests/tours_request_spec.rb deleted file mode 100755 index a0b877c0..00000000 --- a/spec/requests/tours_request_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'V3::Tours', type: :request do - describe 'GET /:tenant/tours' do - before { - get "/#{Apartment::Tenant.current}/tours", headers: { 'HTTP_USER_AGENT': 'bot' } - } - - it 'returns only published tours' do - expect(json.size).to eq(Tour.published.count) - end - - it 'returns status code 200' do - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/requests/v3/tours_request_spec.rb b/spec/requests/v3/tours_request_spec.rb new file mode 100755 index 00000000..2d99da44 --- /dev/null +++ b/spec/requests/v3/tours_request_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V3::Tours', type: :request) do + describe 'GET /:tenant/tours' do + let(:tour_set) { create(:tour_set) } + let!(:setup) do + Apartment::Tenant.switch!(tour_set.subdir) + create(:tour, published: true) + create(:tour, published: true) + create(:tour, published: false) + begin + Tour.search_index.delete + rescue StandardError + nil + end + Tour.reindex + end + + before { get "/#{tour_set.subdir}/tours", headers: { 'HTTP_USER_AGENT': 'bot' } } + + it 'returns only published tours' do + Apartment::Tenant.switch!(tour_set.subdir) + expect(json.size).to(eq(Tour.published.count)) + end + + it 'returns status code 200' do + expect(response).to(have_http_status(:ok)) + end + end +end diff --git a/spec/requests/v4/admin/access_requests_spec.rb b/spec/requests/v4/admin/access_requests_spec.rb new file mode 100644 index 00000000..e8cd11b3 --- /dev/null +++ b/spec/requests/v4/admin/access_requests_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Full CRUD behaviour (including authenticated paths) is covered in +# spec/controllers/v4/admin/access_requests_controller_spec.rb. +# These request specs verify the route exists and rejects unauthenticated access. + +RSpec.describe('V4::Admin::AccessRequests', type: :request) do + let(:tour_set) { create(:tour_set) } + + describe 'GET /:tenant/v4/admin/access_requests' do + it 'returns 401 when unauthenticated' do + get "/#{tour_set.subdir}/v4/admin/access_requests" + expect(response).to(have_http_status(:unauthorized)) + end + end + + describe 'POST /:tenant/v4/admin/access_requests' do + it 'returns 401 when unauthenticated' do + post "/#{tour_set.subdir}/v4/admin/access_requests" + expect(response).to(have_http_status(:unauthorized)) + end + end +end diff --git a/spec/requests/v4/admin/crud_spec.rb b/spec/requests/v4/admin/crud_spec.rb new file mode 100644 index 00000000..96422111 --- /dev/null +++ b/spec/requests/v4/admin/crud_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V4::Admin::Crud', type: :request) do + describe 'POST /:tenant/v4/admin/crud' do + let(:file) do + Rack::Test::UploadedFile.new( + Rails.root.join('spec/factories/images/0.jpg'), + 'image/jpeg', + ) + end + + context 'with valid file' do + it 'uploads the file' do + post "/#{Apartment::Tenant.current}/v4/admin/crud", params: { model: 'medium', medium: { file:, filename: '0.jpg' } } + expect(response).to(have_http_status(:unauthorized)) + end + end + end +end diff --git a/spec/requests/v4/admin/services_spec.rb b/spec/requests/v4/admin/services_spec.rb new file mode 100644 index 00000000..430b3526 --- /dev/null +++ b/spec/requests/v4/admin/services_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V4::Admin::Services', type: :request) do + describe 'GET /:tenant/v4/admin/resolve_url' do + context 'with valid url' do + it 'returns resolved url' do + stub_request(:head, 'https://skfb.ly/owuDQ') + .to_return(status: 301, headers: { 'Location' => 'https://sketchfab.com:443/s/owuDQ' }) + stub_request(:head, 'https://sketchfab.com:443/s/owuDQ') + .to_return(status: 301, headers: { 'Location' => 'https://sketchfab.com/3d-models/apis-bull-statuette-f6fd7079d7364f8399af1c66b1e53730' }) + stub_request(:head, 'https://sketchfab.com/3d-models/apis-bull-statuette-f6fd7079d7364f8399af1c66b1e53730') + .to_return(status: 200) + + get "/#{Apartment::Tenant.current}/v4/admin/resolve_url", params: { url: 'https://skfb.ly/owuDQ' } + expect(response).to(have_http_status(:ok)) + expect(all_json[:resolved_url]).to(eq('https://sketchfab.com/3d-models/apis-bull-statuette-f6fd7079d7364f8399af1c66b1e53730')) + end + end + + context 'without a url param' do + it 'returns 422' do + get "/#{Apartment::Tenant.current}/v4/admin/resolve_url" + expect(response).to(have_http_status(:unprocessable_entity)) + end + end + end +end diff --git a/spec/requests/v4/v4_stops_request_spec.rb b/spec/requests/v4/v4_stops_request_spec.rb new file mode 100755 index 00000000..f0c48059 --- /dev/null +++ b/spec/requests/v4/v4_stops_request_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V4::Public::Stops', type: :request) do + let(:tour_set) { create(:tour_set) } + + describe 'GET /:tenant/v4/public/stops' do + it 'returns 401 when unauthenticated' do + get "/#{tour_set.subdir}/v4/public/stops" + expect(response).to(have_http_status(:unauthorized)) + end + end + + describe 'GET /:tenant/v4/public/stops/:slug' do + it 'returns 404 when stop slug does not exist' do + get "/#{tour_set.subdir}/v4/public/stops/nonexistent-slug" + expect(response).to(have_http_status(:not_found)) + expect(v4_json[:errors]).to(include('Not found')) + end + + it 'returns stop data when stop belongs to a published tour' do + Apartment::Tenant.switch!(tour_set.subdir) + stop = create(:stop) + create(:tour, published: true, stops: [stop]) + get "/#{tour_set.subdir}/v4/public/stops/#{stop.slug}" + expect(response).to(have_http_status(:ok)) + end + end +end diff --git a/spec/requests/v4/v4_tour_sets_request_spec.rb b/spec/requests/v4/v4_tour_sets_request_spec.rb new file mode 100755 index 00000000..e07be6f9 --- /dev/null +++ b/spec/requests/v4/v4_tour_sets_request_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V4::TourSets', type: :request) do + describe 'GET /public/v4/public/tour-sets' do + let!(:setup) do + # Create tour sets: some with published tours (should appear), some without (should not) + with_tours = create_list(:tour_set, 2) + with_tours.each do |ts| + Apartment::Tenant.switch!(ts.subdir) + create(:tour_with_stops, published: true) + end + create(:tour_set) # no published tours — should be excluded + Apartment::Tenant.switch!('public') + begin + TourSet.search_index.delete + rescue StandardError + nil + end + TourSet.reindex + end + + before { get '/public/v4/public/tour-sets', headers: { 'HTTP_USER_AGENT': 'bot' } } + + it 'returns only tour sets that have published tours' do + Apartment::Tenant.switch!('public') + expected = TourSet.all.select { |ts| ts.published_tours.count > 0 } + expect(v4_json.size).to(eq(expected.count)) + end + + it 'returns status code 200' do + expect(response).to(have_http_status(:ok)) + end + end +end diff --git a/spec/requests/v4/v4_tours_request_spec.rb b/spec/requests/v4/v4_tours_request_spec.rb new file mode 100755 index 00000000..eb924a02 --- /dev/null +++ b/spec/requests/v4/v4_tours_request_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe('V4::Tours', type: :request) do + describe 'GET /:tenant/v4/public/tours' do + let(:tour_set) { create(:tour_set) } + let(:tenant) { tour_set.subdir } + + let!(:published_count) do + Apartment::Tenant.switch!(tenant) + create(:tour, published: true) + create(:tour, published: true) + create(:tour, published: false) + begin + Tour.search_index.delete + rescue StandardError + nil + end + Tour.reindex + Tour.published.count + end + + before do + get "/#{tenant}/v4/public/tours", headers: { 'HTTP_USER_AGENT': 'bot' } + end + + it 'returns only published tours' do + expect(v4_json.size).to(eq(published_count)) + end + + it 'returns status code 200' do + expect(response).to(have_http_status(:ok)) + end + end +end diff --git a/spec/services/google_directions_spec.rb b/spec/services/google_directions_spec.rb new file mode 100644 index 00000000..2638c03c --- /dev/null +++ b/spec/services/google_directions_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(GoogleDirections) do + let(:origin) { [33.749, -84.388] } + let(:destinations) { [[33.750, -84.389], [33.751, -84.390]] } + let(:stops_count) { 2 } + + describe '#duration' do + it 'returns summed duration plus stop overhead for BICYCLING' do + directions = described_class.new(origin, destinations, stops_count, 'bicycling') + expect(directions.duration).to(eq(5536)) + end + + it 'returns nil when Google returns INVALID_REQUEST (DRIVING)' do + directions = described_class.new(origin, destinations, stops_count, 'driving') + expect(directions.duration).to(be_nil) + end + + it 'returns nil when Google returns ZERO_RESULTS (WALKING)' do + directions = described_class.new(origin, destinations, stops_count, 'walking') + expect(directions.duration).to(be_nil) + end + end + + describe '#matrix' do + it 'returns nil when status is not OK' do + directions = described_class.new(origin, destinations, stops_count, 'driving') + expect(directions.matrix).to(be_nil) + end + end +end diff --git a/spec/services/html_accessibility_enforcer_spec.rb b/spec/services/html_accessibility_enforcer_spec.rb new file mode 100644 index 00000000..a7c27f41 --- /dev/null +++ b/spec/services/html_accessibility_enforcer_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(HtmlAccessibilityEnforcer) do + describe '.process' do + it 'returns html unchanged when no _blank links present' do + html = '

Hello world

' + expect(described_class.process(html)).to(include('href="http://example.com"')) + expect(described_class.process(html)).not_to(include('noopener')) + end + + it 'returns nil/blank input unchanged' do + expect(described_class.process(nil)).to(be_nil) + expect(described_class.process('')).to(eq('')) + end + + it 'adds rel="noopener noreferrer" to _blank links' do + html = 'click' + result = described_class.process(html) + expect(result).to(include('noopener')) + expect(result).to(include('noreferrer')) + end + + it 'preserves existing rel values and adds noopener/noreferrer' do + html = 'click' + result = described_class.process(html) + expect(result).to(include('nofollow')) + expect(result).to(include('noopener')) + expect(result).to(include('noreferrer')) + end + + it 'adds aria-label with "(opens in a new tab)" when missing' do + html = 'click here' + result = described_class.process(html) + expect(result).to(include('aria-label="click here (opens in a new tab)"')) + end + + it 'does not overwrite existing aria-label' do + html = 'click' + result = described_class.process(html) + expect(result).to(include('aria-label="custom label"')) + expect(result).not_to(include('opens in a new tab')) + end + + it 'processes multiple _blank links' do + html = 'A B' + result = described_class.process(html) + expect(result.scan('noopener').count).to(eq(2)) + end + end +end diff --git a/spec/services/url_resolver_spec.rb b/spec/services/url_resolver_spec.rb new file mode 100644 index 00000000..c5786158 --- /dev/null +++ b/spec/services/url_resolver_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(UrlResolver) do + describe '.resolve' do + it 'returns the url immediately when no redirect' do + stub_request(:head, 'https://example.com/page') + .to_return(status: 200) + expect(described_class.resolve('https://example.com/page')).to(eq('https://example.com/page')) + end + + it 'follows a single redirect' do + stub_request(:head, 'https://short.ly/abc') + .to_return(status: 301, headers: { 'Location' => 'https://example.com/final' }) + stub_request(:head, 'https://example.com/final') + .to_return(status: 200) + expect(described_class.resolve('https://short.ly/abc')).to(eq('https://example.com/final')) + end + + it 'follows a chain of redirects' do + stub_request(:head, 'https://short.ly/abc') + .to_return(status: 301, headers: { 'Location' => 'https://example.com/step2' }) + stub_request(:head, 'https://example.com/step2') + .to_return(status: 302, headers: { 'Location' => 'https://example.com/final' }) + stub_request(:head, 'https://example.com/final') + .to_return(status: 200) + expect(described_class.resolve('https://short.ly/abc')).to(eq('https://example.com/final')) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 77c71181..6afb8011 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,42 +3,10 @@ # if ENV['COV'] == 'simple' # require 'simplecov' # else -# require 'coveralls' -# Coveralls.wear! # end -# require 'simplecov' -# SimpleCov.start - -require 'coveralls' -require 'simplecov' -require 'simplecov-lcov' - -SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( - [ - SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter, - SimpleCov::Formatter::LcovFormatter - ] -) - -# SimpleCov.start 'rails' -SimpleCov.start 'rails' do - add_filter [ - '/lib/snippets.rb', - '/app/channels/', - '/app/mailers/', - '/app/jobs/', - '/app/uploaders/' -] -end - require 'webmock/rspec' -WebMock.disable_net_connect!(allow_localhost: true) -WebMock.disable_net_connect!(allow: '45.33.24.119') -WebMock.disable_net_connect!(allow: 'http://test.host') +WebMock.disable_net_connect!(allow: ['localhost', 'localhost:9200', '45.33.24.119', 'http://test.host']) # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. @@ -59,7 +27,7 @@ # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. - config.expect_with :rspec do |expectations| + config.expect_with(:rspec) do |expectations| # This option will default to `true` in RSpec 4. It makes the `description` # and `failure_message` of custom matchers include text for helper methods # defined using `chain`, e.g.: @@ -72,7 +40,7 @@ # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| + config.mock_with(:rspec) do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to # `true` in RSpec 4. diff --git a/spec/string_spec.rb b/spec/string_spec.rb index c87d4411..04c23e1c 100644 --- a/spec/string_spec.rb +++ b/spec/string_spec.rb @@ -2,15 +2,15 @@ require 'rails_helper' -RSpec.describe 'Using custom parameterize_intl method' do +RSpec.describe(String) do it 'uses custom parameterize_intl method' do - expect('My Awesome Tour'.parameterize_intl).to eq('my-awesome-tour') - expect('Csodálatos túrám'.parameterize_intl).to eq('csodalatos-turam') - expect('הסיור המדהים שלי'.parameterize_intl).to eq('הסיור-המדהים-שלי') - expect('我的精 彩之旅!!'.parameterize_intl).to eq('我的精-彩之旅') - expect('Мій чудовий тур'.parameterize_intl).to eq('мій-чудовий-тур') - expect('جولتي الرائعة'.parameterize_intl).to eq('جولتي-الرائعة') - expect('我的精 彩之旅!! (mix)'.parameterize_intl).to eq('我的精-彩之旅-mix') - expect('💩 🤯 🤷🏼'.parameterize_intl).to eq('💩-🤯-🤷🏼') + expect('My Awesome Tour'.parameterize_intl).to(eq('my-awesome-tour')) + expect('Csodálatos túrám'.parameterize_intl).to(eq('csodalatos-turam')) + expect('הסיור המדהים שלי'.parameterize_intl).to(eq('הסיור-המדהים-שלי')) + expect('我的精 彩之旅!!'.parameterize_intl).to(eq('我的精-彩之旅')) + expect('Мій чудовий тур'.parameterize_intl).to(eq('мій-чудовий-тур')) + expect('جولتي الرائعة'.parameterize_intl).to(eq('جولتي-الرائعة')) + expect('我的精 彩之旅!! (mix)'.parameterize_intl).to(eq('我的精-彩之旅-mix')) + expect('💩 🤯 🤷🏼'.parameterize_intl).to(eq('💩-🤯-🤷🏼')) end end diff --git a/spec/support/cookies.rb b/spec/support/cookies.rb index b611978a..d20dae9f 100644 --- a/spec/support/cookies.rb +++ b/spec/support/cookies.rb @@ -1,6 +1,12 @@ +# frozen_string_literal: true + # spec/support/cookies.rb -class Rack::Test::CookieJar - def encrypted; self; end - def signed; self; end - def permanent; self; end # I needed this, too -end \ No newline at end of file +module Rack + module Test + class CookieJar + def encrypted = self + def signed = self + def permanent = self # I needed this, too + end + end +end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 69f033be..15c30dff 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -1,25 +1,25 @@ +# frozen_string_literal: true + RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end - config.before(:each) do + config.before do DatabaseCleaner.strategy = :transaction end - config.before(:each, js: true) do + config.before(:each, :js) do DatabaseCleaner.strategy = :truncation end - config.before(:each) do + config.before do DatabaseCleaner.start end - config.after(:each) do - begin - DatabaseCleaner.clean - rescue NoMethodError - # IDK - end + config.after do + DatabaseCleaner.clean + rescue NoMethodError + # IDK end end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb index 9ad508bb..37b55203 100644 --- a/spec/support/factory_bot.rb +++ b/spec/support/factory_bot.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.configure do |config| - config.include FactoryBot::Syntax::Methods + config.include(FactoryBot::Syntax::Methods) # config.before(:suite) do # FactoryBot.find_definitions diff --git a/spec/support/request_spec_helper.rb b/spec/support/request_spec_helper.rb index 5f587f24..19cc2e1f 100644 --- a/spec/support/request_spec_helper.rb +++ b/spec/support/request_spec_helper.rb @@ -2,11 +2,19 @@ # spec/support/request_spec_helper module RequestSpecHelper + def all_json + JSON.parse(response.body).with_indifferent_access + end + # Parse JSON response to ruby hash def json JSON.parse(response.body).with_indifferent_access[:data] end + def v4_json + JSON.parse(response.body, symbolize_names: true) + end + def errors JSON.parse(response.body).with_indifferent_access[:errors].map { |e| e[:detail] } end @@ -16,9 +24,8 @@ def response_id end def attributes - if json.is_a?(Array) - return json.map { |record| record[:attributes] } - end + return json.map { |record| record[:attributes] } if json.is_a?(Array) + json['attributes'] end @@ -32,21 +39,21 @@ def included def hash_to_json_api(model, attributes) { - data: { - type: model, - attributes: attributes - } + data: { + type: model, + attributes: attributes, + }, } end def factory_to_json_api(model) { - data: { - type: ActiveModel::Naming.plural(model), - attributes: model.attributes - }.tap do |hash| - hash[:id] = model.id if model.persisted? - end + data: { + type: ActiveModel::Naming.plural(model), + attributes: model.attributes, + }.tap do |hash| + hash[:id] = model.id if model.persisted? + end, } end end diff --git a/spec/support/signed_cookie.rb b/spec/support/signed_cookie.rb index 9a86c888..e5bfa9d5 100755 --- a/spec/support/signed_cookie.rb +++ b/spec/support/signed_cookie.rb @@ -13,7 +13,17 @@ def signed_cookie(user) value: login.tokens.first.token, httponly: true, same_site: :none, - secure: 'Secure' + secure: 'Secure', + } + end + + # Used for mocking requests with bad credentials. + def invalid_signed_cooke + cookies.signed[:auth] = { + value: JWT.encode(Faker::Beer.style, Faker::Address.zip, 'HS256'), + httponly: true, + same_site: :none, + secure: 'Secure', } end end diff --git a/temp_gemfile b/temp_gemfile deleted file mode 100644 index e69de29b..00000000