|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
3 | 3 | require 'rails_helper' |
| 4 | +require_relative '../../../app/services/common/jwt_wrapper' |
4 | 5 |
|
5 | 6 | describe Common::JwtWrapper do |
6 | | - subject { described_class.new(settings) } |
| 7 | + subject { described_class.new(settings, service_config) } |
7 | 8 |
|
8 | | - let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) } |
| 9 | + let(:service_name) { 'TestService' } |
| 10 | + let(:service_config) { instance_double(VAOS::Configuration, service_name:) } |
9 | 11 | let(:settings) do |
10 | | - OpenStruct.new({ |
11 | | - key_path: '/path/to/key.pem', |
12 | | - client_id: 'test_client', |
13 | | - kid: 'test_kid', |
14 | | - audience_claim_url: 'https://test.example.com' |
15 | | - }) |
| 12 | + OpenStruct.new( |
| 13 | + key_path: '/path/to/key.pem', |
| 14 | + client_id: 'test_client', |
| 15 | + kid: 'test_kid', |
| 16 | + audience_claim_url: 'http://test.example.com/token' |
| 17 | + ) |
16 | 18 | end |
17 | 19 |
|
| 20 | + let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) } |
| 21 | + |
18 | 22 | before do |
19 | | - allow(File).to receive(:read).with('/path/to/key.pem').and_return(rsa_key) |
| 23 | + allow(File).to receive(:exist?).and_call_original |
| 24 | + allow(File).to receive(:exist?).with('/path/to/key.pem').and_return(true) |
| 25 | + allow(File).to receive(:read).with('/path/to/key.pem').and_return(rsa_key.to_s) |
20 | 26 | end |
21 | 27 |
|
22 | 28 | describe 'constants' do |
|
36 | 42 | end |
37 | 43 |
|
38 | 44 | describe '#sign_assertion' do |
39 | | - let(:jwt_token) { subject.sign_assertion } |
40 | | - let(:decoded_token) do |
41 | | - JWT.decode( |
42 | | - jwt_token, |
43 | | - rsa_key.public_key, |
44 | | - true, |
45 | | - algorithm: described_class::SIGNING_ALGORITHM |
46 | | - ) |
47 | | - end |
| 45 | + let(:encoded_token) { 'encoded.jwt.token' } |
| 46 | + let(:test_time) { Time.zone.at(1_234_567_890) } |
48 | 47 |
|
49 | | - it 'returns a valid JWT token' do |
50 | | - expect { decoded_token }.not_to raise_error |
| 48 | + before do |
| 49 | + # Fix the time for deterministic testing |
| 50 | + allow(Time.zone).to receive(:now).and_return(test_time) |
| 51 | + allow(JWT).to receive(:encode).and_return(encoded_token) |
| 52 | + allow(Rails).to receive(:logger).and_return(double('Logger').as_null_object) |
| 53 | + allow(OpenSSL::PKey::RSA).to receive(:new).and_return(rsa_key) |
51 | 54 | end |
52 | 55 |
|
53 | | - it 'includes the correct headers' do |
54 | | - headers = decoded_token.last |
55 | | - expect(headers['kid']).to eq('test_kid') |
56 | | - expect(headers['typ']).to eq('JWT') |
57 | | - expect(headers['alg']).to eq('RS512') |
| 56 | + context 'when JWT encoding is successful' do |
| 57 | + it 'returns the encoded JWT token' do |
| 58 | + expect(subject.sign_assertion).to eq(encoded_token) |
| 59 | + end |
| 60 | + |
| 61 | + it 'encodes with the correct parameters' do |
| 62 | + # Just check that claims hash contains required keys |
| 63 | + expect(JWT).to receive(:encode) |
| 64 | + .with( |
| 65 | + hash_including(:iss, :sub, :aud, :iat, :exp), |
| 66 | + kind_of(OpenSSL::PKey::RSA), |
| 67 | + 'RS512', |
| 68 | + hash_including(:kid, :typ, :alg) |
| 69 | + ) |
| 70 | + .and_return(encoded_token) |
| 71 | + |
| 72 | + subject.sign_assertion |
| 73 | + end |
58 | 74 | end |
59 | 75 |
|
60 | | - it 'includes the correct claims' do |
61 | | - claims = decoded_token.first |
62 | | - expect(claims['iss']).to eq('test_client') |
63 | | - expect(claims['sub']).to eq('test_client') |
64 | | - expect(claims['aud']).to eq('https://test.example.com') |
65 | | - expect(claims['iat']).to be_within(5).of(Time.zone.now.to_i) |
66 | | - expect(claims['exp']).to be_within(5).of(5.minutes.from_now.to_i) |
| 76 | + context 'when RSA key loading fails' do |
| 77 | + let(:wrapper_with_error) { described_class.new(settings, service_config) } |
| 78 | + |
| 79 | + context 'when key file does not exist' do |
| 80 | + before do |
| 81 | + allow(File).to receive(:exist?).with('/path/to/key.pem').and_return(false) |
| 82 | + end |
| 83 | + |
| 84 | + it 'logs the error and raises a VAOS::Exceptions::ConfigurationError' do |
| 85 | + expect(Rails.logger).to receive(:error).with(/Service Configuration Error: RSA key file not found/) |
| 86 | + expect { wrapper_with_error.sign_assertion }.to raise_error(VAOS::Exceptions::ConfigurationError) |
| 87 | + end |
| 88 | + end |
| 89 | + |
| 90 | + context 'when key path is nil' do |
| 91 | + let(:settings_with_nil_path) do |
| 92 | + OpenStruct.new( |
| 93 | + key_path: nil, |
| 94 | + client_id: 'test_client', |
| 95 | + kid: 'test_kid', |
| 96 | + audience_claim_url: 'http://test.example.com/token' |
| 97 | + ) |
| 98 | + end |
| 99 | + |
| 100 | + let(:wrapper_with_nil_path) { described_class.new(settings_with_nil_path, service_config) } |
| 101 | + |
| 102 | + it 'logs the error and raises a VAOS::Exceptions::ConfigurationError' do |
| 103 | + expect(Rails.logger).to receive(:error).with(/Service Configuration Error: RSA key path is not configured/) |
| 104 | + expect { wrapper_with_nil_path.sign_assertion }.to raise_error(VAOS::Exceptions::ConfigurationError) |
| 105 | + end |
| 106 | + end |
| 107 | + |
| 108 | + it 'includes the service name in the raised error' do |
| 109 | + allow(File).to receive(:exist?).with('/path/to/key.pem').and_return(false) |
| 110 | + |
| 111 | + exception = nil |
| 112 | + begin |
| 113 | + wrapper_with_error.sign_assertion |
| 114 | + rescue VAOS::Exceptions::ConfigurationError => e |
| 115 | + exception = e |
| 116 | + end |
| 117 | + expect(exception).not_to be_nil |
| 118 | + expect(exception.errors.first.detail).to include(service_name) |
| 119 | + end |
67 | 120 | end |
68 | 121 | end |
69 | 122 |
|
70 | 123 | describe '#rsa_key' do |
71 | 124 | it 'reads the key from the specified path' do |
72 | | - expect(File).to receive(:read).with('/path/to/key.pem').once.and_return(rsa_key) |
73 | | - 2.times { subject.rsa_key } # Call twice to test memoization |
| 125 | + expect(File).to receive(:read).with('/path/to/key.pem').once |
| 126 | + subject.rsa_key |
74 | 127 | end |
75 | 128 |
|
76 | 129 | it 'returns an RSA key instance' do |
| 130 | + allow(OpenSSL::PKey::RSA).to receive(:new).and_return(rsa_key) |
77 | 131 | expect(subject.rsa_key).to be_a(OpenSSL::PKey::RSA) |
78 | 132 | end |
79 | 133 |
|
80 | 134 | it 'memoizes the RSA key' do |
| 135 | + expect(File).to receive(:read).with('/path/to/key.pem').once |
81 | 136 | first_call = subject.rsa_key |
82 | 137 | second_call = subject.rsa_key |
83 | 138 | expect(first_call).to eq(second_call) |
84 | | - expect(File).to have_received(:read).once |
85 | 139 | end |
86 | 140 | end |
87 | 141 | end |
0 commit comments