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:
2026-09-20 15:05:19 +02:00
parent a8d612f745
commit 8bba64076f
74 changed files with 363 additions and 176 deletions
@@ -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)
}
}
@@ -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))
}
}