summaryrefslogtreecommitdiffstats
path: root/pkg/identity
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2025-11-23 10:38:12 +0000
committerClaude <noreply@anthropic.com>2025-11-23 10:38:12 +0000
commit3d5286365747c7d2418be16544116f67964b8c48 (patch)
treedc0040ee69ced3d8578587461e0fe98abd61e2a9 /pkg/identity
parent69a75acea56bbff39fa1c81d4813ebcb4a617f06 (diff)
downloadnoshitalk-3d5286365747c7d2418be16544116f67964b8c48.tar.gz
noshitalk-3d5286365747c7d2418be16544116f67964b8c48.tar.xz
noshitalk-3d5286365747c7d2418be16544116f67964b8c48.zip
Add tests, Makefile, update README, remove duplicates
- Add comprehensive test suites for all shared packages: - pkg/crypto: padding, encryption, ECDH, auth tests - pkg/protocol: message serialization tests - pkg/identity: identity save/load tests - pkg/tor: onion address validation tests - Add Makefile with build automation targets - Update README with new modular architecture docs - Remove duplicate monolithic .go files from root (code now organized in cmd/ and pkg/ directories)
Diffstat (limited to 'pkg/identity')
-rw-r--r--pkg/identity/identity_test.go426
1 files changed, 426 insertions, 0 deletions
diff --git a/pkg/identity/identity_test.go b/pkg/identity/identity_test.go
new file mode 100644
index 0000000..40981ad
--- /dev/null
+++ b/pkg/identity/identity_test.go
@@ -0,0 +1,426 @@
+package identity
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestGetDefaultKeyPath(t *testing.T) {
+ path, err := GetDefaultKeyPath("test.key")
+ if err != nil {
+ t.Fatalf("GetDefaultKeyPath() error = %v", err)
+ }
+
+ if !strings.Contains(path, DefaultKeyDir) {
+ t.Errorf("Path should contain %q: %s", DefaultKeyDir, path)
+ }
+
+ if !strings.HasSuffix(path, "test.key") {
+ t.Errorf("Path should end with 'test.key': %s", path)
+ }
+}
+
+func TestNew(t *testing.T) {
+ id, err := New("test")
+ if err != nil {
+ t.Fatalf("New() error = %v", err)
+ }
+
+ if id.PrivateKey == nil {
+ t.Error("PrivateKey should not be nil")
+ }
+ if id.PublicKey == nil {
+ t.Error("PublicKey should not be nil")
+ }
+ if !strings.HasPrefix(id.Username, "test_") {
+ t.Errorf("Username should start with 'test_': %s", id.Username)
+ }
+ if len(id.Fingerprint) != 32 {
+ t.Errorf("Fingerprint length = %d, want 32", len(id.Fingerprint))
+ }
+}
+
+func TestNew_DifferentEachTime(t *testing.T) {
+ id1, _ := New("test")
+ id2, _ := New("test")
+
+ if id1.Fingerprint == id2.Fingerprint {
+ t.Error("Two New() calls should produce different identities")
+ }
+}
+
+func TestIdentity_SaveAndLoad(t *testing.T) {
+ // Create temp directory
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ keyPath := filepath.Join(tmpDir, "test-identity.noshikey")
+
+ // Create and save identity
+ original, err := New("test")
+ if err != nil {
+ t.Fatalf("New() error = %v", err)
+ }
+
+ err = original.Save(keyPath)
+ if err != nil {
+ t.Fatalf("Save() error = %v", err)
+ }
+
+ // Verify file exists
+ if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+ t.Fatal("Key file was not created")
+ }
+
+ // Load identity
+ loaded, err := Load(keyPath)
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ // Compare
+ if loaded.Username != original.Username {
+ t.Errorf("Username mismatch: %q != %q", loaded.Username, original.Username)
+ }
+ if loaded.Fingerprint != original.Fingerprint {
+ t.Errorf("Fingerprint mismatch: %q != %q", loaded.Fingerprint, original.Fingerprint)
+ }
+
+ // Private keys should be functionally equivalent
+ origPubKey := original.PrivateKey.PublicKey().Bytes()
+ loadedPubKey := loaded.PrivateKey.PublicKey().Bytes()
+ if hex.EncodeToString(origPubKey) != hex.EncodeToString(loadedPubKey) {
+ t.Error("Public keys derived from private keys don't match")
+ }
+}
+
+func TestIdentity_SaveRaw(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ keyPath := filepath.Join(tmpDir, "raw.key")
+
+ // Create and save identity in raw format
+ original, _ := New("test")
+ err = original.SaveRaw(keyPath)
+ if err != nil {
+ t.Fatalf("SaveRaw() error = %v", err)
+ }
+
+ // Verify file is 32 bytes (raw private key)
+ data, err := os.ReadFile(keyPath)
+ if err != nil {
+ t.Fatalf("ReadFile() error = %v", err)
+ }
+ if len(data) != 32 {
+ t.Errorf("Raw key file size = %d, want 32", len(data))
+ }
+
+ // Load it back
+ loaded, err := Load(keyPath)
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ // Fingerprints should match
+ if loaded.Fingerprint != original.Fingerprint {
+ t.Errorf("Fingerprint mismatch after raw save/load")
+ }
+}
+
+func TestLoad_JSONFormat(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Create a valid JSON identity file manually
+ id, _ := New("test")
+ idFile := IdentityFile{
+ Version: IdentityVersion,
+ Username: id.Username,
+ Fingerprint: id.Fingerprint,
+ EncryptKeyPair: KeyPair{
+ PrivateKey: hex.EncodeToString(id.PrivateKey.Bytes()),
+ PublicKey: hex.EncodeToString(id.PublicKey.Bytes()),
+ },
+ }
+
+ data, _ := json.Marshal(idFile)
+ keyPath := filepath.Join(tmpDir, "identity.json")
+ os.WriteFile(keyPath, data, 0600)
+
+ // Load it
+ loaded, err := Load(keyPath)
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if loaded.Username != id.Username {
+ t.Errorf("Username mismatch")
+ }
+}
+
+func TestLoad_SignKeyPairFormat(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Create JSON with SignKeyPair instead of EncryptKeyPair
+ id, _ := New("test")
+ idFile := IdentityFile{
+ Version: IdentityVersion,
+ Username: id.Username,
+ Fingerprint: id.Fingerprint,
+ SignKeyPair: KeyPair{
+ PrivateKey: hex.EncodeToString(id.PrivateKey.Bytes()),
+ PublicKey: hex.EncodeToString(id.PublicKey.Bytes()),
+ },
+ }
+
+ data, _ := json.Marshal(idFile)
+ keyPath := filepath.Join(tmpDir, "sign-format.json")
+ os.WriteFile(keyPath, data, 0600)
+
+ // Should still load
+ loaded, err := Load(keyPath)
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if loaded.Fingerprint != id.Fingerprint {
+ t.Errorf("Fingerprint mismatch")
+ }
+}
+
+func TestLoad_Errors(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ tests := []struct {
+ name string
+ content []byte
+ wantErr bool
+ }{
+ {
+ name: "nonexistent file",
+ content: nil, // Don't create file
+ wantErr: true,
+ },
+ {
+ name: "invalid json",
+ content: []byte("not json at all"),
+ wantErr: true,
+ },
+ {
+ name: "empty json",
+ content: []byte("{}"),
+ wantErr: true, // No private key
+ },
+ {
+ name: "invalid hex in private key",
+ content: []byte(`{"encryptKeyPair":{"privateKey":"not-hex"}}`),
+ wantErr: true,
+ },
+ {
+ name: "wrong size raw key",
+ content: make([]byte, 16), // Should be 32
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ keyPath := filepath.Join(tmpDir, tt.name+".key")
+
+ if tt.content != nil {
+ os.WriteFile(keyPath, tt.content, 0600)
+ }
+
+ _, err := Load(keyPath)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestLoadOrCreate_ExistingFile(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ keyPath := filepath.Join(tmpDir, "identity.noshikey")
+
+ // Create identity first
+ original, _ := New("test")
+ original.Save(keyPath)
+
+ // LoadOrCreate should load existing
+ loaded, err := LoadOrCreate(keyPath, "test")
+ if err != nil {
+ t.Fatalf("LoadOrCreate() error = %v", err)
+ }
+
+ if loaded.Fingerprint != original.Fingerprint {
+ t.Error("LoadOrCreate should load existing file, not create new")
+ }
+}
+
+func TestLoadOrCreate_NewFile(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ keyPath := filepath.Join(tmpDir, "new-identity.noshikey")
+
+ // File doesn't exist - should create
+ created, err := LoadOrCreate(keyPath, "new")
+ if err != nil {
+ t.Fatalf("LoadOrCreate() error = %v", err)
+ }
+
+ if created == nil {
+ t.Fatal("LoadOrCreate() returned nil identity")
+ }
+
+ // File should now exist
+ if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+ t.Error("LoadOrCreate should create file if not exists")
+ }
+
+ // Loading again should get same identity
+ loaded, _ := Load(keyPath)
+ if loaded.Fingerprint != created.Fingerprint {
+ t.Error("Saved and loaded fingerprints don't match")
+ }
+}
+
+func TestDeriveSharedIdentity(t *testing.T) {
+ // Same seed should produce same identity
+ id1, err := DeriveSharedIdentity("my-secret-seed", "derived")
+ if err != nil {
+ t.Fatalf("DeriveSharedIdentity() error = %v", err)
+ }
+
+ id2, _ := DeriveSharedIdentity("my-secret-seed", "derived")
+
+ if id1.Fingerprint != id2.Fingerprint {
+ t.Error("Same seed should produce same fingerprint")
+ }
+
+ // Different seed should produce different identity
+ id3, _ := DeriveSharedIdentity("different-seed", "derived")
+ if id1.Fingerprint == id3.Fingerprint {
+ t.Error("Different seeds should produce different fingerprints")
+ }
+}
+
+func TestGenerateChallenge(t *testing.T) {
+ challenge, err := GenerateChallenge()
+ if err != nil {
+ t.Fatalf("GenerateChallenge() error = %v", err)
+ }
+
+ if len(challenge) != 32 {
+ t.Errorf("Challenge length = %d, want 32", len(challenge))
+ }
+
+ // Two challenges should be different
+ challenge2, _ := GenerateChallenge()
+ if string(challenge) == string(challenge2) {
+ t.Error("Challenges should be random")
+ }
+}
+
+func TestIdentityFile_JSONRoundTrip(t *testing.T) {
+ original := IdentityFile{
+ Version: IdentityVersion,
+ Username: "testuser",
+ Fingerprint: "abc123def456",
+ EncryptKeyPair: KeyPair{
+ PrivateKey: "0102030405060708",
+ PublicKey: "09101112131415",
+ },
+ }
+
+ data, err := json.Marshal(original)
+ if err != nil {
+ t.Fatalf("Marshal error = %v", err)
+ }
+
+ var loaded IdentityFile
+ if err := json.Unmarshal(data, &loaded); err != nil {
+ t.Fatalf("Unmarshal error = %v", err)
+ }
+
+ if loaded.Username != original.Username {
+ t.Errorf("Username mismatch")
+ }
+ if loaded.Version != original.Version {
+ t.Errorf("Version mismatch")
+ }
+}
+
+func TestSave_CreatesDirectory(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "noshitalk-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Path with non-existent subdirectory
+ keyPath := filepath.Join(tmpDir, "subdir", "deep", "identity.noshikey")
+
+ id, _ := New("test")
+ err = id.Save(keyPath)
+ if err != nil {
+ t.Fatalf("Save() error = %v", err)
+ }
+
+ // Should have created directories
+ if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+ t.Error("Save should create parent directories")
+ }
+}
+
+// Benchmark
+func BenchmarkNew(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ New("bench")
+ }
+}
+
+func BenchmarkSaveLoad(b *testing.B) {
+ tmpDir, _ := os.MkdirTemp("", "noshitalk-bench-*")
+ defer os.RemoveAll(tmpDir)
+
+ id, _ := New("bench")
+ keyPath := filepath.Join(tmpDir, "bench.key")
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ id.Save(keyPath)
+ Load(keyPath)
+ }
+}