Implandação da customização das permissões dos usuarios e das notificaçoes
This commit is contained in:
@@ -10,6 +10,8 @@ FactoryBot.define do
|
||||
factory :gerente do role { :gerente } end
|
||||
factory :operador do role { :operador } end
|
||||
|
||||
factory :externo do role { :externo } end
|
||||
|
||||
factory :motorista do
|
||||
role { :motorista }
|
||||
# email é NOT NULL + único no banco; motorista real não tem e-mail, então
|
||||
@@ -21,6 +23,12 @@ FactoryBot.define do
|
||||
end
|
||||
end
|
||||
|
||||
factory :perfil_acesso do
|
||||
sequence(:nome) { |n| "Perfil #{n}" }
|
||||
# Um conjunto pequeno e realista: ver e fechar, sem pagar nem administrar.
|
||||
permissoes { %w[consolidacao.ver consolidacao.editar consolidacao.finalizar] }
|
||||
end
|
||||
|
||||
factory :configuracao do
|
||||
chave { 'empresa_nome' }
|
||||
valor { 'Reem Transporte' }
|
||||
@@ -53,6 +61,12 @@ FactoryBot.define do
|
||||
valor_aplicado { 15.0 }
|
||||
end
|
||||
|
||||
factory :variavel_personalizada do
|
||||
sequence(:chave) { |n| "variavel_#{n}" }
|
||||
valor { 'Valor de teste' }
|
||||
ativo { true }
|
||||
end
|
||||
|
||||
factory :grupo_contato do
|
||||
sequence(:nome) { |n| "Grupo #{n}" }
|
||||
ativo { true }
|
||||
|
||||
60
spec/models/perfil_acesso_spec.rb
Normal file
60
spec/models/perfil_acesso_spec.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PerfilAcesso do
|
||||
it 'descarta chave que não existe mais no catálogo ao salvar' do
|
||||
perfil = create(:perfil_acesso, permissoes: ['consolidacao.ver', 'permissao.aposentada'])
|
||||
|
||||
expect(perfil.permissoes).to eq(['consolidacao.ver'])
|
||||
expect(perfil).to be_pode('consolidacao.ver')
|
||||
expect(perfil).not_to be_pode('permissao.aposentada')
|
||||
end
|
||||
|
||||
it 'não aceita dois perfis com o mesmo nome' do
|
||||
create(:perfil_acesso, nome: 'Financeiro')
|
||||
expect(build(:perfil_acesso, nome: 'financeiro')).not_to be_valid
|
||||
end
|
||||
|
||||
describe '#apagavel?' do
|
||||
it 'perfil de sistema não é apagável' do
|
||||
expect(build(:perfil_acesso, sistema: true)).not_to be_apagavel
|
||||
end
|
||||
|
||||
it 'perfil em uso não é apagável (o usuário cairia no fallback sem avisar)' do
|
||||
perfil = create(:perfil_acesso)
|
||||
create(:operador, perfil_acesso: perfil)
|
||||
|
||||
expect(perfil.reload).not_to be_apagavel
|
||||
end
|
||||
|
||||
it 'perfil livre é apagável' do
|
||||
expect(create(:perfil_acesso)).to be_apagavel
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permissões do usuário' do
|
||||
it 'o perfil vence o padrão do papel' do
|
||||
# Gerente por papel veria Configurações; o perfil tira só isso.
|
||||
perfil = create(:perfil_acesso, permissoes: Permissao.do_role('gerente') - ['admin.configuracoes'])
|
||||
usuario = create(:gerente, perfil_acesso: perfil)
|
||||
|
||||
expect(usuario.pode?('consolidacao.registrar_pagamento')).to be(true)
|
||||
expect(usuario.pode?('admin.configuracoes')).to be(false)
|
||||
expect(usuario.pode_ver_config?).to be(false)
|
||||
end
|
||||
|
||||
it 'sem perfil, vale o padrão do papel (e não consulta o banco)' do
|
||||
usuario = build(:gerente) # não persistido, como nos specs de policy
|
||||
|
||||
expect(usuario.pode_consolidar?).to be(true)
|
||||
expect(usuario.pode_ver_config?).to be(true)
|
||||
expect(usuario.pode?('admin.perfis')).to be(false)
|
||||
end
|
||||
|
||||
it 'perfil vazio tira tudo sem precisar desativar a conta' do
|
||||
usuario = create(:operador, perfil_acesso: create(:perfil_acesso, permissoes: []))
|
||||
|
||||
expect(usuario.pode?('consolidacao.ver')).to be(false)
|
||||
expect(usuario.pode_consolidar?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
73
spec/models/permissao_spec.rb
Normal file
73
spec/models/permissao_spec.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# O catálogo é a fonte única de "o que dá para liberar". Estas travas existem
|
||||
# porque um erro aqui não aparece na tela: uma chave escrita errado no
|
||||
# PADRAO_POR_ROLE simplesmente não bate com nada e tira acesso de todo um papel
|
||||
# em silêncio.
|
||||
RSpec.describe Permissao do
|
||||
describe 'catálogo' do
|
||||
it 'toda chave segue o padrão area.acao e pertence a um grupo declarado' do
|
||||
described_class::TODAS.each do |chave, cfg|
|
||||
expect(chave.count('.')).to eq(1), "#{chave} fora do padrão area.acao"
|
||||
expect(described_class::GRUPOS).to have_key(cfg[:grupo]), "#{chave}: grupo #{cfg[:grupo]} não existe"
|
||||
expect(cfg[:label]).to be_present
|
||||
expect(cfg[:descricao]).to be_present, "#{chave} sem descrição (é o texto que o ADM lê na tela)"
|
||||
end
|
||||
end
|
||||
|
||||
it 'por_grupo cobre todas as chaves' do
|
||||
total = described_class.por_grupo.values.sum(&:size)
|
||||
expect(total).to eq(described_class.chaves.size)
|
||||
end
|
||||
|
||||
it 'filtrar descarta chave inexistente e repetida' do
|
||||
expect(described_class.filtrar(['consolidacao.ver', 'nao.existe', 'consolidacao.ver']))
|
||||
.to eq(['consolidacao.ver'])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PADRAO_POR_ROLE' do
|
||||
it 'só usa chaves que existem no catálogo' do
|
||||
described_class::PADRAO_POR_ROLE.each do |role, lista|
|
||||
next if lista == :todas
|
||||
|
||||
lista.each do |chave|
|
||||
expect(described_class).to be_existe(chave), "#{role} referencia chave inexistente: #{chave}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'admin recebe o catálogo inteiro' do
|
||||
expect(described_class.do_role('admin')).to match_array(described_class.chaves)
|
||||
end
|
||||
|
||||
# O fallback é o que mantém o comportamento de hoje para quem ainda não tem
|
||||
# perfil. Se estes pares mudarem, alguém perde acesso no dia do deploy.
|
||||
it 'reproduz pode_consolidar? e pode_ver_config? de cada papel' do
|
||||
{
|
||||
'admin' => [true, true],
|
||||
'gerente' => [true, true],
|
||||
'operador' => [true, false],
|
||||
'motorista' => [false, false],
|
||||
'externo' => [false, false]
|
||||
}.each do |role, (consolidar, config)|
|
||||
chaves = described_class.do_role(role)
|
||||
expect(chaves.include?('consolidacao.ver')).to eq(consolidar), "#{role}: consolidar"
|
||||
expect(chaves.include?('admin.configuracoes')).to eq(config), "#{role}: config"
|
||||
end
|
||||
end
|
||||
|
||||
# As duas mudanças de comportamento deliberadas desta entrega.
|
||||
it 'externo não enxerga o financeiro (era o furo do skip_authorization)' do
|
||||
expect(described_class.do_role('externo')).not_to include('dashboard.financeiro')
|
||||
end
|
||||
|
||||
it 'operador não enxerga a auditoria (o menu nunca ofereceu)' do
|
||||
expect(described_class.do_role('operador')).not_to include('admin.auditoria')
|
||||
end
|
||||
|
||||
it 'motorista não usa permissão — o acesso dele é por tipo de conta' do
|
||||
expect(described_class.do_role('motorista')).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
27
spec/models/variavel_personalizada_spec.rb
Normal file
27
spec/models/variavel_personalizada_spec.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VariavelPersonalizada do
|
||||
it 'normaliza a chave (minúscula, sem espaço)' do
|
||||
v = create(:variavel_personalizada, chave: ' Telefone Suporte ', valor: '4002-8922')
|
||||
expect(v.chave).to eq('telefone_suporte')
|
||||
expect(v.marcador).to eq('{{telefone_suporte}}')
|
||||
end
|
||||
|
||||
it 'recusa chave com formato que o renderizador não substituiria' do
|
||||
expect(build(:variavel_personalizada, chave: '2telefone')).not_to be_valid
|
||||
expect(build(:variavel_personalizada, chave: 'telefone-suporte')).not_to be_valid
|
||||
end
|
||||
|
||||
# Deixar o ADM criar {{valor}} faria a variável dele brigar com a do sistema —
|
||||
# e o preview não mostraria o problema, só o envio real.
|
||||
it 'recusa chave que já existe no sistema' do
|
||||
v = build(:variavel_personalizada, chave: 'valor')
|
||||
expect(v).not_to be_valid
|
||||
expect(v.errors[:chave].join).to include('já existe no sistema')
|
||||
end
|
||||
|
||||
it 'não aceita duas variáveis com a mesma chave' do
|
||||
create(:variavel_personalizada, chave: 'site', valor: 'a')
|
||||
expect(build(:variavel_personalizada, chave: 'SITE', valor: 'b')).not_to be_valid
|
||||
end
|
||||
end
|
||||
101
spec/requests/admin/perfis_acesso_spec.rb
Normal file
101
spec/requests/admin/perfis_acesso_spec.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Perfis de acesso: quem entra na tela, e as travas que impedem o ADM de se
|
||||
# trancar para fora do próprio sistema.
|
||||
RSpec.describe 'Admin::PerfisAcesso', type: :request do
|
||||
# Perfil com acesso total, para o usuário que administra nos testes.
|
||||
let(:perfil_admin) { create(:perfil_acesso, nome: 'Administrador', permissoes: Permissao.chaves) }
|
||||
let(:administrador) { create(:admin, perfil_acesso: perfil_admin) }
|
||||
|
||||
describe 'quem entra' do
|
||||
it 'exige login' do
|
||||
get admin_perfis_path
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
|
||||
it 'nega quem não tem admin.perfis' do
|
||||
sign_in create(:gerente, perfil_acesso: create(:perfil_acesso, permissoes: ['consolidacao.ver']))
|
||||
get admin_perfis_path
|
||||
expect(response).to have_http_status(:redirect)
|
||||
end
|
||||
|
||||
it 'permite quem tem admin.perfis' do
|
||||
sign_in administrador
|
||||
get admin_perfis_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'criar e editar' do
|
||||
before { sign_in administrador }
|
||||
|
||||
it 'cria perfil só com as chaves do catálogo' do
|
||||
post admin_perfis_path, params: {
|
||||
perfil_acesso: { nome: 'Financeiro', descricao: 'Paga',
|
||||
ativo: '1', permissoes: ['consolidacao.ver', 'inventada.chave'] }
|
||||
}
|
||||
|
||||
perfil = PerfilAcesso.find_by(nome: 'Financeiro')
|
||||
expect(perfil.permissoes).to eq(['consolidacao.ver'])
|
||||
end
|
||||
|
||||
it 'desmarcar tudo realmente zera as permissões' do
|
||||
perfil = create(:perfil_acesso, permissoes: ['consolidacao.ver'])
|
||||
|
||||
# O form manda um valor vazio quando nenhuma caixa está marcada; sem ele o
|
||||
# parâmetro sumiria e "desmarcar tudo" viraria "não mexeu".
|
||||
patch admin_perfil_path(perfil), params: {
|
||||
perfil_acesso: { nome: perfil.nome, ativo: '1', permissoes: [''] }
|
||||
}
|
||||
|
||||
expect(perfil.reload.permissoes).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'trava do último administrador' do
|
||||
before { sign_in administrador }
|
||||
|
||||
it 'impede tirar admin.perfis do único perfil que administra' do
|
||||
patch admin_perfil_path(perfil_admin), params: {
|
||||
perfil_acesso: { nome: perfil_admin.nome, ativo: '1',
|
||||
permissoes: Permissao.chaves - ['admin.perfis'] }
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(perfil_admin.reload.permissoes).to include('admin.perfis')
|
||||
end
|
||||
|
||||
it 'libera quando outra pessoa mantém o acesso' do
|
||||
outro = create(:perfil_acesso, nome: 'Admin 2', permissoes: Permissao.chaves)
|
||||
create(:admin, perfil_acesso: outro)
|
||||
|
||||
patch admin_perfil_path(perfil_admin), params: {
|
||||
perfil_acesso: { nome: perfil_admin.nome, ativo: '1',
|
||||
permissoes: Permissao.chaves - ['admin.perfis'] }
|
||||
}
|
||||
|
||||
expect(perfil_admin.reload.permissoes).not_to include('admin.perfis')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'exclusão' do
|
||||
before { sign_in administrador }
|
||||
|
||||
it 'não exclui perfil de sistema' do
|
||||
perfil = create(:perfil_acesso, sistema: true)
|
||||
expect { delete admin_perfil_path(perfil) }.not_to change(PerfilAcesso, :count)
|
||||
end
|
||||
|
||||
it 'não exclui perfil em uso' do
|
||||
perfil = create(:perfil_acesso)
|
||||
create(:operador, perfil_acesso: perfil)
|
||||
|
||||
expect { delete admin_perfil_path(perfil) }.not_to change(PerfilAcesso, :count)
|
||||
end
|
||||
|
||||
it 'exclui perfil livre' do
|
||||
perfil = create(:perfil_acesso)
|
||||
expect { delete admin_perfil_path(perfil) }.to change(PerfilAcesso, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
89
spec/requests/controle_de_acesso_spec.rb
Normal file
89
spec/requests/controle_de_acesso_spec.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# As três falhas de acesso que existiam antes dos perfis. Cada exemplo aqui é um
|
||||
# furo real que foi fechado — se algum voltar a passar "verde ao contrário", o
|
||||
# sistema regrediu para o estado em que qualquer usuário virava admin.
|
||||
RSpec.describe 'Controle de acesso', type: :request do
|
||||
describe 'auto-promoção a administrador' do
|
||||
it 'ignora role e perfil enviados por quem edita a PRÓPRIA ficha' do
|
||||
operador = create(:operador)
|
||||
perfil_total = create(:perfil_acesso, permissoes: Permissao.chaves)
|
||||
sign_in operador
|
||||
|
||||
patch admin_usuario_path(operador), params: {
|
||||
user: { nome: 'Nome Novo', role: 'admin', perfil_acesso_id: perfil_total.id }
|
||||
}
|
||||
|
||||
operador.reload
|
||||
expect(operador.nome).to eq('Nome Novo') # o que ele PODE mudar, mudou
|
||||
expect(operador.role).to eq('operador') # o que ele não pode, não mudou
|
||||
expect(operador.perfil_acesso_id).to be_nil
|
||||
expect(operador.pode?('admin.usuarios_gerenciar')).to be(false)
|
||||
end
|
||||
|
||||
it 'quem administra usuários continua podendo trocar o perfil de outra pessoa' do
|
||||
perfil_admin = create(:perfil_acesso, permissoes: Permissao.chaves)
|
||||
admin = create(:admin, perfil_acesso: perfil_admin)
|
||||
alvo = create(:operador)
|
||||
novo = create(:perfil_acesso, nome: 'Financeiro', permissoes: ['consolidacao.ver'])
|
||||
sign_in admin
|
||||
|
||||
patch admin_usuario_path(alvo), params: { user: { nome: alvo.nome, perfil_acesso_id: novo.id } }
|
||||
|
||||
expect(alvo.reload.perfil_acesso_id).to eq(novo.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'papel externo no dashboard' do
|
||||
it 'entra no dashboard mas não vê custo nem pagamento' do
|
||||
sign_in create(:externo)
|
||||
|
||||
get dashboard_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).not_to include('Consolidado / Pago')
|
||||
expect(response.body).not_to include('Ticket médio')
|
||||
end
|
||||
|
||||
it 'não baixa o relatório financeiro nem abre as entregas em aberto' do
|
||||
sign_in create(:externo)
|
||||
|
||||
get dashboard_relatorio_financeiro_path
|
||||
expect(response).to have_http_status(:redirect)
|
||||
|
||||
get dashboard_pendentes_path
|
||||
expect(response).to have_http_status(:redirect)
|
||||
end
|
||||
|
||||
it 'gerente continua vendo o financeiro' do
|
||||
sign_in create(:gerente)
|
||||
|
||||
get dashboard_path
|
||||
expect(response.body).to include('Consolidado / Pago')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'conta desativada' do
|
||||
it 'não faz login por e-mail e senha' do
|
||||
usuario = create(:gerente, ativo: false, password: 'Gade@2026!')
|
||||
|
||||
post user_session_path, params: { user: { email: usuario.email, password: 'Gade@2026!' } }
|
||||
|
||||
expect(response).not_to redirect_to(dashboard_path)
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sem nenhuma permissão' do
|
||||
it 'cai na tela de sem-acesso em vez de entrar em loop com a raiz' do
|
||||
usuario = create(:operador, perfil_acesso: create(:perfil_acesso, permissoes: []))
|
||||
sign_in usuario
|
||||
|
||||
get dashboard_path
|
||||
expect(response).to redirect_to(sem_acesso_path)
|
||||
|
||||
get sem_acesso_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('não tem acesso')
|
||||
end
|
||||
end
|
||||
end
|
||||
83
spec/services/notificacao/resolvedor_variaveis_spec.rb
Normal file
83
spec/services/notificacao/resolvedor_variaveis_spec.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# O resolvedor é o que permite escrever qualquer variável numa mensagem sem que o
|
||||
# gatilho conheça essa variável. As travas aqui protegem duas coisas: não
|
||||
# sobrescrever o contexto real do acontecimento, e nunca derrubar um disparo.
|
||||
RSpec.describe Notificacao::ResolvedorVariaveis do
|
||||
describe '.usadas' do
|
||||
it 'acha variáveis dentro dos blocos aninhados do template' do
|
||||
blocos = [
|
||||
{ 'tipo' => 'cabecalho', 'titulo' => 'Olá {{contato}}' },
|
||||
{ 'tipo' => 'tabela', 'linhas' => [{ 'rotulo' => 'Valor', 'valor' => '{{valor}}' }] }
|
||||
]
|
||||
|
||||
expect(described_class.usadas([blocos, 'Assunto {{empresa}}']))
|
||||
.to match_array(%w[contato valor empresa])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.completar' do
|
||||
it 'não sobrescreve o que o gatilho já mandou' do
|
||||
dados = described_class.completar(['{{empresa}} {{valor}}'],
|
||||
{ 'empresa' => 'Contexto Ltda', 'valor' => 'R$ 10,00' })
|
||||
|
||||
expect(dados['empresa']).to eq('Contexto Ltda')
|
||||
expect(dados['valor']).to eq('R$ 10,00')
|
||||
end
|
||||
|
||||
it 'resolve variável de sistema' do
|
||||
dados = described_class.completar(['{{data}}'], {})
|
||||
expect(dados['data']).to eq(Date.current.strftime('%d/%m/%Y'))
|
||||
end
|
||||
|
||||
it 'resolve variável personalizada do ADM' do
|
||||
create(:variavel_personalizada, chave: 'telefone_suporte', valor: '(11) 4002-8922')
|
||||
|
||||
dados = described_class.completar(['Ligue {{telefone_suporte}}'], {})
|
||||
expect(dados['telefone_suporte']).to eq('(11) 4002-8922')
|
||||
end
|
||||
|
||||
it 'ignora variável desativada (a mensagem sai sem ela)' do
|
||||
create(:variavel_personalizada, chave: 'promo', valor: 'X', ativo: false)
|
||||
|
||||
expect(described_class.completar(['{{promo}}'], {})).not_to have_key('promo')
|
||||
end
|
||||
|
||||
it 'variável inexistente não vira valor inventado' do
|
||||
expect(described_class.completar(['{{nao_existe}}'], {})).not_to have_key('nao_existe')
|
||||
end
|
||||
|
||||
# Notificação não pode derrubar fechamento: se a consulta falhar, o disparo
|
||||
# segue e a variável sai vazia.
|
||||
it 'engole erro de uma variável de dados' do
|
||||
allow(Entrega).to receive(:atendidas).and_raise(ActiveRecord::StatementInvalid, 'boom')
|
||||
|
||||
expect { described_class.completar(['{{entregas_mes}}'], {}) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'só consulta o banco para a variável realmente escrita' do
|
||||
expect(Entrega).not_to receive(:atendidas)
|
||||
|
||||
described_class.completar(['Sem número aqui, só {{empresa}}'], {})
|
||||
end
|
||||
end
|
||||
|
||||
describe Notificacao::CatalogoVariaveis do
|
||||
it 'marca a variável de outro gatilho como não aplicável (em vez de escondê-la)' do
|
||||
expect(described_class.aplicavel?('valor', 'pagamento_efetuado')).to be(true)
|
||||
expect(described_class.aplicavel?('valor', 'operacao_alterada')).to be(false)
|
||||
end
|
||||
|
||||
it 'variável de dados e de sistema valem em qualquer gatilho' do
|
||||
expect(described_class.aplicavel?('entregas_mes', 'manual')).to be(true)
|
||||
expect(described_class.aplicavel?('empresa', 'operacao_alterada')).to be(true)
|
||||
end
|
||||
|
||||
it 'inclui as variáveis criadas pelo ADM' do
|
||||
create(:variavel_personalizada, chave: 'horario_atendimento', valor: '8h às 18h')
|
||||
|
||||
expect(described_class.todas).to have_key('horario_atendimento')
|
||||
expect(described_class.por_origem[:personalizada].map { |v| v[:chave] }).to include('horario_atendimento')
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user