t.me/atinfo_chat Telegram группа по автоматизации тестирования

Запуск тестового браузера от имени другой AD-записи

Теги: #<Tag:0x00007f74898889c0> #<Tag:0x00007f7489888830> #<Tag:0x00007f7489888768> #<Tag:0x00007f74898886a0> #<Tag:0x00007f7489888588> #<Tag:0x00007f7489888470>

Всем привет!
Хочу вам рассказать, как мне удалось решить проблему запуска автотестов в следующем сценарии:

  • у меня есть набор тестовых AD-учетных записей;
  • этим учетным записям сопоставлены пользователи в тестируемой системе 1-к-1;
  • при входе в систему проверяется токен, который браузер получает от винды, и по которому система аутентифицирует пользователя;
  • в соответствии с учетной записью AD мы авторизуемся в системе с нужными правами.

В чем проблема?
Запуск браузера руками от имени другого пользователя осуществляется легко:
с зажатой клавишей Shift правой кнопкой мыши жмем по ярлыку, например, хрома, там запуск от имени другого пользователя, домен-логин-пароль, всё, браузер запущен.

В автотестах все не так просто. Вспомним схему запуска браузера и работы с ним:

Код ↔ Драйвер браузера ↔ Браузер

_driver = new ChromeDriver(path_to_chromedriver, options);

В таком сценарии наша задача просто определить опции запуска браузера, проинициализировать драйвер, передав в конструктор опции, а драйвер себе уже запустит браузер, который найдет в %PATH% или передан в опциях.

Подобный вопрос уже подымался на этом форуме. Моё решение основано вот на этом совете stack overflow.

Итак, что и как происходит:

using (new ActiveDirectory.User(CurrentBankUser, domainName, password))
{
    Browser = new RemoteWebDriver(new Uri($"http://localhost:{BrowserPort}", UriKind.Absolute), options);
}

Браузер стартуем как удаленный вебдрайвер на локалхосте, к коротому цепляемся по определенному порту, лежащему в переменной BrowserPort. Директивой Using мы указываем, в каком потоке должен стартовать драйвер, к которому мы цепляемся:

public class User : IDisposable
{
    public User(string userName, string domainName, string password)
    {
           BrowserPort = (9000 + AppDomain.GetCurrentThreadId()).ToString();

           ProcessStartInfo processStartInfo = new ProcessStartInfo(ChromeDriverPath, $"--port={BrowserPort}");
            processStartInfo.UserName = userName;
            System.Security.SecureString securePassword = new System.Security.SecureString();
            foreach (char c in password)
            {
                securePassword.AppendChar(c);
            }
            processStartInfo.Password = securePassword;
            processStartInfo.Domain = domainName;
            processStartInfo.UseShellExecute = false;
            processStartInfo.LoadUserProfile = true;
            processStartInfo.WorkingDirectory = ProjectEnvironment.ChromeDriverDir;
            processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            Thread startThread = new Thread(() =>
            {
                 BaseSteps.driverProcess = Process.Start(processStartInfo);
                 BaseSteps.driverProcess.WaitForExit();
            })
                 { IsBackground = true };
            startThread.Start();
            }

public void Dispose()
      {}
}

Вот здесь BrowserPort = (9000 + AppDomain.GetCurrentThreadId()).ToString(); как раз таки объявляется тот порт, на котором стартанёт драйвер, и по которому мы будем к нему цепляться.
Здесь ProcessStartInfo processStartInfo = new ProcessStartInfo(ChromeDriverPath, $"--port={BrowserPort}"); мы передаем процесс, который будем запускать (хромдрайвер), и указываем опциональный флаг --port, если его не указать, то больше одного теста в параллели вы запустить не сможете, потому что хромдрайвер по дефолту стартует на порту 9515:

и при попытке запускать тесты в параллели будут падения.
Дальше по коду мы передаём данные доменной учетки (домен/логин и пароль), указываем всякие данные и стартуем поток, в котором будет запущен драйвер от имени указанной AD-учетной записи.

Почему метод Dispose() пустой? Потому что этот метод будет вызван автоматически, как только мы выйдем отсюда:

using (new ActiveDirectory.User(CurrentBankUser, domainName, password))
{
   Browser = new RemoteWebDriver(new Uri($"http://localhost:{BrowserPort}", UriKind.Absolute), options);
// Тут должен быть выполнен код тестов
}
// Вот здесь будет выполнен Dispose()

Так как у меня Specflow, а драйвер я храню в ThreadStatic переменной, то я просто запускаю драйвер с браузером, в методе Dispose() не происходит ничего, что мне и нужно. Главное по окончании теста не забыть правильно очистить ресурсы.
Для этого у меня есть глобальный метод Dispose()

public void Dispose()
        {
            Browser.Quit();
            
            driverProcess = GetChromeDriverProcess();
            driverProcess.Kill();
        }

Здесь мы выключаем браузер, ищем нужный нам процесс хромдрайвера и убиваем его.

Поиск процесса осуществляется так:

        public static Process GetChromeDriverProcess()
        {
            var processes = Process.GetProcesses().ToList();
            var ADprocesses = new List<Process>();

            ADprocesses.AddRange(processes.FindAll(process => process.ProcessName.Equals("chromedriver")));

            var cmdlines = new List<string>();

            foreach (var process in ADprocesses)
            {
                    cmdlines.Add(GetCommandLine(process));
            }

            return ADprocesses[cmdlines.FindIndex(cmd => cmd.Contains("--port=" + BrowserPort))];
        }

        private static string GetCommandLine(Process process)
        {
                using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id))
                using (ManagementObjectCollection objects = searcher.Get())
                {
                    return objects.Cast<ManagementBaseObject>().SingleOrDefault()?["CommandLine"]?.ToString();
                }
        }

Здесь мы просто ищем все процессы, среди них хромдрайвер, потом получаем CommandLine свойства процессов хромдрайверов, и ищем тот, что содержит изначально заданный нами порт. Чтобы ManagementObjectSearcher не ругался, надо добавить в зависимости System.Management.Instrumentation

Выводы:

  1. Это работает, параллельное выполнение работает тоже;
  2. Если вам кажется, что это какие-то костыли - вам не кажется :smiley:

Ну и лично у меня немного шок вызывает осознание того, что браузер в таком контексте становится тестовыми данными, потому что надо менеджить, под кем запущен браузер, и устраивает ли это нас. Мне пришлось написать парсер feature-файлов, который ищет нужный нам feature-файл в каталоге тестов, потом в нем ищет выполняемый тест и дальше по регулярке вытаскивает логин AD, лезет в Vault за доменом и паролем, и только затем запускает браузер.
Надеюсь, вам не придётся столкнуться с подобным, а если и придётся, то эта статья поможет решить проблему.
Спасибо за внимание!

6 Симпатий

Ни разу не приходилось запускать браузер от имени другой учетки AD. И пока если честно мало себе представляю когда это может понадобится. Но может потому что я не админ :wink:

у меня иного способа входа в приложение нет, кроме как запуск браузера от имени другого пользователя

1 Симпатия

Спасибо :slightly_smiling_face: