2@testable import ParseableViewer
4final class ParseableClientTests: XCTestCase {
5 // MARK: - Query response parsing
7 func testQueryResponseWrappedFormat() throws {
9 {"records": [{"message": "hello"}], "fields": ["message"]}
10 """.data(using: .utf8)!
12 let wrapped = try JSONDecoder().decode(QueryResponse.self, from: json)
13 XCTAssertEqual(wrapped.records.count, 1)
14 XCTAssertEqual(wrapped.records[0]["message"], .string("hello"))
15 XCTAssertEqual(wrapped.fields, ["message"])
18 func testQueryResponseDirectArrayFormat() throws {
20 [{"level": "info", "msg": "test"}]
21 """.data(using: .utf8)!
23 let records = try JSONDecoder().decode([LogRecord].self, from: json)
24 XCTAssertEqual(records.count, 1)
25 XCTAssertEqual(records[0]["level"], .string("info"))
26 XCTAssertEqual(records[0]["msg"], .string("test"))
29 func testQueryResponseEmptyArray() throws {
30 let json = "[]".data(using: .utf8)!
31 let records = try JSONDecoder().decode([LogRecord].self, from: json)
32 XCTAssertTrue(records.isEmpty)
35 func testQueryResponseWrappedEmptyRecords() throws {
37 {"records": [], "fields": ["a", "b"]}
38 """.data(using: .utf8)!
40 let wrapped = try JSONDecoder().decode(QueryResponse.self, from: json)
41 XCTAssertTrue(wrapped.records.isEmpty)
42 XCTAssertEqual(wrapped.fields, ["a", "b"])
45 // MARK: - Alert config decoding
47 func testAlertConfigKeyedFormat() throws {
49 {"alerts": [{"name": "high-cpu", "message": "CPU > 90%"}], "version": "v1"}
50 """.data(using: .utf8)!
52 let config = try JSONDecoder().decode(AlertConfig.self, from: json)
53 XCTAssertEqual(config.alerts?.count, 1)
54 XCTAssertEqual(config.alerts?[0].name, "high-cpu")
55 XCTAssertEqual(config.alerts?[0].displayName, "high-cpu")
56 XCTAssertEqual(config.version, "v1")
59 func testAlertConfigDirectArrayFormat() throws {
61 [{"name": "disk-full", "message": "Disk usage > 95%"}]
62 """.data(using: .utf8)!
64 let config = try JSONDecoder().decode(AlertConfig.self, from: json)
65 XCTAssertEqual(config.alerts?.count, 1)
66 XCTAssertEqual(config.alerts?[0].name, "disk-full")
67 XCTAssertEqual(config.alerts?[0].displayName, "disk-full")
68 XCTAssertNil(config.version)
71 func testAlertConfigMemberwiseInit() {
72 let config = AlertConfig(alerts: [], version: nil)
73 XCTAssertEqual(config.alerts?.count, 0)
74 XCTAssertNil(config.version)
77 func testAlertConfigModernAPIFormat() throws {
79 [{"id": "01HQ3...", "title": "High CPU Alert", "severity": "Critical", "state": "Triggered", "alertType": "Threshold", "datasets": ["backend-logs"], "created": "2025-01-01T00:00:00Z"}]
80 """.data(using: .utf8)!
82 let config = try JSONDecoder().decode(AlertConfig.self, from: json)
83 XCTAssertEqual(config.alerts?.count, 1)
84 let alert = try XCTUnwrap(config.alerts?.first)
85 XCTAssertEqual(alert.title, "High CPU Alert")
86 XCTAssertEqual(alert.displayName, "High CPU Alert")
87 XCTAssertEqual(alert.severity, "Critical")
88 XCTAssertEqual(alert.state, "Triggered")
89 XCTAssertEqual(alert.alertType, "Threshold")
90 XCTAssertEqual(alert.datasets, ["backend-logs"])
91 XCTAssertEqual(alert.alertId, "01HQ3...")
92 XCTAssertNil(alert.name)
95 func testAlertRuleDisplayNamePrefersTitle() throws {
97 {"title": "My Alert", "name": "my-alert"}
98 """.data(using: .utf8)!
100 let alert = try JSONDecoder().decode(AlertRule.self, from: json)
101 XCTAssertEqual(alert.displayName, "My Alert")
104 func testAlertRuleDisplayNameFallsBackToName() throws {
106 {"name": "legacy-alert"}
107 """.data(using: .utf8)!
109 let alert = try JSONDecoder().decode(AlertRule.self, from: json)
110 XCTAssertEqual(alert.displayName, "legacy-alert")
113 // MARK: - Retention config decoding
115 func testRetentionConfigArray() throws {
117 [{"description": "Keep 30 days", "duration": "30d", "action": "delete"}]
118 """.data(using: .utf8)!
120 let configs = try JSONDecoder().decode([RetentionConfig].self, from: json)
121 XCTAssertEqual(configs.count, 1)
122 XCTAssertEqual(configs[0].duration, "30d")
125 func testRetentionConfigSingleObject() throws {
127 {"description": "Keep 7 days", "duration": "7d", "action": "delete"}
128 """.data(using: .utf8)!
130 let config = try JSONDecoder().decode(RetentionConfig.self, from: json)
131 XCTAssertEqual(config.duration, "7d")
132 XCTAssertEqual(config.action, "delete")
135 // MARK: - Stream schema decoding
137 func testStreamSchemaKeyedFormat() throws {
139 {"fields": [{"name": "level", "data_type": "Utf8"}]}
140 """.data(using: .utf8)!
142 let schema = try JSONDecoder().decode(StreamSchema.self, from: json)
143 XCTAssertEqual(schema.fields.count, 1)
144 XCTAssertEqual(schema.fields[0].name, "level")
145 XCTAssertEqual(schema.fields[0].dataType, "Utf8")
148 func testStreamSchemaDirectArrayFormat() throws {
150 [{"name": "count", "data_type": "Int64"}]
151 """.data(using: .utf8)!
153 let schema = try JSONDecoder().decode(StreamSchema.self, from: json)
154 XCTAssertEqual(schema.fields.count, 1)
155 XCTAssertEqual(schema.fields[0].dataType, "Int64")
158 func testSchemaFieldObjectDataType() throws {
160 {"name": "tags", "data_type": {"List": "Utf8"}}
161 """.data(using: .utf8)!
163 let field = try JSONDecoder().decode(SchemaField.self, from: json)
164 XCTAssertEqual(field.name, "tags")
165 XCTAssertFalse(field.dataType.isEmpty)
168 // MARK: - User info decoding
170 func testUserInfoKeyedFormat() throws {
172 {"id": "admin", "method": "native"}
173 """.data(using: .utf8)!
175 let user = try JSONDecoder().decode(UserInfo.self, from: json)
176 XCTAssertEqual(user.id, "admin")
177 XCTAssertEqual(user.method, "native")
180 func testUserInfoPlainString() throws {
183 """.data(using: .utf8)!
185 let user = try JSONDecoder().decode(UserInfo.self, from: json)
186 XCTAssertEqual(user.id, "readonly-user")
187 XCTAssertNil(user.method)
190 // MARK: - LogStream decoding
192 func testLogStreamKeyedFormat() throws {
194 {"name": "backend-logs"}
195 """.data(using: .utf8)!
197 let stream = try JSONDecoder().decode(LogStream.self, from: json)
198 XCTAssertEqual(stream.name, "backend-logs")
201 func testLogStreamPlainString() throws {
204 """.data(using: .utf8)!
206 let stream = try JSONDecoder().decode(LogStream.self, from: json)
207 XCTAssertEqual(stream.name, "frontend-logs")
210 // MARK: - ServerAbout decoding
212 func testServerAboutPartialFields() throws {
214 {"version": "1.0.0", "mode": "standalone"}
215 """.data(using: .utf8)!
217 let about = try JSONDecoder().decode(ServerAbout.self, from: json)
218 XCTAssertEqual(about.version, "1.0.0")
219 XCTAssertEqual(about.mode, "standalone")
220 XCTAssertNil(about.commit)
221 XCTAssertNil(about.grpcPort)
224 func testServerAboutEmptyObject() throws {
225 let json = "{}".data(using: .utf8)!
226 let about = try JSONDecoder().decode(ServerAbout.self, from: json)
227 XCTAssertNil(about.version)
228 XCTAssertNil(about.mode)
231 // MARK: - Auth header
233 func testClientInitFromConnection() throws {
234 let connection = ServerConnection(
236 url: "https://example.com",
240 let client = try ParseableClient(connection: connection)
241 XCTAssertEqual(client.username, "admin")
242 XCTAssertEqual(client.baseURL.absoluteString, "https://example.com")
245 func testClientInitInvalidURL() {
246 let connection = ServerConnection(
252 XCTAssertThrowsError(try ParseableClient(connection: connection))
255 // MARK: - ParseableError
257 func testServerErrorDescription() {
258 let error = ParseableError.serverError(500, "Internal Server Error")
259 XCTAssertTrue(error.errorDescription?.contains("500") ?? false)
262 func testUnauthorizedDescription() {
263 let error = ParseableError.unauthorized
264 XCTAssertTrue(error.errorDescription?.contains("Authentication") ?? false)
267 func testNotConnectedDescription() {
268 let error = ParseableError.notConnected
269 XCTAssertTrue(error.errorDescription?.contains("Not connected") ?? false)
272 // MARK: - User-friendly error messages
274 func testUserFriendlyMessageForUnauthorized() {
275 let msg = ParseableError.userFriendlyMessage(for: ParseableError.unauthorized)
276 XCTAssertTrue(msg.contains("username"))
277 XCTAssertTrue(msg.contains("password"))
280 func testUserFriendlyMessageForServerError() {
281 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(500, "fail"))
282 XCTAssertTrue(msg.contains("500"))
283 XCTAssertTrue(msg.contains("fail"))
286 func testUserFriendlyMessageForServerErrorIncludesDetail() {
287 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(400, "Stream not found"))
288 XCTAssertEqual(msg, "Server error (400): Stream not found")
291 func testUserFriendlyMessageForServerErrorEmptyBody() {
292 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(502, ""))
293 XCTAssertEqual(msg, "Server returned error 502.")
296 func testUserFriendlyMessageForServerErrorTrimsWhitespace() {
297 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(400, " bad query \n"))
298 XCTAssertEqual(msg, "Server error (400): bad query")
301 func testUserFriendlyMessageForServerErrorUnknown() {
302 let msg = ParseableError.userFriendlyMessage(for: ParseableError.serverError(400, "Unknown error"))
303 XCTAssertEqual(msg, "Server returned error 400.")
306 func testUserFriendlyMessageForInvalidURL() {
307 let msg = ParseableError.userFriendlyMessage(for: ParseableError.invalidURL)
308 XCTAssertTrue(msg.lowercased().contains("url"))
311 func testUserFriendlyMessageForNetworkTimeout() {
312 let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)
313 let msg = ParseableError.userFriendlyMessage(for: error)
314 XCTAssertTrue(msg.lowercased().contains("timed out"))
317 func testUserFriendlyMessageForNoInternet() {
318 let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet, userInfo: nil)
319 let msg = ParseableError.userFriendlyMessage(for: error)
320 XCTAssertTrue(msg.lowercased().contains("internet") || msg.lowercased().contains("network"))
323 func testUserFriendlyMessageForCannotFindHost() {
324 let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost, userInfo: nil)
325 let msg = ParseableError.userFriendlyMessage(for: error)
326 XCTAssertTrue(msg.lowercased().contains("server") || msg.lowercased().contains("find"))
329 func testUserFriendlyMessageForCannotConnect() {
330 let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost, userInfo: nil)
331 let msg = ParseableError.userFriendlyMessage(for: error)
332 XCTAssertTrue(msg.lowercased().contains("connect"))
335 func testUserFriendlyMessageForSSLError() {
336 let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorSecureConnectionFailed, userInfo: nil)
337 let msg = ParseableError.userFriendlyMessage(for: error)
338 XCTAssertTrue(msg.lowercased().contains("ssl") || msg.lowercased().contains("tls"))
341 func testUserFriendlyMessageForGenericError() {
342 let error = NSError(domain: "custom", code: 42, userInfo: [NSLocalizedDescriptionKey: "Something broke"])
343 let msg = ParseableError.userFriendlyMessage(for: error)
344 XCTAssertEqual(msg, "Something broke")
347 // MARK: - Stream name validation
349 func testValidStreamName() {
350 XCTAssertNil(SidebarView.validateStreamName("my-stream_v2.0"))
353 func testStreamNameWithSpaces() {
354 let error = SidebarView.validateStreamName("my stream")
355 XCTAssertNotNil(error)
356 XCTAssertTrue(error?.contains("letters") ?? false)
359 func testStreamNameTooLong() {
360 let name = String(repeating: "a", count: 256)
361 let error = SidebarView.validateStreamName(name)
362 XCTAssertNotNil(error)
363 XCTAssertTrue(error?.contains("255") ?? false)
366 func testStreamNameStartsWithDot() {
367 let error = SidebarView.validateStreamName(".hidden")
368 XCTAssertNotNil(error)
369 XCTAssertTrue(error?.contains("start") ?? false)
372 func testStreamNameStartsWithHyphen() {
373 let error = SidebarView.validateStreamName("-bad")
374 XCTAssertNotNil(error)
375 XCTAssertTrue(error?.contains("start") ?? false)
378 func testStreamNameWithSpecialChars() {
379 XCTAssertNotNil(SidebarView.validateStreamName("stream@1"))
380 XCTAssertNotNil(SidebarView.validateStreamName("stream/path"))
381 XCTAssertNotNil(SidebarView.validateStreamName("stream name"))
384 func testStreamNameMaxLength() {
385 let name = String(repeating: "a", count: 255)
386 XCTAssertNil(SidebarView.validateStreamName(name))
389 // MARK: - SQL identifier escaping
391 func testEscapeSQLIdentifierSimple() {
392 XCTAssertEqual(QueryViewModel.escapeSQLIdentifier("logs"), "\"logs\"")
395 func testEscapeSQLIdentifierWithQuotes() {
396 XCTAssertEqual(QueryViewModel.escapeSQLIdentifier("my\"stream"), "\"my\"\"stream\"")
399 func testEscapeSQLIdentifierWithSpaces() {
400 XCTAssertEqual(QueryViewModel.escapeSQLIdentifier("my stream"), "\"my stream\"")
403 // MARK: - CSV export
405 func testBuildCSVEmpty() {
406 XCTAssertEqual(QueryViewModel.buildCSV(records: [], columns: []), "")
409 func testBuildCSVWithData() {
410 let records: [LogRecord] = [
411 ["level": .string("info"), "msg": .string("hello")]
413 let csv = QueryViewModel.buildCSV(records: records, columns: ["level", "msg"])
414 XCTAssertTrue(csv.hasPrefix("level,msg\n"))
415 XCTAssertTrue(csv.contains("info"))
416 XCTAssertTrue(csv.contains("hello"))
419 func testBuildCSVEscapesCommas() {
420 let records: [LogRecord] = [
421 ["msg": .string("hello, world")]
423 let csv = QueryViewModel.buildCSV(records: records, columns: ["msg"])
424 XCTAssertTrue(csv.contains("\"hello, world\""))
427 func testBuildCSVEscapesQuotes() {
428 let records: [LogRecord] = [
429 ["msg": .string("say \"hi\"")]
431 let csv = QueryViewModel.buildCSV(records: records, columns: ["msg"])
432 XCTAssertTrue(csv.contains("\"say \"\"hi\"\"\""))
435 // MARK: - Malformed response decoding
437 func testQueryResponseMalformedThrows() throws {
438 // Malformed data that is neither QueryResponse nor [LogRecord]
439 let data = "not json at all".data(using: .utf8)!
441 // Neither format should decode
442 XCTAssertNil(try? JSONDecoder().decode(QueryResponse.self, from: data))
443 XCTAssertNil(try? JSONDecoder().decode([LogRecord].self, from: data))
446 func testRetentionConfigEmptyDataReturnsEmpty() throws {
447 // Empty data should be treated as no retention (tested via the Data.isEmpty check)
448 let emptyData = Data()
449 // JSONDecoder would fail on empty data
450 XCTAssertThrowsError(try JSONDecoder().decode([RetentionConfig].self, from: emptyData))
453 func testAlertConfigMalformedThrows() {
454 // Completely invalid JSON should fail AlertConfig decoding
455 let data = "<<<not json>>>".data(using: .utf8)!
456 XCTAssertThrowsError(try JSONDecoder().decode(AlertConfig.self, from: data))
459 func testRetentionConfigMalformedThrows() {
460 // A plain string is not valid as RetentionConfig or [RetentionConfig]
461 let data = "\"just a string\"".data(using: .utf8)!
462 XCTAssertNil(try? JSONDecoder().decode([RetentionConfig].self, from: data))
463 XCTAssertNil(try? JSONDecoder().decode(RetentionConfig.self, from: data))
466 // MARK: - Stream name validation (empty)
468 func testStreamNameEmpty() {
469 let error = SidebarView.validateStreamName("")
470 XCTAssertNotNil(error)
471 XCTAssertTrue(error?.lowercased().contains("empty") ?? false)
474 func testStreamNameWhitespaceOnly() {
475 let error = SidebarView.validateStreamName(" ")
476 XCTAssertNotNil(error)
477 XCTAssertTrue(error?.lowercased().contains("empty") ?? false)
480 // MARK: - ParseableFilter decoding
482 func testFilterFullResponse() throws {
485 "filter_id": "abc-123",
486 "filter_name": "Error logs",
487 "stream_name": "backend",
488 "query": {"filter_type": "sql", "filter_query": "SELECT * FROM backend WHERE level = 'error'"},
491 "time_filter": {"start": "2024-01-01T00:00:00Z", "end": "2024-01-02T00:00:00Z"}
493 """.data(using: .utf8)!
495 let filter = try JSONDecoder().decode(ParseableFilter.self, from: json)
496 XCTAssertEqual(filter.filterId, "abc-123")
497 XCTAssertEqual(filter.filterName, "Error logs")
498 XCTAssertEqual(filter.streamName, "backend")
499 XCTAssertEqual(filter.query.filterType, "sql")
500 XCTAssertEqual(filter.query.filterQuery, "SELECT * FROM backend WHERE level = 'error'")
501 XCTAssertEqual(filter.version, "v1")
502 XCTAssertEqual(filter.userId, "admin")
503 XCTAssertNotNil(filter.timeFilter)
506 func testFilterMinimalFields() throws {
509 "filter_name": "Quick query",
510 "stream_name": "logs",
511 "query": {"filter_type": "sql"}
513 """.data(using: .utf8)!
515 let filter = try JSONDecoder().decode(ParseableFilter.self, from: json)
516 XCTAssertNil(filter.filterId)
517 XCTAssertEqual(filter.filterName, "Quick query")
518 XCTAssertEqual(filter.streamName, "logs")
519 XCTAssertEqual(filter.query.filterType, "sql")
520 XCTAssertNil(filter.query.filterQuery)
521 XCTAssertNil(filter.version)
522 XCTAssertNil(filter.userId)
523 XCTAssertNil(filter.timeFilter)
526 func testFilterArrayDecoding() throws {
529 {"filter_name": "A", "stream_name": "s1", "query": {"filter_type": "sql"}},
530 {"filter_name": "B", "stream_name": "s2", "query": {"filter_type": "filter"}}
532 """.data(using: .utf8)!
534 let filters = try JSONDecoder().decode([ParseableFilter].self, from: json)
535 XCTAssertEqual(filters.count, 2)
536 XCTAssertEqual(filters[0].filterName, "A")
537 XCTAssertEqual(filters[1].query.filterType, "filter")
540 func testFilterEncodingForCreate() throws {
541 let filter = ParseableFilter(
542 filterName: "My filter",
543 streamName: "app-logs",
544 query: FilterQuery(filterType: "sql", filterQuery: "SELECT * FROM \"app-logs\"")
546 let data = try JSONEncoder().encode(filter)
547 let dict = try JSONSerialization.jsonObject(with: data) as! [String: Any]
548 XCTAssertEqual(dict["filter_name"] as? String, "My filter")
549 XCTAssertEqual(dict["stream_name"] as? String, "app-logs")
550 let query = dict["query"] as! [String: Any]
551 XCTAssertEqual(query["filter_type"] as? String, "sql")
552 XCTAssertEqual(query["filter_query"] as? String, "SELECT * FROM \"app-logs\"")
555 func testFilterUnknownFieldsTolerated() throws {
559 "filter_name": "Test",
560 "stream_name": "logs",
561 "query": {"filter_type": "sql", "filter_query": "SELECT 1", "some_new_field": true},
562 "brand_new_field": 42
564 """.data(using: .utf8)!
566 let filter = try JSONDecoder().decode(ParseableFilter.self, from: json)
567 XCTAssertEqual(filter.filterId, "xyz")
568 XCTAssertEqual(filter.filterName, "Test")
571 func testFilterQueryWithBuilder() throws {
574 "filter_type": "filter",
575 "filter_query": null,
576 "filter_builder": {"rules": [{"field": "level", "operator": "=", "value": "error"}]}
578 """.data(using: .utf8)!
580 let query = try JSONDecoder().decode(FilterQuery.self, from: json)
581 XCTAssertEqual(query.filterType, "filter")
582 XCTAssertNil(query.filterQuery)
583 XCTAssertNotNil(query.filterBuilder)
586 func testFilterIdentifiableWithId() {
587 let filter = ParseableFilter(
588 filterId: "server-id-1",
591 query: FilterQuery(filterType: "sql")
593 XCTAssertEqual(filter.id, "server-id-1")
596 func testFilterIdentifiableFallback() {
597 let filter = ParseableFilter(
600 query: FilterQuery(filterType: "sql")
602 XCTAssertEqual(filter.id, "logs:Test")
605 // MARK: - URL scheme validation
607 func testValidateURLAcceptsHTTPS() {
608 // ConnectionSheet.validateURL is private, so we test via ServerConnection.baseURL
609 let conn = ServerConnection(name: "t", url: "https://example.com", username: "u", password: "p")
610 XCTAssertNotNil(conn.baseURL)
611 XCTAssertEqual(conn.baseURL?.scheme, "https")
614 func testValidateURLAcceptsHTTP() {
615 let conn = ServerConnection(name: "t", url: "http://example.com", username: "u", password: "p")
616 XCTAssertNotNil(conn.baseURL)
617 XCTAssertEqual(conn.baseURL?.scheme, "http")
620 // MARK: - CSV export edge cases
622 func testBuildCSVEscapesNewlines() {
623 let records: [LogRecord] = [
624 ["msg": .string("line1\nline2")]
626 let csv = QueryViewModel.buildCSV(records: records, columns: ["msg"])
627 // Newlines in values should be wrapped in quotes
628 XCTAssertTrue(csv.contains("\"line1\nline2\""))
631 func testBuildCSVMissingColumn() {
632 let records: [LogRecord] = [
633 ["level": .string("info")]
635 let csv = QueryViewModel.buildCSV(records: records, columns: ["level", "msg"])
636 // Missing column value should produce empty field
637 let lines = csv.components(separatedBy: "\n")
638 XCTAssertTrue(lines[1].hasSuffix(",") || lines[1].components(separatedBy: ",").count == 2)
641 func testBuildCSVEscapesCarriageReturn() {
642 let records: [LogRecord] = [
643 ["msg": .string("line1\r\nline2")]
645 let csv = QueryViewModel.buildCSV(records: records, columns: ["msg"])
646 // Carriage returns in values should be wrapped in quotes per RFC 4180
647 XCTAssertTrue(csv.contains("\"line1\r\nline2\""))
650 // MARK: - Server response key spellings
652 func testServerAboutDecodesCamelCaseKeys() throws {
653 let json = #"{"uiVersion": "v1.2", "deploymentId": "01H", "grpcPort": 8001}"#.data(using: .utf8)!
654 let about = try JSONDecoder().decode(ServerAbout.self, from: json)
655 XCTAssertEqual(about.uiVersion, "v1.2")
656 XCTAssertEqual(about.deploymentId, "01H")
657 XCTAssertEqual(about.grpcPort, 8001)
660 func testStreamInfoDecodesSnakeCaseKeys() throws {
661 let json = #"{"created-at": "c", "first-event-at": "f", "time_partition": "ts", "static_schema_flag": "true", "cache_enabled": true}"#
663 let info = try JSONDecoder().decode(StreamInfo.self, from: json)
664 XCTAssertEqual(info.createdAt, "c")
665 XCTAssertEqual(info.firstEventAt, "f")
666 XCTAssertEqual(info.timePartition, "ts")
667 XCTAssertEqual(info.staticSchemaFlag, true)
668 XCTAssertEqual(info.cacheEnabled, true)
671 func testStreamInfoDecodesCamelCaseKeys() throws {
672 let json = #"{"createdAt": "c", "firstEventAt": "f", "timePartition": "ts", "staticSchemaFlag": "true"}"#
674 let info = try JSONDecoder().decode(StreamInfo.self, from: json)
675 XCTAssertEqual(info.createdAt, "c")
676 XCTAssertEqual(info.firstEventAt, "f")
677 XCTAssertEqual(info.timePartition, "ts")
678 XCTAssertEqual(info.staticSchemaFlag, true)
681 func testStorageStatsReadsFormatAndUnitSuffixedSizes() throws {
682 let json = #"{"storage": {"size": "2048 Bytes", "format": "parquet"}}"#.data(using: .utf8)!
683 let stats = try JSONDecoder().decode(StreamStats.self, from: json)
684 XCTAssertEqual(stats.storage?.type, "parquet")
685 XCTAssertEqual(stats.storage?.size, ByteCountFormatter.string(fromByteCount: 2048, countStyle: .file))
688 func testSchemaFieldDescribesParameterizedArrowTypes() throws {
689 let json = #"{"name": "p_timestamp", "data_type": {"Timestamp": ["Millisecond", null]}}"#.data(using: .utf8)!
690 let field = try JSONDecoder().decode(SchemaField.self, from: json)
691 XCTAssertEqual(field.dataType, "Timestamp(Millisecond, None)")
694 func testLegacyAlertRuleWithObjectConfigKeepsRuleType() throws {
695 let json = #"{"name": "a", "rule": {"type": "column", "config": {"column": "status", "operator": "=", "value": 500}}}"#
697 let rule = try JSONDecoder().decode(AlertRule.self, from: json)
698 XCTAssertEqual(rule.rule?.type, "column")
699 XCTAssertNotNil(rule.rule?.config)
702 func testServerAboutDecodesObjectLicense() throws {
703 let json = #"{"license": {"name": "AGPL-3.0-only", "plan": "OSS"}}"#.data(using: .utf8)!
704 let about = try JSONDecoder().decode(ServerAbout.self, from: json)
705 XCTAssertEqual(about.license, "AGPL-3.0-only (OSS)")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list