Back to Repositories

Testing S3 Storage Implementation in Paperclip

This test suite validates the S3 storage functionality in Paperclip, focusing on live integration with Amazon S3 for file attachments. It covers direct file assignments, URL generation, and server-side encryption scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of S3 storage integration:
  • Direct S3 attachment assignments between models
  • Expiring URL generation for non-existent attachments
  • File handling with spaces in filenames
  • AES256 server-side encryption implementation

Implementation Analysis

The testing approach employs RSpec’s context-based structure to organize related test scenarios. It uses before/after hooks for setup and cleanup, and implements isolated test environments through model rebuilding between contexts.

The implementation validates both functional aspects (file storage, URL generation) and security features (encryption, access controls).

Technical Details

Key technical components include:
  • Environment-based AWS credentials configuration
  • Model rebuilding with specific S3 storage parameters
  • File fixture handling for test data
  • Custom path and URL formatting
  • S3 bucket and region configuration

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Proper resource cleanup after tests
  • Environment variable usage for sensitive credentials
  • Isolated test contexts to prevent cross-test contamination
  • Comprehensive edge case coverage
  • Clear test organization and naming conventions

thoughtbot/paperclip

spec/paperclip/storage/s3_live_spec.rb

            
require 'spec_helper'

unless ENV["S3_BUCKET"].blank?
  describe Paperclip::Storage::S3, 'Live S3' do
    context "when assigning an S3 attachment directly to another model" do
      before do
        rebuild_model styles: { thumb: "100x100", square: "32x32#" },
                      storage: :s3,
                      bucket: ENV["S3_BUCKET"],
                      path: ":class/:attachment/:id/:style.:extension",
                      s3_region: ENV["S3_REGION"],
                      s3_credentials: {
                        access_key_id: ENV['AWS_ACCESS_KEY_ID'],
                        secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
                      }

        @file = File.new(fixture_file("5k.png"))
      end

      it "does not raise any error" do
        @attachment = Dummy.new.avatar
        @attachment.assign(@file)
        @attachment.save

        @attachment2 = Dummy.new.avatar
        @attachment2.assign(@file)
        @attachment2.save
      end

      it "allows assignment from another S3 object" do
        @attachment = Dummy.new.avatar
        @attachment.assign(@file)
        @attachment.save

        @attachment2 = Dummy.new.avatar
        @attachment2.assign(@attachment)
        @attachment2.save
      end

      after { @file.close }
    end

    context "Generating an expiring url on a nonexistant attachment" do
      before do
        rebuild_model styles: { thumb: "100x100", square: "32x32#" },
                      storage: :s3,
                      bucket: ENV["S3_BUCKET"],
                      path: ":class/:attachment/:id/:style.:extension",
                      s3_region: ENV["S3_REGION"],
                      s3_credentials: {
                        access_key_id: ENV['AWS_ACCESS_KEY_ID'],
                        secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
                      }

        @dummy = Dummy.new
      end

      it "returns a missing url" do
        expect(@dummy.avatar.expiring_url).to eq @dummy.avatar.url
      end
    end

    context "Using S3 for real, an attachment with S3 storage" do
      before do
        rebuild_model styles: { thumb: "100x100", square: "32x32#" },
                      storage: :s3,
                      bucket: ENV["S3_BUCKET"],
                      path: ":class/:attachment/:id/:style.:extension",
                      s3_region: ENV["S3_REGION"],
                      s3_credentials: {
                        access_key_id: ENV['AWS_ACCESS_KEY_ID'],
                        secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
                      }

        Dummy.delete_all
        @dummy = Dummy.new
      end

      it "is extended by the S3 module" do
        assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
      end

      context "when assigned" do
        before do
          @file = File.new(fixture_file('5k.png'), 'rb')
          @dummy.avatar = @file
        end

        after do
          @file.close
          @dummy.destroy
        end

        context "and saved" do
          before do
            @dummy.save
          end

          it "is on S3" do
            assert true
          end
        end
      end
    end

    context "An attachment that uses S3 for storage and has spaces in file name" do
      before do
        rebuild_model styles: { thumb: "100x100", square: "32x32#" },
          storage: :s3,
          bucket: ENV["S3_BUCKET"],
          s3_region: ENV["S3_REGION"],
          url: ":s3_domain_url",
          path: "/:class/:attachment/:id_partition/:style/:filename",
          s3_credentials: {
            access_key_id: ENV['AWS_ACCESS_KEY_ID'],
            secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
          }

        Dummy.delete_all
        @file = File.new(fixture_file('spaced file.png'), 'rb')
        @dummy = Dummy.new
        @dummy.avatar = @file
        @dummy.save
      end

      it "returns a replaced version for path" do
        assert_match /.+\/spaced_file\.png/, @dummy.avatar.path
      end

      it "returns a replaced version for url" do
        assert_match /.+\/spaced_file\.png/, @dummy.avatar.url
      end

      it "is accessible" do
        assert_success_response @dummy.avatar.url
      end

      it "is reprocessable" do
        assert @dummy.avatar.reprocess!
      end

      it "is destroyable" do
        url = @dummy.avatar.url
        @dummy.destroy
        assert_forbidden_response url
      end
    end

    context "An attachment that uses S3 for storage and uses AES256 encryption" do
      before do
        rebuild_model styles: { thumb: "100x100", square: "32x32#" },
                      storage: :s3,
                      bucket: ENV["S3_BUCKET"],
                      path: ":class/:attachment/:id/:style.:extension",
                      s3_region: ENV["S3_REGION"],
                      s3_credentials: {
                        access_key_id: ENV['AWS_ACCESS_KEY_ID'],
                        secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
                      },
                      s3_server_side_encryption: "AES256"
        Dummy.delete_all
        @dummy = Dummy.new
      end

      context "when assigned" do
        before do
          @file = File.new(fixture_file('5k.png'), 'rb')
          @dummy.avatar = @file
        end

        after do
          @file.close
          @dummy.destroy
        end

        context "and saved" do
          before do
            @dummy.save
          end

          it "is encrypted on S3" do
            assert @dummy.avatar.s3_object.server_side_encryption == "AES256"
          end
        end
      end
    end
  end
end