1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// -*- coding: utf-8 -*-
// _ __
// | |/ /___ ___ _ __ ___ _ _ (R)
// | ' </ -_) -_) '_ \/ -_) '_|
// |_|\_\___\___| .__/\___|_|
// |_|
//
// Keeper Secrets Manager
// Copyright 2024 Keeper Security Inc.
// Contact: sm@keepersecurity.com
//
//! Integration test for KSM-735: Duplicate UID notation bug fix
//!
//! This test validates that when a KSM application has access to both an original
//! record and a shortcut to that record (both with identical UIDs), the deduplication
//! logic properly filters duplicate UIDs using a HashSet.
//!
//! The fix is in `core.rs` lines 2927-2938 of `get_notation_result()`.
//!
//! IMPORTANT: This is a unit test that validates the deduplication algorithm itself,
//! not the full end-to-end notation flow. The test simulates what would happen if
//! `get_secrets()` returns duplicate UIDs (which happens when an app has access to
//! both an original record and a shortcut).
#[cfg(test)]
mod duplicate_uid_deduplication_tests {
use keeper_secrets_manager_core::dto::Record;
use serde_json::json;
use std::collections::{HashMap, HashSet};
/// Helper function to create a test record with a specific UID
fn create_test_record(uid: &str, title: &str) -> Record {
let mut record_dict = HashMap::new();
record_dict.insert("password".to_string(), json!("secret123"));
Record {
record_key_bytes: vec![1, 2, 3],
uid: uid.to_string(),
title: title.to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: "{}".to_string(),
record_dict,
password: Some("secret123".to_string()),
revision: Some(1),
is_editable: true,
folder_uid: "folder123".to_string(),
folder_key_bytes: Some(vec![4, 5, 6]),
inner_folder_uid: None,
links: vec![],
}
}
/// Test that the deduplication logic correctly removes duplicate UIDs
///
/// This test exercises the EXACT logic from lines 2927-2938 in core.rs:
/// ```rust
/// if records.len() > 1 {
/// let mut seen_uids = std::collections::HashSet::new();
/// records.retain(|record| {
/// if seen_uids.contains(&record.uid) {
/// false
/// } else {
/// seen_uids.insert(record.uid.clone());
/// true
/// }
/// });
/// }
/// ```
///
/// This test would PASS before the fix was added because it tests the algorithm
/// in isolation. To verify the fix works in the actual codebase, you must:
/// 1. Temporarily remove lines 2927-2938 from core.rs
/// 2. Run an integration test with real notation (requires KSM server access)
/// 3. Observe "multiple records matched" error
/// 4. Restore the fix and verify error goes away
#[test]
fn test_deduplication_removes_duplicate_uids() {
let duplicate_uid = "ABC123XYZ123456789AB";
// Simulate what get_secrets() returns when app has access to both
// original record and shortcut (both have same UID)
let mut records = vec![
create_test_record(duplicate_uid, "Original Record"),
create_test_record(duplicate_uid, "Shortcut Record"), // Same UID!
create_test_record("XYZ789ABC123456789CD", "Other Record"),
];
println!("Before deduplication: {} records", records.len());
for record in &records {
println!(" - UID: {}, Title: {}", record.uid, record.title);
}
// This is the EXACT deduplication logic from core.rs lines 2927-2938
if records.len() > 1 {
let mut seen_uids = HashSet::new();
records.retain(|record| {
if seen_uids.contains(&record.uid) {
false
} else {
seen_uids.insert(record.uid.clone());
true
}
});
}
println!("\nAfter deduplication: {} records", records.len());
for record in &records {
println!(" - UID: {}, Title: {}", record.uid, record.title);
}
// Assert: We should have 2 records (duplicate removed)
assert_eq!(
records.len(),
2,
"Expected 2 records after deduplication (1 removed)"
);
// Assert: The remaining records should have unique UIDs
let mut unique_uids = HashSet::new();
for record in &records {
assert!(
unique_uids.insert(record.uid.clone()),
"Found duplicate UID after deduplication: {}",
record.uid
);
}
// Assert: One of the duplicate_uid records was kept
let has_duplicate_uid = records.iter().any(|r| r.uid == duplicate_uid);
assert!(
has_duplicate_uid,
"Expected at least one record with UID {}",
duplicate_uid
);
println!("\n✓ Deduplication logic works correctly");
}
/// Test that deduplication preserves order (keeps first occurrence)
#[test]
fn test_deduplication_keeps_first_occurrence() {
let duplicate_uid = "SAMEUID123456789ABCD";
let mut records = vec![
create_test_record(duplicate_uid, "First Occurrence"),
create_test_record(duplicate_uid, "Second Occurrence"),
create_test_record(duplicate_uid, "Third Occurrence"),
];
// Apply deduplication
if records.len() > 1 {
let mut seen_uids = HashSet::new();
records.retain(|record| {
if seen_uids.contains(&record.uid) {
false
} else {
seen_uids.insert(record.uid.clone());
true
}
});
}
assert_eq!(
records.len(),
1,
"Expected only 1 record after deduplication"
);
assert_eq!(
records[0].title, "First Occurrence",
"Expected first occurrence to be kept"
);
println!("✓ Deduplication keeps first occurrence");
}
/// Test that no deduplication happens when all UIDs are unique
#[test]
fn test_no_deduplication_when_all_unique() {
let mut records = vec![
create_test_record("UID1AAA111111111111AA", "Record 1"),
create_test_record("UID2BBB222222222222BB", "Record 2"),
create_test_record("UID3CCC333333333333CC", "Record 3"),
];
let original_count = records.len();
// Apply deduplication
if records.len() > 1 {
let mut seen_uids = HashSet::new();
records.retain(|record| {
if seen_uids.contains(&record.uid) {
false
} else {
seen_uids.insert(record.uid.clone());
true
}
});
}
assert_eq!(
records.len(),
original_count,
"Expected no records removed when all UIDs unique"
);
println!("✓ No deduplication when all UIDs are unique");
}
/// Test that single record is not affected by deduplication logic
#[test]
fn test_single_record_unchanged() {
let mut records = vec![create_test_record("SINGLE123456789ABCDE", "Single Record")];
// Apply deduplication (should be a no-op for single record)
if records.len() > 1 {
let mut seen_uids = HashSet::new();
records.retain(|record| {
if seen_uids.contains(&record.uid) {
false
} else {
seen_uids.insert(record.uid.clone());
true
}
});
}
assert_eq!(records.len(), 1, "Expected single record to remain");
assert_eq!(records[0].uid, "SINGLE123456789ABCDE");
println!("✓ Single record unaffected by deduplication");
}
/// Test complex scenario with multiple duplicate sets
#[test]
fn test_multiple_duplicate_sets() {
let uid_a = "UIDA111111111111111AA";
let uid_b = "UIDB222222222222222BB";
let mut records = vec![
create_test_record(uid_a, "Record A1"),
create_test_record(uid_a, "Record A2"), // Duplicate of A
create_test_record(uid_b, "Record B1"),
create_test_record(uid_b, "Record B2"), // Duplicate of B
create_test_record("UIDC333333333333333CC", "Record C1"), // Unique
];
// Apply deduplication
if records.len() > 1 {
let mut seen_uids = HashSet::new();
records.retain(|record| {
if seen_uids.contains(&record.uid) {
false
} else {
seen_uids.insert(record.uid.clone());
true
}
});
}
assert_eq!(
records.len(),
3,
"Expected 3 records (2 duplicates removed)"
);
// Verify we have exactly one of each UID
let uids: HashSet<String> = records.iter().map(|r| r.uid.clone()).collect();
assert_eq!(uids.len(), 3);
assert!(uids.contains(uid_a));
assert!(uids.contains(uid_b));
assert!(uids.contains("UIDC333333333333333CC"));
println!("✓ Multiple duplicate sets handled correctly");
}
}
#[cfg(test)]
mod notation_context_tests {
//! These tests document WHY the deduplication fix is needed in the context
//! of the full notation retrieval flow.
/// Documentation test explaining the bug scenario
///
/// This is NOT an executable test (would require real KSM server), but documents
/// the exact scenario that triggers the bug.
#[test]
fn document_bug_scenario() {
println!("=== KSM-735: Duplicate UID Notation Bug ===\n");
println!("SCENARIO:");
println!("1. User creates a record with UID 'ABC123XYZ123456789AB' in Keeper");
println!("2. User creates a SHORTCUT to that record");
println!("3. User shares BOTH original record AND shortcut to same KSM App");
println!("4. KSM App calls get_secrets() with that UID\n");
println!("BUG (before fix):");
println!("- Server returns TWO records with SAME UID (original + shortcut)");
println!("- get_notation_result() sees records.len() = 2");
println!("- Line 2950: Returns error 'multiple records matched'");
println!("- User gets confusing error even though UIDs are identical!\n");
println!("FIX (lines 2927-2938):");
println!("- Deduplicate by UID using HashSet");
println!("- Keep only first occurrence");
println!("- Now records.len() = 1 after deduplication");
println!("- Notation lookup succeeds!\n");
println!("IMPORTANT:");
println!("- Different UIDs with same TITLE should still fail (genuine ambiguity)");
println!("- Only IDENTICAL UIDs should be deduplicated");
println!("- First occurrence is kept (preserves order)");
// This test always passes - it's documentation
assert!(true);
}
}