ActiveStorage, Ruby on Rails 7, GraphQL and RSpec
Tags: rails, graphql, activestorage, rspec
Published: 2022-01-07 15:06:27

I have an article already on [File Uploading in GraphQL API in Rails with ActiveStorage](https://sulmanweb.com/file-uploading-in-graphql-api-in-rails-with-activestorage/ "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:
```ruby
# 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
```

## GraphQL Document Type:

The Document Type for the GraphQL remains the same as well:
```ruby
# 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
```

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](https://rubygems.org/gems/apollo_upload_server). After adding the gem to the Gemfile now we can create our mutation. My mutation looks like this: (It is changed from previous implementation)
```ruby
# 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
```

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](https://rubygems.org/gems/factory_bot_rails) 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:
```ruby
# 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 
```

## Document Spec

Now the part of testing the document model:
```ruby
# 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
```

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:
```ruby
# 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
```

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](https://gist.github.com/sulmanweb/64c878292de356c62481ea3a81ed3ff5)

Happy Coding!