feat: copper accent theme, always-expanded group filter, app icon; rename MyPass to KeeVault
- Custom copper AccentColor and warm VaultBackground color assets (light/dark), applied to both the main app and the AutoFill extension (which needed its own copy of the asset catalog plus ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME and an explicit .tint() modifier, since extensions don't pick up the app-wide accent color the same way a full SwiftUI App/Scene does). - GroupFilterView no longer uses DisclosureGroup for subgroups -- the whole tree renders always-expanded (indented per depth) so active filters are visible at a glance instead of requiring tap-to-expand. - Added a placeholder app icon (padlock glyph, light/dark/tinted variants plus macOS sizes) -- previously the icon slot existed but had no actual images, which blocks App Store/TestFlight archive validation. - Added ITSAppUsesNonExemptEncryption = NO (standard encryption only, via KeePassKit) to skip the export-compliance prompt on each upload. - Renamed the app from MyPass to KeeVault throughout: Xcode project/targets/ schemes, the MyPassCore package (now KeeVaultCore) and every import site, folder and file names, bundle identifiers (org.antiloop222.keevault) and their App Group/Keychain-group entitlements, and remaining UI/string references -- MyPass was already taken as an App Store app name.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class CredentialMatcherTests: XCTestCase {
|
||||
func makeEntry(url: String) -> Entry {
|
||||
Entry(title: "Test", url: url)
|
||||
}
|
||||
|
||||
func test_exactURLMatch() {
|
||||
let e = makeEntry(url: "https://github.com/login")
|
||||
XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com"))
|
||||
}
|
||||
|
||||
func test_wwwStripped() {
|
||||
let e = makeEntry(url: "https://www.github.com")
|
||||
XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com"))
|
||||
}
|
||||
|
||||
func test_subdomainMatch() {
|
||||
let e = makeEntry(url: "https://api.github.com")
|
||||
XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com"))
|
||||
}
|
||||
|
||||
func test_differentDomain_noMatch() {
|
||||
let e = makeEntry(url: "https://gitlab.com")
|
||||
XCTAssertFalse(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com"))
|
||||
}
|
||||
|
||||
func test_emptyURL_noMatch() {
|
||||
let e = makeEntry(url: "")
|
||||
XCTAssertFalse(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com"))
|
||||
}
|
||||
|
||||
func test_bareDomainSubdomainMatch() {
|
||||
let e = makeEntry(url: "allocine.fr")
|
||||
XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "mon.allocine.fr"))
|
||||
}
|
||||
|
||||
func test_bareDomainExactMatch() {
|
||||
let e = makeEntry(url: "allocine.fr")
|
||||
XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "allocine.fr"))
|
||||
}
|
||||
|
||||
func test_bareDomainDifferentDomain_noMatch() {
|
||||
let e = makeEntry(url: "allocine.fr")
|
||||
XCTAssertFalse(CredentialMatcher.matches(entry: e, serviceIdentifier: "notallocine.fr"))
|
||||
}
|
||||
|
||||
func test_filter_suggestedAndRest() {
|
||||
let entries = [
|
||||
makeEntry(url: "https://github.com"),
|
||||
makeEntry(url: "https://gitlab.com"),
|
||||
makeEntry(url: ""),
|
||||
]
|
||||
let result = CredentialMatcher.filter(entries: entries, for: ["https://github.com"])
|
||||
XCTAssertEqual(result.suggested.count, 1)
|
||||
XCTAssertEqual(result.suggested[0].url, "https://github.com")
|
||||
XCTAssertEqual(result.all.count, 2)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class GroupSelectionTests: XCTestCase {
|
||||
private func makeSampleTree() -> (root: Group, work: Group, email: Group, personal: Group) {
|
||||
let email = Group(name: "Email")
|
||||
let work = Group(name: "Work", subgroups: [email])
|
||||
let personal = Group(name: "Personal")
|
||||
let root = Group(name: "Root", subgroups: [work, personal])
|
||||
return (root, work, email, personal)
|
||||
}
|
||||
|
||||
func test_subtreeIds_includesGroupAndAllDescendants() {
|
||||
let (root, work, email, _) = makeSampleTree()
|
||||
let ids = GroupSelection.subtreeIds(of: work.id, in: root)
|
||||
XCTAssertEqual(ids, [work.id, email.id])
|
||||
}
|
||||
|
||||
func test_subtreeIds_leafGroup_returnsOnlyItself() {
|
||||
let (root, _, email, _) = makeSampleTree()
|
||||
let ids = GroupSelection.subtreeIds(of: email.id, in: root)
|
||||
XCTAssertEqual(ids, [email.id])
|
||||
}
|
||||
|
||||
func test_toggling_selectsGroupAndSubtree() {
|
||||
let (root, work, email, _) = makeSampleTree()
|
||||
let result = GroupSelection.toggling(work.id, in: root, current: [])
|
||||
XCTAssertEqual(result, [work.id, email.id])
|
||||
}
|
||||
|
||||
func test_toggling_deselectsGroupAndSubtree() {
|
||||
let (root, work, email, _) = makeSampleTree()
|
||||
let result = GroupSelection.toggling(work.id, in: root, current: [work.id, email.id])
|
||||
XCTAssertEqual(result, [])
|
||||
}
|
||||
|
||||
func test_toggling_doesNotAffectUnrelatedSelection() {
|
||||
let (root, work, email, personal) = makeSampleTree()
|
||||
let result = GroupSelection.toggling(work.id, in: root, current: [personal.id])
|
||||
XCTAssertEqual(result, [personal.id, work.id, email.id])
|
||||
}
|
||||
|
||||
func test_subtreeIds_groupNotFound_returnsEmptySet() {
|
||||
let (root, _, _, _) = makeSampleTree()
|
||||
let nonExistentId = UUID()
|
||||
let ids = GroupSelection.subtreeIds(of: nonExistentId, in: root)
|
||||
XCTAssertEqual(ids, [])
|
||||
}
|
||||
|
||||
func test_toggling_groupNotFound_returnsCurrentUnchanged() {
|
||||
let (root, _, _, personal) = makeSampleTree()
|
||||
let nonExistentId = UUID()
|
||||
let current: Set<UUID> = [personal.id]
|
||||
let result = GroupSelection.toggling(nonExistentId, in: root, current: current)
|
||||
XCTAssertEqual(result, current)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class GroupTreeTests: XCTestCase {
|
||||
private func makeSampleTree() -> (root: Group, work: Group, email: Group) {
|
||||
let supportEntry = Entry(title: "Support Login", username: "alice")
|
||||
let email = Group(name: "Email", entries: [supportEntry])
|
||||
let bankEntry = Entry(title: "Bank", username: "bob")
|
||||
let work = Group(name: "Work", subgroups: [email], entries: [bankEntry])
|
||||
let rootEntry = Entry(title: "Root Entry", username: "root-user")
|
||||
let root = Group(name: "Root", subgroups: [work], entries: [rootEntry])
|
||||
return (root, work, email)
|
||||
}
|
||||
|
||||
func test_flattenEntries_rootEntry_hasEmptyBreadcrumb() {
|
||||
let (root, _, _) = makeSampleTree()
|
||||
let flat = flattenEntries(in: root)
|
||||
let rootEntry = flat.first { $0.entry.title == "Root Entry" }
|
||||
XCTAssertEqual(rootEntry?.breadcrumb, [])
|
||||
XCTAssertEqual(rootEntry?.parentGroupId, root.id)
|
||||
}
|
||||
|
||||
func test_flattenEntries_directChildEntry_hasSingleGroupBreadcrumb() {
|
||||
let (root, work, _) = makeSampleTree()
|
||||
let flat = flattenEntries(in: root)
|
||||
let bankEntry = flat.first { $0.entry.title == "Bank" }
|
||||
XCTAssertEqual(bankEntry?.breadcrumb.map(\.name), ["Work"])
|
||||
XCTAssertEqual(bankEntry?.parentGroupId, work.id)
|
||||
}
|
||||
|
||||
func test_flattenEntries_nestedEntry_hasFullBreadcrumb() {
|
||||
let (root, _, email) = makeSampleTree()
|
||||
let flat = flattenEntries(in: root)
|
||||
let supportEntry = flat.first { $0.entry.title == "Support Login" }
|
||||
XCTAssertEqual(supportEntry?.breadcrumb.map(\.name), ["Work", "Email"])
|
||||
XCTAssertEqual(supportEntry?.parentGroupId, email.id)
|
||||
}
|
||||
|
||||
func test_flattenGroups_includesRootFirstWithEmptyBreadcrumb() {
|
||||
let (root, _, _) = makeSampleTree()
|
||||
let flat = flattenGroups(in: root)
|
||||
XCTAssertEqual(flat.first?.group.id, root.id)
|
||||
XCTAssertEqual(flat.first?.breadcrumb, [])
|
||||
}
|
||||
|
||||
func test_flattenGroups_nestedGroup_hasBreadcrumb() {
|
||||
let (root, _, _) = makeSampleTree()
|
||||
let flat = flattenGroups(in: root)
|
||||
let email = flat.first { $0.group.name == "Email" }
|
||||
XCTAssertEqual(email?.breadcrumb.map(\.name), ["Work"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class KDBXDocumentTests: XCTestCase {
|
||||
var fixtureURL: URL!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
fixtureURL = Bundle.module.url(forResource: "test", withExtension: "kdbx", subdirectory: "Fixtures")!
|
||||
}
|
||||
|
||||
func test_read_parsesRootGroup() throws {
|
||||
let doc = KDBXDocument(url: fixtureURL)
|
||||
let db = try doc.read(password: "1234")
|
||||
XCTAssertFalse(db.root.name.isEmpty)
|
||||
}
|
||||
|
||||
func test_read_wrongPassword_throwsInvalidPassword() throws {
|
||||
let doc = KDBXDocument(url: fixtureURL)
|
||||
XCTAssertThrowsError(try doc.read(password: "wrong")) { error in
|
||||
XCTAssertEqual(error as? KDBXError, .invalidPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func test_roundTrip_preservesEntryTitle() throws {
|
||||
let doc = KDBXDocument(url: fixtureURL)
|
||||
var db = try doc.read(password: "1234")
|
||||
let newEntry = Entry(title: "RoundTripTest", username: "user", password: ProtectedString("pass"))
|
||||
db.root.entries.append(newEntry)
|
||||
let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("roundtrip.kdbx")
|
||||
let tmpDoc = KDBXDocument(url: tmpURL)
|
||||
try tmpDoc.write(db, password: "1234")
|
||||
let reloaded = try KDBXDocument(url: tmpURL).read(password: "1234")
|
||||
XCTAssertTrue(reloaded.root.entries.contains { $0.title == "RoundTripTest" })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class KeePassIconMapperTests: XCTestCase {
|
||||
func test_knownIndex_returnsMappedSymbol() {
|
||||
XCTAssertEqual(KeePassIconMapper.symbolName(forIconIndex: 14), "envelope.fill")
|
||||
}
|
||||
|
||||
func test_indexZero_isKeyFill() {
|
||||
XCTAssertEqual(KeePassIconMapper.symbolName(forIconIndex: 0), "key.fill")
|
||||
}
|
||||
|
||||
func test_unmappedIndex_fallsBackToKeyFill() {
|
||||
XCTAssertEqual(KeePassIconMapper.symbolName(forIconIndex: 999), "key.fill")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class ProtectedStringTests: XCTestCase {
|
||||
func test_reveal_returnsOriginalValue() {
|
||||
let ps = ProtectedString("hunter2", isProtected: true)
|
||||
XCTAssertEqual(ps.reveal(), "hunter2")
|
||||
}
|
||||
|
||||
func test_description_isRedactedWhenProtected() {
|
||||
let ps = ProtectedString("secret", isProtected: true)
|
||||
XCTAssertEqual("\(ps)", "***")
|
||||
}
|
||||
|
||||
func test_description_isPlainWhenNotProtected() {
|
||||
let ps = ProtectedString("visible", isProtected: false)
|
||||
XCTAssertEqual("\(ps)", "visible")
|
||||
}
|
||||
|
||||
func test_equality_matchesByRevealedValue() {
|
||||
let a = ProtectedString("abc", isProtected: true)
|
||||
let b = ProtectedString("abc", isProtected: true)
|
||||
XCTAssertEqual(a, b)
|
||||
}
|
||||
|
||||
func test_debugDescription_neverRevealSecret() {
|
||||
let ps = ProtectedString("topsecret", isProtected: true)
|
||||
XCTAssertFalse(ps.debugDescription.contains("topsecret"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import XCTest
|
||||
@testable import KeeVaultCore
|
||||
|
||||
final class TOTPGeneratorTests: XCTestCase {
|
||||
// RFC 6238 Section 8 test vectors for SHA-1
|
||||
// secret = "12345678901234567890" (ASCII), base32 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
let sha1Secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|
||||
func test_sha1_at59s() {
|
||||
let config = TOTPConfig(secret: sha1Secret, period: 30, digits: 8, algorithm: .sha1)
|
||||
let date = Date(timeIntervalSince1970: 59)
|
||||
XCTAssertEqual(TOTPGenerator.generate(config: config, at: date), "94287082")
|
||||
}
|
||||
|
||||
func test_sha1_at1111111109s() {
|
||||
let config = TOTPConfig(secret: sha1Secret, period: 30, digits: 8, algorithm: .sha1)
|
||||
let date = Date(timeIntervalSince1970: 1111111109)
|
||||
XCTAssertEqual(TOTPGenerator.generate(config: config, at: date), "07081804")
|
||||
}
|
||||
|
||||
func test_secondsRemaining_isWithinPeriod() {
|
||||
let config = TOTPConfig(secret: sha1Secret, period: 30)
|
||||
let remaining = TOTPGenerator.secondsRemaining(config: config, at: Date())
|
||||
XCTAssertGreaterThan(remaining, 0)
|
||||
XCTAssertLessThanOrEqual(remaining, 30)
|
||||
}
|
||||
|
||||
func test_generate_defaultSixDigits() {
|
||||
let config = TOTPConfig(secret: sha1Secret)
|
||||
let code = TOTPGenerator.generate(config: config, at: Date())
|
||||
XCTAssertEqual(code.count, 6)
|
||||
XCTAssertNotNil(Int(code))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user