310 lines
8.7 KiB
Go
310 lines
8.7 KiB
Go
|
|
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
|
||
|
|
}
|
||
|
|
|