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