identity_test.go raw
1 package bridge
2
3 import (
4 "encoding/hex"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "next.orly.dev/pkg/nostr/crypto/keys"
10 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 func TestResolveIdentity_FromConfig_Hex(t *testing.T) {
16 // Generate a secret key in hex
17 sk, err := keys.GenerateSecretKey()
18 require.NoError(t, err)
19
20 hexKey := hex.EncodeToString(sk)
21 sign, source, err := ResolveIdentity(hexKey, nil, t.TempDir())
22 require.NoError(t, err)
23 assert.Equal(t, IdentityFromConfig, source)
24 assert.NotNil(t, sign)
25 assert.Len(t, sign.Pub(), 32)
26 }
27
28 func TestResolveIdentity_FromConfig_Nsec(t *testing.T) {
29 sk, err := keys.GenerateSecretKey()
30 require.NoError(t, err)
31
32 nsec, err := bech32encoding.BinToNsec(sk)
33 require.NoError(t, err)
34
35 sign, source, err := ResolveIdentity(string(nsec), nil, t.TempDir())
36 require.NoError(t, err)
37 assert.Equal(t, IdentityFromConfig, source)
38 assert.NotNil(t, sign)
39 }
40
41 func TestResolveIdentity_FromDB(t *testing.T) {
42 sk, err := keys.GenerateSecretKey()
43 require.NoError(t, err)
44
45 dbGetter := func() ([]byte, error) {
46 return sk, nil
47 }
48
49 sign, source, err := ResolveIdentity("", dbGetter, t.TempDir())
50 require.NoError(t, err)
51 assert.Equal(t, IdentityFromDB, source)
52 assert.NotNil(t, sign)
53 }
54
55 func TestResolveIdentity_FromFile_Generate(t *testing.T) {
56 dir := t.TempDir()
57
58 sign, source, err := ResolveIdentity("", nil, dir)
59 require.NoError(t, err)
60 assert.Equal(t, IdentityFromFile, source)
61 assert.NotNil(t, sign)
62
63 // File should have been created
64 nsecPath := filepath.Join(dir, "bridge.nsec")
65 _, err = os.Stat(nsecPath)
66 assert.NoError(t, err, "bridge.nsec file should exist")
67
68 // Reading the file should give us a valid nsec
69 data, err := os.ReadFile(nsecPath)
70 require.NoError(t, err)
71 assert.True(t, len(data) > 0)
72 assert.True(t, string(data[:4]) == "nsec", "file should contain nsec bech32")
73 }
74
75 func TestResolveIdentity_FromFile_Existing(t *testing.T) {
76 dir := t.TempDir()
77
78 // Generate and write an nsec file
79 sk, err := keys.GenerateSecretKey()
80 require.NoError(t, err)
81 nsec, err := bech32encoding.BinToNsec(sk)
82 require.NoError(t, err)
83 require.NoError(t, os.WriteFile(filepath.Join(dir, "bridge.nsec"), nsec, 0600))
84
85 // Should read the existing file
86 sign, source, err := ResolveIdentity("", nil, dir)
87 require.NoError(t, err)
88 assert.Equal(t, IdentityFromFile, source)
89 assert.NotNil(t, sign)
90 }
91
92 func TestResolveIdentity_Priority(t *testing.T) {
93 // Config takes priority over DB and file
94 sk1, err := keys.GenerateSecretKey()
95 require.NoError(t, err)
96 sk2, err := keys.GenerateSecretKey()
97 require.NoError(t, err)
98
99 hexKey := hex.EncodeToString(sk1)
100 dbGetter := func() ([]byte, error) {
101 return sk2, nil
102 }
103
104 sign, source, err := ResolveIdentity(hexKey, dbGetter, t.TempDir())
105 require.NoError(t, err)
106 assert.Equal(t, IdentityFromConfig, source)
107 assert.NotNil(t, sign)
108 }
109
110 func TestResolveIdentity_DBFailsFallsToFile(t *testing.T) {
111 dbGetter := func() ([]byte, error) {
112 return nil, os.ErrNotExist
113 }
114
115 sign, source, err := ResolveIdentity("", dbGetter, t.TempDir())
116 require.NoError(t, err)
117 assert.Equal(t, IdentityFromFile, source)
118 assert.NotNil(t, sign)
119 }
120
121 func TestResolveIdentity_InvalidNsec(t *testing.T) {
122 _, _, err := ResolveIdentity("nsecINVALID", nil, t.TempDir())
123 assert.Error(t, err)
124 }
125
126 func TestResolveIdentity_InvalidHex(t *testing.T) {
127 _, _, err := ResolveIdentity("not-hex-at-all", nil, t.TempDir())
128 assert.Error(t, err)
129 }
130
131 func TestResolveIdentity_ShortHex(t *testing.T) {
132 // Valid hex but not 32 bytes
133 _, _, err := ResolveIdentity("aabbccdd", nil, t.TempDir())
134 assert.Error(t, err)
135 }
136
137 func TestResolveIdentity_DBReturnsWrongLength(t *testing.T) {
138 // DB returns key that's not 32 bytes — should fall through to file
139 dbGetter := func() ([]byte, error) {
140 return []byte{1, 2, 3}, nil // too short
141 }
142
143 sign, source, err := ResolveIdentity("", dbGetter, t.TempDir())
144 require.NoError(t, err)
145 assert.Equal(t, IdentityFromFile, source)
146 assert.NotNil(t, sign)
147 }
148
149 func TestTrimBytes(t *testing.T) {
150 tests := []struct {
151 input []byte
152 want string
153 }{
154 {[]byte("hello\n"), "hello"},
155 {[]byte("hello\r\n"), "hello"},
156 {[]byte("hello \t\n"), "hello"},
157 {[]byte("hello"), "hello"},
158 {[]byte(""), ""},
159 {[]byte("\n\r\t "), ""},
160 }
161 for _, tt := range tests {
162 got := string(trimBytes(tt.input))
163 if got != tt.want {
164 t.Errorf("trimBytes(%q) = %q, want %q", tt.input, got, tt.want)
165 }
166 }
167 }
168
169 func TestIdentityFromFile_EmptyNsecFile(t *testing.T) {
170 dir := t.TempDir()
171 // Write an empty nsec file
172 os.WriteFile(filepath.Join(dir, "bridge.nsec"), []byte(""), 0600)
173
174 // Should generate a new key since the file is empty
175 sign, err := identityFromFile(dir)
176 assert.NoError(t, err)
177 assert.NotNil(t, sign)
178 }
179
180 func TestIdentityFromFile_WhitespaceNsecFile(t *testing.T) {
181 dir := t.TempDir()
182 os.WriteFile(filepath.Join(dir, "bridge.nsec"), []byte(" \n\t "), 0600)
183
184 sign, err := identityFromFile(dir)
185 assert.NoError(t, err)
186 assert.NotNil(t, sign)
187 }
188
189 func TestSignerFromNSEC_HexKey(t *testing.T) {
190 sk, _ := keys.GenerateSecretKey()
191 hexKey := hex.EncodeToString(sk)
192
193 sign, err := signerFromNSEC(hexKey)
194 assert.NoError(t, err)
195 assert.NotNil(t, sign)
196 }
197
198 func TestSignerFromNSEC_NsecKey(t *testing.T) {
199 sk, _ := keys.GenerateSecretKey()
200 nsec, _ := bech32encoding.BinToNsec(sk)
201
202 sign, err := signerFromNSEC(string(nsec))
203 assert.NoError(t, err)
204 assert.NotNil(t, sign)
205 }
206
207 func TestIdentityFromFile_UnwritableDir(t *testing.T) {
208 // Use /dev/null as the data dir — MkdirAll will fail
209 _, err := identityFromFile("/dev/null/impossible")
210 assert.Error(t, err)
211 }
212
213 func TestResolveIdentity_IdentityFileError(t *testing.T) {
214 // No config, no DB, and file path is impossible
215 _, _, err := ResolveIdentity("", nil, "/dev/null/impossible")
216 assert.Error(t, err)
217 }
218
219 func TestSignerFromSecretKey_InvalidKey(t *testing.T) {
220 // Invalid key length — InitSec should fail
221 _, err := signerFromSecretKey([]byte{1, 2, 3})
222 if err == nil {
223 t.Error("expected error for invalid key length")
224 }
225 }
226
227 func TestResolveIdentity_DBGoodKeyButSignerFails(t *testing.T) {
228 // DB returns exactly 32 bytes but signerFromSecretKey
229 // might still fail if the key is degenerate. In practice p8k.New()
230 // succeeds and InitSec with 32 bytes succeeds, so this tests the
231 // normal DB path fully.
232 dbGetter := func() ([]byte, error) {
233 sk, _ := keys.GenerateSecretKey()
234 return sk, nil
235 }
236 sign, source, err := ResolveIdentity("", dbGetter, t.TempDir())
237 assert.NoError(t, err)
238 assert.Equal(t, IdentityFromDB, source)
239 assert.NotNil(t, sign)
240 }
241