Skip to content

Commit 5dff3b8

Browse files
authored
Merge pull request #5 from Globy-App/3-improve-test-coverage
Added tests to test the Hasher standalone class
2 parents f03f248 + 918252c commit 5dff3b8

File tree

9 files changed

+620
-196
lines changed

9 files changed

+620
-196
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
- name: Run test suite
7474
run: |
7575
if [[ ${{ matrix.php-version }} == '8.1' ]]; then
76-
composer coverage --min=90 --coverage-clover=coverage.xml
76+
composer coverage --min=100 --coverage-clover=coverage.xml
7777
else
7878
composer pest
7979
fi

src/Hasher.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,19 @@ protected function traverseInputArray(array $inputArray, array $sensitiveKeys):
138138
/**
139139
* Traverse an object and replace all values to be redacted with a hashed version of the value
140140
*
141-
* @param object $object Object to redact values from
141+
* @param object $object Object to redact values from
142142
* @param array<array-key, mixed> $sensitiveKeys Keys for which to hash the value
143143
*
144144
* @return object The object with redacted values hashed
145145
*/
146146
protected function traverseObject(object $object, array $sensitiveKeys): object
147147
{
148148
foreach (get_object_vars($object) as $key => $value) {
149+
if ($value === null) {
150+
// Nothing to hash or process
151+
continue;
152+
}
153+
149154
// If the value is not an array or an object, hash it if it is a sensitive key
150155
if (is_scalar($value)) {
151156
if (in_array($key, $sensitiveKeys) || array_key_exists($key, $sensitiveKeys)) {

tests/HashSensitiveArrayTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HashSensitiveTests;
6+
7+
use GlobyApp\HashSensitive\HashSensitiveProcessor;
8+
use TypeError;
9+
10+
it('redacts records contexts', function (): void {
11+
$processor = new HashSensitiveProcessor($this->simpleExample->getSensitiveKeys());
12+
13+
$record = $this->getRecord(context: $this->simpleExample->getInput());
14+
expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']);
15+
});
16+
17+
it('works without sensitive key subtrees', function (): void {
18+
$processor = new HashSensitiveProcessor($this->nestedExample->getSensitiveKeys());
19+
20+
$record = $this->getRecord(context: $this->nestedExample->getInput());
21+
expect($processor($record)->context)->toBe(['test' => 'c413de2c94a3a668b82ae2207da4b6961eeeccaff97623e2143d978610cb4746']);
22+
});
23+
24+
it('truncates masked characters', function (): void {
25+
$processor = new HashSensitiveProcessor($this->simpleExample->getSensitiveKeys(), lengthLimit: 5);
26+
27+
$record = $this->getRecord(context: $this->simpleExample->getInput());
28+
// Only `fooba` should be hashed, the first 5 characters of `foobar`
29+
expect($processor($record)->context)->toBe(['test' => '41cbe1a87981490351ccad5346d96da0ac10678670b31fc0ab209aed1b5bc515']);
30+
});
31+
32+
it('doesn\'t truncate more than the string length', function (): void {
33+
$processor = new HashSensitiveProcessor($this->simpleExample->getSensitiveKeys(), lengthLimit: 10);
34+
35+
$record = $this->getRecord(context: $this->simpleExample->getInput());
36+
expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']);
37+
});
38+
39+
it('doesn\'t truncate when length limit is 0', function (): void {
40+
$processor = new HashSensitiveProcessor($this->simpleExample->getSensitiveKeys(), lengthLimit: 0);
41+
42+
$record = $this->getRecord(context: $this->simpleExample->getInput());
43+
expect($processor($record)->context)->toBe(['test' => null]);
44+
});
45+
46+
it('doesn\'t truncate when length limit is not set', function (): void {
47+
$processor = new HashSensitiveProcessor($this->simpleExample->getSensitiveKeys(), lengthLimit: null);
48+
49+
$record = $this->getRecord(context: $this->simpleExample->getInput());
50+
expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']);
51+
});
52+
53+
it('redacts nested arrays', function (): void {
54+
$processor = new HashSensitiveProcessor($this->nestedRedaction->getSensitiveKeys());
55+
56+
$record = $this->getRecord(context: $this->nestedRedaction->getInput());
57+
expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']]);
58+
});
59+
60+
it('keeps non redacted nested arrays intact', function (): void {
61+
$processor = new HashSensitiveProcessor($this->nestedNoHash->getSensitiveKeys());
62+
63+
$record = $this->getRecord(context: $this->nestedNoHash->getInput());
64+
expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', 'no_hash' => 'foobar']]);
65+
});
66+
67+
it('doesn\'t break on null values in input array', function (): void {
68+
$processor = new HashSensitiveProcessor($this->nestedNull->getSensitiveKeys());
69+
70+
$record = $this->getRecord(context: $this->nestedNull->getInput());
71+
expect($processor($record)->context)->toBe(['test' => ['nested' => null, 'no_hash' => null, null]]);
72+
});
73+
74+
it('doesn\'t break on null values in sensitive keys (array)', function (): void {
75+
$processor = new HashSensitiveProcessor($this->nestedSensitiveNull->getSensitiveKeys());
76+
77+
$record = $this->getRecord(context: $this->nestedSensitiveNull->getInput());
78+
expect($processor($record)->context)->toBe($this->nestedSensitiveNull->getInput());
79+
});
80+
81+
it('redacts inside nested arrays', function (): void {
82+
$processor = new HashSensitiveProcessor($this->insideNested->getSensitiveKeys());
83+
84+
$record = $this->getRecord(context: $this->insideNested->getInput());
85+
expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']]);
86+
});

tests/HashSensitiveObjectTest.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HashSensitiveTests;
6+
7+
use GlobyApp\HashSensitive\HashSensitiveProcessor;
8+
use TypeError;
9+
10+
it('redacts nested objects', function (): void {
11+
$nested = new \stdClass();
12+
$nested->value = 'foobar';
13+
$nested->nested = ['value' => 'bazqux'];
14+
15+
$input = ['test' => ['nested' => $nested]];
16+
$sensitive_keys = ['test' => ['nested' => ['value', 'nested' => ['value']]]];
17+
$processor = new HashSensitiveProcessor($sensitive_keys);
18+
19+
$record = $this->getRecord(context: $input);
20+
21+
expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]])
22+
->and($nested->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
23+
->and($nested->nested['value'])->toBe('972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04');
24+
});
25+
26+
it('works without sensitive key subobjects', function (): void {
27+
$nested = new \stdClass();
28+
$nested->foobar = "test";
29+
30+
$obj = new \stdClass();
31+
$obj->test = $nested;
32+
33+
$input = ['obj' => $obj];
34+
$sensitive_keys = ['test'];
35+
$processor = new HashSensitiveProcessor($sensitive_keys);
36+
37+
$record = $this->getRecord(context: $input);
38+
expect($processor($record)->context)->toBe(['obj' => $obj])
39+
->and($obj->test)->toBe('914dba76d2c953789b8ec73425b85bea1c8298815dd0afc1e4fc6c2d8be69648');
40+
});
41+
42+
it('keeps non redacted nested objects intact', function (): void {
43+
$nested = new \stdClass();
44+
$nested->value = 'bazqux';
45+
$nested->no_hash = 'foobar';
46+
47+
$value = new \stdClass();
48+
$value->value = 'foobar';
49+
$value->nested = $nested;
50+
51+
$input = ['test' => ['nested' => $value]];
52+
$sensitive_keys = ['test' => ['nested' => ['value', 'nested' => ['value']]]];
53+
$processor = new HashSensitiveProcessor($sensitive_keys);
54+
55+
$record = $this->getRecord(context: $input);
56+
57+
expect($processor($record)->context)->toBe(['test' => ['nested' => $value]])
58+
->and($value->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
59+
->and($value->nested)->toBe($nested)
60+
->and($nested->value)->toBe('972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04')
61+
->and($nested->no_hash)->toBe('foobar');
62+
});
63+
64+
it('doesn\'t break on null values in input object', function (): void {
65+
$nested = new \stdClass();
66+
$nested->value = null;
67+
$nested->no_hash = null;
68+
69+
$value = new \stdClass();
70+
$value->value = 'foobar';
71+
$value->nested = $nested;
72+
73+
$input = ['test' => ['nested' => $value]];
74+
$sensitive_keys = ['test' => ['nested' => ['value', 'nested' => ['value']]]];
75+
$processor = new HashSensitiveProcessor($sensitive_keys);
76+
77+
$record = $this->getRecord(context: $input);
78+
79+
expect($processor($record)->context)->toBe(['test' => ['nested' => $value]])
80+
->and($value->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
81+
->and($value->nested)->toBe($nested)
82+
->and($nested->value)->toBeNull()
83+
->and($nested->no_hash)->toBeNull();
84+
});
85+
86+
it('doesn\'t break on null values in sensitive keys (object)', function (): void {
87+
$nested = new \stdClass();
88+
$nested->value = 'foobar';
89+
$nested->nested = ['value' => 'bazqux', 'no_hash' => 'foobar'];
90+
91+
$input = ['test' => ['nested' => $nested]];
92+
$sensitive_keys = ['test' => ['nested' => [null, 'nested' => [null], null], null], null];
93+
$processor = new HashSensitiveProcessor($sensitive_keys);
94+
95+
$record = $this->getRecord(context: $input);
96+
97+
expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]])
98+
->and($nested->nested['no_hash'])->toBe('foobar');
99+
});
100+
101+
it('redacts inside nested objects', function (): void {
102+
$nested = new \stdClass();
103+
$nested->value = 'foobar';
104+
$nested->nested = ['value' => 'bazqux'];
105+
106+
$input = ['test' => ['nested' => $nested]];
107+
$sensitive_keys = ['nested' => ['value']];
108+
$processor = new HashSensitiveProcessor($sensitive_keys);
109+
110+
$record = $this->getRecord(context: $input);
111+
112+
expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]])
113+
->and($nested->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
114+
->and($nested->nested['value'])->toBe('972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04');
115+
});
116+
117+
it('it hashes all instances with exclusiveSubtree false in nested objects', function (): void {
118+
$nested = new \stdClass();
119+
$nested->to_hash = 'test_value';
120+
$nested->test = 'test';
121+
122+
$value = new \stdClass();
123+
$value->test_key = 'test_value';
124+
$value->test_subkey = $nested;
125+
126+
$input = ['nested' => $value];
127+
$sensitive_keys = ['test', 'test_subkey' => ['to_hash']];
128+
$processor = new HashSensitiveProcessor($sensitive_keys, exclusiveSubtree: false);
129+
130+
$record = $this->getRecord(context: $input);
131+
expect($processor($record)->context)->toBe(['nested' => $value])
132+
->and($value->test_key)->toBe('test_value')
133+
->and($value->test_subkey)->toBe($nested)
134+
->and($nested->to_hash)->toBe('4f7f6a4ae46676d9751fdccdf15ae1e6a200ed0de5653e06390148928c642006')
135+
->and($nested->test)->toBe('9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08');
136+
});
137+
138+
it('ensures exclusiveSubtree is turned on by default for objects', function (): void {
139+
$nested = new \stdClass();
140+
$nested->to_hash = 'test_value';
141+
$nested->test = 'test';
142+
143+
$value = new \stdClass();
144+
$value->test_key = 'test_value';
145+
$value->test_subkey = $nested;
146+
147+
$input = ['nested' => $value];
148+
$sensitive_keys = ['test', 'test_subkey' => ['to_hash']];
149+
$processor = new HashSensitiveProcessor($sensitive_keys);
150+
151+
$record = $this->getRecord(context: $input);
152+
expect($processor($record)->context)->toBe(['nested' => $value])
153+
->and($value->test_key)->toBe('test_value')
154+
->and($value->test_subkey)->toBe($nested)
155+
->and($nested->to_hash)->toBe('4f7f6a4ae46676d9751fdccdf15ae1e6a200ed0de5653e06390148928c642006')
156+
->and($nested->test)->toBe('test');
157+
});
158+
159+
it('preserves empty values in objects', function (): void {
160+
$nested = new \stdClass();
161+
$nested->test = 'foobar';
162+
$nested->optionalKey = '';
163+
164+
$input = ['nested' => $nested];
165+
$sensitive_keys = ['test', 'optionalKey'];
166+
$processor = new HashSensitiveProcessor($sensitive_keys);
167+
168+
$record = $this->getRecord(context: $input);
169+
expect($processor($record)->context)->toBe(['nested' => $nested])
170+
->and($nested->test)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')
171+
->and($nested->optionalKey)->toBeNull();
172+
});

0 commit comments

Comments
 (0)