Files
IRC-kosmi-relay/cmd/test-browser-login/main.go

310 lines
8.7 KiB
Go
Raw Normal View History

2025-11-01 21:00:16 -04:00
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: test-browser-login <email> <password>")
os.Exit(1)
}
email := os.Args[1]
password := os.Args[2]
// Set up Chrome options - VISIBLE browser for debugging
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false), // NOT headless - we want to see it
chromedp.Flag("disable-gpu", false),
)
// Create allocator context
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
// Create context with timeout
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Set a reasonable timeout for the entire login process
ctx, cancel = context.WithTimeout(ctx, 120*time.Second)
defer cancel()
var token string
// Run the browser automation tasks
err := chromedp.Run(ctx,
// Navigate to Kosmi
chromedp.Navigate("https://app.kosmi.io"),
// Wait for page to load completely
chromedp.WaitReady("body"),
chromedp.Sleep(2*time.Second),
// Click Login button
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for Log in button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
fmt.Printf("Found buttons: %v\n", buttonTexts)
var found bool
if err := chromedp.Evaluate(`
(() => {
const buttons = Array.from(document.querySelectorAll('button'));
// Try both "Login" and "Log in"
const btn = buttons.find(el => {
const text = el.textContent.trim();
return text === 'Login' || text === 'Log in';
});
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("Login button not found")
}
fmt.Println("✓ Clicked Login button")
return nil
}),
// Wait for login modal
chromedp.Sleep(3*time.Second),
// Click "Login with Email" button
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for Login with Email button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
`, &buttonTexts).Do(ctx)
fmt.Printf("Found buttons: %v\n", buttonTexts)
var found bool
if err := chromedp.Evaluate(`
(() => {
const btn = Array.from(document.querySelectorAll('button')).find(el => el.textContent.includes('Email'));
if (btn) {
btn.click();
return true;
}
return false;
})()
`, &found).Do(ctx); err != nil {
return err
}
if !found {
return fmt.Errorf("Login with Email button not found")
}
fmt.Println("✓ Clicked Login with Email button")
return nil
}),
// Wait for email form
chromedp.Sleep(3*time.Second),
chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Password input found, filling in email...")
var inputTypes []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('input')).map(el => el.type + (el.placeholder ? ' ('+el.placeholder+')' : ''))
`, &inputTypes).Do(ctx)
fmt.Printf("Available inputs: %v\n", inputTypes)
return nil
}),
// Click on the email input to focus it
chromedp.Click(`input[placeholder*="Email"], input[placeholder*="Username"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
// Type email character by character
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Printf("Typing email: %s\n", email)
return chromedp.SendKeys(`input[placeholder*="Email"], input[placeholder*="Username"]`, email, chromedp.ByQuery).Do(ctx)
}),
chromedp.Sleep(500*time.Millisecond),
// Click on the password input to focus it
chromedp.Click(`input[type="password"]`, chromedp.ByQuery),
chromedp.Sleep(200*time.Millisecond),
// Type password character by character
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Printf("Typing password (length: %d)...\n", len(password))
return chromedp.SendKeys(`input[type="password"]`, password, chromedp.ByQuery).Do(ctx)
}),
// Verify password was filled correctly
chromedp.ActionFunc(func(ctx context.Context) error {
var actualLength int
chromedp.Evaluate(`
(() => {
const passwordInput = document.querySelector('input[type="password"]');
return passwordInput ? passwordInput.value.length : 0;
})()
`, &actualLength).Do(ctx)
fmt.Printf("✓ Password filled (actual length in browser: %d, expected: %d)\n", actualLength, len(password))
if actualLength != len(password) {
return fmt.Errorf("password length mismatch: got %d, expected %d", actualLength, len(password))
}
return nil
}),
chromedp.Sleep(500*time.Millisecond),
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Looking for submit button...")
var buttonTexts []string
chromedp.Evaluate(`
Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim() + (el.disabled ? ' (disabled)' : ''))
`, &buttonTexts).Do(ctx)
fmt.Printf("Submit buttons available: %v\n", buttonTexts)
return nil
}),
// Wait a moment for form validation
chromedp.Sleep(1*time.Second),
// Click the login submit button (be very specific)
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("Attempting to click submit button...")
var result string
if err := chromedp.Evaluate(`
(() => {
const buttons = Array.from(document.querySelectorAll('button'));
console.log('All buttons:', buttons.map(b => ({
text: b.textContent.trim(),
disabled: b.disabled,
visible: b.offsetParent !== null
})));
// Find the submit button in the login form
// It should be visible, enabled, and contain "Login" but not be the main nav button
const submitBtn = buttons.find(el => {
const text = el.textContent.trim();
const isLoginBtn = text === 'Login' || text.startsWith('Login');
const isEnabled = !el.disabled;
const isVisible = el.offsetParent !== null;
const isInForm = el.closest('form') !== null || el.closest('[role="dialog"]') !== null;
return isLoginBtn && isEnabled && isVisible && isInForm;
});
if (submitBtn) {
console.log('Found submit button:', submitBtn.textContent.trim());
submitBtn.click();
return 'CLICKED: ' + submitBtn.textContent.trim();
}
return 'NOT_FOUND';
})()
`, &result).Do(ctx); err != nil {
return err
}
fmt.Printf("Submit button result: %s\n", result)
if result == "NOT_FOUND" {
return fmt.Errorf("Login submit button not found or not clickable")
}
fmt.Println("✓ Clicked submit button")
return nil
}),
// Wait for login to complete
chromedp.Sleep(5*time.Second),
// Check for errors
chromedp.ActionFunc(func(ctx context.Context) error {
var errorText string
chromedp.Evaluate(`
(() => {
const errorEl = document.querySelector('[role="alert"], .error, .alert-error');
return errorEl ? errorEl.textContent : '';
})()
`, &errorText).Do(ctx)
if errorText != "" {
fmt.Printf("❌ Login error: %s\n", errorText)
return fmt.Errorf("login failed: %s", errorText)
}
fmt.Println("✓ No error messages")
return nil
}),
// Extract token from localStorage
chromedp.Evaluate(`localStorage.getItem('token')`, &token),
// Check token details
chromedp.ActionFunc(func(ctx context.Context) error {
var userInfo string
chromedp.Evaluate(`
(() => {
try {
const token = localStorage.getItem('token');
if (!token) return 'NO_TOKEN';
const parts = token.split('.');
if (parts.length !== 3) return 'INVALID_TOKEN';
const payload = JSON.parse(atob(parts[1]));
return JSON.stringify(payload, null, 2);
} catch (e) {
return 'ERROR: ' + e.message;
}
})()
`, &userInfo).Do(ctx)
fmt.Printf("\n=== Token Payload ===\n%s\n", userInfo)
return nil
}),
// Keep browser open for inspection
chromedp.ActionFunc(func(ctx context.Context) error {
fmt.Println("\n✓ Login complete! Browser will stay open for 30 seconds for inspection...")
return nil
}),
chromedp.Sleep(30*time.Second),
)
if err != nil {
fmt.Printf("\n❌ Error: %v\n", err)
os.Exit(1)
}
if token == "" {
fmt.Println("\n❌ No token found in localStorage")
os.Exit(1)
}
fmt.Printf("\n✅ Token obtained (length: %d)\n", len(token))
fmt.Printf("First 50 chars: %s...\n", token[:min(50, len(token))])
}
func min(a, b int) int {
if a < b {
return a
}
return b
}