DEV Community

Cover image for ActiveStorage, Ruby on Rails 7, GraphQL and RSpec
Sulman Baig
Sulman Baig

Posted on • Originally published at sulmanweb.com

ActiveStorage, Ruby on Rails 7, GraphQL and RSpec

I have an article already on File Uploading in GraphQL API in Rails with ActiveStorage. After that article, codelion asked how to unit test the Upload! Type in GraphQL. I was looking for the answers of the unit test myself and meanwhile Rails 7 launched. So, I started working on ActiveStorage with Rails 7 and I finally got the answers of both. So writing the article again to work with Rails 7.

Document Model:

I kept the document model same as before:

# app/models/document.rb

class Document < ApplicationRecord
  ## RELATIONSHIPS
  has_one_attached :doc
  belongs_to :documentable, polymorphic: true, optional: true
  belongs_to :user, optional: true

  ## VALIDATIONS
  validate :doc_presence, on: :create

  def doc_presence
    pattern = %r{^(image|application|text)/(.)+$}
    unless doc.attached? && pattern.match?(doc.attachment.blob.content_type)
      errors.add(:doc, I18n.t("errors.models.document.file_presence"))
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

GraphQL Document Type:

The Document Type for the GraphQL remains the same as well:

# app/graphql/types/objects/document_type.rb

module Types
  module Objects
    class DocumentType < Types::BaseObject
      field :id, Integer, null: false
      field :documentable_type, String, null: true
      field :documentable_id, Integer, null: true
      field :content_type, String, null: true
      field :url, String, null: false
      field :created_at, GraphQL::Types::ISO8601DateTime, null: false
      field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

      def url
        (Rails.env.development? || Rails.env.test?) ? Rails.application.routes.url_helpers.rails_blob_path(object.doc) : object.doc.service_url
      end

      def content_type
        object.doc.present? ? object.doc.blob.content_type : nil
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

I have url, and I am using s3 service so showing the service url. But in other environment I am using local storage, that is why I have different document URLs for the environment. All other parameters are fairly descriptive.

Apollo Upload Server:

Now, comes the point to create a mutation resolver for uploading the file to the system. The complete documentation of ApolloUploadServer is here. After adding the gem to the Gemfile now we can create our mutation. My mutation looks like this: (It is changed from previous implementation)

# app/graphql/mutations/create_document.rb

module Mutations
  class CreateDocument < Mutations::BaseMutation
    description "Create a document"

    argument :doc, ApolloUploadServer::Upload, required: true, description: "The document to upload"

    field :document, Types::Objects::DocumentType, null: false
    field :code, Types::Enums::CodeEnum, null: false

    def resolve(doc:)
      authenticate_user
      # First you need to create blob file in case of active storage. https://stackoverflow.com/q/70550808/4011757
      attachment = ActiveStorage::Blob::create_and_upload!(io: doc, filename: doc.original_filename, content_type: doc.content_type)
      document = context[:current_user].documents.build(doc: attachment)
      if document.save
        {document: document, code: "SUCCESS"}
      else
        raise GraphQL::ExecutionError.new(document.errors.full_messages.join(", "), extensions: {code: "UNPROCESSABLE_ENTITY", errors: document.errors})
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Here, doc is the active storage parameter saving the file, so, it is being used of ApolloUploadServer. The output will be the type we created before.

Now, here I changed according to the Rails 7 ActiveStorage. So, we have to create a blob before building the document.

ActiveStorage::Blob::create_and_upload!(io: doc, filename: doc.original_filename, content_type: doc.content_type)

The rails 7 has method of create_and_upload! But before rails 7 it was create_after_upload!.

Documents Factory:

I use FactoryBot for creating the factory, for stubbing the factory. My factory is containing an image that has to be uploaded and attached as doc every time a factory created:

# spec/factories/documents.rb

FactoryBot.define do
  factory :document do
    association :user

    trait :with_image do
      after :build do |document|
        file_name = 'image.png'
        file_path = Rails.root.join('spec', 'support', 'fixtures', file_name)
        document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'image/png')
      end
    end

    trait :with_video do
      after :build do |document|
        file_name = 'video.mp4'
        file_path = Rails.root.join('spec', 'support', 'fixtures', file_name)
        document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'video/mp4')
      end
    end

    trait :with_pdf do
      after :build do |document|
        file_name = 'pdf.pdf'
        file_path = Rails.root.join('spec', 'support', 'fixtures', file_name)
        document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'application/pdf')
      end
    end

    trait :with_csv do
      after :build do |document|
        file_name = 'import_csv.csv'
        file_path = Rails.root.join('spec', 'support', 'fixtures', file_name)
        document.doc.attach(io: File.open(file_path), filename: file_name, content_type: 'text/csv')
      end
    end
  end
end 
Enter fullscreen mode Exit fullscreen mode

Document Spec

Now the part of testing the document model:

# spec/models/document_spec.rb

require 'rails_helper'

RSpec.describe Document, type: :model do
  it 'has a valid factory' do
    document = FactoryBot.build(:document)
    expect(document.valid?).to be_falsey
    document = FactoryBot.build(:document, :with_image)
    expect(document.valid?).to be_truthy
    expect(document.doc.attached?).to be_truthy
    document.save!
    expect(document.doc.attached?).to be_truthy
    document = FactoryBot.build(:document, :with_video)
    expect(document.valid?).to be_truthy
    expect(document.doc.attached?).to be_truthy
    document.save!
    expect(document.doc.attached?).to be_truthy
    document = FactoryBot.build(:document, :with_pdf)
    expect(document.valid?).to be_truthy
    document = FactoryBot.build(:document, :with_csv)
    expect(document.valid?).to be_truthy
  end

  it 'must have user' do
    document = FactoryBot.build(:document, user: nil)
    expect(document.valid?).to be_falsey
  end

  it 'must contain file' do
    document = FactoryBot.build(:document, doc: nil)
    expect(document.valid?).to be_falsey
  end
end
Enter fullscreen mode Exit fullscreen mode

All the tests should pass.

Create Document Spec:

To test the above mutation, I had to use the factory of fixture, but that was changing the type of uploader type. So, I had to do the direct method:

# spec/graphql/mutations/create_document_spec.rb

require 'rails_helper'

module Mutations
  RSpec.describe CreateDocument, type: :request do
    describe '.resolve' do
      let(:session) {FactoryBot.create(:session)}
      it 'create a document for the user' do
        session.user.confirm
        headers = sign_in_test_headers session
        params = FactoryBot.attributes_for(:document, :with_image, user_id: session.user_id)
        query = <<-GRAPHQL
        mutation ($input: CreateDocumentInput!) {
          createDocument(input: $input) {
            document {
              id
              url
            }
          }
        }
        GRAPHQL
        variables = {input: {doc: ::ApolloUploadServer::Wrappers::UploadedFile.new(ActionDispatch::Http::UploadedFile.new(filename: "image.png", type: "image/png", tempfile: File.new("spec/support/fixtures/image.png")))}}
        response = ApiSchema.execute(query, variables: variables, context: {current_user: session.user})
        expect(response.to_h["data"]["createDocument"]["document"]["url"]).not_to be_nil
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This test should return success using the gem’s method ApolloUploadServer::Wrappers::UploadedFile and uploaded file method ActionDispatch::Http::UploadedFile.


The code is at https://gist.github.com/sulmanweb/64c878292de356c62481ea3a81ed3ff5

Happy Coding!

Discussion (0)