Skip to content

Commit d7792e2

Browse files
committed
tests++
1 parent f95142d commit d7792e2

File tree

7 files changed

+612
-11
lines changed

7 files changed

+612
-11
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(swift test)",
5+
"Bash(swift test:*)"
6+
],
7+
"deny": []
8+
}
9+
}

CLAUDE.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
CornucopiaCore is a Swift Package Manager library providing essential utilities for Swift developers across Apple platforms (iOS 14+, macOS 12+, tvOS 14+, watchOS 8+). It's a collection of extensions, utilities, and helper classes designed to augment Foundation and platform-specific frameworks.
8+
9+
## Build System & Commands
10+
11+
This project uses Swift Package Manager with the following commands:
12+
13+
- **Build**: `swift build`
14+
- **Test**: `swift test`
15+
- **Clean**: `swift package clean`
16+
17+
The project targets Swift 5.10+ and has continuous integration via GitHub Actions that runs on both Ubuntu and macOS.
18+
19+
## Dependencies
20+
21+
The project has three external dependencies:
22+
- `swift-crypto` (3.0.0+): Apple's cryptographic library
23+
- `SWCompression` (4.8.5+): Compression utilities
24+
- `AnyCodable` (0.6.6+): Type-erased Codable support (re-exported at module level)
25+
26+
## Code Architecture
27+
28+
### Module Structure
29+
The main module `CornucopiaCore` is organized into several logical groups:
30+
31+
#### Core Namespace
32+
All public APIs are nested under `Cornucopia.Core` namespace. The main entry point is `Sources/CornucopiaCore/CornucopiaCore.swift` which defines the namespace and re-exports `AnyCodable`.
33+
34+
#### Major Component Categories
35+
36+
1. **Extensions** (`Extensions/`): Platform and Foundation type extensions
37+
- Organized by type (Array, String, Data, etc.)
38+
- Follow `CC_` prefix convention for public methods
39+
- Cover networking, data manipulation, validation, and utility functions
40+
41+
2. **Features** (`Features/`): Standalone functionality modules
42+
- `DeviceInfo`: Cross-platform device identification with UUID persistence
43+
- `JWT`: JSON Web Token support
44+
- `Benchmarking`: Performance measurement utilities
45+
- `Concurrency`: Async/await utilities and timeout mechanisms
46+
- `Environment`: Environment variable handling
47+
48+
3. **Logging** (`Logging/`): Flexible logging system
49+
- `Logger`: Main logging interface with configurable sinks
50+
- Supports multiple output targets (print, syslog, file, OSLog)
51+
- Environment/UserDefaults configurable via `LOGLEVEL` and `LOGSINK`
52+
- Thread-safe with background dispatch queue
53+
54+
4. **PropertyWrappers** (`PropertyWrappers/`): Utility property wrappers
55+
- `@Default`: Codable with default values
56+
- `@Clamped`: Value constraints
57+
- `@HexEncoded`: Automatic hex encoding/decoding
58+
- `@Protected`: Thread-safe property access
59+
60+
5. **Storage** (`Storage/`): Data persistence abstractions
61+
- `Keychain`: Secure storage interface
62+
- `ExtendedFileAttributes`: File metadata handling
63+
- Storage backend protocol for pluggable backends
64+
65+
6. **Networking** (`Networking/`): HTTP utilities
66+
- HTTP constants and status codes
67+
- Networking helper functions
68+
69+
### Naming Conventions
70+
71+
- Public extension methods use `CC_` prefix
72+
- Internal static constants use descriptive names
73+
- Property wrappers follow Swift conventions
74+
- Logger categories derived from `#fileID` by default
75+
76+
### Platform Considerations
77+
78+
The codebase includes extensive platform-specific compilation conditions:
79+
- `#if canImport(ObjectiveC)` for Apple vs. Linux differentiation
80+
- Platform-specific imports (`UIKit`, `WatchKit`, etc.)
81+
- Conditional feature availability (syslog not available on watchOS)
82+
- Architecture-specific code paths in DeviceInfo
83+
84+
### Testing Structure
85+
86+
Tests mirror the source structure under `Tests/CornucopiaCoreTests/` and cover:
87+
- Extension functionality
88+
- Property wrapper behavior
89+
- Core features like logging and device info
90+
- Cross-platform compatibility
91+
92+
## Development Practices
93+
94+
- All files include copyright header: `// Cornucopia – (C) Dr. Lauer Information Technology`
95+
- Extensions are organized by the type they extend
96+
- Comprehensive `#if` conditions ensure cross-platform compatibility
97+
- Property wrappers follow Codable protocols where applicable
98+
- Logger uses background queue for thread safety
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//
2+
// Cornucopia – (C) Dr. Lauer Information Technology
3+
//
4+
import XCTest
5+
@testable import CornucopiaCore
6+
7+
final class DataArrayConversionTests: XCTestCase {
8+
9+
func testDataFromUInt8Array() {
10+
let bytes: [UInt8] = [0x01, 0x02, 0x03, 0x04]
11+
let data = Data.CC_fromArray(bytes)
12+
XCTAssertEqual(data.count, 4)
13+
XCTAssertEqual(Array(data), [0x01, 0x02, 0x03, 0x04])
14+
}
15+
16+
func testDataFromUInt16Array() {
17+
let values: [UInt16] = [0x0102, 0x0304]
18+
let data = Data.CC_fromArray(values)
19+
XCTAssertEqual(data.count, 4)
20+
}
21+
22+
func testDataFromUInt32Array() {
23+
let values: [UInt32] = [0x01020304, 0x05060708]
24+
let data = Data.CC_fromArray(values)
25+
XCTAssertEqual(data.count, 8)
26+
}
27+
28+
func testDataFromInt32Array() {
29+
let values: [Int32] = [-1, 0, 1]
30+
let data = Data.CC_fromArray(values)
31+
XCTAssertEqual(data.count, 12)
32+
}
33+
34+
func testDataFromEmptyArray() {
35+
let empty: [UInt8] = []
36+
let data = Data.CC_fromArray(empty)
37+
XCTAssertEqual(data.count, 0)
38+
XCTAssertTrue(data.isEmpty)
39+
}
40+
41+
func testDataToUInt8Array() {
42+
let data = Data([0x01, 0x02, 0x03, 0x04])
43+
let result: [UInt8] = data.CC_toArray(of: UInt8.self)
44+
XCTAssertEqual(result, [0x01, 0x02, 0x03, 0x04])
45+
}
46+
47+
func testDataToUInt16Array() {
48+
let data = Data([0x01, 0x02, 0x03, 0x04])
49+
let result: [UInt16] = data.CC_toArray(of: UInt16.self)
50+
XCTAssertEqual(result.count, 2)
51+
}
52+
53+
func testDataToUInt32Array() {
54+
let data = Data([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
55+
let result: [UInt32] = data.CC_toArray(of: UInt32.self)
56+
XCTAssertEqual(result.count, 2)
57+
}
58+
59+
func testDataToInt8Array() {
60+
let data = Data([0xFF, 0x00, 0x7F])
61+
let result: [Int8] = data.CC_toArray(of: Int8.self)
62+
XCTAssertEqual(result, [-1, 0, 127])
63+
}
64+
65+
func testDataToEmptyArray() {
66+
let data = Data()
67+
let result: [UInt8] = data.CC_toArray(of: UInt8.self)
68+
XCTAssertTrue(result.isEmpty)
69+
}
70+
71+
func testDataPartialConversion() {
72+
// Test with data that doesn't align perfectly with target type
73+
let data = Data([0x01, 0x02, 0x03]) // 3 bytes
74+
let result: [UInt16] = data.CC_toArray(of: UInt16.self) // Should produce 1 UInt16 (2 bytes used)
75+
XCTAssertEqual(result.count, 1)
76+
}
77+
78+
func testRoundTripConversion() {
79+
let original: [UInt32] = [0x12345678, 0x9ABCDEF0, 0x11223344]
80+
let data = Data.CC_fromArray(original)
81+
let converted: [UInt32] = data.CC_toArray(of: UInt32.self)
82+
XCTAssertEqual(converted, original)
83+
}
84+
85+
func testDataNumbersConversion() {
86+
let data = Data([0x01, 0x02, 0x03])
87+
let numbers = data.CC_numbers
88+
XCTAssertEqual(numbers.count, 3)
89+
XCTAssertEqual(numbers[0].uint8Value, 0x01)
90+
XCTAssertEqual(numbers[1].uint8Value, 0x02)
91+
XCTAssertEqual(numbers[2].uint8Value, 0x03)
92+
}
93+
94+
func testDataNumbersEmptyData() {
95+
let data = Data()
96+
let numbers = data.CC_numbers
97+
XCTAssertTrue(numbers.isEmpty)
98+
}
99+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// Cornucopia – (C) Dr. Lauer Information Technology
3+
//
4+
import XCTest
5+
@testable import CornucopiaCore
6+
7+
final class URLExtensionsTests: XCTestCase {
8+
9+
func testURLBasename() throws {
10+
let fileURL = URL(fileURLWithPath: "/path/to/file.txt")
11+
XCTAssertEqual(fileURL.CC_basename, "file.txt")
12+
13+
let directoryURL = URL(fileURLWithPath: "/path/to/directory/")
14+
XCTAssertEqual(directoryURL.CC_basename, "directory")
15+
16+
let httpURL = try XCTUnwrap(URL(string: "https://example.com/path/to/file.html"))
17+
XCTAssertEqual(httpURL.CC_basename, "file.html")
18+
19+
let rootURL = URL(fileURLWithPath: "/")
20+
XCTAssertEqual(rootURL.CC_basename, "file:")
21+
22+
let noExtensionURL = URL(fileURLWithPath: "/path/to/filename")
23+
XCTAssertEqual(noExtensionURL.CC_basename, "filename")
24+
}
25+
26+
func testURLBasenameWithQueryParameters() throws {
27+
let urlWithQuery = try XCTUnwrap(URL(string: "https://example.com/file.html?param=value"))
28+
XCTAssertEqual(urlWithQuery.CC_basename, "file.html?param=value")
29+
}
30+
31+
func testURLBasenameWithFragment() throws {
32+
let urlWithFragment = try XCTUnwrap(URL(string: "https://example.com/file.html#section"))
33+
XCTAssertEqual(urlWithFragment.CC_basename, "file.html#section")
34+
}
35+
}
36+
37+
final class URLRequestAuthorizationTests: XCTestCase {
38+
39+
func testSetBasicAuthorizationHeader() {
40+
var request = URLRequest(url: URL(string: "https://example.com")!)
41+
request.CC_setBasicAuthorizationHeader(username: "user", password: "pass")
42+
43+
let expectedCredentials = "user:pass".data(using: .utf8)!.base64EncodedString()
44+
let expectedHeader = "Basic \(expectedCredentials)"
45+
46+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), expectedHeader)
47+
}
48+
49+
func testSetBasicAuthorizationHeaderWithSpecialCharacters() {
50+
var request = URLRequest(url: URL(string: "https://example.com")!)
51+
request.CC_setBasicAuthorizationHeader(username: "[email protected]", password: "p@ss:word!")
52+
53+
let expectedCredentials = "[email protected]:p@ss:word!".data(using: .utf8)!.base64EncodedString()
54+
let expectedHeader = "Basic \(expectedCredentials)"
55+
56+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), expectedHeader)
57+
}
58+
59+
func testSetBearerAuthorizationHeaderWithBase64() {
60+
var request = URLRequest(url: URL(string: "https://example.com")!)
61+
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
62+
request.CC_setBearerAuthorizationHeader(base64: token)
63+
64+
let expectedHeader = "Bearer \(token)"
65+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), expectedHeader)
66+
}
67+
68+
func testOverwriteExistingAuthorizationHeader() {
69+
var request = URLRequest(url: URL(string: "https://example.com")!)
70+
71+
// Set initial authorization
72+
request.setValue("Initial Auth", forHTTPHeaderField: "Authorization")
73+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Initial Auth")
74+
75+
// Overwrite with basic auth
76+
request.CC_setBasicAuthorizationHeader(username: "user", password: "pass")
77+
78+
let expectedCredentials = "user:pass".data(using: .utf8)!.base64EncodedString()
79+
let expectedHeader = "Basic \(expectedCredentials)"
80+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), expectedHeader)
81+
}
82+
83+
func testMultipleAuthorizationUpdates() {
84+
var request = URLRequest(url: URL(string: "https://example.com")!)
85+
86+
// Set basic auth
87+
request.CC_setBasicAuthorizationHeader(username: "user1", password: "pass1")
88+
let basicAuth = request.value(forHTTPHeaderField: "Authorization")
89+
XCTAssertTrue(basicAuth?.starts(with: "Basic ") == true)
90+
91+
// Switch to bearer auth
92+
request.CC_setBearerAuthorizationHeader(base64: "token123")
93+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Bearer token123")
94+
95+
// Switch back to basic auth with different credentials
96+
request.CC_setBasicAuthorizationHeader(username: "user2", password: "pass2")
97+
let newBasicAuth = request.value(forHTTPHeaderField: "Authorization")
98+
XCTAssertTrue(newBasicAuth?.starts(with: "Basic ") == true)
99+
XCTAssertNotEqual(newBasicAuth, basicAuth)
100+
}
101+
102+
func testEmptyCredentials() {
103+
var request = URLRequest(url: URL(string: "https://example.com")!)
104+
request.CC_setBasicAuthorizationHeader(username: "", password: "")
105+
106+
let expectedCredentials = ":".data(using: .utf8)!.base64EncodedString()
107+
let expectedHeader = "Basic \(expectedCredentials)"
108+
109+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), expectedHeader)
110+
}
111+
}

Tests/CornucopiaCoreTests/Logging/FileLogger.swift

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,49 @@ import CornucopiaCore
44

55
class FileLogger: XCTestCase {
66

7-
func testFilelogger() {
7+
override func tearDown() {
8+
// Reset to print logger to avoid affecting other tests
9+
let printLogger = Cornucopia.Core.PrintLogger()
10+
Cornucopia.Core.Logger.overrideSink(printLogger, level: "INFO")
11+
super.tearDown()
12+
}
813

9-
let logFileURL: URL = "file:///tmp/logfile.log"
14+
func testFilelogger() {
15+
// Use a unique filename for each test run to avoid race conditions
16+
let logFileName = "logfile-\(UUID().uuidString).log"
17+
let logFileURL: URL = "file:///tmp/\(logFileName)"
1018
let helloWorld = "Hello World!"
1119

12-
try? FileManager.default.removeItem(at: logFileURL) // might fail, if not present…
13-
let logFile = Cornucopia.Core.FileLogger(url: logFileURL)
20+
// Clean up any existing file
21+
try? FileManager.default.removeItem(at: logFileURL)
1422

23+
// Test FileLogger directly without using the global Logger to avoid static state issues
24+
let fileLogger = Cornucopia.Core.FileLogger(url: logFileURL)
25+
26+
// Create log entry directly
1527
let entry = Cornucopia.Core.LogEntry(
16-
level: .info,
28+
level: .trace,
1729
app: "CornucopiaCoreTests",
18-
subsystem: "Test",
30+
subsystem: "CornucopiaCoreTests",
1931
category: "FileLogger",
20-
thread: 1,
32+
thread: 0,
2133
message: helloWorld
2234
)
23-
logFile.log(entry)
35+
36+
fileLogger.log(entry)
2437

38+
// Verify the file was created and contains expected content
39+
XCTAssertTrue(FileManager.default.fileExists(atPath: logFileURL.path), "Log file should exist")
40+
2541
let data = try! Data(contentsOf: logFileURL)
2642
let string = data.CC_string
27-
XCTAssertTrue(string.contains("Test:FileLogger"))
28-
XCTAssertTrue(string.contains("1"))
29-
XCTAssertTrue(string.contains(helloWorld))
43+
44+
XCTAssertTrue(string.contains("CornucopiaCoreTests"), "Should contain CornucopiaCoreTests, but got: '\(string)'")
45+
XCTAssertTrue(string.contains("FileLogger"), "Should contain FileLogger, but got: '\(string)'")
46+
XCTAssertFalse(string.contains("FileLogger.swift"), "Should not contain FileLogger.swift, but got: '\(string)'")
47+
XCTAssertTrue(string.contains(helloWorld), "Should contain '\(helloWorld)', but got: '\(string)'")
48+
49+
// Clean up
50+
try? FileManager.default.removeItem(at: logFileURL)
3051
}
3152
}

0 commit comments

Comments
 (0)