From eead1b2bc4414a04356f0c58b44f8e75b64f53c6 Mon Sep 17 00:00:00 2001 From: Oleksandr Turchyn Date: Sun, 25 Aug 2024 23:24:59 +0300 Subject: [PATCH] add basic API specs --- .rubocop.yml | 3 + spec/factories/submission_events.rb | 18 ++ spec/factories/submissions.rb | 18 ++ spec/factories/submitters.rb | 2 + spec/requests/submissions_spec.rb | 247 ++++++++++++++++++++++++++++ spec/requests/submitters_spec.rb | 113 +++++++++++++ spec/requests/templates_spec.rb | 226 +++++++++++++++++++++++++ 7 files changed, 627 insertions(+) create mode 100644 spec/factories/submission_events.rb create mode 100644 spec/requests/submissions_spec.rb create mode 100644 spec/requests/submitters_spec.rb create mode 100644 spec/requests/templates_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 5263bcc3..cf883596 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -34,6 +34,7 @@ Metrics/MethodLength: Max: 30 Exclude: - 'db/migrate/**' + - 'spec/**/*' Metrics/CyclomaticComplexity: Max: 15 @@ -46,6 +47,8 @@ Layout/LineLength: Metrics/AbcSize: Max: 45 + Exclude: + - spec/**/* Metrics/ModuleLength: Max: 500 diff --git a/spec/factories/submission_events.rb b/spec/factories/submission_events.rb new file mode 100644 index 00000000..4b857cd3 --- /dev/null +++ b/spec/factories/submission_events.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :submission_event do + submission + submitter + event_type { 'view_form' } + event_timestamp { Time.zone.now } + data do + { + ip: Faker::Internet.ip_v4_address, + ua: Faker::Internet.user_agent, + sid: SecureRandom.base58(10), + uid: Faker::Number.number(digits: 4) + } + end + end +end diff --git a/spec/factories/submissions.rb b/spec/factories/submissions.rb index 3a210447..7a055dbe 100644 --- a/spec/factories/submissions.rb +++ b/spec/factories/submissions.rb @@ -11,5 +11,23 @@ FactoryBot.define do submission.template_schema = submission.template.schema submission.template_submitters = submission.template.submitters end + + trait :with_submitters do + after(:create) do |submission, _| + submission.template_submitters.each do |template_submitter| + create(:submitter, submission:, + account_id: submission.account_id, + uuid: template_submitter['uuid']) + end + end + end + + trait :with_events do + after(:create) do |submission, _| + submission.submitters.each do |submitter| + create(:submission_event, submission:, submitter:) + end + end + end end end diff --git a/spec/factories/submitters.rb b/spec/factories/submitters.rb index 1167ad77..7bd771b0 100644 --- a/spec/factories/submitters.rb +++ b/spec/factories/submitters.rb @@ -4,6 +4,8 @@ FactoryBot.define do factory :submitter do submission email { Faker::Internet.email } + name { Faker::Name.name } + phone { Faker::PhoneNumber.phone_number } before(:create) do |submitter, _| submitter.account_id = submitter.submission.account_id diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb new file mode 100644 index 00000000..81cdb6c2 --- /dev/null +++ b/spec/requests/submissions_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Submission API', type: :request do + let!(:account) { create(:account) } + let!(:author) { create(:user, account:) } + let!(:folder) { create(:template_folder, account:) } + let!(:templates) { create_list(:template, 2, account:, author:, folder:) } + + describe 'GET /api/submissions' do + it 'returns a list of submissions' do + submissions = [ + create(:submission, :with_submitters, + template: templates[0], + created_by_user: author), + create(:submission, :with_submitters, + template: templates[1], + created_by_user: author) + ].reverse + + get '/api/submissions', headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['pagination']).to eq(JSON.parse({ + count: submissions.size, + next: submissions.last.id, + prev: submissions.first.id + }.to_json)) + expect(response.parsed_body['data']).to eq(JSON.parse(submissions.map { |t| index_submission_body(t) }.to_json)) + end + end + + describe 'GET /api/submissions/:id' do + it 'returns a submission' do + submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author) + + get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json)) + end + end + + describe 'POST /api/submissions' do + it 'creates a submission' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + send_email: true, + submitters: [{ role: 'First Role', email: 'john.doe@example.com' }] + }.to_json + + expect(response).to have_http_status(:ok) + + submission = Submission.last + + expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) + end + end + + describe 'POST /api/submissions/emails' do + it 'creates a submission using email' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + emails: 'john.doe@example.com' + }.to_json + + expect(response).to have_http_status(:ok) + + submission = Submission.last + + expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) + end + end + + describe 'DELETE /api/submissions/:id' do + it 'archives a submission' do + submission = create(:submission, :with_submitters, template: templates[0], created_by_user: author) + + delete "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + + submission.reload + + expect(submission.archived_at).not_to be_nil + expect(response.parsed_body).to eq(JSON.parse({ + id: submission.id, + archived_at: submission.archived_at + }.to_json)) + end + end + + private + + def index_submission_body(submission) + submitters = submission.submitters.map do |submitter| + { + id: submitter.id, + submission_id: submission.id, + uuid: submitter.uuid, + email: submitter.email, + slug: submitter.slug, + sent_at: submitter.sent_at, + opened_at: submitter.opened_at, + completed_at: submitter.completed_at, + declined_at: nil, + created_at: submitter.created_at, + updated_at: submitter.updated_at, + name: submitter.name, + phone: submitter.phone, + status: submitter.status, + role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'], + external_id: nil, + application_key: nil, # Backward compatibility + metadata: {}, + preferences: {} + } + end + + { + id: submission.id, + source: 'link', + submitters_order: 'random', + slug: submission.slug, + audit_log_url: nil, + combined_document_url: nil, + expire_at: nil, + completed_at: nil, + created_at: submission.created_at, + updated_at: submission.updated_at, + archived_at: nil, + status: 'pending', + submitters:, + template: { + id: submission.template.id, + name: submission.template.name, + external_id: nil, + folder_name: folder.name, + created_at: submission.template.created_at, + updated_at: submission.template.updated_at + }, + created_by_user: { + id: author.id, + first_name: author.first_name, + last_name: author.last_name, + email: author.email + } + } + end + + def show_submission_body(submission) + submitters = submission.submitters.map do |submitter| + { + id: submitter.id, + submission_id: submission.id, + uuid: submitter.uuid, + email: submitter.email, + slug: submitter.slug, + sent_at: submitter.sent_at, + opened_at: submitter.opened_at, + completed_at: submitter.completed_at, + declined_at: nil, + created_at: submitter.created_at, + updated_at: submitter.updated_at, + name: submitter.name, + phone: submitter.phone, + status: submitter.status, + external_id: nil, + application_key: nil, # Backward compatibility + metadata: {}, + preferences: {}, + role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'], + documents: [], + values: [] + } + end + + { + id: submission.id, + source: 'link', + status: 'pending', + submitters_order: 'random', + slug: submission.slug, + audit_log_url: nil, + combined_document_url: nil, + expire_at: nil, + completed_at: nil, + created_at: submission.created_at, + updated_at: submission.updated_at, + archived_at: nil, + submitters:, + template: { + id: submission.template.id, + name: submission.template.name, + external_id: nil, + folder_name: folder.name, + created_at: submission.template.created_at, + updated_at: submission.template.updated_at + }, + created_by_user: { + id: author.id, + first_name: author.first_name, + last_name: author.last_name, + email: author.email + }, + documents: [], + submission_events: submission.submission_events.map do |event| + { + id: event.id, + submitter_id: event.submitter_id, + event_type: event.event_type, + event_timestamp: event.event_timestamp, + data: event.data.slice(:reason) + } + end + } + end + + def create_submission_body(submission) + submission.submitters.map do |submitter| + { + id: submitter.id, + submission_id: submission.id, + uuid: submitter.uuid, + email: submitter.email, + slug: submitter.slug, + sent_at: submitter.sent_at, + opened_at: submitter.opened_at, + completed_at: submitter.completed_at, + declined_at: nil, + created_at: submitter.created_at, + updated_at: submitter.updated_at, + name: submitter.name, + phone: submitter.phone, + status: submitter.status, + external_id: nil, + application_key: nil, # Backward compatibility + metadata: {}, + preferences: { send_email: true, send_sms: false }, + role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'], + embed_src: "#{Docuseal::DEFAULT_APP_URL}/s/#{submitter.slug}", + values: [] + } + end + end +end diff --git a/spec/requests/submitters_spec.rb b/spec/requests/submitters_spec.rb new file mode 100644 index 00000000..37ceecce --- /dev/null +++ b/spec/requests/submitters_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Submitter API', type: :request do + let!(:account) { create(:account) } + let!(:author) { create(:user, account:) } + let!(:folder) { create(:template_folder, account:) } + let!(:templates) { create_list(:template, 2, account:, author:, folder:) } + + describe 'GET /api/submitters' do + it 'returns a list of submitters' do + submitters = [ + create(:submission, :with_submitters, :with_events, + template: templates[0], + created_by_user: author), + create(:submission, :with_submitters, + template: templates[1], + created_by_user: author) + ].map(&:submitters).flatten.reverse + + get '/api/submitters', headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['pagination']).to eq(JSON.parse({ + count: submitters.size, + next: submitters.last.id, + prev: submitters.first.id + }.to_json)) + expect(response.parsed_body['data']).to eq(JSON.parse(submitters.map { |t| submitter_body(t) }.to_json)) + end + end + + describe 'GET /api/submitters/:id' do + it 'returns a submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: templates[0], + created_by_user: author).submitters.first + + get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq(JSON.parse(submitter_body(submitter).to_json)) + end + end + + describe 'PUT /api/submitters' do + it 'update a submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: templates[0], + created_by_user: author).submitters.first + + put "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + email: 'john.doe+updated@example.com' + }.to_json + + expect(response).to have_http_status(:ok) + + submitter.reload + + expect(submitter.email).to eq('john.doe+updated@example.com') + expect(response.parsed_body).to eq(JSON.parse(update_submitter_body(submitter).to_json)) + end + end + + private + + def submitter_body(submitter) + { + id: submitter.id, + submission_id: submitter.submission_id, + uuid: submitter.uuid, + email: submitter.email, + status: submitter.status, + slug: submitter.slug, + sent_at: submitter.sent_at, + opened_at: submitter.opened_at, + completed_at: submitter.completed_at, + declined_at: submitter.declined_at, + created_at: submitter.created_at, + updated_at: submitter.updated_at, + name: submitter.name, + phone: submitter.phone, + external_id: nil, + application_key: nil, # Backward compatibility + template: { + id: submitter.template.id, + name: submitter.template.name, + created_at: submitter.template.created_at, + updated_at: submitter.template.updated_at + }, + metadata: {}, + preferences: {}, + submission_events: submitter.submission_events.map do |event| + { + id: event.id, + submitter_id: event.submitter_id, + event_type: event.event_type, + event_timestamp: event.event_timestamp, + data: event.data.slice(:reason) + } + end, + values: [], + documents: [], + role: submitter.template.submitters.find { |s| s['uuid'] == submitter.uuid }['name'] + } + end + + def update_submitter_body(submitter) + submitter_body(submitter).except(:template, :submission_events) + .merge(embed_src: "#{Docuseal::DEFAULT_APP_URL}/s/#{submitter.slug}") + end +end diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb new file mode 100644 index 00000000..65d542b3 --- /dev/null +++ b/spec/requests/templates_spec.rb @@ -0,0 +1,226 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Templates API', type: :request do + let!(:account) { create(:account) } + let!(:author) { create(:user, account:) } + let!(:folder) { create(:template_folder, account:) } + let!(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } + + describe 'GET /api/templates' do + it 'returns a list of templates' do + templates = [ + create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences), + create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + ].reverse + + get '/api/templates', headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['pagination']).to eq(JSON.parse({ + count: templates.size, + next: templates.last.id, + prev: templates.first.id + }.to_json)) + expect(response.parsed_body['data']).to eq(JSON.parse(templates.map { |t| template_body(t) }.to_json)) + end + end + + describe 'GET /api/templates/:id' do + it 'returns a template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + get "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq(JSON.parse(template_body(template).to_json)) + end + end + + describe 'PUT /api/templates' do + it 'update a template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: { + name: 'Updated Template Name', + external_id: '123456' + }.to_json + + expect(response).to have_http_status(:ok) + + template.reload + + expect(template.name).to eq('Updated Template Name') + expect(template.external_id).to eq('123456') + expect(response.parsed_body).to eq(JSON.parse({ + id: template.id, + updated_at: template.updated_at + }.to_json)) + end + end + + describe 'DELETE /api/templates/:id' do + it 'archives a template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + delete "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:ok) + + template.reload + + expect(template.archived_at).not_to be_nil + expect(response.parsed_body).to eq(JSON.parse({ + id: template.id, + archived_at: template.archived_at + }.to_json)) + end + end + + describe 'POST /api/templates/:id/clone' do + it 'clones a template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + expect do + post "/api/templates/#{template.id}/clone", headers: { 'x-auth-token': author.access_token.token }, params: { + name: 'Cloned Template Name', + external_id: '123456' + }.to_json + end.to change(Template, :count) + + expect(response).to have_http_status(:ok) + + cloned_template = Template.last + + expect(cloned_template.name).to eq('Cloned Template Name') + expect(cloned_template.external_id).to eq('123456') + expect(response.parsed_body).to eq(JSON.parse(clone_template_body(cloned_template).to_json)) + end + end + + private + + def template_body(template) + template_attachment_uuid = template.schema.first['attachment_uuid'] + attachment = template.schema_documents.preload(:blob).find { |e| e.uuid == template_attachment_uuid } + first_page_blob = + ActiveStorage::Attachment.joins(:blob) + .where(blob: { filename: '0.jpg' }) + .where(record_id: template.schema_documents.map(&:id), + record_type: 'ActiveStorage::Attachment', + name: :preview_images) + .preload(:blob) + .first + .blob + + { + id: template.id, + slug: template.slug, + name: template.name, + fields: [ + { + 'uuid' => '21637fc9-0655-45df-8952-04ec64949e85', + 'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c', + 'name' => 'First Name', + 'type' => 'text', + 'required' => true, + 'areas' => [ + { + 'x' => 0.09027777777777778, + 'y' => 0.1197252208047105, + 'w' => 0.3069444444444444, + 'h' => 0.03336604514229637, + 'attachment_uuid' => template_attachment_uuid, + 'page' => 0 + } + ] + }, + { + 'uuid' => '1f97f8e3-dc82-4586-aeea-6ebed6204e46', + 'submitter_uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c', + 'name' => '', + 'type' => 'signature', + 'required' => true, + 'areas' => [] + } + ], + submitters: [ + { + name: 'First Party', + uuid: template.submitters.first['uuid'] + } + ], + author: { + id: author.id, + first_name: author.first_name, + last_name: author.last_name, + email: author.email + }, + documents: [ + { + id: template.documents.first.id, + uuid: template.documents.first.uuid, + url: ActiveStorage::Blob.proxy_url(attachment.blob), + preview_image_url: ActiveStorage::Blob.proxy_url(first_page_blob), + filename: 'sample-document.pdf' + } + ], + preferences: { + 'request_email_subject' => 'Subject text', + 'request_email_body' => 'Body Text' + }, + schema: [ + { + attachment_uuid: template_attachment_uuid, + name: 'sample-document' + } + ], + author_id: author.id, + archived_at: nil, + created_at: template.created_at, + updated_at: template.updated_at, + folder_id: folder.id, + folder_name: folder.name, + source: 'native', + external_id: template.external_id, + application_key: template.external_id # Backward compatibility + } + end + + def clone_template_body(cloned_template) + body = template_body(cloned_template).merge(source: 'api') + body[:fields].each_with_index do |field, index| + field.merge!( + 'submitter_uuid' => cloned_template.fields[index]['submitter_uuid'], + 'uuid' => cloned_template.fields[index]['uuid'] + ) + end + + body + end +end