|
| 1 | +// |
| 2 | +// SnowflakeAuthTests.swift |
| 3 | +// TableProTests |
| 4 | +// |
| 5 | +// Tests for SnowflakeAccount, SnowflakeConnectionsTOML, and the SPKI |
| 6 | +// wrapping used for key-pair JWT fingerprints (compiled via symlink from |
| 7 | +// SnowflakeDriverPlugin). |
| 8 | +// |
| 9 | + |
| 10 | +import Foundation |
| 11 | +import Testing |
| 12 | + |
| 13 | +@Suite("Snowflake Account Parsing") |
| 14 | +struct SnowflakeAccountTests { |
| 15 | + @Test("Plain locator gets the Snowflake domain appended") |
| 16 | + func testHostFromLocator() { |
| 17 | + #expect(SnowflakeAccount.host(forAccount: "xy12345.us-east-1") == "xy12345.us-east-1.snowflakecomputing.com") |
| 18 | + #expect(SnowflakeAccount.host(forAccount: "myorg-myaccount") == "myorg-myaccount.snowflakecomputing.com") |
| 19 | + } |
| 20 | + |
| 21 | + @Test("Full hostnames pass through unchanged, case-insensitively") |
| 22 | + func testHostPassthrough() { |
| 23 | + #expect( |
| 24 | + SnowflakeAccount.host(forAccount: "abc.snowflakecomputing.com") == "abc.snowflakecomputing.com" |
| 25 | + ) |
| 26 | + #expect( |
| 27 | + SnowflakeAccount.host(forAccount: "Abc.SnowflakeComputing.Com") == "Abc.SnowflakeComputing.Com" |
| 28 | + ) |
| 29 | + } |
| 30 | + |
| 31 | + @Test("URL forms resolve to their host") |
| 32 | + func testHostFromURL() { |
| 33 | + #expect( |
| 34 | + SnowflakeAccount.host(forAccount: "https://abc.snowflakecomputing.com/console") == |
| 35 | + "abc.snowflakecomputing.com" |
| 36 | + ) |
| 37 | + } |
| 38 | + |
| 39 | + @Test("Whitespace is trimmed before resolution") |
| 40 | + func testHostTrimsWhitespace() { |
| 41 | + #expect(SnowflakeAccount.host(forAccount: " abc \n") == "abc.snowflakecomputing.com") |
| 42 | + } |
| 43 | + |
| 44 | + @Test("Issuer account name drops domain and region, then uppercases") |
| 45 | + func testIssuerAccountName() { |
| 46 | + #expect(SnowflakeAccount.issuerAccountName(forAccount: "xy12345.us-east-1") == "XY12345") |
| 47 | + #expect( |
| 48 | + SnowflakeAccount.issuerAccountName(forAccount: "xy12345.us-east-1.snowflakecomputing.com") == "XY12345" |
| 49 | + ) |
| 50 | + #expect(SnowflakeAccount.issuerAccountName(forAccount: "myorg-myaccount") == "MYORG-MYACCOUNT") |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +@Suite("Snowflake Connections TOML") |
| 55 | +struct SnowflakeConnectionsTOMLTests { |
| 56 | + @Test("Parses sections with key-value pairs") |
| 57 | + func testBasicSection() { |
| 58 | + let toml = """ |
| 59 | + [default] |
| 60 | + account = "xy12345" |
| 61 | + user = jane |
| 62 | + """ |
| 63 | + let parsed = SnowflakeConnectionsTOML.parse(toml) |
| 64 | + #expect(parsed["default"]?["account"] == "xy12345") |
| 65 | + #expect(parsed["default"]?["user"] == "jane") |
| 66 | + } |
| 67 | + |
| 68 | + @Test("Strips the connections. prefix from config.toml sections") |
| 69 | + func testConnectionsPrefix() { |
| 70 | + let parsed = SnowflakeConnectionsTOML.parse("[connections.dev]\naccount = 'abc'\n") |
| 71 | + #expect(parsed["dev"]?["account"] == "abc") |
| 72 | + } |
| 73 | + |
| 74 | + @Test("Quoted section names are unquoted") |
| 75 | + func testQuotedSectionName() { |
| 76 | + let parsed = SnowflakeConnectionsTOML.parse("[connections.\"my conn\"]\nrole = \"ADMIN\"\n") |
| 77 | + #expect(parsed["my conn"]?["role"] == "ADMIN") |
| 78 | + } |
| 79 | + |
| 80 | + @Test("Comments are stripped outside strings and kept inside both quote styles") |
| 81 | + func testCommentHandling() { |
| 82 | + let toml = """ |
| 83 | + [default] |
| 84 | + account = "abc" # trailing comment |
| 85 | + password = "p#ss" |
| 86 | + token = 'a#b' |
| 87 | + """ |
| 88 | + let parsed = SnowflakeConnectionsTOML.parse(toml) |
| 89 | + #expect(parsed["default"]?["account"] == "abc") |
| 90 | + #expect(parsed["default"]?["password"] == "p#ss") |
| 91 | + #expect(parsed["default"]?["token"] == "a#b") |
| 92 | + } |
| 93 | + |
| 94 | + @Test("Key-value pairs before any section are ignored") |
| 95 | + func testKeysOutsideSectionIgnored() { |
| 96 | + let parsed = SnowflakeConnectionsTOML.parse("account = \"abc\"\n[dev]\nuser = \"u\"\n") |
| 97 | + #expect(parsed.count == 1) |
| 98 | + #expect(parsed["dev"]?["user"] == "u") |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +@Suite("Snowflake SPKI Wrapping") |
| 103 | +struct SnowflakeSPKIWrappingTests { |
| 104 | + private static let rsaAlgorithmID: [UInt8] = [ |
| 105 | + 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, |
| 106 | + 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 |
| 107 | + ] |
| 108 | + |
| 109 | + @Test("Short keys use single-byte DER lengths") |
| 110 | + func testShortFormLength() { |
| 111 | + let pkcs1 = Data((0..<10).map { UInt8($0) }) |
| 112 | + let spki = [UInt8](SnowflakeKeyPairAuth.wrapPKCS1IntoSPKI(pkcs1)) |
| 113 | + |
| 114 | + #expect(spki[0] == 0x30) |
| 115 | + #expect(Int(spki[1]) == spki.count - 2) |
| 116 | + #expect(Array(spki[2..<17]) == Self.rsaAlgorithmID) |
| 117 | + #expect(spki[17] == 0x03) |
| 118 | + #expect(Int(spki[18]) == pkcs1.count + 1) |
| 119 | + #expect(spki[19] == 0x00) |
| 120 | + #expect(Array(spki.suffix(pkcs1.count)) == [UInt8](pkcs1)) |
| 121 | + } |
| 122 | + |
| 123 | + @Test("Keys past 127 bytes use long-form DER lengths") |
| 124 | + func testLongFormLength() { |
| 125 | + let pkcs1 = Data(repeating: 0xAB, count: 270) |
| 126 | + let spki = [UInt8](SnowflakeKeyPairAuth.wrapPKCS1IntoSPKI(pkcs1)) |
| 127 | + |
| 128 | + #expect(spki[0] == 0x30) |
| 129 | + #expect(spki[1] == 0x82) |
| 130 | + let bodyLength = (Int(spki[2]) << 8) | Int(spki[3]) |
| 131 | + #expect(bodyLength == spki.count - 4) |
| 132 | + #expect(Array(spki[4..<19]) == Self.rsaAlgorithmID) |
| 133 | + #expect(spki[19] == 0x03) |
| 134 | + #expect(spki[20] == 0x82) |
| 135 | + let bitStringLength = (Int(spki[21]) << 8) | Int(spki[22]) |
| 136 | + #expect(bitStringLength == pkcs1.count + 1) |
| 137 | + #expect(spki[23] == 0x00) |
| 138 | + #expect(Array(spki.suffix(pkcs1.count)) == [UInt8](pkcs1)) |
| 139 | + } |
| 140 | +} |
0 commit comments