nostrb 0.1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Rakefile +24 -0
- data/VERSION +1 -0
- data/lib/nostrb/event.rb +154 -0
- data/lib/nostrb/filter.rb +144 -0
- data/lib/nostrb/json.rb +19 -0
- data/lib/nostrb/names.rb +71 -0
- data/lib/nostrb/oj.rb +6 -0
- data/lib/nostrb/relay.rb +142 -0
- data/lib/nostrb/sequel.rb +222 -0
- data/lib/nostrb/source.rb +111 -0
- data/lib/nostrb/sqlite.rb +441 -0
- data/lib/nostrb.rb +111 -0
- data/nostrb.gemspec +18 -0
- data/test/common.rb +25 -0
- data/test/event.rb +193 -0
- data/test/nostrb.rb +87 -0
- data/test/relay.rb +360 -0
- data/test/source.rb +136 -0
- metadata +74 -0
data/test/event.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'nostrb/event'
|
2
|
+
require_relative 'common.rb'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
|
5
|
+
include Nostrb
|
6
|
+
|
7
|
+
describe Event do
|
8
|
+
def text_note(content = '')
|
9
|
+
Event.new(content, kind: 1, pk: Test::PK)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "class functions" do
|
13
|
+
it "computes a 32 byte digest of a JSON serialization" do
|
14
|
+
a = SignedEvent.serialize(Test::STATIC_HASH)
|
15
|
+
d = Event.digest(a)
|
16
|
+
expect(d).must_be_kind_of String
|
17
|
+
expect(d.length).must_equal 32
|
18
|
+
expect(d.encoding).must_equal Encoding::BINARY
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "initialization" do
|
23
|
+
it "wraps a string of content" do
|
24
|
+
content = 'hello world'
|
25
|
+
expect(text_note(content).content).must_equal content
|
26
|
+
end
|
27
|
+
|
28
|
+
it "requires a _kind_ integer, defaulting to 1" do
|
29
|
+
expect(Event.new(kind: 0, pk: Test::PK).kind).must_equal 0
|
30
|
+
expect(Event.new(pk: Test::PK).kind).must_equal 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it "requires a public key in binary format" do
|
34
|
+
expect(Test::EVENT.pk).must_equal Test::PK
|
35
|
+
expect {
|
36
|
+
Event.new(kind: 1, pk: SchnorrSig.bin2hex(Test::PK))
|
37
|
+
}.must_raise EncodingError
|
38
|
+
expect {
|
39
|
+
Event.new(kind: 1, pk: "0123456789abcdef".b)
|
40
|
+
}.must_raise SizeError
|
41
|
+
expect { Event.new }.must_raise
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "provides its content in a string context" do
|
46
|
+
s = text_note('hello').to_s
|
47
|
+
expect(s).must_equal 'hello'
|
48
|
+
end
|
49
|
+
|
50
|
+
it "serializes to an array starting with 0, length 6" do
|
51
|
+
a = text_note('hello').to_a
|
52
|
+
expect(a).must_be_kind_of Array
|
53
|
+
expect(a[0]).must_equal 0
|
54
|
+
expect(a.length).must_equal 6
|
55
|
+
expect(a[5]).must_equal 'hello'
|
56
|
+
end
|
57
|
+
|
58
|
+
it "has a pubkey in hex format" do
|
59
|
+
pubkey = Test::EVENT.pubkey
|
60
|
+
expect(pubkey).must_be_kind_of String
|
61
|
+
expect(pubkey.length).must_equal 64
|
62
|
+
end
|
63
|
+
|
64
|
+
it "requires a timestamp to create a SHA256 digest" do
|
65
|
+
e = Test::EVENT
|
66
|
+
d = e.digest(Time.now.to_i)
|
67
|
+
expect(d).must_be_kind_of String
|
68
|
+
expect(d.length).must_equal 32
|
69
|
+
expect(d.encoding).must_equal Encoding::BINARY
|
70
|
+
end
|
71
|
+
|
72
|
+
it "provides a SignedEvent when signed with a secret key" do
|
73
|
+
expect(Test::EVENT.sign(Test::SK)).must_be_kind_of SignedEvent
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "event tags" do
|
77
|
+
it "supports tags in the form of Array[Array[String]]" do
|
78
|
+
e = text_note()
|
79
|
+
expect(e.tags).must_be_kind_of Array
|
80
|
+
expect(e.tags).must_be_empty
|
81
|
+
|
82
|
+
e.add_tag('tag', 'value')
|
83
|
+
expect(e.tags).wont_be_empty
|
84
|
+
expect(e.tags.length).must_equal 1
|
85
|
+
|
86
|
+
tags0 = e.tags[0]
|
87
|
+
expect(tags0.length).must_equal 2
|
88
|
+
expect(tags0[0]).must_equal 'tag'
|
89
|
+
expect(tags0[1]).must_equal 'value'
|
90
|
+
|
91
|
+
e.add_tag('foo', 'bar', 'baz')
|
92
|
+
expect(e.tags.length).must_equal 2
|
93
|
+
|
94
|
+
tags1 = e.tags[1]
|
95
|
+
expect(tags1.length).must_equal 3
|
96
|
+
expect(tags1[2]).must_equal 'baz'
|
97
|
+
end
|
98
|
+
|
99
|
+
it "references prior events" do
|
100
|
+
p = text_note()
|
101
|
+
s = p.sign(Test::SK)
|
102
|
+
|
103
|
+
e = text_note()
|
104
|
+
e.ref_event(s.id)
|
105
|
+
expect(e.tags).wont_be_empty
|
106
|
+
expect(e.tags.length).must_equal 1
|
107
|
+
expect(e.tags[0][0]).must_equal 'e'
|
108
|
+
expect(e.tags[0][1]).must_equal s.id
|
109
|
+
end
|
110
|
+
|
111
|
+
it "references known public keys" do
|
112
|
+
e = text_note()
|
113
|
+
pubkey = SchnorrSig.bin2hex Test::PK
|
114
|
+
e.ref_pubkey(pubkey)
|
115
|
+
expect(e.tags).wont_be_empty
|
116
|
+
expect(e.tags.length).must_equal 1
|
117
|
+
expect(e.tags[0][0]).must_equal 'p'
|
118
|
+
expect(e.tags[0][1]).must_equal pubkey
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe SignedEvent do
|
124
|
+
def signed_note(content = '')
|
125
|
+
Event.new(content, kind: 1, pk: Test::PK).sign(Test::SK)
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "class functions" do
|
129
|
+
it "validates a JSON parsed hash" do
|
130
|
+
h = SignedEvent.validate!(Test::STATIC_HASH)
|
131
|
+
expect(h).must_be_kind_of Hash
|
132
|
+
%w[id pubkey kind content tags created_at sig].each { |k|
|
133
|
+
expect(h.key?(k)).must_equal true
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
it "verifies a JSON parsed hash" do
|
138
|
+
h = SignedEvent.verify(Test::STATIC_HASH)
|
139
|
+
expect(h).must_be_kind_of Hash
|
140
|
+
end
|
141
|
+
|
142
|
+
it "serializes a JSON parsed hash" do
|
143
|
+
a = SignedEvent.serialize(Test::STATIC_HASH)
|
144
|
+
expect(a).must_be_kind_of Array
|
145
|
+
expect(a.length).must_equal 6
|
146
|
+
end
|
147
|
+
|
148
|
+
it "digests a hash JSON parsed hash, which it will serialize" do
|
149
|
+
a = SignedEvent.serialize(Test::STATIC_HASH)
|
150
|
+
d = Event.digest(a)
|
151
|
+
d2 = SignedEvent.digest(Test::STATIC_HASH)
|
152
|
+
expect(d2).must_equal d
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "generates a timestamp at creation time" do
|
157
|
+
expect(signed_note().created_at).must_be_kind_of Integer
|
158
|
+
end
|
159
|
+
|
160
|
+
it "signs the event, given a private key in binary format" do
|
161
|
+
signed = signed_note()
|
162
|
+
expect(signed).must_be_kind_of SignedEvent
|
163
|
+
expect(signed.id).must_be_kind_of String
|
164
|
+
expect(signed.created_at).must_be_kind_of Integer
|
165
|
+
|
166
|
+
# check signature
|
167
|
+
signature = signed.signature
|
168
|
+
expect(signature).must_be_kind_of String
|
169
|
+
expect(signature.encoding).must_equal Encoding::BINARY
|
170
|
+
expect(signature.length).must_equal 64
|
171
|
+
|
172
|
+
# check sig hex
|
173
|
+
sig = signed.sig
|
174
|
+
expect(sig).must_be_kind_of String
|
175
|
+
expect(sig.encoding).wont_equal Encoding::BINARY
|
176
|
+
expect(sig.length).must_equal 128
|
177
|
+
end
|
178
|
+
|
179
|
+
it "has a formalized Key-Value format" do
|
180
|
+
h = signed_note().to_h
|
181
|
+
expect(h).must_be_kind_of Hash
|
182
|
+
expect(h.fetch "content").must_be_kind_of String
|
183
|
+
expect(h.fetch "pubkey").must_be_kind_of String
|
184
|
+
expect(h["pubkey"]).wont_be_empty
|
185
|
+
expect(h.fetch "kind").must_be_kind_of Integer
|
186
|
+
expect(h.fetch "tags").must_be_kind_of Array
|
187
|
+
expect(h.fetch "created_at").must_be_kind_of Integer
|
188
|
+
expect(h.fetch "id").must_be_kind_of String
|
189
|
+
expect(h["id"]).wont_be_empty
|
190
|
+
expect(h.fetch "sig").must_be_kind_of String
|
191
|
+
expect(h["sig"]).wont_be_empty
|
192
|
+
end
|
193
|
+
end
|
data/test/nostrb.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'nostrb'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
describe Nostrb do
|
5
|
+
describe "module functions" do
|
6
|
+
describe "type enforcement" do
|
7
|
+
it "can check any class" do
|
8
|
+
str = 'asdf'
|
9
|
+
expect(Nostrb.check!(str, String)).must_equal str
|
10
|
+
expect { Nostrb.check!(str, Range) }.must_raise TypeError
|
11
|
+
|
12
|
+
range = (0..10)
|
13
|
+
expect(Nostrb.check!(range, Range)).must_equal range
|
14
|
+
expect { Nostrb.check!(range, Symbol) }.must_raise TypeError
|
15
|
+
|
16
|
+
sym = :symbol
|
17
|
+
expect(Nostrb.check!(sym, Symbol)).must_equal sym
|
18
|
+
expect { Nostrb.check!(sym, String) }.must_raise TypeError
|
19
|
+
end
|
20
|
+
|
21
|
+
it "validates a text (possibly hex) string" do
|
22
|
+
hex = "0123456789abcdef"
|
23
|
+
|
24
|
+
expect(Nostrb.txt!(hex)).must_equal hex
|
25
|
+
expect(Nostrb.txt!(hex, length: 16)).must_equal hex
|
26
|
+
expect { Nostrb.txt!(hex, length: 8) }.must_raise Nostrb::SizeError
|
27
|
+
expect { Nostrb.txt!("0123".b) }.must_raise EncodingError
|
28
|
+
end
|
29
|
+
|
30
|
+
it "enforces Integer class where expected" do
|
31
|
+
int = 1234
|
32
|
+
expect(Nostrb.int!(int)).must_equal int
|
33
|
+
expect { Nostrb.int!('1234') }.must_raise TypeError
|
34
|
+
end
|
35
|
+
|
36
|
+
it "enforces a particular tag structure where expected" do
|
37
|
+
# Array[Array[String]]
|
38
|
+
tags = [['a', 'b', 'c'], ['1', '2', '3', '4']]
|
39
|
+
expect(Nostrb.tags!(tags)).must_equal tags
|
40
|
+
|
41
|
+
[
|
42
|
+
['a', 'b', 'c', '1' , '2', '3', '4'], # Array[String]
|
43
|
+
[['a', 'b', 'c'], [1, 2, 3, 4]], # Array[Array[String|Integer]]
|
44
|
+
['a', 'b', 'c', ['1' , '2', '3', '4']],# Array[Array | String]
|
45
|
+
'a', # String
|
46
|
+
[[:a, :b, :c], [1, 2, 3, 4]], # Array[Array[Symbol|String]]
|
47
|
+
].each { |bad|
|
48
|
+
expect { Nostrb.tags!(bad) }.must_raise TypeError
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "JSON I/O" do
|
54
|
+
it "parses a JSON string to a Ruby object" do
|
55
|
+
expect(Nostrb.parse('{}')).must_equal Hash.new
|
56
|
+
expect(Nostrb.parse('[]')).must_equal Array.new
|
57
|
+
end
|
58
|
+
|
59
|
+
it "generates JSON from a Ruby hash or array" do
|
60
|
+
expect(Nostrb.json({})).must_equal '{}'
|
61
|
+
expect(Nostrb.json([])).must_equal '[]'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "SHA256 digest" do
|
66
|
+
it "generates 32 bytes binary, given any string" do
|
67
|
+
strings = ["\x01\x02".b, '1234', 'asdf', '']
|
68
|
+
digests = strings.map { |s| Nostrb.digest(s) }
|
69
|
+
|
70
|
+
digests.each { |d|
|
71
|
+
expect(d).must_be_kind_of String
|
72
|
+
expect(d.encoding).must_equal Encoding::BINARY
|
73
|
+
expect(d.length).must_equal 32
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
it "generates the same output for the same input" do
|
78
|
+
strings = ["\x01\x02".b, '1234', 'asdf', '']
|
79
|
+
digests = strings.map { |s| Nostrb.digest(s) }
|
80
|
+
|
81
|
+
strings.each.with_index { |s, i|
|
82
|
+
expect(Nostrb.digest(s)).must_equal digests[i]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/test/relay.rb
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
require 'nostrb/relay'
|
2
|
+
require 'nostrb/source'
|
3
|
+
require_relative 'common'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
include Nostrb
|
7
|
+
|
8
|
+
|
9
|
+
# this can be set by GitHubActions
|
10
|
+
DB_FILE = ENV['INPUT_DB_FILE'] || 'testing.db'
|
11
|
+
|
12
|
+
# use SQLite backing
|
13
|
+
SQLite::Setup.new(DB_FILE).setup
|
14
|
+
|
15
|
+
describe Server do
|
16
|
+
def valid_response!(resp)
|
17
|
+
types = ["EVENT", "OK", "EOSE", "CLOSED", "NOTICE"]
|
18
|
+
expect(resp).must_be_kind_of Array
|
19
|
+
expect(resp.length).must_be :>=, 2
|
20
|
+
expect(resp.length).must_be :<=, 4
|
21
|
+
resp[0..1].each { |s| expect(s).must_be_kind_of String }
|
22
|
+
expect(resp[0].upcase).must_equal resp[0]
|
23
|
+
expect(types.include?(resp[0])).must_equal true
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "class functions" do
|
27
|
+
it "has an EVENT response, given subscriber_id and requested event" do
|
28
|
+
sid = '1234'
|
29
|
+
resp = Server.event(sid, Test::SIGNED)
|
30
|
+
valid_response!(resp)
|
31
|
+
expect(resp[0]).must_equal "EVENT"
|
32
|
+
expect(resp[1]).must_equal sid
|
33
|
+
expect(resp[2]).must_be_kind_of Hash
|
34
|
+
expect(resp[2]).wont_be_empty
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has an OK response, given an event_id" do
|
38
|
+
# positive ok
|
39
|
+
resp = Server.ok(Test::SIGNED.id)
|
40
|
+
valid_response!(resp)
|
41
|
+
expect(resp[0]).must_equal "OK"
|
42
|
+
expect(resp[1]).must_equal Test::SIGNED.id
|
43
|
+
expect(resp[2]).must_equal true
|
44
|
+
expect(resp[3]).must_be_kind_of String # empty by default
|
45
|
+
|
46
|
+
# negative ok
|
47
|
+
resp = Server.ok(Test::SIGNED.id, "error: testing", ok: false)
|
48
|
+
valid_response!(resp)
|
49
|
+
expect(resp[0]).must_equal "OK"
|
50
|
+
expect(resp[1]).must_equal Test::SIGNED.id
|
51
|
+
expect(resp[2]).must_equal false
|
52
|
+
expect(resp[3]).must_be_kind_of String
|
53
|
+
expect(resp[3]).wont_be_empty
|
54
|
+
|
55
|
+
# ok:false requires nonempty message
|
56
|
+
expect {
|
57
|
+
Server.ok(Test::SIGNED.id, "", ok: false)
|
58
|
+
}.must_raise FormatError
|
59
|
+
expect { Server.ok(Test::SIGNED.id, ok: false) }.must_raise FormatError
|
60
|
+
end
|
61
|
+
|
62
|
+
it "has an EOSE response to conclude a series of EVENT responses" do
|
63
|
+
sid = '1234'
|
64
|
+
resp = Server.eose(sid)
|
65
|
+
valid_response!(resp)
|
66
|
+
expect(resp[0]).must_equal "EOSE"
|
67
|
+
expect(resp[1]).must_equal sid
|
68
|
+
end
|
69
|
+
|
70
|
+
it "has a CLOSED response to shut down a subscriber" do
|
71
|
+
sid = '1234'
|
72
|
+
msg = "closed: bye"
|
73
|
+
resp = Server.closed(sid, msg)
|
74
|
+
valid_response!(resp)
|
75
|
+
expect(resp[0]).must_equal "CLOSED"
|
76
|
+
expect(resp[1]).must_equal sid
|
77
|
+
expect(resp[2]).must_equal msg
|
78
|
+
end
|
79
|
+
|
80
|
+
it "has a NOTICE response to provide any message to the user" do
|
81
|
+
msg = "all i ever really wanna do is get nice, " +
|
82
|
+
"get loose and goof this little slice of life"
|
83
|
+
resp = Server.notice(msg)
|
84
|
+
valid_response!(resp)
|
85
|
+
expect(resp[0]).must_equal "NOTICE"
|
86
|
+
expect(resp[1]).must_equal msg
|
87
|
+
end
|
88
|
+
|
89
|
+
it "formats Exceptions to a common string representation" do
|
90
|
+
r = RuntimeError.new("stuff")
|
91
|
+
expect(r).must_be_kind_of Exception
|
92
|
+
expect(Server.message(r)).must_equal "RuntimeError: stuff"
|
93
|
+
|
94
|
+
e = Nostrb::Error.new("things")
|
95
|
+
expect(e).must_be_kind_of Exception
|
96
|
+
expect(Server.message(e)).must_equal "Error: things"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "uses NOTICE to return errors" do
|
100
|
+
e = RuntimeError.new "stuff"
|
101
|
+
resp = Server.error(e)
|
102
|
+
valid_response!(resp)
|
103
|
+
expect(resp[0]).must_equal "NOTICE"
|
104
|
+
expect(resp[1]).must_equal "RuntimeError: stuff"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "has no initialization parameters" do
|
109
|
+
s = Server.new(DB_FILE)
|
110
|
+
expect(s).must_be_kind_of Server
|
111
|
+
end
|
112
|
+
|
113
|
+
# respond OK: true
|
114
|
+
it "has a single response to EVENT requests" do
|
115
|
+
json = Nostrb.json(Source.publish(Test::SIGNED))
|
116
|
+
responses = Server.new(DB_FILE).ingest(json)
|
117
|
+
expect(responses).must_be_kind_of Array
|
118
|
+
expect(responses.length).must_equal 1
|
119
|
+
|
120
|
+
resp = responses[0]
|
121
|
+
expect(resp).must_be_kind_of Array
|
122
|
+
expect(resp[0]).must_equal "OK"
|
123
|
+
expect(resp[1]).must_equal Test::SIGNED.id
|
124
|
+
expect(resp[2]).must_equal true
|
125
|
+
end
|
126
|
+
|
127
|
+
# store and retrieve with a subscription filter
|
128
|
+
it "stores inbound events" do
|
129
|
+
s = Server.new(DB_FILE)
|
130
|
+
sk, pk = SchnorrSig.keypair
|
131
|
+
e = Event.new('sqlite', pk: pk).sign(sk)
|
132
|
+
resp = s.ingest Nostrb.json(Source.publish(e))
|
133
|
+
expect(resp).must_be_kind_of Array
|
134
|
+
expect(resp[0]).must_be_kind_of Array
|
135
|
+
expect(resp[0][0]).must_equal "OK"
|
136
|
+
|
137
|
+
pubkey = SchnorrSig.bin2hex(pk)
|
138
|
+
|
139
|
+
f = Filter.new
|
140
|
+
f.add_authors pubkey
|
141
|
+
f.add_ids e.id
|
142
|
+
|
143
|
+
resp = s.ingest Nostrb.json(Source.subscribe(pubkey, f))
|
144
|
+
expect(resp).must_be_kind_of Array
|
145
|
+
expect(resp.length).must_equal 2
|
146
|
+
event, eose = *resp
|
147
|
+
expect(event[0]).must_equal 'EVENT'
|
148
|
+
expect(event[1]).must_equal pubkey
|
149
|
+
expect(event[2]).must_be_kind_of Hash
|
150
|
+
expect(event[2]['id']).must_equal e.id
|
151
|
+
expect(eose[0]).must_equal 'EOSE'
|
152
|
+
end
|
153
|
+
|
154
|
+
it "has multiple responses to REQ requets" do
|
155
|
+
s = Server.new(DB_FILE)
|
156
|
+
sk, pk = SchnorrSig.keypair
|
157
|
+
e = Event.new('first', pk: pk).sign(sk)
|
158
|
+
resp = s.ingest Nostrb.json(Source.publish(e))
|
159
|
+
expect(resp).must_be_kind_of Array
|
160
|
+
expect(resp[0]).must_be_kind_of Array
|
161
|
+
expect(resp[0][0]).must_equal "OK"
|
162
|
+
|
163
|
+
e2 = Event.new('second', pk: pk).sign(sk)
|
164
|
+
resp = s.ingest Nostrb.json(Source.publish(e2))
|
165
|
+
expect(resp).must_be_kind_of Array
|
166
|
+
expect(resp[0]).must_be_kind_of Array
|
167
|
+
expect(resp[0][0]).must_equal "OK"
|
168
|
+
|
169
|
+
# with no filters, nothing will match
|
170
|
+
sid = e.pubkey
|
171
|
+
responses = s.ingest(Nostrb.json(Source.subscribe(sid)))
|
172
|
+
expect(responses).must_be_kind_of Array
|
173
|
+
expect(responses.length).must_equal 1
|
174
|
+
resp = responses[0]
|
175
|
+
expect(resp).must_be_kind_of Array
|
176
|
+
expect(resp[0]).must_equal "EOSE"
|
177
|
+
expect(resp[1]).must_equal sid
|
178
|
+
|
179
|
+
# now add a filter based on pubkey
|
180
|
+
f = Filter.new
|
181
|
+
f.add_authors e.pubkey
|
182
|
+
f.add_ids e.id, e2.id
|
183
|
+
|
184
|
+
resp = s.ingest Nostrb.json(Source.subscribe(sid, f))
|
185
|
+
expect(resp).must_be_kind_of Array
|
186
|
+
expect(resp.length).must_equal 3
|
187
|
+
|
188
|
+
# remove EOSE and validate
|
189
|
+
eose = resp.pop
|
190
|
+
expect(eose).must_be_kind_of Array
|
191
|
+
expect(eose[0]).must_equal "EOSE"
|
192
|
+
expect(eose[1]).must_equal sid
|
193
|
+
|
194
|
+
# verify the response event ids
|
195
|
+
resp.each { |event|
|
196
|
+
expect(event).must_be_kind_of Array
|
197
|
+
expect(event[0]).must_equal "EVENT"
|
198
|
+
expect(event[1]).must_equal sid
|
199
|
+
hsh = event[2]
|
200
|
+
expect(hsh).must_be_kind_of Hash
|
201
|
+
expect(SignedEvent.validate!(hsh)).must_equal hsh
|
202
|
+
expect([e.id, e2.id]).must_include hsh["id"]
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
it "has a single response to CLOSE requests" do
|
207
|
+
s = Server.new(DB_FILE)
|
208
|
+
sid = Test::EVENT.pubkey
|
209
|
+
responses = s.ingest(Nostrb.json(Source.close(sid)))
|
210
|
+
|
211
|
+
# respond CLOSED
|
212
|
+
expect(responses).must_be_kind_of Array
|
213
|
+
expect(responses.length).must_equal 1
|
214
|
+
|
215
|
+
resp = responses[0]
|
216
|
+
expect(resp).must_be_kind_of Array
|
217
|
+
expect(resp[0]).must_equal "CLOSED"
|
218
|
+
expect(resp[1]).must_equal sid
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "error handling" do
|
222
|
+
# invalid request type
|
223
|
+
it "handles unknown unknown request types with an error notice" do
|
224
|
+
a = Source.publish Test::SIGNED
|
225
|
+
a[0] = 'NONSENSE'
|
226
|
+
responses = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
227
|
+
expect(responses).must_be_kind_of Array
|
228
|
+
expect(responses.length).must_equal 1
|
229
|
+
|
230
|
+
resp = responses[0]
|
231
|
+
expect(resp).must_be_kind_of Array
|
232
|
+
expect(resp[0]).must_equal "NOTICE"
|
233
|
+
expect(resp[1]).must_be_kind_of String
|
234
|
+
expect(resp[1]).wont_be_empty
|
235
|
+
end
|
236
|
+
|
237
|
+
# replace leading open brace with space
|
238
|
+
it "handles JSON parse errors with an error notice" do
|
239
|
+
j = Nostrb.json(Nostrb::Source.publish(Test::SIGNED))
|
240
|
+
expect(j[9]).must_equal '{'
|
241
|
+
j[9] = ' '
|
242
|
+
resp = Server.new(DB_FILE).ingest(j)
|
243
|
+
expect(resp).must_be_kind_of Array
|
244
|
+
expect(resp.length).must_equal 1
|
245
|
+
|
246
|
+
type, msg = *resp.first
|
247
|
+
expect(type).must_equal "NOTICE"
|
248
|
+
expect(msg).must_be_kind_of String
|
249
|
+
expect(msg).wont_be_empty
|
250
|
+
end
|
251
|
+
|
252
|
+
# add "stuff":"things"
|
253
|
+
it "handles unexpected fields with an error notice" do
|
254
|
+
a = Nostrb::Source.publish(Test::SIGNED)
|
255
|
+
expect(a[1]).must_be_kind_of Hash
|
256
|
+
a[1]["stuff"] = "things"
|
257
|
+
|
258
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
259
|
+
expect(resp).must_be_kind_of Array
|
260
|
+
expect(resp.length).must_equal 1
|
261
|
+
|
262
|
+
type, msg = *resp.first
|
263
|
+
expect(type).must_equal "NOTICE"
|
264
|
+
expect(msg).must_be_kind_of String
|
265
|
+
expect(msg).wont_be_empty
|
266
|
+
end
|
267
|
+
|
268
|
+
# remove "tags"
|
269
|
+
it "handles missing fields with an error notice" do
|
270
|
+
a = Nostrb::Source.publish(Test::SIGNED)
|
271
|
+
expect(a[1]).must_be_kind_of Hash
|
272
|
+
a[1].delete("tags")
|
273
|
+
|
274
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
275
|
+
expect(resp).must_be_kind_of Array
|
276
|
+
expect(resp.length).must_equal 1
|
277
|
+
|
278
|
+
type, msg = *resp.first
|
279
|
+
expect(type).must_equal "NOTICE"
|
280
|
+
expect(msg).must_be_kind_of String
|
281
|
+
expect(msg).wont_be_empty
|
282
|
+
end
|
283
|
+
|
284
|
+
# cut "id" in half
|
285
|
+
it "handles field format errors with an error notice" do
|
286
|
+
a = Nostrb::Source.publish(Test::SIGNED)
|
287
|
+
expect(a[1]).must_be_kind_of Hash
|
288
|
+
a[1]["id"] = a[1]["id"].slice(0, 32)
|
289
|
+
|
290
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
291
|
+
expect(resp).must_be_kind_of Array
|
292
|
+
expect(resp.length).must_equal 1
|
293
|
+
|
294
|
+
type, msg = *resp.first
|
295
|
+
expect(type).must_equal "NOTICE"
|
296
|
+
expect(msg).must_be_kind_of String
|
297
|
+
expect(msg).wont_be_empty
|
298
|
+
end
|
299
|
+
|
300
|
+
# random "sig"
|
301
|
+
it "handles invalid signature with OK:false" do
|
302
|
+
a = Nostrb::Source.publish(Test::SIGNED)
|
303
|
+
expect(a[1]).must_be_kind_of Hash
|
304
|
+
a[1]["sig"] = SchnorrSig.bin2hex(Random.bytes(64))
|
305
|
+
|
306
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
307
|
+
expect(resp).must_be_kind_of Array
|
308
|
+
expect(resp.length).must_equal 1
|
309
|
+
|
310
|
+
type, id, value, msg = *resp.first
|
311
|
+
expect(type).must_equal "OK"
|
312
|
+
expect(id).must_equal a[1]["id"]
|
313
|
+
expect(value).must_equal false
|
314
|
+
expect(msg).must_be_kind_of String
|
315
|
+
expect(msg).wont_be_empty
|
316
|
+
expect(msg).must_match(/SignatureCheck/)
|
317
|
+
end
|
318
|
+
|
319
|
+
# "id" and "sig" spoofed from another event
|
320
|
+
it "handles spoofed id with OK:false" do
|
321
|
+
orig = Source.publish(Test.new_event('orig'))
|
322
|
+
spoof = Source.publish(Test::SIGNED)
|
323
|
+
|
324
|
+
orig[1]["id"] = spoof[1]["id"]
|
325
|
+
orig[1]["sig"] = spoof[1]["sig"]
|
326
|
+
|
327
|
+
# now sig and id agree with each other, but not orig's content/metadata
|
328
|
+
# the signature should verify, but the id should not
|
329
|
+
|
330
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(orig))
|
331
|
+
expect(resp).must_be_kind_of Array
|
332
|
+
expect(resp.length).must_equal 1
|
333
|
+
|
334
|
+
type, id, value, msg = *resp.first
|
335
|
+
expect(type).must_equal "OK"
|
336
|
+
expect(id).must_equal orig[1]["id"]
|
337
|
+
expect(value).must_equal false
|
338
|
+
expect(msg).must_be_kind_of String
|
339
|
+
expect(msg).wont_be_empty
|
340
|
+
expect(msg).must_match(/IdCheck/)
|
341
|
+
end
|
342
|
+
|
343
|
+
# random "id"
|
344
|
+
it "handles invalid id with OK:false" do
|
345
|
+
a = Source.publish(Test::SIGNED)
|
346
|
+
a[1]["id"] = SchnorrSig.bin2hex(Random.bytes(32))
|
347
|
+
|
348
|
+
resp = Server.new(DB_FILE).ingest(Nostrb.json(a))
|
349
|
+
expect(resp).must_be_kind_of Array
|
350
|
+
expect(resp.length).must_equal 1
|
351
|
+
|
352
|
+
type, id, value, msg = *resp.first
|
353
|
+
expect(type).must_equal "OK"
|
354
|
+
expect(id).must_equal a[1]["id"]
|
355
|
+
expect(value).must_equal false
|
356
|
+
expect(msg).must_be_kind_of String
|
357
|
+
expect(msg).wont_be_empty
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|