Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relay Requests Encoding and Tests #17

Merged
merged 6 commits into from
May 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
15 changes: 15 additions & 0 deletions Sources/NostrSDK/EventTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,19 @@ public struct EventTag: Codable, Equatable {
self.contentIdentifier = contentIdentifier
self.recommendedRelayURL = recommendedRelayURL
}

enum CodingKeys: CodingKey {
case identifier
case contentIdentifier
case recommendedRelayURL
}

public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(identifier.rawValue)
try container.encode(contentIdentifier)
if let recommendedRelayURL {
try container.encode(recommendedRelayURL)
}
}
}
30 changes: 30 additions & 0 deletions Sources/NostrSDK/Filter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Filter.swift
//
//
// Created by Joel Klabo on 5/26/23.
//

import Foundation

struct Filter: Codable {
let ids: [String]?
let authors: [String]?
let kinds: [Int]?
let events: [String]?
let pubkeys: [String]?
let since: Int?
let until: Int?
let limit: Int?

private enum CodingKeys: String, CodingKey {
case ids = "ids"
case authors = "authors"
case kinds = "kinds"
case events = "#e"
case pubkeys = "#p"
case since = "since"
case until = "until"
case limit = "limit"
}
}
20 changes: 20 additions & 0 deletions Sources/NostrSDK/Helpers/AnyEncodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// AnyEncodable.swift
//
//
// Created by Joel Klabo on 5/26/23.
//

import Foundation

struct AnyEncodable: Encodable {
private var _encode: (Encoder) throws -> Void

init<T: Encodable>(_ encodable: T) {
self._encode = encodable.encode
}

func encode(to encoder: Encoder) throws {
try _encode(encoder)
}
}
40 changes: 40 additions & 0 deletions Sources/NostrSDK/RelayRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// RelayRequest.swift
//
//
// Created by Joel Klabo on 5/25/23.
//

import Foundation

struct RelayRequest {

static let encoder = JSONEncoder()

static func close(subscriptionId: String) -> String? {
let payload = [AnyEncodable("CLOSE"), AnyEncodable(subscriptionId)]
return encode(payload: payload)
}

static func event(_ event: NostrEvent) -> String? {
let payload = [AnyEncodable("EVENT"), AnyEncodable(event)]
return encode(payload: payload)
}

static func count(subscriptionId: String, filter: Filter) -> String? {
let payload = [AnyEncodable("COUNT"), AnyEncodable(subscriptionId), AnyEncodable(filter)]
return encode(payload: payload)
}

static func request(subscriptionId: String, filter: Filter) -> String? {
let payload = [AnyEncodable("REQ"), AnyEncodable(subscriptionId), AnyEncodable(filter)]
return encode(payload: payload)
}

private static func encode(payload: [AnyEncodable]) -> String? {
guard let payloadData = try? encoder.encode(payload) else {
return nil
}
return String(decoding: payloadData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
}
}
6 changes: 3 additions & 3 deletions Tests/NostrSDKTests/EventDecodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import XCTest
final class EventDecodingTests: XCTestCase, FixtureLoading {

func testDecodeSetMetadata() throws {
let data = try loadFixture("set_metadata")
let data = try loadFixtureData("set_metadata")

let event = try JSONDecoder().decode(NostrEvent.self, from: data)

Expand All @@ -25,7 +25,7 @@ final class EventDecodingTests: XCTestCase, FixtureLoading {
}

func testDecodeTextNote() throws {
let data = try loadFixture("text_note")
let data = try loadFixtureData("text_note")

let event = try JSONDecoder().decode(NostrEvent.self, from: data)

Expand All @@ -44,7 +44,7 @@ final class EventDecodingTests: XCTestCase, FixtureLoading {
}

func testDecodeRepost() throws {
let data = try loadFixture("repost")
let data = try loadFixtureData("repost")

let event = try JSONDecoder().decode(NostrEvent.self, from: data)

Expand Down
32 changes: 32 additions & 0 deletions Tests/NostrSDKTests/FilterEncodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// FilterEncodingTests.swift
//
//
// Created by Joel Klabo on 5/26/23.
//

@testable import NostrSDK
import XCTest

final class FilterEncodingTests: XCTestCase, FixtureLoading, JSONTesting {

func testFilterEncoding() throws {
let filter = Filter(ids: nil,
authors: ["d9fa34214aa9d151c4f4db843e9c2af4f246bab4205137731f91bcfa44d66a62"],
kinds: [3],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: 1)

let expected = try loadFixtureString("filter")

let encoder = JSONEncoder()
let result = try encoder.encode(filter)
let resultString = String(decoding: result, as: UTF8.self)

XCTAssertTrue(areEquivalentJSONObjectStrings(expected, resultString))
}

}
9 changes: 8 additions & 1 deletion Tests/NostrSDKTests/FixtureLoading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ enum FixtureLoadingError: Error {
protocol FixtureLoading {}
extension FixtureLoading {

func loadFixture(_ filename: String) throws -> Data {
func loadFixtureData(_ filename: String) throws -> Data {
// Construct the URL for the fixtures directory.
let bundle = Bundle.module
guard let url = bundle.url(forResource: filename, withExtension: "json", subdirectory: "Fixtures") else {
Expand All @@ -23,4 +23,11 @@ extension FixtureLoading {
// Load the data from the file.
return try Data(contentsOf: url)
}

func loadFixtureString(_ filename: String) throws -> String? {
let data = try loadFixtureData(filename)
let originalString = String(decoding: data, as: UTF8.self)
let trimmedString = originalString.filter { !"\n\t\r".contains($0) }
return trimmedString
}
}
1 change: 1 addition & 0 deletions Tests/NostrSDKTests/Fixtures/close_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["CLOSE","some-subscription-id"]
1 change: 1 addition & 0 deletions Tests/NostrSDKTests/Fixtures/count_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["COUNT", "some-subscription-id", {"kinds": [1, 7], "authors": ["some-pubkey"]}]
18 changes: 18 additions & 0 deletions Tests/NostrSDKTests/Fixtures/event_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
["EVENT", {
"id": "fa5ed84fc8eeb959fd39ad8e48388cfc33075991ef8e50064cfcecfd918bb91b",
"pubkey": "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
"created_at": 1682080184,
"kind": 1,
"tags": [
[
"e",
"93930d65435d49db723499335473920795e7f13c45600dcfad922135cf44bd63"
],
[
"p",
"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"
]
],
"content": "I think it stays persistent on your profile, but interface setting doesn’t persist. Bug. ",
"sig": "96e6667348b2b1fc5f6e73e68fb1605f571ad044077dda62a35c15eb8290f2c4559935db461f8466df3dcf39bc2e11984c5344f65aabee4520dd6653d74cdc09"
}]
1 change: 1 addition & 0 deletions Tests/NostrSDKTests/Fixtures/filter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kinds":[3],"authors":["d9fa34214aa9d151c4f4db843e9c2af4f246bab4205137731f91bcfa44d66a62"],"limit":1}
1 change: 1 addition & 0 deletions Tests/NostrSDKTests/Fixtures/req.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["REQ", "some-subscription-id", {"kinds": [1, 7], "authors": ["some-pubkey"]}]
56 changes: 56 additions & 0 deletions Tests/NostrSDKTests/Helpers/JSONTesting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// JSONTesting.swift
//
//
// Created by Joel Klabo on 5/26/23.
//

import Foundation

protocol JSONTesting {}

extension JSONTesting {
func areEquivalentJSONArrayStrings(_ first: String?, _ second: String?) -> Bool {
guard let first, let second else {
return first == nil && second == nil
}

guard let dataOne = first.data(using: .utf8),
let dataTwo = second.data(using: .utf8) else {
return false
}

do {
if let jsonArrayOne = try JSONSerialization.jsonObject(with: dataOne, options: []) as? [AnyHashable],
let jsonArrayTwo = try JSONSerialization.jsonObject(with: dataTwo, options: []) as? [AnyHashable] {
return Set(jsonArrayOne) == Set(jsonArrayTwo)
} else {
return false
}
} catch {
return false
}
}

func areEquivalentJSONObjectStrings(_ first: String?, _ second: String?) -> Bool {
guard let first, let second else {
return first == nil && second == nil
}

guard let dataOne = first.data(using: .utf8),
let dataTwo = second.data(using: .utf8) else {
return false
}

do {
if let jsonOne = try JSONSerialization.jsonObject(with: dataOne, options: []) as? [String: Any],
let jsonTwo = try JSONSerialization.jsonObject(with: dataTwo, options: []) as? [String: Any] {
return NSDictionary(dictionary: jsonOne).isEqual(to: jsonTwo)
} else {
return false
}
} catch {
return false
}
}
}
44 changes: 44 additions & 0 deletions Tests/NostrSDKTests/JSONEqualityTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// JSONEqualityTests.swift
//
//
// Created by Joel Klabo on 5/27/23.
//

import XCTest

final class JSONEqualityTests: XCTestCase, JSONTesting {

func testJSONArrayEquality() {
// Test with identical arrays
XCTAssertTrue(areEquivalentJSONArrayStrings("[1, 2, 3]", "[1, 2, 3]"))

// Test with identical arrays in different order
XCTAssertTrue(areEquivalentJSONArrayStrings("[1, 2, 3]", "[3, 2, 1]"))

// Test with different arrays
XCTAssertFalse(areEquivalentJSONArrayStrings("[1, 2, 3]", "[4, 5, 6]"))

// Test with nil values
XCTAssertTrue(areEquivalentJSONArrayStrings(nil, nil))
XCTAssertFalse(areEquivalentJSONArrayStrings(nil, "[1, 2, 3]"))
XCTAssertFalse(areEquivalentJSONArrayStrings("[1, 2, 3]", nil))
}

func testJSONObjectEquality() {
// Test with identical objects
XCTAssertTrue(areEquivalentJSONObjectStrings("{\"key1\": \"value1\", \"key2\": \"value2\"}", "{\"key1\": \"value1\", \"key2\": \"value2\"}"))

// Test with identical objects with keys in different order
XCTAssertTrue(areEquivalentJSONObjectStrings("{\"key1\": \"value1\", \"key2\": \"value2\"}", "{\"key2\": \"value2\", \"key1\": \"value1\"}"))

// Test with different objects
XCTAssertFalse(areEquivalentJSONObjectStrings("{\"key1\": \"value1\", \"key2\": \"value2\"}", "{\"key3\": \"value3\", \"key4\": \"value4\"}"))

// Test with nil values
XCTAssertTrue(areEquivalentJSONObjectStrings(nil, nil))
XCTAssertFalse(areEquivalentJSONObjectStrings(nil, "{\"key1\": \"value1\", \"key2\": \"value2\"}"))
XCTAssertFalse(areEquivalentJSONObjectStrings("{\"key1\": \"value1\", \"key2\": \"value2\"}", nil))
}

}
68 changes: 68 additions & 0 deletions Tests/NostrSDKTests/RelayRequestEncodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// RelayRequestEncodingTests.swift
//
//
// Created by Joel Klabo on 5/25/23.
//

@testable import NostrSDK
import XCTest

final class RelayRequestEncodingTests: XCTestCase, FixtureLoading, JSONTesting {

func testEncodeClose() throws {
let request = try XCTUnwrap(RelayRequest.close(subscriptionId: "some-subscription-id"), "failed to encode request")
let expected = try loadFixtureString("close_request")

XCTAssertEqual(request, expected)
}

func testEncodeEvent() throws {
let eventTag = EventTag(identifier: .event, contentIdentifier: "93930d65435d49db723499335473920795e7f13c45600dcfad922135cf44bd63")
let pubkeyTag = EventTag(identifier: .pubkey, contentIdentifier: "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9")
let event = NostrEvent(id: "fa5ed84fc8eeb959fd39ad8e48388cfc33075991ef8e50064cfcecfd918bb91b",
pubkey: "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
createdAt: 1682080184,
kind: .textNote,
tags: [eventTag, pubkeyTag],
content: "I think it stays persistent on your profile, but interface setting doesn’t persist. Bug. ",
signature: "96e6667348b2b1fc5f6e73e68fb1605f571ad044077dda62a35c15eb8290f2c4559935db461f8466df3dcf39bc2e11984c5344f65aabee4520dd6653d74cdc09")

let request = try XCTUnwrap(RelayRequest.event(event), "failed to encode request")
let expected = try loadFixtureString("event_request")

XCTAssertTrue(areEquivalentJSONArrayStrings(request, expected))
}

func testEncodeCount() throws {
let filter = Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil)

let request = try XCTUnwrap(RelayRequest.count(subscriptionId: "some-subscription-id", filter: filter), "failed to encode request")
let expected = try loadFixtureString("count_request")

XCTAssertTrue(areEquivalentJSONArrayStrings(request, expected))
}

func testEncodeReq() throws {
let filter = Filter(ids: nil,
authors: ["some-pubkey"],
kinds: [1, 7],
events: nil,
pubkeys: nil,
since: nil,
until: nil,
limit: nil)

let request = try XCTUnwrap(RelayRequest.request(subscriptionId: "some-subscription-id", filter: filter), "failed to encode request")
let expected = try loadFixtureString("req")

XCTAssertTrue(areEquivalentJSONArrayStrings(request, expected))
}
}
Loading