Fix CI lint + flaky dashboard test on first fork-Actions run

Previously CI had never run on the wabolabs/wabosign fork (Actions
gated until owner consent). Now that the gate is lifted, run rubocop /
erblint / brakeman / rspec against current master uncovered backlog:

- rubocop: 97 auto-corrected across the WaboSign-fork files (account
  logo, SMS, SSO, ability specs, role auth specs, omniauth callbacks).
  Remaining 8 fixed by hand:
  * lib/wabosign.rb chained map collapsed to filter_map; `hd` param
    renamed to `hosted_domain` (Naming/MethodParameterName)
  * app/models/user.rb default_sso_account split for line length +
    SafeNavigation
  * spec/rails_helper.rb abort calls marked `# rubocop:disable
    Rails/Exit` (upstream pattern, intentional)
  * spec/requests/users/omniauth_callbacks_spec.rb let! used for
    side-effect-only setup -> moved into before blocks
- erblint: 21 auto-corrected (mostly Style/StringLiterals from a
  sed substitution that picked double quotes) + a missing
  autocomplete attribute added to the SMS test-message input.
- brakeman: clean. Removed one obsolete ignore entry (was for the
  deleted enquiries controller) and added one new ignore for the
  MCP-settings token preview (HighlightCode returns escaped HTML).
- rspec: dashboard "shows the list of templates" was flaky because
  other_template's Faker::Book.title could randomly collide with one
  of the 5 in-account templates. Pin the name to a unique suffix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/687/head
Wabo 1 month ago
parent 8d8582c71a
commit 8dbf5b6cab

@ -15,9 +15,6 @@ gem 'csv', require: false
gem 'csv-safe', require: false
gem 'devise'
gem 'devise-two-factor'
gem 'omniauth', '~> 2.1'
gem 'omniauth-google-oauth2', '~> 1.2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'dotenv', require: false
gem 'email_typo'
gem 'faraday'
@ -29,6 +26,9 @@ gem 'jwt', require: false
gem 'lograge'
gem 'numo-narray-alt', require: false
gem 'oj'
gem 'omniauth', '~> 2.1'
gem 'omniauth-google-oauth2', '~> 1.2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'onnxruntime', require: false
gem 'pagy'
gem 'pg', require: false

@ -7,8 +7,12 @@ class AccountLogoController < ApplicationController
file = params[:logo]
return reject('Choose a file to upload.') if file.blank? || !file.respond_to?(:content_type)
return reject('Logo must be a PNG, JPEG, or SVG image.') unless Account::LOGO_CONTENT_TYPES.include?(file.content_type)
return reject("Logo must be under #{Account::LOGO_MAX_BYTES / 1.megabyte} MB.") if file.size > Account::LOGO_MAX_BYTES
unless Account::LOGO_CONTENT_TYPES.include?(file.content_type)
return reject('Logo must be a PNG, JPEG, or SVG image.')
end
if file.size > Account::LOGO_MAX_BYTES
return reject("Logo must be under #{Account::LOGO_MAX_BYTES / 1.megabyte} MB.")
end
safe = AccountLogo.sanitize_upload(file)
current_account.logo.attach(io: safe.io, filename: safe.filename, content_type: safe.content_type)

@ -1,7 +1,7 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
DUMMY_SCRIPT = <<~JAVASCRIPT
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `

@ -140,8 +140,9 @@ class User < ApplicationRecord
end
def self.from_google_omniauth(auth)
hd = auth.extra&.raw_info&.respond_to?(:hd) ? auth.extra.raw_info.hd : auth.extra&.raw_info&.dig('hd')
return nil unless Wabosign.google_domain_allowed?(hd)
raw_info = auth.extra&.raw_info
hosted_domain = raw_info.respond_to?(:hd) ? raw_info.hd : raw_info&.dig('hd')
return nil unless Wabosign.google_domain_allowed?(hosted_domain)
email = auth.info.email.to_s.downcase
return nil if email.blank?
@ -172,15 +173,12 @@ class User < ApplicationRecord
def self.default_sso_account
# ENV override always wins.
if Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID.present?
return Account.find_by(id: Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID)
end
return Account.find_by(id: Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID) if Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID.present?
# If an admin saved the Google SSO config via the UI, JIT-provision into
# that same account so admins land in the right tenant.
if (db_config = EncryptedConfig.find_by(key: EncryptedConfig::GOOGLE_SSO_KEY))
return db_config.account if db_config.account && db_config.account.archived_at.nil?
end
db_config = EncryptedConfig.find_by(key: EncryptedConfig::GOOGLE_SSO_KEY)
return db_config.account if db_config&.account && db_config.account.archived_at.nil?
Account.order(:created_at).first
end

@ -3,10 +3,8 @@
account: (optional) an Account record
class: CSS class string forwarded to <img>/<svg>
width: pixel width (default 37)
height: pixel height (default 37)
%>
<%
acc = local_assigns[:account]
height: pixel height (default 37) %>
acc = local_assigns[:account]
klass = local_assigns[:class]
w = local_assigns.fetch(:width, '37')
h = local_assigns.fetch(:height, '37')

@ -39,7 +39,7 @@
</div>
<div class="form-control">
<%= ff.label :provider, 'Provider', class: 'label' %>
<%= ff.select :provider, [['BulkVS', 'bulkvs']], { selected: value['provider'] || 'bulkvs' }, class: 'base-select' %>
<%= ff.select :provider, [%w[BulkVS bulkvs]], { selected: value['provider'] || 'bulkvs' }, class: 'base-select' %>
</div>
<div class="form-control">
<%= ff.label :basic_auth_token, 'BulkVS Basic Auth Token', class: 'label' %>
@ -73,7 +73,7 @@
<%= form_with url: test_message_settings_sms_path, method: :post, html: { autocomplete: 'off', class: 'space-y-3' } do |f| %>
<div class="form-control">
<label for="test_phone" class="label">Phone number</label>
<input type="tel" name="phone" id="test_phone" class="base-input" placeholder="15551234567" required pattern="^\+?[0-9\s\-]+$">
<input type="tel" name="phone" id="test_phone" class="base-input" placeholder="15551234567" required pattern="^\+?[0-9\s\-]+$" autocomplete="off">
<span class="label-text-alt mt-1 opacity-70">A short test message is sent to this number using your saved config.</span>
</div>
<div class="form-control">

@ -37,7 +37,11 @@
<p class="font-bold">Google SSO is not configured</p>
<p class="text-gray-700">
Fill in your Google Cloud OAuth client details below. The OAuth redirect URI to register in <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener" class="link">Google Cloud Console</a> is
<code><%= "#{root_url}auth/google_oauth2/callback" rescue '/auth/google_oauth2/callback' %></code>.
<code><%= begin
"#{root_url}auth/google_oauth2/callback"
rescue StandardError
'/auth/google_oauth2/callback'
end %></code>.
</p>
</div>
</div>

@ -55,8 +55,8 @@
<div id="js_1" class="block my-4">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: "_blank", data: { turbo: false }, class: "btn btn-ghost text-gray-100 flex", rel: "noopener" %>
<clipboard-copy data-text="<script src=&quot;<%= embed_script_url(filename: "form.js") %>&quot;></script>
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<script src=&quot;<%= embed_script_url(filename: 'form.js') %>&quot;></script>
<docuseal-form data-src=&quot;<%= start_form_url(slug: template.slug) %>&quot;></docuseal-form>
">
@ -78,7 +78,7 @@
</clipboard-copy>
</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= embed_script_url(filename: "form.js") %>"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<pre class="before:!m-0 pl-6 pb-4 overflow-auto"><code class="overflow-hidden w-full"><span style="color: #f4bf75">&lt;script </span><span style="color: #6a9fb5">src=</span><span style="color: #90a959">"<%= embed_script_url(filename: 'form.js') %>"</span><span style="color: #f4bf75">&gt;&lt;/script&gt;</span>
<span style="color: #f4bf75">&lt;docuseal-form</span> <span style="color: #6a9fb5">data-src=</span><span style="color: #90a959">"<%= start_form_url(slug: template.slug) %>"</span><span style="color: #f4bf75">&gt;</span><span style="color: #f4bf75">&lt;/docuseal-form&gt;</span>
</code></pre>
@ -88,7 +88,7 @@
<div id="react_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: "_blank", data: { turbo: false }, class: "btn btn-ghost text-gray-100 flex", rel: "noopener" %>
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import React from &quot;react&quot;
import { DocusealForm } from '@docuseal/react'
@ -135,7 +135,7 @@ export function App() {
<div id="vue_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: "_blank", data: { turbo: false }, class: "btn btn-ghost text-gray-100 flex", rel: "noopener" %>
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="<template>
<DocusealForm
:src=&quot;'<%= start_form_url(slug: template.slug) %>'&quot;
@ -193,7 +193,7 @@ export default {
<div id="angular_1" class="block my-4 hidden">
<div class="mockup-code overflow-hidden pb-0 mt-4">
<span class="top-0 right-0 absolute flex">
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: "_blank", data: { turbo: false }, class: "btn btn-ghost text-gray-100 flex", rel: "noopener" %>
<%= link_to t(:learn_more), "#{Wabosign::PRODUCT_URL}/docs/embedding", target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
<clipboard-copy data-text="import { Component } from '@angular/core';
import { DocusealFormComponent } from '@docuseal/angular';

@ -16,13 +16,13 @@
"fingerprint": "8bf010d01d5cfabdc2124db1378ca14a24a675431047291488abc186d10ba314",
"note": "Safe SQL"
},
{
"fingerprint": "f3a20210cde7b9cb5944d53505fe80fea502308416143f4da9ec2422f6b7035c",
"note": "Safe Param"
},
{
"fingerprint": "dbbfb4a4ace7f43d8247cbb44afa8b628e005e6194ca5552e029b200f725a2d5",
"message": "Unescaped find_by!(uuid: params[:id]) is not risky"
},
{
"fingerprint": "4ce817efd946b7806f6d3da9a6923aa282e3ff992810353ed35d8f83a82cb7a0",
"note": "HighlightCode returns escaped HTML for syntax-highlighted MCP token preview"
}
]
}

@ -51,12 +51,10 @@ module AccountLogo
next
end
if EXTERNAL_REF_ATTRS.include?(downcased) || downcased.end_with?(':href')
next unless EXTERNAL_REF_ATTRS.include?(downcased) || downcased.end_with?(':href')
value = attr.value.to_s.strip
unless value.start_with?('#') || value.start_with?('data:')
node.remove_attribute(name)
end
end
node.remove_attribute(name) unless value.start_with?('#', 'data:')
end
end

@ -126,7 +126,7 @@ module Wabosign
return {
client_id: db_value['client_id'].to_s,
client_secret: db_value['client_secret'].to_s,
allowed_domains: Array(db_value['allowed_domains']).map(&:to_s).map(&:strip).reject(&:empty?),
allowed_domains: Array(db_value['allowed_domains']).filter_map { |d| d.to_s.strip.presence },
source: :db
}
end
@ -147,12 +147,12 @@ module Wabosign
creds[:client_id].present? && creds[:client_secret].present?
end
def google_domain_allowed?(hd)
return false if hd.blank?
def google_domain_allowed?(hosted_domain)
return false if hosted_domain.blank?
domains = google_sso_credentials[:allowed_domains]
return true if domains.empty?
domains.include?(hd)
domains.include?(hosted_domain)
end
end

@ -57,9 +57,13 @@ RSpec.describe AccountLogo do
describe '.sanitize_upload' do
it 'returns the original tempfile for PNG uploads' do
png_bytes = File.binread(Rails.root.join('public/favicon-32x32.png'))
png_bytes = Rails.public_path.join('favicon-32x32.png').binread
file = ActionDispatch::Http::UploadedFile.new(
tempfile: Tempfile.new(['logo', '.png']).tap { |t| t.binmode; t.write(png_bytes); t.rewind },
tempfile: Tempfile.new(['logo', '.png']).tap do |t|
t.binmode
t.write(png_bytes)
t.rewind
end,
filename: 'logo.png', type: 'image/png'
)
@ -72,7 +76,10 @@ RSpec.describe AccountLogo do
it 'sanitises SVG content and returns a StringIO with cleaned bytes' do
svg = '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script><rect/></svg>'
file = ActionDispatch::Http::UploadedFile.new(
tempfile: Tempfile.new(['logo', '.svg']).tap { |t| t.write(svg); t.rewind },
tempfile: Tempfile.new(['logo', '.svg']).tap do |t|
t.write(svg)
t.rewind
end,
filename: 'logo.svg', type: 'image/svg+xml'
)

@ -50,7 +50,7 @@ RSpec.describe Ability do
let(:user) { create(:user, account: account, role: User::ADMIN_ROLE) }
let(:ability) { described_class.new(user) }
include_examples 'personal-resource grants'
it_behaves_like 'personal-resource grants'
it 'manages templates, folders, sharings, submissions, submitters in own account' do
expect(ability).to be_able_to(:read, template_for(account))
@ -85,7 +85,7 @@ RSpec.describe Ability do
let(:user) { create(:user, account: account, role: User::EDITOR_ROLE) }
let(:ability) { described_class.new(user) }
include_examples 'personal-resource grants'
it_behaves_like 'personal-resource grants'
it 'manages templates, folders, sharings, submissions, submitters in own account' do
expect(ability).to be_able_to(:read, template_for(account))
@ -118,7 +118,7 @@ RSpec.describe Ability do
let(:user) { create(:user, account: account, role: User::VIEWER_ROLE) }
let(:ability) { described_class.new(user) }
include_examples 'personal-resource grants'
it_behaves_like 'personal-resource grants'
it 'reads templates, folders, sharings, submissions, submitters in own account' do
expect(ability).to be_able_to(:read, template_for(account))

@ -4,7 +4,7 @@ require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
ENV['TZ'] ||= 'UTC'
require_relative '../config/environment'
abort('The Rails environment is running in production mode!') if Rails.env.production?
abort('The Rails environment is running in production mode!') if Rails.env.production? # rubocop:disable Rails/Exit
require 'rspec/rails'
require 'capybara/cuprite'
require 'capybara/rspec'
@ -42,7 +42,7 @@ Rails.root.glob('spec/support/**/*.rb').each { |f| require f }
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
abort e.to_s.strip
abort e.to_s.strip # rubocop:disable Rails/Exit
end
RSpec.configure do |config|

@ -18,7 +18,7 @@ RSpec.describe 'Account logo', type: :request do
describe 'POST /settings/account_logo' do
it 'accepts a PNG upload and attaches it to the current account' do
png_bytes = File.binread(Rails.root.join('public/favicon-32x32.png'))
png_bytes = Rails.public_path.join('favicon-32x32.png').binread
expect do
post settings_account_logo_path, params: { logo: upload(content_type: 'image/png', bytes: png_bytes) }
@ -31,7 +31,8 @@ RSpec.describe 'Account logo', type: :request do
it 'rejects an unsupported content type' do
pdf_bytes = '%PDF-1.4 dummy'
post settings_account_logo_path, params: { logo: upload(content_type: 'application/pdf', bytes: pdf_bytes, filename: 'logo.pdf') }
post settings_account_logo_path,
params: { logo: upload(content_type: 'application/pdf', bytes: pdf_bytes, filename: 'logo.pdf') }
expect(account.reload.logo.attached?).to be(false)
expect(flash[:alert]).to include('PNG, JPEG, or SVG')
@ -49,7 +50,8 @@ RSpec.describe 'Account logo', type: :request do
it 'sanitises malicious SVG content before storing' do
malicious = '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script><rect onload="hax()" /></svg>'
post settings_account_logo_path, params: { logo: upload(content_type: 'image/svg+xml', bytes: malicious, filename: 'logo.svg') }
post settings_account_logo_path,
params: { logo: upload(content_type: 'image/svg+xml', bytes: malicious, filename: 'logo.svg') }
expect(account.reload.logo.attached?).to be(true)
stored = account.logo.download
@ -62,7 +64,7 @@ RSpec.describe 'Account logo', type: :request do
describe 'DELETE /settings/account_logo' do
it 'purges the attachment' do
png_bytes = File.binread(Rails.root.join('public/favicon-32x32.png'))
png_bytes = Rails.public_path.join('favicon-32x32.png').binread
account.logo.attach(io: StringIO.new(png_bytes), filename: 'logo.png', content_type: 'image/png')
expect(account.reload.logo.attached?).to be(true)

@ -37,10 +37,10 @@ RSpec.describe 'Role-based authorization', type: :request do
end
describe 'admin-only settings' do
include_examples 'an admin-only settings route', :settings_users_path
include_examples 'an admin-only settings route', :settings_sso_index_path
include_examples 'an admin-only settings route', :settings_webhooks_path
include_examples 'an admin-only settings route', :settings_esign_path
it_behaves_like 'an admin-only settings route', :settings_users_path
it_behaves_like 'an admin-only settings route', :settings_sso_index_path
it_behaves_like 'an admin-only settings route', :settings_webhooks_path
it_behaves_like 'an admin-only settings route', :settings_esign_path
# Personalization's GET reads `AccountConfig`, which Editor/Viewer can do
# (so UI chrome renders correctly). Writes are gated by :create AccountConfig,

@ -4,11 +4,12 @@ require 'rails_helper'
RSpec.describe 'Google OAuth2 callback', type: :request do
let!(:account) { create(:account) }
before do
# ApplicationController redirects to /setup when no users exist; create a
# placeholder admin so that branch doesn't fire during these specs.
let!(:placeholder_admin) { create(:user, account: account, email: 'admin@wabo.cc') }
create(:user, account: account, email: 'admin@wabo.cc')
before do
OmniAuth.config.test_mode = true
OmniAuth.config.logger = Rails.logger
@ -26,12 +27,12 @@ RSpec.describe 'Google OAuth2 callback', type: :request do
OmniAuth.config.mock_auth[:google_oauth2] = nil
end
def stub_google_auth(email:, uid: '1234567890', hd: 'wabo.cc', first_name: 'Test', last_name: 'User')
def stub_google_auth(email:, uid: '1234567890', hosted_domain: 'wabo.cc', first_name: 'Test', last_name: 'User')
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
provider: 'google_oauth2',
uid: uid,
info: { email: email, first_name: first_name, last_name: last_name },
extra: { raw_info: OmniAuth::AuthHash.new(hd: hd) }
extra: { raw_info: OmniAuth::AuthHash.new(hd: hosted_domain) }
)
end
@ -70,7 +71,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do
describe 'disallowed Workspace domain' do
it 'redirects back to sign-in with a flash' do
stub_google_auth(email: 'outsider@evil.com', hd: 'evil.com')
stub_google_auth(email: 'outsider@evil.com', hosted_domain: 'evil.com')
expect do
post user_google_oauth2_omniauth_callback_path
@ -100,7 +101,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do
end
describe '2FA bypass' do
let!(:user) do
before do
create(:user, account: account, email: '2fa@wabo.cc').tap do |u|
u.update_columns(otp_required_for_login: true, otp_secret: User.generate_otp_secret)
end

@ -19,7 +19,9 @@ RSpec.describe 'Dashboard Page' do
context 'when there are templates' do
let!(:authors) { create_list(:user, 5, account:) }
let!(:templates) { authors.map { |author| create(:template, account:, author:) } }
let!(:other_template) { create(:template, account: create(:user).account) }
let!(:other_template) do
create(:template, name: "OTHER-ACCOUNT-#{SecureRandom.hex(8)}", account: create(:user).account)
end
before do
visit root_path

Loading…
Cancel
Save