Skip to content

Commit 115194b

Browse files
author
Guilherme Souza
committed
test(postgrest): increase code coverage
1 parent faf78f6 commit 115194b

12 files changed

+2000
-71
lines changed

Sources/Helpers/Codable.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Codable.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 20/01/25.
6+
//
7+
8+
import ConcurrencyExtras
9+
import Foundation
10+
11+
extension JSONDecoder {
12+
private static let supportedDateFormatters: [UncheckedSendable<ISO8601DateFormatter>] = [
13+
ISO8601DateFormatter.iso8601WithFractionalSeconds,
14+
ISO8601DateFormatter.iso8601,
15+
]
16+
17+
/// Default `JSONDecoder` for decoding types from Supabase.
18+
package static let `default`: JSONDecoder = {
19+
let decoder = JSONDecoder()
20+
decoder.dateDecodingStrategy = .custom { decoder in
21+
let container = try decoder.singleValueContainer()
22+
let string = try container.decode(String.self)
23+
24+
for formatter in supportedDateFormatters {
25+
if let date = formatter.value.date(from: string) {
26+
return date
27+
}
28+
}
29+
30+
throw DecodingError.dataCorruptedError(
31+
in: container, debugDescription: "Invalid date format: \(string)"
32+
)
33+
}
34+
return decoder
35+
}()
36+
}
Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,70 @@
11
import HTTPTypes
22

3-
package extension HTTPFields {
4-
init(_ dictionary: [String: String]) {
3+
extension HTTPFields {
4+
package init(_ dictionary: [String: String]) {
55
self.init(dictionary.map { .init(name: .init($0.key)!, value: $0.value) })
66
}
7-
8-
var dictionary: [String: String] {
7+
8+
package var dictionary: [String: String] {
99
let keyValues = self.map {
1010
($0.name.rawName, $0.value)
1111
}
12-
12+
1313
return .init(keyValues, uniquingKeysWith: { $1 })
1414
}
15-
16-
mutating func merge(with other: Self) {
15+
16+
package mutating func merge(with other: Self) {
1717
for field in other {
1818
self[field.name] = field.value
1919
}
2020
}
21-
22-
func merging(with other: Self) -> Self {
21+
22+
package func merging(with other: Self) -> Self {
2323
var copy = self
24-
24+
2525
for field in other {
2626
copy[field.name] = field.value
2727
}
2828

2929
return copy
3030
}
31+
32+
/// Append or update a value in header.
33+
///
34+
/// Example:
35+
/// ```swift
36+
/// var headers: HTTPFields = [
37+
/// "Prefer": "count=exact,return=representation"
38+
/// ]
39+
///
40+
/// headers.appendOrUpdate(.prefer, value: "return=minimal")
41+
/// #expect(headers == ["Prefer": "count=exact,return=minimal"]
42+
/// ```
43+
package mutating func appendOrUpdate(
44+
_ name: HTTPField.Name,
45+
value: String,
46+
separator: String = ","
47+
) {
48+
if let currentValue = self[name] {
49+
var components = currentValue.components(separatedBy: separator)
50+
51+
if let key = value.split(separator: "=").first,
52+
let index = components.firstIndex(where: { $0.hasPrefix("\(key)=") })
53+
{
54+
components[index] = value
55+
} else {
56+
components.append(value)
57+
}
58+
59+
self[name] = components.joined(separator: separator)
60+
} else {
61+
self[name] = value
62+
}
63+
}
3164
}
3265

33-
package extension HTTPField.Name {
34-
static let xClientInfo = HTTPField.Name("X-Client-Info")!
35-
static let xRegion = HTTPField.Name("x-region")!
36-
static let xRelayError = HTTPField.Name("x-relay-error")!
66+
extension HTTPField.Name {
67+
package static let xClientInfo = HTTPField.Name("X-Client-Info")!
68+
package static let xRegion = HTTPField.Name("x-region")!
69+
package static let xRelayError = HTTPField.Name("x-relay-error")!
3770
}

Sources/PostgREST/PostgrestBuilder.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,7 @@ public class PostgrestBuilder: @unchecked Sendable {
106106
}
107107

108108
if let count = $0.fetchOptions.count {
109-
if let prefer = $0.request.headers[.prefer] {
110-
$0.request.headers[.prefer] = "\(prefer),count=\(count.rawValue)"
111-
} else {
112-
$0.request.headers[.prefer] = "count=\(count.rawValue)"
113-
}
109+
$0.request.headers.appendOrUpdate(.prefer, value: "count=\(count.rawValue)")
114110
}
115111

116112
if $0.request.headers[.accept] == nil {

Sources/PostgREST/PostgrestTransformBuilder.swift

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
2323
.joined(separator: "")
2424
mutableState.withValue {
2525
$0.request.query.appendOrUpdate(URLQueryItem(name: "select", value: cleanedColumns))
26-
27-
if let prefer = $0.request.headers[.prefer] {
28-
var components = prefer.components(separatedBy: ",")
29-
30-
if let index = components.firstIndex(where: { $0.hasPrefix("return=") }) {
31-
components[index] = "return=representation"
32-
} else {
33-
components.append("return=representation")
34-
}
35-
36-
$0.request.headers[.prefer] = components.joined(separator: ",")
37-
} else {
38-
$0.request.headers[.prefer] = "return=representation"
39-
}
26+
$0.request.headers.appendOrUpdate(.prefer, value: "return=representation")
4027
}
4128
return self
4229
}
@@ -64,7 +51,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
6451
"\(column).\(ascending ? "asc" : "desc").\(nullsFirst ? "nullsfirst" : "nullslast")"
6552

6653
if let existingOrderIndex,
67-
let currentValue = $0.request.query[existingOrderIndex].value
54+
let currentValue = $0.request.query[existingOrderIndex].value
6855
{
6956
$0.request.query[existingOrderIndex] = URLQueryItem(
7057
name: key,
@@ -85,11 +72,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
8572
public func limit(_ count: Int, referencedTable: String? = nil) -> PostgrestTransformBuilder {
8673
mutableState.withValue {
8774
let key = referencedTable.map { "\($0).limit" } ?? "limit"
88-
if let index = $0.request.query.firstIndex(where: { $0.name == key }) {
89-
$0.request.query[index] = URLQueryItem(name: key, value: "\(count)")
90-
} else {
91-
$0.request.query.append(URLQueryItem(name: key, value: "\(count)"))
92-
}
75+
$0.request.query.appendOrUpdate(URLQueryItem(name: key, value: "\(count)"))
9376
}
9477
return self
9578
}
@@ -113,24 +96,10 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
11396
let keyLimit = referencedTable.map { "\($0).limit" } ?? "limit"
11497

11598
mutableState.withValue {
116-
if let index = $0.request.query.firstIndex(where: { $0.name == keyOffset }) {
117-
$0.request.query[index] = URLQueryItem(name: keyOffset, value: "\(from)")
118-
} else {
119-
$0.request.query.append(URLQueryItem(name: keyOffset, value: "\(from)"))
120-
}
99+
$0.request.query.appendOrUpdate(URLQueryItem(name: keyOffset, value: "\(from)"))
121100

122101
// Range is inclusive, so add 1
123-
if let index = $0.request.query.firstIndex(where: { $0.name == keyLimit }) {
124-
$0.request.query[index] = URLQueryItem(
125-
name: keyLimit,
126-
value: "\(to - from + 1)"
127-
)
128-
} else {
129-
$0.request.query.append(URLQueryItem(
130-
name: keyLimit,
131-
value: "\(to - from + 1)"
132-
))
133-
}
102+
$0.request.query.appendOrUpdate(URLQueryItem(name: keyLimit, value: "\(to - from + 1)"))
134103
}
135104

136105
return self
@@ -195,7 +164,8 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
195164
.compactMap { $0 }
196165
.joined(separator: "|")
197166
let forMediaType = $0.request.headers[.accept] ?? "application/json"
198-
$0.request.headers[.accept] = "application/vnd.pgrst.plan+\"\(format)\"; for=\(forMediaType); options=\(options);"
167+
$0.request.headers[.accept] =
168+
"application/vnd.pgrst.plan+\"\(format)\"; for=\(forMediaType); options=\(options);"
199169
}
200170

201171
return self

Sources/PostgREST/URLQueryRepresentable.swift

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,15 @@ extension Optional: URLQueryRepresentable where Wrapped: URLQueryRepresentable {
6868
extension JSONObject: URLQueryRepresentable {
6969
public var queryValue: String {
7070
let value = mapValues(\.value)
71-
return JSONSerialization.stringfy(value)
71+
return JSONSerialization.stringfy(value)!
7272
}
7373
}
7474

7575
extension JSONSerialization {
76-
static func stringfy(_ object: Any) -> String {
77-
guard
78-
let data = try? data(
79-
withJSONObject: object, options: [.withoutEscapingSlashes, .sortedKeys]
80-
),
81-
let string = String(data: data, encoding: .utf8)
82-
else {
83-
return "{}"
84-
}
85-
return string
76+
static func stringfy(_ object: Any) -> String? {
77+
let data = try? data(
78+
withJSONObject: object, options: [.withoutEscapingSlashes, .sortedKeys]
79+
)
80+
return data.flatMap { String(data: $0, encoding: .utf8) }
8681
}
8782
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// PostgrestQueryTests.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 21/01/25.
6+
//
7+
8+
import InlineSnapshotTesting
9+
import Mocker
10+
import PostgREST
11+
import TestHelpers
12+
import XCTest
13+
14+
class PostgrestQueryTests: XCTestCase {
15+
let url = URL(string: "http://localhost:54321/rest/v1")!
16+
17+
let sessionConfiguration: URLSessionConfiguration = {
18+
let configuration = URLSessionConfiguration.default
19+
configuration.protocolClasses = [MockingURLProtocol.self]
20+
return configuration
21+
}()
22+
23+
lazy var session = URLSession(configuration: sessionConfiguration)
24+
25+
lazy var sut = PostgrestClient(
26+
url: url,
27+
headers: [
28+
"apikey":
29+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
30+
],
31+
logger: nil,
32+
fetch: {
33+
try await self.session.data(for: $0)
34+
},
35+
encoder: {
36+
let encoder = PostgrestClient.Configuration.jsonEncoder
37+
encoder.outputFormatting = [.sortedKeys]
38+
return encoder
39+
}()
40+
)
41+
42+
struct User: Codable {
43+
let id: Int
44+
let username: String
45+
}
46+
47+
struct Country: Decodable {
48+
let name: String
49+
let cities: [City]
50+
51+
struct City: Decodable {
52+
let name: String
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)