mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
398 lines
12 KiB
Ruby
398 lines
12 KiB
Ruby
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||
|
||
require 'rails_helper'
|
||
|
||
RSpec.describe Token, type: :model do
|
||
subject(:token) { create(:password_reset_token, user: user) }
|
||
|
||
let(:user) { create(:user) }
|
||
|
||
describe '.check' do
|
||
context 'with name and action matching existing token' do
|
||
it 'returns the token’s user' do
|
||
expect(described_class.check(action: token.action, token: token.token)).to eq(token.user)
|
||
end
|
||
end
|
||
|
||
context 'with invalid name' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: '1NV4L1D')).to be_nil
|
||
end
|
||
end
|
||
|
||
context 'with invalid action' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: 'PasswordReset_NotExisting', token: token.token)).to be_nil
|
||
end
|
||
end
|
||
|
||
describe 'persistence handling' do
|
||
context 'for persistent token' do
|
||
subject(:token) { create(:ical_token, persistent: true, created_at: created_at) }
|
||
|
||
context 'at any time' do
|
||
let(:created_at) { 1.month.ago }
|
||
|
||
it 'returns the token’s user' do
|
||
expect(described_class.check(action: token.action, token: token.token)).to eq(token.user)
|
||
end
|
||
|
||
it 'does not delete the token' do
|
||
token # create token
|
||
|
||
expect { described_class.check(action: token.action, token: token.token) }
|
||
.not_to change(described_class, :count)
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'for non-persistent token' do
|
||
subject(:token) { create(:password_reset_token, persistent: false, created_at: created_at) }
|
||
|
||
context 'less than one day after creation' do
|
||
let(:created_at) { 1.day.ago + 5 }
|
||
|
||
it 'returns the token’s user' do
|
||
expect(described_class.check(action: token.action, token: token.token)).to eq(token.user)
|
||
end
|
||
|
||
it 'does not delete the token' do
|
||
token # create token
|
||
|
||
expect { described_class.check(action: token.action, token: token.token) }
|
||
.not_to change(described_class, :count)
|
||
end
|
||
end
|
||
|
||
context 'at least one day after creation' do
|
||
let(:created_at) { 1.day.ago }
|
||
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: token.token)).to be_nil
|
||
end
|
||
|
||
it 'deletes the token' do
|
||
token # create token
|
||
|
||
expect { described_class.check(action: token.action, token: token.token) }
|
||
.to change(described_class, :count).by(-1)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe 'permission matching' do
|
||
subject(:token) { create(:api_token, user: agent, preferences: preferences) }
|
||
|
||
let(:agent) { create(:agent) }
|
||
let(:preferences) { { permission: %w[admin ticket.agent] } } # agent has no access to admin.*
|
||
|
||
context 'with a permission shared by both token.user and token.preferences' do
|
||
it 'returns token.user' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'ticket.agent')).to eq(agent)
|
||
end
|
||
end
|
||
|
||
context 'with the child of a permission shared by both token.user and token.preferences' do
|
||
it 'returns token.user' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'ticket.agent.foo')).to eq(agent)
|
||
end
|
||
end
|
||
|
||
context 'with the parent of a permission shared by both token.user and token.preferences' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'ticket')).to be_nil
|
||
end
|
||
end
|
||
|
||
context 'with a permission in token.preferences, but not on token.user' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'admin')).to be_nil
|
||
end
|
||
end
|
||
|
||
context 'with a permission not in token.preferences, but on token.user' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'cti.agent')).to be_nil
|
||
end
|
||
end
|
||
|
||
context 'with non-existent permission' do
|
||
it 'returns nil' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: 'foo')).to be_nil
|
||
end
|
||
end
|
||
|
||
context 'with multiple permissions, where at least one is shared by both token.user and token.preferences' do
|
||
it 'returns token.user' do
|
||
expect(described_class.check(action: token.action, token: token.token, permission: %w[foo ticket.agent])).to eq(agent)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#fetch' do
|
||
it 'returns nil when not present' do
|
||
expect(described_class.fetch('token', user)).to be_nil
|
||
end
|
||
|
||
it 'returns token when present' do
|
||
token
|
||
|
||
expect(described_class.fetch(token.action, token.user.id)).to eq(token)
|
||
end
|
||
end
|
||
|
||
describe '.ensure_token!' do
|
||
it 'returns token when not present' do
|
||
expect(described_class.ensure_token!('token', user.id)).to be_present
|
||
end
|
||
|
||
it 'returns token when present' do
|
||
token
|
||
|
||
expect(described_class.ensure_token!(token.action, token.user.id)).to eq(token.token)
|
||
end
|
||
|
||
describe 'with persistent argument' do
|
||
it 'creates not-persistent token if argument omitted' do
|
||
described_class.ensure_token!('token', user.id)
|
||
expect(described_class.find_by(action: 'token')).not_to be_persistent
|
||
end
|
||
|
||
it 'creates persistent token when flag given' do
|
||
described_class.ensure_token!('token', user.id, persistent: true)
|
||
expect(described_class.find_by(action: 'token')).to be_persistent
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#renew_token!' do
|
||
it 'changes token' do
|
||
expect { token.renew_token! }.to change { token.reload.token }
|
||
end
|
||
end
|
||
|
||
describe '.renew_token!' do
|
||
it 'creates token when not present' do
|
||
expect(described_class.renew_token!('token', user.id)).to be_present
|
||
end
|
||
|
||
it 'returns token when present' do
|
||
token
|
||
|
||
expect { described_class.renew_token!(token.action, token.user.id) }.to change { token.reload.token }
|
||
end
|
||
|
||
describe 'with persistent argument' do
|
||
it 'creates not-persistent token if argument omitted' do
|
||
described_class.renew_token!('token', user.id)
|
||
|
||
expect(described_class.find_by(action: 'token')).not_to be_persistent
|
||
end
|
||
|
||
it 'creates persistent token when flag given' do
|
||
described_class.renew_token!('token', user.id, persistent: true)
|
||
|
||
expect(described_class.find_by(action: 'token')).to be_persistent
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#visible_in_frontend?' do
|
||
it 'persistent api token is visible in frontend' do
|
||
token = create(:token)
|
||
|
||
expect(token).to be_visible_in_frontend
|
||
end
|
||
|
||
it 'persistent non-api token is not visible in frontend' do
|
||
token = create(:token, action: :nonapi)
|
||
|
||
expect(token).not_to be_visible_in_frontend
|
||
end
|
||
|
||
it 'non-persistent api token is not visible in frontend' do
|
||
token = create(:token, persistent: false)
|
||
|
||
expect(token).not_to be_visible_in_frontend
|
||
end
|
||
end
|
||
|
||
describe '#trigger_user_subscription' do
|
||
it 'triggers subscription when token is created' do
|
||
allow(Gql::Subscriptions::User::Current::AccessTokenUpdates).to receive(:trigger)
|
||
|
||
create(:token)
|
||
|
||
expect(Gql::Subscriptions::User::Current::AccessTokenUpdates).to have_received(:trigger)
|
||
end
|
||
|
||
it 'triggers subscription when token is destroyed' do
|
||
token = create(:token)
|
||
|
||
allow(Gql::Subscriptions::User::Current::AccessTokenUpdates).to receive(:trigger)
|
||
|
||
token.destroy
|
||
|
||
expect(Gql::Subscriptions::User::Current::AccessTokenUpdates).to have_received(:trigger)
|
||
end
|
||
|
||
it 'does not trigger subscription when token is updated' do
|
||
token = create(:token)
|
||
|
||
allow(Gql::Subscriptions::User::Current::AccessTokenUpdates).to receive(:trigger)
|
||
|
||
token.touch
|
||
|
||
expect(Gql::Subscriptions::User::Current::AccessTokenUpdates).not_to have_received(:trigger)
|
||
end
|
||
|
||
it 'does not trigger subscription when non-api token is created' do
|
||
allow(Gql::Subscriptions::User::Current::AccessTokenUpdates).to receive(:trigger)
|
||
|
||
create(:token, action: :nonapi)
|
||
|
||
expect(Gql::Subscriptions::User::Current::AccessTokenUpdates).not_to have_received(:trigger)
|
||
end
|
||
end
|
||
|
||
describe '.cleanup' do
|
||
context 'when token is non persistent and old' do
|
||
let(:token) { create(:token, persistent: false, created_at: 1.year.ago) }
|
||
|
||
it 'is removed' do
|
||
expect { described_class.cleanup }
|
||
.to change { described_class.exists? token.id }
|
||
.to false
|
||
end
|
||
end
|
||
|
||
context 'when token is non persistent and fresh' do
|
||
let(:token) { create(:token, persistent: false, created_at: 1.day.ago) }
|
||
|
||
it 'is not removed' do
|
||
expect { described_class.cleanup }
|
||
.not_to change { described_class.exists? token.id }
|
||
.from true
|
||
end
|
||
end
|
||
|
||
context 'when token is non persistent and has an expiration date in the past' do
|
||
let(:token) { create(:token, persistent: false, expires_at: 30.minutes.ago, created_at: 1.hour.ago) }
|
||
|
||
it 'is removed' do
|
||
expect { described_class.cleanup }
|
||
.to change { described_class.exists? token.id }
|
||
.to false
|
||
end
|
||
end
|
||
|
||
context 'when token is non persistent and has an expiration date in the future' do
|
||
let(:token) { create(:token, persistent: false, expires_at: 30.minutes.from_now, created_at: 30.minutes.ago) }
|
||
|
||
it 'is not removed' do
|
||
expect { described_class.cleanup }
|
||
.not_to change { described_class.exists? token.id }
|
||
.from true
|
||
end
|
||
end
|
||
|
||
context 'when token is persistent and old' do
|
||
let(:token) { create(:token, persistent: true, created_at: 31.days.ago) }
|
||
|
||
it 'is not removed' do
|
||
expect { described_class.cleanup }
|
||
.not_to change { described_class.exists? token.id }
|
||
.from true
|
||
end
|
||
end
|
||
end
|
||
|
||
describe '#expired?' do
|
||
context 'when token has an expiration date' do
|
||
subject(:token) { create(:token, persistent: false, expires_at: expires_at) }
|
||
|
||
context 'with the expiration date in the future' do
|
||
context 'with day precision' do
|
||
let(:expires_at) { 1.day.from_now }
|
||
|
||
it { is_expected.not_to be_expired }
|
||
end
|
||
|
||
context 'with hour precision' do
|
||
let(:expires_at) { 1.hour.from_now }
|
||
|
||
it { is_expected.not_to be_expired }
|
||
end
|
||
end
|
||
|
||
context 'with the expiration date in the past' do
|
||
context 'with day precision' do
|
||
let(:expires_at) { 1.day.ago }
|
||
|
||
it { is_expected.to be_expired }
|
||
end
|
||
|
||
context 'with hour precision' do
|
||
let(:expires_at) { 1.hour.ago }
|
||
|
||
it { is_expected.to be_expired }
|
||
end
|
||
end
|
||
end
|
||
|
||
context 'when token has no expiration date date' do
|
||
let(:token) { create(:token, persistent: false) }
|
||
|
||
it { is_expected.not_to be_expired }
|
||
end
|
||
end
|
||
|
||
describe '.validate!' do
|
||
let(:action) { 'example' }
|
||
let(:user) { create(:user) }
|
||
let(:expires_at) { nil }
|
||
let(:token) { create(:token, action:, expires_at:, user:) }
|
||
|
||
before { token }
|
||
|
||
it 'returns token if all is good' do
|
||
expect(described_class.validate!(action:, token: token.token, user:))
|
||
.to eq(token)
|
||
end
|
||
|
||
it 'returns token if all is good for current user', current_user_id: -> { user.id } do
|
||
expect(described_class.validate!(action:, token: token.token))
|
||
.to eq(token)
|
||
end
|
||
|
||
it 'raises error if token does not exist' do
|
||
expect { described_class.validate!(action:, token: 'nonexistant') }
|
||
.to raise_error(Token::TokenAbsent)
|
||
end
|
||
|
||
it 'raises error if token belongs to another user' do
|
||
expect { described_class.validate!(action:, token: token.token, user: create(:agent)) }
|
||
.to raise_error(Token::TokenAbsent)
|
||
end
|
||
|
||
context 'when token has expiration date in the future' do
|
||
let(:expires_at) { 1.day.from_now }
|
||
|
||
it 'returns token' do
|
||
expect(described_class.validate!(action:, token: token.token, user:))
|
||
.to eq(token)
|
||
end
|
||
end
|
||
|
||
context 'when token has expiration date in the past' do
|
||
let(:expires_at) { 1.day.ago }
|
||
|
||
it 'raises error' do
|
||
expect { described_class.validate!(action:, token: token.token, user:) }
|
||
.to raise_error(Token::TokenExpired)
|
||
end
|
||
end
|
||
end
|
||
end
|