import XCTest
import CryptoKit
@testable import CxIDE
// MARK: - Credential Store Tests
final class CredentialStoreTests: XCTestCase {
private var store: CredentialStore!
private var tempDir: URL!
override func setUpWithError() throws {
store = CredentialStore()
tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cxide-test-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
}
override func tearDownWithError() throws {
try? FileManager.default.removeItem(at: tempDir)
}
func testSetAndGet() {
store.set("TEST_KEY", value: "hello")
XCTAssertEqual(store.get("TEST_KEY"), "hello")
}
func testGetMissing() {
XCTAssertNil(store.get("NONEXISTENT"))
}
func testRemove() {
store.set("KEY", value: "val")
XCTAssertTrue(store.has("KEY"))
store.remove("KEY")
XCTAssertFalse(store.has("KEY"))
XCTAssertNil(store.get("KEY"))
}
func testKeys() {
store.set("B_KEY", value: "1")
store.set("A_KEY", value: "2")
let keys = store.keys()
XCTAssertEqual(keys, ["A_KEY", "B_KEY"]) // sorted
}
func testHas() {
XCTAssertFalse(store.has("FOO"))
store.set("FOO", value: "bar")
XCTAssertTrue(store.has("FOO"))
}
func testSaveAndLoad() throws {
store.set("AWS_KEY", value: "AKIAEXAMPLE")
store.set("AWS_SECRET", value: "secretValue123")
try store.save(to: tempDir)
let store2 = CredentialStore()
try store2.load(from: tempDir)
XCTAssertEqual(store2.get("AWS_KEY"), "AKIAEXAMPLE")
XCTAssertEqual(store2.get("AWS_SECRET"), "secretValue123")
}
func testLoadFromEmptyDir() throws {
let emptyDir = tempDir.appendingPathComponent("empty")
try FileManager.default.createDirectory(at: emptyDir, withIntermediateDirectories: true)
try store.load(from: emptyDir) // Should not throw
XCTAssertTrue(store.keys().isEmpty)
}
func testOverwriteValue() {
store.set("KEY", value: "first")
store.set("KEY", value: "second")
XCTAssertEqual(store.get("KEY"), "second")
}
func testDeleteStore() throws {
store.set("DATA", value: "val")
try store.save(to: tempDir)
try store.deleteStore(in: tempDir)
XCTAssertTrue(store.keys().isEmpty)
}
func testGoDaddyConvenience() {
store.setGoDaddyOTE(key: "oteKey", secret: "oteSecret")
XCTAssertEqual(store.godaddyOTEKey, "oteKey")
XCTAssertEqual(store.godaddyOTESecret, "oteSecret")
store.setGoDaddyProduction(key: "prodKey", secret: "prodSecret")
XCTAssertEqual(store.godaddyProdKey, "prodKey")
XCTAssertEqual(store.godaddyProdSecret, "prodSecret")
}
func testAWSConvenience() {
store.setAWS(accessKeyId: "AKIA123", secretAccessKey: "secret456", region: "eu-west-1")
XCTAssertEqual(store.awsAccessKeyId, "AKIA123")
XCTAssertEqual(store.awsSecretAccessKey, "secret456")
XCTAssertEqual(store.awsDefaultRegion, "eu-west-1")
}
func testAWSDefaultRegion() {
XCTAssertEqual(store.awsDefaultRegion, "us-east-1") // default when not set
}
func testCredentialErrorDescriptions() {
let e1 = CredentialStore.CredentialError.encryptionFailed
XCTAssertNotNil(e1.errorDescription)
XCTAssertTrue(e1.errorDescription!.contains("encrypt"))
let e2 = CredentialStore.CredentialError.decryptionFailed
XCTAssertTrue(e2.errorDescription!.contains("decrypt"))
let e3 = CredentialStore.CredentialError.invalidFormat
XCTAssertTrue(e3.errorDescription!.contains("Invalid"))
}
func testEncryptedFilePermissions() throws {
store.set("KEY", value: "value")
try store.save(to: tempDir)
let fileURL = tempDir.appendingPathComponent(".cxide-credentials.enc")
let attrs = try FileManager.default.attributesOfItem(atPath: fileURL.path)
let perms = attrs[.posixPermissions] as? Int
XCTAssertEqual(perms, 0o600)
}
}
// MARK: - AWS XML Parser Tests
final class AWSXMLParserTests: XCTestCase {
func testParseSimpleElement() throws {
let xml = "hello"
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
XCTAssertEqual(root.name, "Root")
XCTAssertEqual(root.child("Name")?.text, "hello")
}
func testParseNestedElements() throws {
let xml = """
bucket-a
bucket-b
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
let buckets = root.allDescendants("Bucket")
XCTAssertEqual(buckets.count, 2)
XCTAssertEqual(buckets[0].child("Name")?.text, "bucket-a")
XCTAssertEqual(buckets[1].child("Name")?.text, "bucket-b")
}
func testDescendantSearch() throws {
let xml = """
deep
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
XCTAssertEqual(root.descendant("D")?.text, "deep")
XCTAssertNil(root.descendant("Missing"))
}
func testAllDescendants() throws {
let xml = """
1
2
3
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
let values = root.allDescendants("Value")
XCTAssertEqual(values.count, 3)
XCTAssertEqual(values.map(\.text), ["1", "2", "3"])
}
func testChildVsDescendant() throws {
let xml = "directnested"
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
// child only finds direct children
XCTAssertEqual(root.child("B")?.text, "direct")
// allChildren only finds direct children
XCTAssertEqual(root.allChildren("B").count, 1)
// allDescendants finds all
XCTAssertEqual(root.allDescendants("B").count, 2)
}
func testEmptyElement() throws {
let xml = ""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
XCTAssertEqual(root.child("Empty")?.text, "")
}
func testInvalidXMLThrows() {
let bad = ""
XCTAssertThrowsError(try AWSXMLParser().parse(data: Data(bad.utf8)))
}
func testParseS3ListBucketsResponse() throws {
let xml = """
owner123
my-website
2026-01-15T10:30:00.000Z
my-backups
2026-03-20T14:00:00.000Z
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
let buckets = root.allDescendants("Bucket")
XCTAssertEqual(buckets.count, 2)
XCTAssertEqual(buckets[0].child("Name")?.text, "my-website")
XCTAssertEqual(buckets[1].child("Name")?.text, "my-backups")
}
func testParseS3ListObjectsResponse() throws {
let xml = """
my-site
index.html
2048
2026-04-01T12:00:00.000Z
"abc123"
css/style.css
512
2026-04-01T12:00:01.000Z
"def456"
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
let contents = root.allDescendants("Contents")
XCTAssertEqual(contents.count, 2)
XCTAssertEqual(contents[0].child("Key")?.text, "index.html")
XCTAssertEqual(contents[0].child("Size")?.text, "2048")
XCTAssertEqual(contents[1].child("Key")?.text, "css/style.css")
}
func testParseRoute53HostedZonesResponse() throws {
let xml = """
/hostedzone/Z1234ABC
example.com.
8
My domainfalse
"""
let root = try AWSXMLParser().parse(data: Data(xml.utf8))
let zones = root.allDescendants("HostedZone")
XCTAssertEqual(zones.count, 1)
XCTAssertEqual(zones[0].child("Id")?.text, "/hostedzone/Z1234ABC")
XCTAssertEqual(zones[0].child("Name")?.text, "example.com.")
XCTAssertEqual(zones[0].descendant("Comment")?.text, "My domain")
}
}
// MARK: - AWS Error Tests
final class AWSErrorTests: XCTestCase {
func testAllErrorsHaveDescriptions() {
let errors: [AWSError] = [
.notAuthenticated,
.invalidURL("https://bad"),
.invalidResponse,
.apiError(statusCode: 403, body: "Forbidden"),
.bucketNotFound("my-bucket"),
.objectNotFound("file.txt"),
.bucketAlreadyExists("existing"),
.accessDenied("s3://bucket"),
.invalidXML("parse error"),
.deploymentFailed("timeout"),
.distributionNotFound("EXYZ123"),
.hostedZoneNotFound("example.com"),
.functionNotFound("myFunc"),
.timeout,
]
for error in errors {
XCTAssertNotNil(error.errorDescription, "\(error) should have a description")
XCTAssertFalse(error.errorDescription!.isEmpty)
}
}
func testNotAuthenticatedMessage() {
let err = AWSError.notAuthenticated
XCTAssertTrue(err.errorDescription!.contains("credential"))
}
func testApiErrorIncludesStatusCode() {
let err = AWSError.apiError(statusCode: 500, body: "Internal Server Error")
XCTAssertTrue(err.errorDescription!.contains("500"))
XCTAssertTrue(err.errorDescription!.contains("Internal Server Error"))
}
func testApiErrorTruncatesLongBody() {
let longBody = String(repeating: "x", count: 1000)
let err = AWSError.apiError(statusCode: 400, body: longBody)
// Should truncate to 500 chars
XCTAssertTrue(err.errorDescription!.count < 600)
}
}
// MARK: - AWS Service Credential Tests
final class AWSServiceTests: XCTestCase {
func testDefaultRegion() {
let store = CredentialStore()
let aws = AWSService(credentialStore: store)
XCTAssertEqual(aws.defaultRegion, "us-east-1")
}
func testSetAndGetCredentials() {
let store = CredentialStore()
let aws = AWSService(credentialStore: store)
aws.setCredentials(accessKeyId: "AKIA123", secretAccessKey: "secret", region: "eu-west-1")
XCTAssertEqual(aws.accessKeyId, "AKIA123")
XCTAssertEqual(aws.secretAccessKey, "secret")
XCTAssertEqual(aws.defaultRegion, "eu-west-1")
}
func testIsAuthenticated() {
let store = CredentialStore()
let aws = AWSService(credentialStore: store)
XCTAssertFalse(aws.isAuthenticated)
aws.setCredentials(accessKeyId: "AKIA123", secretAccessKey: "secret")
XCTAssertTrue(aws.isAuthenticated)
}
func testIsNotAuthenticatedWithEmpty() {
let store = CredentialStore()
let aws = AWSService(credentialStore: store)
store.set("AWS_ACCESS_KEY_ID", value: "")
store.set("AWS_SECRET_ACCESS_KEY", value: "")
XCTAssertFalse(aws.isAuthenticated)
}
func testSessionToken() {
let store = CredentialStore()
let aws = AWSService(credentialStore: store)
XCTAssertNil(aws.sessionToken)
store.set("AWS_SESSION_TOKEN", value: "tok123")
XCTAssertEqual(aws.sessionToken, "tok123")
}
func testRegionEnum() {
XCTAssertEqual(AWSService.Region.usEast1.rawValue, "us-east-1")
XCTAssertEqual(AWSService.Region.euCentral1.rawValue, "eu-central-1")
XCTAssertEqual(AWSService.Region.allCases.count, 10)
}
}
// MARK: - SHA256/Data Hex Extension Tests
final class HexStringTests: XCTestCase {
func testSHA256Hex() {
let hash = SHA256.hash(data: Data("hello".utf8))
XCTAssertEqual(hash.hexString.count, 64) // SHA256 = 32 bytes = 64 hex chars
XCTAssertEqual(hash.hexString, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
}
func testDataHex() {
let data = Data([0x00, 0xFF, 0xAB, 0x12])
XCTAssertEqual(data.hexString, "00ffab12")
}
func testEmptyDataHex() {
XCTAssertEqual(Data().hexString, "")
}
}
// MARK: - S3 Model Tests
final class S3ModelTests: XCTestCase {
func testS3ObjectFormattedSize() {
XCTAssertEqual(S3Object(key: "a", size: 500, lastModified: nil, etag: nil).formattedSize, "500 B")
XCTAssertTrue(S3Object(key: "b", size: 2048, lastModified: nil, etag: nil).formattedSize.contains("KB"))
XCTAssertTrue(S3Object(key: "c", size: 5_242_880, lastModified: nil, etag: nil).formattedSize.contains("MB"))
XCTAssertTrue(S3Object(key: "d", size: 2_147_483_648, lastModified: nil, etag: nil).formattedSize.contains("GB"))
}
func testS3DeployResult() {
let result = S3DeployResult(
success: true, bucket: "test-bucket", region: "us-east-1",
uploadedFiles: ["index.html", "style.css"], errors: [],
websiteURL: "http://test-bucket.s3-website-us-east-1.amazonaws.com", duration: 2.5
)
XCTAssertTrue(result.success)
XCTAssertEqual(result.uploadedFiles.count, 2)
XCTAssertTrue(result.errors.isEmpty)
XCTAssertNotNil(result.websiteURL)
}
func testS3BucketModel() {
let bucket = S3Bucket(name: "my-site", creationDate: "2026-01-01T00:00:00Z")
XCTAssertEqual(bucket.name, "my-site")
XCTAssertEqual(bucket.creationDate, "2026-01-01T00:00:00Z")
}
}
// MARK: - Route 53 Model Tests
final class Route53ModelTests: XCTestCase {
func testHostedZoneDomainName() {
let zone = Route53HostedZone(id: "Z123", name: "example.com.", recordCount: 5, comment: nil, isPrivate: false)
XCTAssertEqual(zone.domainName, "example.com") // trailing dot removed
}
func testHostedZoneNoDot() {
let zone = Route53HostedZone(id: "Z123", name: "example.com", recordCount: 5, comment: nil, isPrivate: false)
XCTAssertEqual(zone.domainName, "example.com")
}
func testRecordSetIsAlias() {
let alias = Route53AliasTarget(hostedZoneId: "Z2FD", dnsName: "d123.cloudfront.net", evaluateTargetHealth: false)
let record = Route53RecordSet(name: "example.com.", type: "A", ttl: 300, values: [], aliasTarget: alias)
XCTAssertTrue(record.isAlias)
let normal = Route53RecordSet(name: "example.com.", type: "A", ttl: 300, values: ["1.2.3.4"], aliasTarget: nil)
XCTAssertFalse(normal.isAlias)
}
}
// MARK: - CloudFront Model Tests
final class CloudFrontModelTests: XCTestCase {
func testDistribution() {
let dist = CloudFrontDistribution(
id: "E1ABC", domainName: "d123.cloudfront.net",
status: "Deployed", enabled: true,
aliases: ["example.com"], origins: ["my-bucket.s3.amazonaws.com"]
)
XCTAssertEqual(dist.id, "E1ABC")
XCTAssertTrue(dist.enabled)
XCTAssertEqual(dist.aliases, ["example.com"])
}
func testACMCertificate() {
let cert = ACMCertificate(arn: "arn:aws:acm:us-east-1:123:cert/abc", domainName: "example.com", status: "ISSUED")
XCTAssertTrue(cert.arn.hasPrefix("arn:aws:acm"))
XCTAssertEqual(cert.status, "ISSUED")
}
func testCertificateValidationRecord() {
let rec = CertificateValidationRecord(
domain: "example.com",
recordName: "_abc.example.com.",
recordValue: "_xyz.acm-validation.aws.",
recordType: "CNAME"
)
XCTAssertEqual(rec.recordType, "CNAME")
XCTAssertTrue(rec.recordName.hasPrefix("_"))
}
}
// MARK: - Lambda Model Tests
final class LambdaModelTests: XCTestCase {
func testRuntimeRawValues() {
XCTAssertEqual(LambdaRuntime.nodejs20.rawValue, "nodejs20.x")
XCTAssertEqual(LambdaRuntime.python312.rawValue, "python3.12")
XCTAssertEqual(LambdaRuntime.dotnet8.rawValue, "dotnet8")
XCTAssertEqual(LambdaRuntime.provided2023.rawValue, "provided.al2023")
}
func testRuntimeDisplayNames() {
XCTAssertEqual(LambdaRuntime.nodejs20.displayName, "Node.js 20.x")
XCTAssertEqual(LambdaRuntime.python312.displayName, "Python 3.12")
XCTAssertEqual(LambdaRuntime.java21.displayName, "Java 21")
}
func testAllRuntimes() {
XCTAssertEqual(LambdaRuntime.allCases.count, 10)
}
func testLambdaFunctionFormattedCodeSize() {
let small = LambdaFunction(name: "f", arn: "", runtime: "nodejs20.x", handler: "index.handler",
memorySize: 128, timeout: 3, lastModified: nil, codeSize: 500, description: nil)
XCTAssertEqual(small.formattedCodeSize, "500 B")
let medium = LambdaFunction(name: "f", arn: "", runtime: "nodejs20.x", handler: "index.handler",
memorySize: 128, timeout: 3, lastModified: nil, codeSize: 5120, description: nil)
XCTAssertTrue(medium.formattedCodeSize.contains("KB"))
let large = LambdaFunction(name: "f", arn: "", runtime: "nodejs20.x", handler: "index.handler",
memorySize: 128, timeout: 3, lastModified: nil, codeSize: 10_485_760, description: nil)
XCTAssertTrue(large.formattedCodeSize.contains("MB"))
}
func testInvokeResult() {
let result = LambdaInvokeResult(statusCode: 200, payload: "{\"message\":\"ok\"}", parsedPayload: ["message": "ok"])
XCTAssertEqual(result.statusCode, 200)
XCTAssertTrue(result.payload.contains("ok"))
}
}
// MARK: - Website Template Tests
@MainActor
final class WebsiteTemplateTests: XCTestCase {
func testAllTemplatesExist() {
XCTAssertEqual(WebsiteTemplate.allTemplates.count, 6)
}
func testTemplateIDs() {
let ids = WebsiteTemplate.allTemplates.map(\.id)
XCTAssertTrue(ids.contains("blank"))
XCTAssertTrue(ids.contains("landing"))
XCTAssertTrue(ids.contains("portfolio"))
XCTAssertTrue(ids.contains("blog"))
XCTAssertTrue(ids.contains("docs"))
XCTAssertTrue(ids.contains("dashboard"))
}
func testBlankTemplateGeneratesFiles() {
let files = WebsiteTemplate.blank.generateFiles("TestSite")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
XCTAssertTrue(paths.contains("css/style.css"))
XCTAssertTrue(paths.contains("js/main.js"))
}
func testBlankTemplateContainsProjectName() {
let files = WebsiteTemplate.blank.generateFiles("My Cool Site")
let html = files.first { $0.path == "index.html" }?.content ?? ""
XCTAssertTrue(html.contains("My Cool Site"))
}
func testLandingTemplateFiles() {
let files = WebsiteTemplate.landingPage.generateFiles("LandingSite")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
XCTAssertTrue(paths.contains("css/style.css"))
XCTAssertTrue(paths.contains("js/main.js"))
XCTAssertGreaterThan(files.count, 3)
}
func testPortfolioTemplateFiles() {
let files = WebsiteTemplate.portfolio.generateFiles("Portfolio")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
XCTAssertTrue(paths.contains("projects.html"))
}
func testBlogTemplateFiles() {
let files = WebsiteTemplate.blog.generateFiles("Blog")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
XCTAssertTrue(paths.contains("post.html"))
}
func testDocsTemplateFiles() {
let files = WebsiteTemplate.documentation.generateFiles("Docs")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
}
func testDashboardTemplateFiles() {
let files = WebsiteTemplate.dashboard.generateFiles("Dashboard")
let paths = files.map(\.path)
XCTAssertTrue(paths.contains("index.html"))
}
func testAllTemplatesProduceValidHTML() {
for template in WebsiteTemplate.allTemplates {
let files = template.generateFiles("TestProject")
let html = files.first { $0.path == "index.html" }?.content ?? ""
XCTAssertTrue(html.contains(""),
"\(template.id) should have closing html tag")
}
}
func testTemplateCategories() {
for template in WebsiteTemplate.allTemplates {
XCTAssertFalse(template.category.isEmpty, "\(template.id) should have a category")
XCTAssertFalse(template.description.isEmpty, "\(template.id) should have a description")
}
}
func testWebsiteProjectInit() {
let dir = URL(fileURLWithPath: "/tmp/test")
let project = WebsiteProject(name: "site", directory: dir, template: "blank", createdAt: Date())
XCTAssertEqual(project.name, "site")
XCTAssertEqual(project.template, "blank")
XCTAssertNil(project.deployedURL)
XCTAssertNil(project.domain)
}
}
// MARK: - Website Deploy Provider Tests
final class WebsiteDeployProviderTests: XCTestCase {
func testAllProviders() {
XCTAssertEqual(WebsiteDeployService.Provider.allCases.count, 5)
}
func testProviderDescriptions() {
for provider in WebsiteDeployService.Provider.allCases {
XCTAssertFalse(provider.description.isEmpty)
}
}
func testProviderCLIRequirements() {
XCTAssertEqual(WebsiteDeployService.Provider.netlify.requiresCLI, "netlify")
XCTAssertEqual(WebsiteDeployService.Provider.surge.requiresCLI, "surge")
XCTAssertEqual(WebsiteDeployService.Provider.vercel.requiresCLI, "vercel")
XCTAssertEqual(WebsiteDeployService.Provider.githubPages.requiresCLI, "git")
XCTAssertEqual(WebsiteDeployService.Provider.rsync.requiresCLI, "rsync")
}
func testDeployResultModel() {
let result = WebsiteDeployService.DeployResult(
success: true, url: "https://my-site.netlify.app",
output: "Published", provider: .netlify, duration: 3.5
)
XCTAssertTrue(result.success)
XCTAssertEqual(result.url, "https://my-site.netlify.app")
XCTAssertEqual(result.provider, .netlify)
}
}
// MARK: - GoDaddy Model Tests
final class GoDaddyModelTests: XCTestCase {
func testEnvironments() {
XCTAssertTrue(GoDaddyService.Environment.ote.baseURL.contains("ote"))
XCTAssertTrue(GoDaddyService.Environment.production.baseURL.contains("api.godaddy.com"))
XCTAssertEqual(GoDaddyService.Environment.allCases.count, 2)
}
func testDNSRecordTypes() {
XCTAssertEqual(DNSRecordType.A.rawValue, "A")
XCTAssertEqual(DNSRecordType.CNAME.rawValue, "CNAME")
XCTAssertEqual(DNSRecordType.MX.rawValue, "MX")
XCTAssertEqual(DNSRecordType.TXT.rawValue, "TXT")
}
func testGoDaddyErrorDescriptions() {
let errors: [GoDaddyError] = [
.notAuthenticated, .unauthorized, .forbidden,
.notFound("example.com"), .rateLimited, .invalidURL("bad"),
.invalidResponse,
]
for error in errors {
XCTAssertNotNil(error.errorDescription, "\(error) should have description")
}
}
}
// MARK: - AWSS3 Website Endpoint Tests
final class AWSS3WebsiteTests: XCTestCase {
func testWebsiteEndpoint() {
let s3 = AWSS3Service()
let url = s3.websiteEndpoint(bucket: "my-site", region: "us-east-1")
XCTAssertEqual(url, "http://my-site.s3-website-us-east-1.amazonaws.com")
}
func testWebsiteEndpointOtherRegion() {
let s3 = AWSS3Service()
let url = s3.websiteEndpoint(bucket: "eu-site", region: "eu-west-1")
XCTAssertEqual(url, "http://eu-site.s3-website-eu-west-1.amazonaws.com")
}
}