summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/handler/grpc/convert.go40
-rw-r--r--internal/handler/grpc/convert_test.go143
-rw-r--r--internal/handler/grpc/server.go128
-rw-r--r--internal/handler/grpc/server_test.go281
4 files changed, 592 insertions, 0 deletions
diff --git a/internal/handler/grpc/convert.go b/internal/handler/grpc/convert.go
new file mode 100644
index 0000000..19505cd
--- /dev/null
+++ b/internal/handler/grpc/convert.go
@@ -0,0 +1,40 @@
1package grpc
2
3import (
4 pb "northwest.io/nostr-grpc/api/nostr/v1"
5 "northwest.io/nostr-grpc/internal/nostr"
6)
7
8func NostrToPB(n *nostr.Event) *pb.Event {
9 tags := make([]*pb.Tag, len(n.Tags))
10 for i, tag := range n.Tags {
11 tags[i] = &pb.Tag{Values: tag}
12 }
13
14 return &pb.Event{
15 Id: n.ID,
16 Pubkey: n.PubKey,
17 CreatedAt: n.CreatedAt,
18 Kind: int32(n.Kind),
19 Tags: tags,
20 Content: n.Content,
21 Sig: n.Sig,
22 }
23}
24
25func PBToNostr(e *pb.Event) *nostr.Event {
26 tags := make(nostr.Tags, len(e.Tags))
27 for i, tag := range e.Tags {
28 tags[i] = tag.Values
29 }
30
31 return &nostr.Event{
32 ID: e.Id,
33 PubKey: e.Pubkey,
34 CreatedAt: e.CreatedAt,
35 Kind: int(e.Kind),
36 Tags: tags,
37 Content: e.Content,
38 Sig: e.Sig,
39 }
40}
diff --git a/internal/handler/grpc/convert_test.go b/internal/handler/grpc/convert_test.go
new file mode 100644
index 0000000..6da2d89
--- /dev/null
+++ b/internal/handler/grpc/convert_test.go
@@ -0,0 +1,143 @@
1package grpc
2
3import (
4 "testing"
5
6 pb "northwest.io/nostr-grpc/api/nostr/v1"
7 "northwest.io/nostr-grpc/internal/nostr"
8)
9
10func TestNostrToPB(t *testing.T) {
11 nostrEvent := &nostr.Event{
12 ID: "abc123",
13 PubKey: "pubkey123",
14 CreatedAt: 1234567890,
15 Kind: 1,
16 Tags: nostr.Tags{{"e", "event1"}, {"p", "pubkey1"}},
17 Content: "Hello, Nostr!",
18 Sig: "sig123",
19 }
20
21 pbEvent := NostrToPB(nostrEvent)
22
23 if pbEvent.Id != nostrEvent.ID {
24 t.Errorf("ID mismatch: expected %s, got %s", nostrEvent.ID, pbEvent.Id)
25 }
26 if pbEvent.Pubkey != nostrEvent.PubKey {
27 t.Errorf("Pubkey mismatch: expected %s, got %s", nostrEvent.PubKey, pbEvent.Pubkey)
28 }
29 if pbEvent.CreatedAt != nostrEvent.CreatedAt {
30 t.Errorf("CreatedAt mismatch: expected %d, got %d", nostrEvent.CreatedAt, pbEvent.CreatedAt)
31 }
32 if pbEvent.Kind != int32(nostrEvent.Kind) {
33 t.Errorf("Kind mismatch: expected %d, got %d", nostrEvent.Kind, pbEvent.Kind)
34 }
35 if pbEvent.Content != nostrEvent.Content {
36 t.Errorf("Content mismatch: expected %s, got %s", nostrEvent.Content, pbEvent.Content)
37 }
38 if pbEvent.Sig != nostrEvent.Sig {
39 t.Errorf("Sig mismatch: expected %s, got %s", nostrEvent.Sig, pbEvent.Sig)
40 }
41
42 if len(pbEvent.Tags) != len(nostrEvent.Tags) {
43 t.Fatalf("Tags length mismatch: expected %d, got %d", len(nostrEvent.Tags), len(pbEvent.Tags))
44 }
45
46 for i, tag := range pbEvent.Tags {
47 if len(tag.Values) != len(nostrEvent.Tags[i]) {
48 t.Errorf("Tag[%d] values length mismatch", i)
49 continue
50 }
51 for j, val := range tag.Values {
52 if val != nostrEvent.Tags[i][j] {
53 t.Errorf("Tag[%d][%d] mismatch: expected %s, got %s", i, j, nostrEvent.Tags[i][j], val)
54 }
55 }
56 }
57}
58
59func TestPBToNostr(t *testing.T) {
60 pbEvent := &pb.Event{
61 Id: "abc123",
62 Pubkey: "pubkey123",
63 CreatedAt: 1234567890,
64 Kind: 1,
65 Tags: []*pb.Tag{{Values: []string{"e", "event1"}}, {Values: []string{"p", "pubkey1"}}},
66 Content: "Hello, Nostr!",
67 Sig: "sig123",
68 }
69
70 nostrEvent := PBToNostr(pbEvent)
71
72 if nostrEvent.ID != pbEvent.Id {
73 t.Errorf("ID mismatch: expected %s, got %s", pbEvent.Id, nostrEvent.ID)
74 }
75 if nostrEvent.PubKey != pbEvent.Pubkey {
76 t.Errorf("Pubkey mismatch: expected %s, got %s", pbEvent.Pubkey, nostrEvent.PubKey)
77 }
78 if nostrEvent.CreatedAt != pbEvent.CreatedAt {
79 t.Errorf("CreatedAt mismatch: expected %d, got %d", pbEvent.CreatedAt, nostrEvent.CreatedAt)
80 }
81 if nostrEvent.Kind != int(pbEvent.Kind) {
82 t.Errorf("Kind mismatch: expected %d, got %d", pbEvent.Kind, nostrEvent.Kind)
83 }
84 if nostrEvent.Content != pbEvent.Content {
85 t.Errorf("Content mismatch: expected %s, got %s", pbEvent.Content, nostrEvent.Content)
86 }
87 if nostrEvent.Sig != pbEvent.Sig {
88 t.Errorf("Sig mismatch: expected %s, got %s", pbEvent.Sig, nostrEvent.Sig)
89 }
90
91 if len(nostrEvent.Tags) != len(pbEvent.Tags) {
92 t.Fatalf("Tags length mismatch: expected %d, got %d", len(pbEvent.Tags), len(nostrEvent.Tags))
93 }
94
95 for i, tag := range nostrEvent.Tags {
96 if len(tag) != len(pbEvent.Tags[i].Values) {
97 t.Errorf("Tag[%d] values length mismatch", i)
98 continue
99 }
100 for j, val := range tag {
101 if val != pbEvent.Tags[i].Values[j] {
102 t.Errorf("Tag[%d][%d] mismatch: expected %s, got %s", i, j, pbEvent.Tags[i].Values[j], val)
103 }
104 }
105 }
106}
107
108func TestRoundTrip(t *testing.T) {
109 original := &nostr.Event{
110 ID: "roundtrip123",
111 PubKey: "pubkey_roundtrip",
112 CreatedAt: 9876543210,
113 Kind: 7,
114 Tags: nostr.Tags{{"e", "evt"}, {"p", "pk", "relay"}},
115 Content: "Round trip test",
116 Sig: "signature",
117 }
118
119 pb := NostrToPB(original)
120 backToNostr := PBToNostr(pb)
121
122 if backToNostr.ID != original.ID {
123 t.Errorf("Round trip ID mismatch")
124 }
125 if backToNostr.PubKey != original.PubKey {
126 t.Errorf("Round trip PubKey mismatch")
127 }
128 if backToNostr.CreatedAt != original.CreatedAt {
129 t.Errorf("Round trip CreatedAt mismatch")
130 }
131 if backToNostr.Kind != original.Kind {
132 t.Errorf("Round trip Kind mismatch")
133 }
134 if backToNostr.Content != original.Content {
135 t.Errorf("Round trip Content mismatch")
136 }
137 if backToNostr.Sig != original.Sig {
138 t.Errorf("Round trip Sig mismatch")
139 }
140 if len(backToNostr.Tags) != len(original.Tags) {
141 t.Fatalf("Round trip Tags length mismatch")
142 }
143}
diff --git a/internal/handler/grpc/server.go b/internal/handler/grpc/server.go
new file mode 100644
index 0000000..a3a3175
--- /dev/null
+++ b/internal/handler/grpc/server.go
@@ -0,0 +1,128 @@
1package grpc
2
3import (
4 "context"
5 "fmt"
6
7 pb "northwest.io/nostr-grpc/api/nostr/v1"
8 "northwest.io/nostr-grpc/internal/storage"
9)
10
11type EventStore interface {
12 StoreEvent(context.Context, *storage.EventData) error
13 QueryEvents(context.Context, []*pb.Filter, *storage.QueryOptions) ([]*pb.Event, error)
14}
15
16type Server struct {
17 pb.UnimplementedNostrRelayServer
18 store EventStore
19}
20
21func NewServer(store EventStore) *Server {
22 return &Server{store: store}
23}
24
25func (s *Server) PublishEvent(ctx context.Context, req *pb.PublishEventRequest) (*pb.PublishEventResponse, error) {
26 if req.Event == nil {
27 return &pb.PublishEventResponse{
28 Accepted: false,
29 Message: "event is required",
30 }, nil
31 }
32
33 nostrEvent := PBToNostr(req.Event)
34
35 if !nostrEvent.CheckID() {
36 return &pb.PublishEventResponse{
37 Accepted: false,
38 Message: "invalid event ID",
39 }, nil
40 }
41
42 if !nostrEvent.Verify() {
43 return &pb.PublishEventResponse{
44 Accepted: false,
45 Message: "invalid signature",
46 }, nil
47 }
48
49 canonicalJSON := nostrEvent.Serialize()
50
51 eventData := &storage.EventData{
52 Event: req.Event,
53 CanonicalJSON: canonicalJSON,
54 }
55
56 err := s.store.StoreEvent(ctx, eventData)
57 if err == storage.ErrEventExists {
58 return &pb.PublishEventResponse{
59 Accepted: false,
60 Message: "duplicate: event already exists",
61 CanonicalJson: canonicalJSON,
62 }, nil
63 }
64 if err != nil {
65 return nil, fmt.Errorf("failed to store event: %w", err)
66 }
67
68 return &pb.PublishEventResponse{
69 Accepted: true,
70 Message: "success",
71 CanonicalJson: canonicalJSON,
72 }, nil
73}
74
75func (s *Server) QueryEvents(ctx context.Context, req *pb.QueryRequest) (*pb.QueryResponse, error) {
76 opts := &storage.QueryOptions{
77 IncludeCanonical: req.IncludeCanonicalJson,
78 Limit: req.PageSize,
79 }
80
81 if opts.Limit == 0 {
82 opts.Limit = 100
83 }
84
85 events, err := s.store.QueryEvents(ctx, req.Filters, opts)
86 if err != nil {
87 return nil, fmt.Errorf("query failed: %w", err)
88 }
89
90 return &pb.QueryResponse{
91 Events: events,
92 }, nil
93}
94
95func (s *Server) CountEvents(ctx context.Context, req *pb.CountRequest) (*pb.CountResponse, error) {
96 events, err := s.store.QueryEvents(ctx, req.Filters, &storage.QueryOptions{Limit: 0})
97 if err != nil {
98 return nil, fmt.Errorf("count failed: %w", err)
99 }
100
101 return &pb.CountResponse{
102 Count: int64(len(events)),
103 }, nil
104}
105
106func (s *Server) PublishBatch(ctx context.Context, req *pb.PublishBatchRequest) (*pb.PublishBatchResponse, error) {
107 results := make([]*pb.PublishEventResponse, len(req.Events))
108
109 for i, event := range req.Events {
110 resp, err := s.PublishEvent(ctx, &pb.PublishEventRequest{Event: event})
111 if err != nil {
112 return nil, err
113 }
114 results[i] = resp
115 }
116
117 return &pb.PublishBatchResponse{
118 Results: results,
119 }, nil
120}
121
122func (s *Server) Subscribe(req *pb.SubscribeRequest, stream pb.NostrRelay_SubscribeServer) error {
123 return fmt.Errorf("not implemented yet")
124}
125
126func (s *Server) Unsubscribe(ctx context.Context, req *pb.UnsubscribeRequest) (*pb.Empty, error) {
127 return nil, fmt.Errorf("not implemented yet")
128}
diff --git a/internal/handler/grpc/server_test.go b/internal/handler/grpc/server_test.go
new file mode 100644
index 0000000..12dde92
--- /dev/null
+++ b/internal/handler/grpc/server_test.go
@@ -0,0 +1,281 @@
1package grpc
2
3import (
4 "context"
5 "fmt"
6 "testing"
7 "time"
8
9 pb "northwest.io/nostr-grpc/api/nostr/v1"
10 "northwest.io/nostr-grpc/internal/nostr"
11 "northwest.io/nostr-grpc/internal/storage"
12)
13
14func TestPublishEvent(t *testing.T) {
15 store, err := storage.New(":memory:")
16 if err != nil {
17 t.Fatalf("failed to create storage: %v", err)
18 }
19 defer store.Close()
20
21 server := NewServer(store)
22 ctx := context.Background()
23
24 key, err := nostr.GenerateKey()
25 if err != nil {
26 t.Fatalf("failed to generate key: %v", err)
27 }
28
29 nostrEvent := &nostr.Event{
30 PubKey: key.Public(),
31 CreatedAt: time.Now().Unix(),
32 Kind: 1,
33 Tags: nostr.Tags{},
34 Content: "test event",
35 }
36
37 if err := key.Sign(nostrEvent); err != nil {
38 t.Fatalf("failed to sign event: %v", err)
39 }
40
41 pbEvent := NostrToPB(nostrEvent)
42
43 resp, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent})
44 if err != nil {
45 t.Fatalf("PublishEvent failed: %v", err)
46 }
47
48 if !resp.Accepted {
49 t.Errorf("expected event to be accepted, got: %s", resp.Message)
50 }
51 if resp.Message != "success" {
52 t.Errorf("expected success message, got: %s", resp.Message)
53 }
54 if len(resp.CanonicalJson) == 0 {
55 t.Error("expected canonical JSON to be returned")
56 }
57}
58
59func TestPublishEventDuplicate(t *testing.T) {
60 store, err := storage.New(":memory:")
61 if err != nil {
62 t.Fatalf("failed to create storage: %v", err)
63 }
64 defer store.Close()
65
66 server := NewServer(store)
67 ctx := context.Background()
68
69 key, err := nostr.GenerateKey()
70 if err != nil {
71 t.Fatalf("failed to generate key: %v", err)
72 }
73
74 nostrEvent := &nostr.Event{
75 PubKey: key.Public(),
76 CreatedAt: time.Now().Unix(),
77 Kind: 1,
78 Tags: nostr.Tags{},
79 Content: "duplicate test",
80 }
81
82 if err := key.Sign(nostrEvent); err != nil {
83 t.Fatalf("failed to sign event: %v", err)
84 }
85
86 pbEvent := NostrToPB(nostrEvent)
87
88 resp1, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent})
89 if err != nil {
90 t.Fatalf("first PublishEvent failed: %v", err)
91 }
92 if !resp1.Accepted {
93 t.Fatalf("first event should be accepted")
94 }
95
96 resp2, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent})
97 if err != nil {
98 t.Fatalf("second PublishEvent failed: %v", err)
99 }
100 if resp2.Accepted {
101 t.Error("duplicate event should not be accepted")
102 }
103 if resp2.Message != "duplicate: event already exists" {
104 t.Errorf("expected duplicate message, got: %s", resp2.Message)
105 }
106}
107
108func TestPublishEventInvalidSignature(t *testing.T) {
109 store, err := storage.New(":memory:")
110 if err != nil {
111 t.Fatalf("failed to create storage: %v", err)
112 }
113 defer store.Close()
114
115 server := NewServer(store)
116 ctx := context.Background()
117
118 key, err := nostr.GenerateKey()
119 if err != nil {
120 t.Fatalf("failed to generate key: %v", err)
121 }
122
123 nostrEvent := &nostr.Event{
124 PubKey: key.Public(),
125 CreatedAt: time.Now().Unix(),
126 Kind: 1,
127 Tags: nostr.Tags{},
128 Content: "test event",
129 }
130
131 if err := key.Sign(nostrEvent); err != nil {
132 t.Fatalf("failed to sign event: %v", err)
133 }
134
135 pbEvent := NostrToPB(nostrEvent)
136 pbEvent.Sig = "invalid_signature"
137
138 resp, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent})
139 if err != nil {
140 t.Fatalf("PublishEvent failed: %v", err)
141 }
142
143 if resp.Accepted {
144 t.Error("event with invalid signature should not be accepted")
145 }
146 if resp.Message != "invalid signature" {
147 t.Errorf("expected invalid signature message, got: %s", resp.Message)
148 }
149}
150
151func TestPublishEventInvalidID(t *testing.T) {
152 store, err := storage.New(":memory:")
153 if err != nil {
154 t.Fatalf("failed to create storage: %v", err)
155 }
156 defer store.Close()
157
158 server := NewServer(store)
159 ctx := context.Background()
160
161 pbEvent := &pb.Event{
162 Id: "wrong_id",
163 Pubkey: "pubkey123",
164 CreatedAt: time.Now().Unix(),
165 Kind: 1,
166 Tags: []*pb.Tag{},
167 Content: "test",
168 Sig: "sig123",
169 }
170
171 resp, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent})
172 if err != nil {
173 t.Fatalf("PublishEvent failed: %v", err)
174 }
175
176 if resp.Accepted {
177 t.Error("event with invalid ID should not be accepted")
178 }
179 if resp.Message != "invalid event ID" {
180 t.Errorf("expected invalid ID message, got: %s", resp.Message)
181 }
182}
183
184func TestQueryEvents(t *testing.T) {
185 store, err := storage.New(":memory:")
186 if err != nil {
187 t.Fatalf("failed to create storage: %v", err)
188 }
189 defer store.Close()
190
191 server := NewServer(store)
192 ctx := context.Background()
193
194 key, err := nostr.GenerateKey()
195 if err != nil {
196 t.Fatalf("failed to generate key: %v", err)
197 }
198
199 for i := 0; i < 3; i++ {
200 nostrEvent := &nostr.Event{
201 PubKey: key.Public(),
202 CreatedAt: time.Now().Unix(),
203 Kind: 1,
204 Tags: nostr.Tags{},
205 Content: fmt.Sprintf("test event %d", i),
206 }
207
208 if err := key.Sign(nostrEvent); err != nil {
209 t.Fatalf("failed to sign event: %v", err)
210 }
211
212 pbEvent := NostrToPB(nostrEvent)
213 if _, err := server.PublishEvent(ctx, &pb.PublishEventRequest{Event: pbEvent}); err != nil {
214 t.Fatalf("failed to publish event: %v", err)
215 }
216
217 time.Sleep(time.Millisecond)
218 }
219
220 resp, err := server.QueryEvents(ctx, &pb.QueryRequest{
221 Filters: []*pb.Filter{
222 {Authors: []string{key.Public()}},
223 },
224 })
225 if err != nil {
226 t.Fatalf("QueryEvents failed: %v", err)
227 }
228
229 if len(resp.Events) != 3 {
230 t.Errorf("expected 3 events, got %d", len(resp.Events))
231 }
232}
233
234func TestPublishBatch(t *testing.T) {
235 store, err := storage.New(":memory:")
236 if err != nil {
237 t.Fatalf("failed to create storage: %v", err)
238 }
239 defer store.Close()
240
241 server := NewServer(store)
242 ctx := context.Background()
243
244 key, err := nostr.GenerateKey()
245 if err != nil {
246 t.Fatalf("failed to generate key: %v", err)
247 }
248
249 var events []*pb.Event
250 for i := 0; i < 3; i++ {
251 nostrEvent := &nostr.Event{
252 PubKey: key.Public(),
253 CreatedAt: time.Now().Unix(),
254 Kind: 1,
255 Tags: nostr.Tags{},
256 Content: fmt.Sprintf("batch test %d", i),
257 }
258
259 if err := key.Sign(nostrEvent); err != nil {
260 t.Fatalf("failed to sign event: %v", err)
261 }
262
263 events = append(events, NostrToPB(nostrEvent))
264 time.Sleep(time.Millisecond)
265 }
266
267 resp, err := server.PublishBatch(ctx, &pb.PublishBatchRequest{Events: events})
268 if err != nil {
269 t.Fatalf("PublishBatch failed: %v", err)
270 }
271
272 if len(resp.Results) != 3 {
273 t.Fatalf("expected 3 results, got %d", len(resp.Results))
274 }
275
276 for i, result := range resp.Results {
277 if !result.Accepted {
278 t.Errorf("event %d should be accepted, got: %s", i, result.Message)
279 }
280 }
281}