nostrb 0.1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|