diff --git a/src/cli/auth/oauth_test.go b/src/cli/auth/oauth_test.go new file mode 100644 index 0000000..5797dfb --- /dev/null +++ b/src/cli/auth/oauth_test.go @@ -0,0 +1,89 @@ +package auth + +import ( + "encoding/base64" + "testing" +) + +func TestHashSha256(t *testing.T) { + input := "hello world" + // echo -n "hello world" | openssl sha256 -binary | base64 + // uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= + expectedBase64 := "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=" + + got := hash_sha256(input) + gotBase64 := base64.StdEncoding.EncodeToString(got) + + if gotBase64 != expectedBase64 { + t.Errorf("hash_sha256(%q) = %q, want %q", input, gotBase64, expectedBase64) + } +} + +func TestGenerateCodeVerifier(t *testing.T) { + u := &User{} + u.generate_code_verifier() + + if len(u.Code_verifier) < 43 || len(u.Code_verifier) > 128 { + t.Errorf("generate_code_verifier() length = %d, want between 43 and 128", len(u.Code_verifier)) + } + + // Check for invalid characters + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + for _, char := range u.Code_verifier { + isAllowed := false + for _, a := range allowed { + if char == a { + isAllowed = true + break + } + } + if !isAllowed { + t.Errorf("generate_code_verifier() contains invalid character: %c", char) + } + } +} + +func TestGenerateCodeChallenge(t *testing.T) { + u := &User{} + // Set a fixed verifier for reproducibility + u.Code_verifier = "hello_world_verifier_1234567890" + + // sha256("hello_world_verifier_1234567890") + // echo -n "hello_world_verifier_1234567890" | openssl sha256 -binary | base64 + // hash = 84c3c33379967676e828114f851080d859e3557451965b1285268c375531d041 + // Base64URL(hash) (without padding) + + u.generate_code_challenge() + + // Verify the result + // The implementation calls hash_sha256 then Base64 RawURL encoding + hash := hash_sha256(u.Code_verifier) + expected := base64.RawURLEncoding.EncodeToString(hash) + + if u.Code_challenge != expected { + t.Errorf("generate_code_challenge() = %q, want %q", u.Code_challenge, expected) + } +} + +func TestGenerateState(t *testing.T) { + u := &User{} + length := 127 + u.generate_state(length) + + // The implementation generates random bytes of 'length' then Base64 URL encodes them. + // So the resulting string length will be roughly length * 4/3. + + if u.State == "" { + t.Error("generate_state() produced empty state") + } + + // Decode back to check byte length + decoded, err := base64.URLEncoding.DecodeString(u.State) + if err != nil { + t.Errorf("generate_state() produced invalid base64: %v", err) + } + + if len(decoded) != length { + t.Errorf("generate_state() decoded length = %d, want %d", len(decoded), length) + } +} diff --git a/src/cli/auth/url_test.go b/src/cli/auth/url_test.go new file mode 100644 index 0000000..4caa97d --- /dev/null +++ b/src/cli/auth/url_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "strings" + "testing" +) + +func TestGenerateAuthUrl(t *testing.T) { + u := &User{ + State: "test_state", + Code_challenge: "test_challenge", + } + + u.generate_auth_url() + + if u.Auth_URL == "" { + t.Error("generate_auth_url() resulted in empty Auth_URL") + } + + expectedParts := []string{ + "https://twitter.com/i/oauth2/authorize", + "response_type=code", + "client_id=emJHZzZHMUdHMF9QRlRIdk45QjY6MTpjaQ", + "redirect_uri=https://x-blush.vercel.app/api/auth", + "scope=tweet.read%20tweet.write%20users.read%20users.read%20follows.read%20follows.write%20offline.access", + "state=test_state", + "code_challenge=test_challenge", + "code_challenge_method=S256", + } + + for _, part := range expectedParts { + if !strings.Contains(u.Auth_URL, part) { + t.Errorf("Auth_URL missing part: %s", part) + } + } +} diff --git a/src/cli/auth/validate_test.go b/src/cli/auth/validate_test.go new file mode 100644 index 0000000..0cafcc8 --- /dev/null +++ b/src/cli/auth/validate_test.go @@ -0,0 +1,10 @@ +package auth + +import "testing" + +func TestIsAuthenticated(t *testing.T) { + got := IsAuthenticated() + if got != false { + t.Errorf("IsAuthenticated() = %v, want false", got) + } +} diff --git a/src/cli/lock/file_test.go b/src/cli/lock/file_test.go new file mode 100644 index 0000000..d1d139a --- /dev/null +++ b/src/cli/lock/file_test.go @@ -0,0 +1,71 @@ +package lock + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLicenseFileOperations(t *testing.T) { + // Create a temporary directory to act as HOME + tempDir, err := os.MkdirTemp("", "locktest") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Set HOME environment variable to tempDir + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + licenseKey := "test-license-key-123" + + // Test WriteLicenseKeyToFile + t.Run("WriteLicenseKeyToFile", func(t *testing.T) { + err := WriteLicenseKeyToFile(licenseKey) + if err != nil { + t.Errorf("WriteLicenseKeyToFile() error = %v", err) + } + + // Verify file exists + expectedPath := filepath.Join(tempDir, ".tempxcli") + content, err := os.ReadFile(expectedPath) + if err != nil { + t.Errorf("Failed to read license file: %v", err) + } + if string(content) != licenseKey { + t.Errorf("File content = %q, want %q", string(content), licenseKey) + } + }) + + // Test ReadLicenseKeyFromFile + t.Run("ReadLicenseKeyFromFile", func(t *testing.T) { + readKey, err := ReadLicenseKeyFromFile() + if err != nil { + t.Errorf("ReadLicenseKeyFromFile() error = %v", err) + } + if readKey != licenseKey { + t.Errorf("ReadLicenseKeyFromFile() = %q, want %q", readKey, licenseKey) + } + }) + + // Test ClearLicenseFile + t.Run("ClearLicenseFile", func(t *testing.T) { + err := ClearLicenseFile() + if err != nil { + t.Errorf("ClearLicenseFile() error = %v", err) + } + + // Verify file is gone + _, err = ReadLicenseKeyFromFile() + if err == nil { + t.Error("ReadLicenseKeyFromFile() should fail after clear, but it succeeded") + } + + expectedPath := filepath.Join(tempDir, ".tempxcli") + if _, err := os.Stat(expectedPath); !os.IsNotExist(err) { + t.Error("License file should not exist after clear") + } + }) +}