#!/usr/local/cpanel/3rdparty/bin/perl -w

# cpanel - t/medium/Cpanel-Sitejet-Publish.t       Copyright 2023 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package t::medium::Cpanel::Sitejet::Publish;

use cPstrict;

use Test2::V0 -no_utf8 => 1;
use Test2::Tools::Explain;
use Test2::Plugin::NoWarnings;
use Test2::Tools::Exception qw/dies/;

use Test::MockFile;
use Test::MockObject;
use Test::MockTime;
use Test::Warn;

use Try::Tiny;

use Test::Cpanel::Policy;

use parent qw(
  Cpanel::TestObj
);

use Cpanel::Sitejet::Publish ();

our $test_message;

__PACKAGE__->new()->runtests() if !caller;

#----------------------------------------------------------------------

#e.g., num_method_tests()
sub new ( $class, @args ) {
    my $self = $class->SUPER::new(@args);
    return $self;
}

#e.g., set environs before running tests
sub runtests ( $self, @args ) {
    $self->{'mock_cpuser_file_DNE'} = Test::MockFile->file('/var/cpanel/users/user_name');
    $self->{'mock_log_file'}        = Test::MockFile->file( '/usr/local/cpanel/logs/error_log',     'fake content' );
    $self->{'mock_cache_file'}      = Test::MockFile->file( '/var/cpanel/userdata/test_user/cache', 'fake content' );
    return $self->SUPER::runtests(@args);
}

sub test_get_document_root : Tests(1) ($self) {
    my @mocks = (
        $self->mock_subs(
            'Cpanel::Config::userdata::Cache::load_cache' => { 'domain.test' => [ 'test_user', 'root', 'main', 'localhost.here', '/home/user/public_html' ] },
        )
    );
    local $Cpanel::user = 'test_user';
    my $got = Cpanel::Sitejet::Publish::get_document_root('domain.test');
    ok( $got, 'get_document_root returns location of public_html file', explain $got);
    return;
}

sub test_isBinary : Tests(32) ($self) {

    my $case         = 0;
    my @binaries     = map { chr } ( 0x09 .. 0x0A, 0x0D );
    my @not_binaries = map { chr } ( 0x00 .. 0x08, 0x0B .. 0x0C, 0x0E .. 0x1F );

    foreach my $binary (@binaries) {
        $case++;
        my $result           = Cpanel::Sitejet::Publish::isBinary($binary);
        my $binary_as_string = sprintf "0x%02x", ord $binary;
        is( $result, 1, "test_isBinary Valid case$case : $binary_as_string" );
    }

    foreach my $not_binary (@not_binaries) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::isBinary($not_binary);
        my $not_binary_as_string = sprintf "0x%02x", ord $not_binary;
        is( $result, '', "test_isBinary invalid case$case : $not_binary_as_string" );
    }
    return;
}

sub test_isStatic : Tests(17) ($self) {
    my $case = 0;

    $Cpanel::Sitejet::Publish::origin = 'https://anorigin.com';

    my @static_urls = (
        '/images/ibm_logo.svg',
        '/g/bistro.png',
        '/uploads/smallerbox/tiny.jpg',
    );
    my @nonStatic_urls = (
        'https://ibm.com/photos/ceo.png',
        'https://sitejet.dev/gap/pants.png',
        'bigbox/moreuploads/smallerbox/tiny.jpg',
    );

    my @extended = (
        'https://ibm.com/images/ibm_logo.svg',
        'https://ibm.com/webcard/something.html',
        'https://anorigin.com/js/custom.js',
        'https://anorigin.com/css/custom.css',
    );

    my @not_extended = (
        'https://ibm.com/webcord/something.html',
        'https://norigin.com/js',
        'https://anorigin.com/jb',
        'https://norigin.com/custom.js',
        'https://anorigin.com/costom.js',
        'https://norigin.com/css/custom.css',
        'https://anorigin.com/ccs/custom.css',
    );

    foreach my $url (@static_urls) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::isStatic($url);
        is( $result, 1, "test_isStatic Valid case$case : $url" );
    }

    foreach my $url (@extended) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::isStatic( $url, 1 );
        is( $result, 1, "test_isStatic Valid case$case : $url" );
    }

    foreach my $url (@nonStatic_urls) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::isStatic($url);
        is( $result, '', "test_isStatic Invalid case$case : $url" );
    }

    foreach my $url (@not_extended) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::isStatic( $url, 1 );
        is( $result, '', "test_isStatic Invalid case$case : $url" );
    }

    return;
}

sub test_createFilename : Tests(50) ($self) {
    my @mocks = (
        $self->mock_subs(
            'File::MimeInfo::extensions' => sub {
                return 'html' if !$_[0] || $_[0] eq 'text/html';
                return 'js'   if $_[0] eq 'text/javascript';
                return 'css'  if $_[0] eq 'text/css';
                return 'jpg'  if $_[0] eq 'image/jpeg';
                return 'png'  if $_[0] eq 'image/png';
            },
        )
    );

    my $case = 0;
    $Cpanel::Sitejet::Publish::document_root = '/home/jim/public_html';

    my %cases = (

        # Test case                     Expected Result                               Mime Type
        '.'                                  => [ '/home/jim/public_html/./index.html',                       'text/html' ],
        '..'                                 => [ '/home/jim/public_html/../index.html',                      'text/html' ],
        './'                                 => [ '/home/jim/public_html/./index.html',                       'text/html' ],
        'bro.htm'                            => [ '/home/jim/public_html/bro.htm',                            'text/html' ],
        'http://def.org/img.png'             => [ '/home/jim/public_html/img.png',                            'image/png' ],
        'http://you.com/'                    => [ '/home/jim/public_html/index.html',                         'text/html' ],
        'http://you.com/js/ft.js'            => [ '/home/jim/public_html/js/ft.js',                           'text/javascript' ],
        'https://abc.org/img.png'            => [ '/home/jim/public_html/img.png',                            'image/png' ],
        'https://mo.com/css/p.jpg'           => [ '/home/jim/public_html/css/p.jpg',                          'image/jpeg' ],
        'https://mo.com/fo?n=jim&'           => [ '/home/jim/public_html/fo/index.html',                      'text/html' ],
        'mile.jpg'                           => [ '/home/jim/public_html/mile.jpg',                           'image/jpeg' ],
        't/g/fonts/css/nk.css'               => [ '/home/jim/public_html/t/g/fonts.css/nk.css',               'text/css' ],
        't/g/foots/css/nk.css'               => [ '/home/jim/public_html/t/g/foots/css/nk.css',               'text/css' ],
        'toad/////'                          => [ '/home/jim/public_html/toad/index.html',                    'text/html' ],
        'images/NorMaL.png'                  => [ '/home/jim/public_html/images/NorMaL.png',                  'image/jpeg' ],
        'images/file.with.multiple.dots.jpg' => [ '/home/jim/public_html/images/file.with.multiple.dots.jpg', 'image/jpeg' ],
        'images/with space.png'              => [ '/home/jim/public_html/images/with space.png',              'image/jpeg' ],
        'g/[brackets].js'                    => [ '/home/jim/public_html/g/[brackets].js',                    'image/javascript' ],
        'images/file"w`th"quotes.jpg'        => [ '/home/jim/public_html/images/file"w`th"quotes.jpg',        'image/jpeg' ],
        'images/spaces%20in%20filename.jpg'  => [ '/home/jim/public_html/images/spaces%20in%20filename.jpg',  'image/jpeg' ],
        'images/encoded%2Ffilename.jpg'      => [ '/home/jim/public_html/images/encoded%2Ffilename.jpg',      'image/jpeg' ],
        'images/foreign_字符.jpg'              => [ '/home/jim/public_html/images/foreign_字符.jpg',              'image/jpeg' ],
        'g/चित्र.png'                        => [ '/home/jim/public_html/g/चित्र.png',                        'image/jng' ],
        'js/スクリプト.js'                        => [ '/home/jim/public_html/js/スクリプト.js',                        'text/javascript' ],
        'css/スタイル.css'                       => [ '/home/jim/public_html/css/スタイル.css',                       'text/css' ],
    );
    my @uris = sort keys %cases;

    foreach my $uri (@uris) {
        $case++;
        my $new_name = Cpanel::Sitejet::Publish::createFilename($uri);
        is( $new_name, $cases{$uri}[0], "test_createFilename Valid case$case : $new_name" );
    }

    foreach my $uri (@uris) {
        $case++;
        my $new_name = Cpanel::Sitejet::Publish::createFilename( $uri, $cases{$uri}[1] );
        is( $new_name, $cases{$uri}[0], "test_createFilename Valid case$case : $new_name" );
    }

    return;
}

sub test_executeRequests : Tests(4) ($self) {
    require HTTP::Response;
    require Cpanel::Logger;

    my @ua_inputs;
    my @logged;
    my @mocks = (
        $self->mock_subs(
            'LWP::UserAgent::request' => sub {
                @ua_inputs = @_;
                return HTTP::Response->new(200);
            },
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @logged, @msgs;
            },
        )
    );
    my $user_log_file = '/home/mockuser/logs/publish_2023-09-27T11:39:07.log';
    push @mocks, Test::MockFile->file( $user_log_file, 'fake content' );

    my $logger = Cpanel::Logger->new( { 'alternate_logfile' => $user_log_file } );
    my $url    = 'http://1fb11c-5f2c7.preview.sitehub.io/';

    my $resp = Cpanel::Sitejet::Publish::executeRequests($url);

    isa_ok( $ua_inputs[1], ['HTTP::Request'], "UserAgent GET request was sent an HTTP::Request" );
    is( $resp->{_rc}, 200, 'Returned successful response' );
    like( $logged[0], qr/Prepare request.*$url/, "Request logged as prepared" );
    like( $logged[1], qr/URI:.*$url.*200/,       "URI reported correctly and with 200" );

    return;
}

sub test_processResponse : Tests(10) ($self) {
    require HTTP::Response;
    require Cpanel::Logger;

    my $expected_content  = 'mocked content';
    my $expected_uri      = 'http://mock4-5f623.preview.3rdpartyservice.io/';
    my $expected_etag     = 'mocked741bc6f4ddc27c52955dbedef9';
    my $expected_mimeType = 'text/html';

    my @logged;
    my ( $uri_to_store, $bodyToStore, $mimeType_to_store );
    my @mocks = (
        $self->mock_subs(
            'HTTP::Response::content' => $expected_content,
            'HTTP::Response::header'  => sub {
                my ( $resp_obj, $req ) = @_;
                if ( $req eq 'etag' ) {
                    return $expected_etag;
                }
                elsif ( $req eq 'Content-Type' ) {
                    return 'text/html; charset=utf-8';
                }
                else { die "unexpected call" }
            },
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @logged, @msgs;
            },
            'Cpanel::Sitejet::Publish::storeContent' => sub {
                ( $uri_to_store, $bodyToStore, $mimeType_to_store ) = @_;
            },
        ),
    );

    # more inputs
    $Cpanel::Sitejet::Publish::origin = 'https://anorigin.com';
    my $checkEtag = 1;

    my %cases = (
        not_successful_response => {
            desc            => 'Response is not successful',
            etag            => $expected_etag,
            expected_result => '',
            response        => HTTP::Response->new(404),
            returns         => '',
        },
        successful_response => {
            desc            => 'Successful response',
            etag            => $expected_etag,
            expected_result => $expected_content,
            response        => HTTP::Response->new(200),
        },
    );

    foreach my $case_key ( keys %cases ) {
        my $case_hr = $cases{$case_key};
        ( $uri_to_store, $bodyToStore, $mimeType_to_store ) = '';
        my $got_content;
        @logged = ();
        ok(
            lives {
                $got_content = Cpanel::Sitejet::Publish::processResponse( $case_hr->{response}, $expected_uri, $case_hr->{etag} );
            },
            "Did not die when $case_key"
        ) or note($@);
        if ( exists $case_hr->{returns} ) {
            is( $got_content, $case_hr->{returns}, "processResponse returns expected content for $case_hr->{desc}" );
            ok( !defined($uri_to_store) || $uri_to_store eq '', "uri_to_store is either not defined or empty when $case_key" );
            ok( !defined($bodyToStore)  || $bodyToStore eq '',  "bodyToStore is either not defined or empty when $case_key" );
        }
        else {
            is( $got_content,  $expected_content, "processResponse returns expected content for $case_hr->{desc}" );
            is( $uri_to_store, $expected_uri,     "Stored uri correctly" );
            is( $bodyToStore,  $expected_content, "Stored bodyToStore correctly" );
        }
        like( $logged[0], qr/Process response of $expected_uri/, "Response properly logged for $case_key" );
    }

    return 1;
}

sub test_log_message : Tests(1) ($self) {

    $Cpanel::Sitejet::Publish::start  = time;
    $Cpanel::Sitejet::Publish::logger = Test::MockObject->new();
    $Cpanel::Sitejet::Publish::logger->mock(
        'info',
        sub {
            my $self = shift;
            $t::medium::Cpanel::Sitejet::Publish::test_message = shift;
            return 1;
        }
    );

    my $case      = 1;
    my $a_message = 'My message: shh!';
    Cpanel::Sitejet::Publish::log_message($a_message);
    my ($elapsed) = $t::medium::Cpanel::Sitejet::Publish::test_message =~ /^\[(\d+) /;
    is( $t::medium::Cpanel::Sitejet::Publish::test_message, "[$elapsed s] $a_message", "test_log_message case$case : $a_message" );

    return;
}

sub test_publish : Tests(23) ($self) {
    my $domain = 'testdom.test';

    my $user_log_file  = '/home/mockuser/logs/publish_2023-09-27T11:39:07.log';
    my $this_file_name = 'a-mockfile-name-called_this_in-the-code';

    my $expected_docroot = '/home/mockuser/docroot';
    my ( $expected_urls_ar, $expected_etag ) = (
        [
            'http://mocked-url01.preview.sitehub.io/',
            'http://mocked-url01.preview.sitehub.io/menu',
            'http://mocked-url01.preview.sitehub.io/book-a-table',
            'http://mocked-url01.preview.sitehub.io/legal-notice',
            'http://mocked-url01.preview.sitehub.io/privacy',
        ],
        'mockedPreviewEtag50ae26ce3bd3ebc',
    );
    my $expected_sitemap_hr = {
        'http://1fb634-5f623.preview.sitehub.io/' => bless(
            {
                '_headers' => bless(
                    {
                        'etag'         => '"8568464da0bf6b419ea3376c6a7d3db0"',
                        'title'        => 'mockuser - Berlin',
                        'content-type' => 'text/html; charset=utf-8',
                    },
                    'HTTP::Headers'
                ),
                '_msg'     => 'OK',
                '_content' => '<!DOCTYPE html><html lang="en"><head><title>mockuser - Berlin</title>...',
            },
            'HTTP::Response'
        ),
        'http://mocked-url01.preview.sitehub.io/menu' => bless(
            {
                '_headers' => bless(
                    {
                        'etag'         => '"8568464da0bf6b419ea3376c6a7d3db0"',
                        'title'        => 'Menu - mockuser',
                        'content-type' => 'text/html; charset=utf-8',
                    },
                    'HTTP::Headers'
                ),
                '_msg'     => 'OK',
                '_content' => '<!DOCTYPE html><html lang="en"><head><title>Menu - mockuser</title>...',
            },
            'HTTP::Response',
        ),
    };

    my @got_logged;
    my $got_domain_1;
    my ( $got_connector, $got_domain_2 );
    my ( $got_etag, $url, $response );
    my $add_url_origin_url;
    my $exec_reqs_url;

    my ( @got_responses, @got_urls, @got_etags, @got_check_etags );
    my $expected_response   = 'mocked-response';
    my $expected_url        = 'http://mocked-url01.preview.sitehub.io/menu';
    my $expected_check_etag = 1;

    my ( $got_responses_ar, $got_finishedUrls_ar, $parseAssets_called );
    my ( $got_connector_2,  $got_domain_3,        $got_publish_data_hr );
    my $api_php_tmpl   = '/var/cpanel/plugins/sitejet/resources/api.php.tmpl';
    my $api_php        = "$expected_docroot/api.php";
    my $nginx_cache_ok = '';

    my @mocks = (
        $self->mock_subs(
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @got_logged, @msgs;
            },
            'Cpanel::Sitejet::Publish::get_document_root' => sub {
                $got_domain_1 = shift;
                return $expected_docroot;
            },
            'Cpanel::Sitejet::Publish::cp_process_sitemap' => sub {
                ( $got_connector, $got_domain_2 ) = @_;
                return ( $expected_urls_ar, $expected_etag );
            },
            'Cpanel::Sitejet::Publish::cleanup'    => 1,
            'Cpanel::Sitejet::Publish::_is_latest' => sub {
                ( $url, $got_etag, $response ) = @_;
                push @got_etags, $got_etag;
            },
            'Cpanel::Sitejet::Publish::isStatic'        => 0,
            'Cpanel::Sitejet::Publish::createFilename'  => $this_file_name,
            'Cpanel::Sitejet::Publish::_add_url_origin' => sub {
                $add_url_origin_url = shift;
                return $add_url_origin_url;
            },
            'Cpanel::Sitejet::Publish::executeRequests' => sub {
                $exec_reqs_url = shift;
                return 'mocked-response';
            },
            'Cpanel::Sitejet::Publish::processResponse' => sub {
                my ( $got_response, $got_url ) = @_;
                push @got_responses, $got_response;
                push @got_urls,      $got_url;
                return 'a processed response';
            },
            'Cpanel::Sitejet::Publish::parseAssets' => sub {
                ( $got_responses_ar, $got_finishedUrls_ar ) = @_;
                $parseAssets_called++;
                return $expected_urls_ar;
            },
            'Cpanel::Sitejet::Connector::cp_update_sitejet_metadata' => sub {
                ( $got_connector_2, $got_domain_3, $got_publish_data_hr ) = @_;
                return 1;
            },
            'Cpanel::Sitejet::Connector::cp_load_sitejet_metadata' => sub {
                return { 'publish_status' => 1 };
            },
            'Cpanel::Sitejet::Publish::parseAndInstallLanguages' => 1,
            'Cpanel::Sitejet::Connector::installFlags'           => 1,
            'Cpanel::Sitejet::Connector::_check_domain_config'   => sub { return; },
            'Cpanel::API::execute'                               => sub {
                $nginx_cache_ok = 1;
                return { status => 1 };
            },
            'Cpanel::Sitejet::Publish::_is_request_success'      => 1,
            'Cpanel::SSL::Domain::get_best_ssldomain_for_object' => sub { return '', { is_currently_valid => 0 }; },
        ),
        Test::MockFile->file( $user_log_file,                                       'fake content' ),
        Test::MockFile->file( $this_file_name,                                      'fake content' ),
        Test::MockFile->file( "$Cpanel::homedir/.cpanel/sitejet/$domain",           '{"fake":"json"}' ),
        Test::MockFile->file( $api_php_tmpl,                                        'fake content' ),
        Test::MockFile->file( $api_php,                                             '' ),
        Test::MockFile->file( "$Cpanel::homedir/.cpanel/sitejet/$domain-files",     '' ),
        Test::MockFile->file( "$Cpanel::homedir/.cpanel/sitejet/$domain-files_new", '' ),
        Test::MockFile->file( "$Cpanel::API::NginxCaching::_nginx_installed_file",  'fake content' ),
    );

    Cpanel::Sitejet::Publish::publish( $domain, $user_log_file );

    is( $got_domain_1, 'testdom.test', 'test_publish - get_document_root is sent the domain' );

    is( $got_domain_2, 'testdom.test', 'test_publish - cp_process_sitemap is sent the domain' );

    for my $processResponse_got_resp (@got_responses) {
        ok( $processResponse_got_resp eq $expected_response, 'test_publish - processResponse got the expected response' );
    }

    like( \@got_urls, $expected_urls_ar, 'test_publish - processResponse is sent the urls' );

    for my $processResponse_got_etag (@got_etags) {
        ok( $processResponse_got_etag eq $expected_etag, 'test_publish - processResponse got the expected etag' );
    }

    for my $parseAssets_got_resp (@$got_responses_ar) {
        ok( $parseAssets_got_resp eq $expected_response, 'test_publish - parseAssets got the expected responses' );
    }
    my @sort_finishedUrls = sort @{$got_finishedUrls_ar};
    my @sort_expectedUrls = sort @{$expected_urls_ar};
    like( @sort_finishedUrls, @sort_expectedUrls, 'test_publish - parseAssets is sent the finishedUrls' );
    is( $parseAssets_called, 5, 'test_publish - parseAssets was called twice' );

    isa_ok(
        $got_connector_2,
        ['Cpanel::Sitejet::Connector'],
        'test_publish - cp_update_sitejet_metadata is given a connector object'
    );
    is(
        $got_domain_3,
        $domain,
        'test_publish - cp_update_sitejet_metadata is given the domain'
    );
    is(
        $got_publish_data_hr->{'publish_status'},
        1,
        'test_publish - cp_update_sitejet_metadata is given the right publish_status'
    );
    is(
        $got_publish_data_hr->{'document_root'},
        '/home/mockuser/docroot',
        'test_publish - cp_update_sitejet_metadata is given the right document_root'
    );
    is(
        ( $got_publish_data_hr->{'latest_publish_date'} >= $Cpanel::Sitejet::Publish::start ),
        1,
        'test_publish - cp_update_sitejet_metadata is given an epoch date equal to or after test began'
    );

    is( $nginx_cache_ok, 1, 'Nginx (and its cache) enabled' );
    $nginx_cache_ok = '';                                        # Reset cache check
    unlink $Cpanel::API::NginxCaching::_nginx_installed_file;    # Disable cache
    Cpanel::Sitejet::Publish::publish( $domain, $user_log_file );
    is( $nginx_cache_ok, '', 'Nginx (and its cache) not enabled' );

    return 1;
}

sub test_cp_process_sitemap : Tests(2) ($self) {
    my $domain        = 'testdom.test';
    my $document_root = '/home/mockuser/docroot';
    $Cpanel::Sitejet::Publish::document_root = $document_root;

    my @logged;
    my @mocks = (
        Test::MockFile->file( $document_root, 'mocked contents' ),
        $self->mock_subs(
            'Cpanel::Sitejet::Connector::loadApiKey'               => 'mockedToken7a605',
            'Cpanel::Sitejet::Connector::getPreviewDataForWebsite' => {
                'etag' => 'mockedPreviewEtag50ae26ce3bd3ebc',
                'url'  => 'mocked-url01.preview.sitehub.io',
            },
            'Cpanel::Sitejet::Publish::fetchUrlsFromSitemap' => [
                'http://mocked-url01.preview.sitehub.io/',
                'http://mocked-url01.preview.sitehub.io/menu',
                'http://mocked-url01.preview.sitehub.io/book-a-table',
                'http://mocked-url01.preview.sitehub.io/legal-notice',
                'http://mocked-url01.preview.sitehub.io/privacy',
            ],
            'Cpanel::Chdir::new'                    => 1,
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @logged, @msgs;
            },
            'Cpanel::Sitejet::Connector::_check_domain_config' => sub { return; },
        ),
    );

    my $conn = Cpanel::Sitejet::Connector->new();
    my ( $urls_ar, $etag ) = Cpanel::Sitejet::Publish::cp_process_sitemap( $conn, $domain, $document_root );

    is( $etag, 'mockedPreviewEtag50ae26ce3bd3ebc', "cp_process_sitemap found and returned the proper etag" );
    is(
        $urls_ar,
        [
            'http://mocked-url01.preview.sitehub.io/',
            'http://mocked-url01.preview.sitehub.io/menu',
            'http://mocked-url01.preview.sitehub.io/book-a-table',
            'http://mocked-url01.preview.sitehub.io/legal-notice',
            'http://mocked-url01.preview.sitehub.io/privacy',
        ],
        'cp_process_sitemap found and returned the proper urls in an array ref',
    );

    return;
}

sub test__etag_mismatch : Tests(11) ($self) {

    my $case = 0;

    # It is considered a match if 'src' and 'cur' are the same or if 'cur' is false.
    my @matches = (
        {
            src => 'b093fb3ab09d6ccce76bfa1e1e73003a',
            cur => 'b093fb3ab09d6ccce76bfa1e1e73003a'
        },
        {
            src => 0,
            cur => 0,
        },
        {
            src => 0,
            cur => undef,
        },
        {
            src => undef,
            cur => undef,
        },
        {
            src => undef,
            cur => 0,
        },
        {
            src => '7abe34f91644b61510eaab887429a0eb',
            cur => '',
        },
        {
            src => '7abe34f91644b61510eaab887429a0eb',
            cur => 0,
        },
        {
            src => '7abe34f91644b61510eaab887429a0eb',
            cur => undef,
        },
    );
    my @mismatches = (
        {
            src => 'b093fb3ab09d6ccce76bfa1e1e73003a',
            cur => '7abe34f91644b61510eaab887429a0eb',
        },
        {
            src => '',
            cur => '7abe34f91644b61510eaab887429a0eb',
        },
        {
            src => 0,
            cur => '7abe34f91644b61510eaab887429a0eb',
        },

        #  Ignoring case where src => undef because things
        #  will break long before _etag_mismatch() is ever
        #  called if this happens.
    );

    foreach my $match (@matches) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::_etag_mismatch( $match->{src}, $match->{cur} );
        $match->{src} //= 'undef';
        $match->{cur} //= 'undef';
        is( $result, 0, "test__etag_mismatch Valid case$case : matched '$match->{src}' and '$match->{cur}'" );
    }

    foreach my $no_match (@mismatches) {
        $case++;
        my $result = Cpanel::Sitejet::Publish::_etag_mismatch( $no_match->{src}, $no_match->{cur} );
        $no_match->{src} //= 'undef';
        $no_match->{cur} //= 'undef';
        is( $result, 1, "test__etag_mismatch Invalid case$case : no match '$no_match->{src}' and '$no_match->{cur}'" );
    }

    return;
}

sub test__get_etag : Tests(10) ($self) {
    my $case = 0;

    my @good_etags = (
        bless( { 'etag' => '"8b6728f71b943e91f969ddf22a3885a1"', }, 'HTTP::Headers' ),
        bless( { 'etag' => '8b6728f71b943e91f969ddf22a3885a1"', },  'HTTP::Headers' ),
        bless( { 'etag' => '"8b6728f71b943e91f969ddf22a3885a1', },  'HTTP::Headers' ),
        bless( { 'etag' => '8b6728f71b943e91f969ddf22a3885a1', },   'HTTP::Headers' ),
    );

    my @bad_etags = (
        bless( { 'etag' => '"This is not a real etag!"', },      'HTTP::Headers' ),
        bless( { 'etag' => 'This is not a real etag either!', }, 'HTTP::Headers' ),
        bless( { 'etag' => '"0"', },                             'HTTP::Headers' ),
        bless( { 'etag' => '""', },                              'HTTP::Headers' ),
        bless( { 'etag' => '"', },                               'HTTP::Headers' ),
        bless( { 'etag' => '', },                                'HTTP::Headers' ),
    );

    foreach my $header (@good_etags) {
        $case++;
        my $etag = Cpanel::Sitejet::Publish::_get_etag($header);
        is( $etag =~ /^[a-f\d]{32}$/, 1, "test__get_etag Valid case$case : $etag" );
    }

    foreach my $header (@bad_etags) {
        $case++;
        my $etag = Cpanel::Sitejet::Publish::_get_etag($header);
        is( $etag =~ /^[a-f\d]{32}$/, '', "test__get_etag Invalid case$case : $etag" );
    }

    return;
}

sub test__add_url_origin : Tests(13) ($self) {
    my $case = 0;

    $Cpanel::Sitejet::Publish::origin = 'http://mi.com';

    my %cases = (

        # Test case                     Expected Result
        ''                           => 'http://mi.com/',
        '.'                          => 'http://mi.com/.',
        '..'                         => 'http://mi.com/..',
        './'                         => 'http://mi.com/./',
        '/pic.jpg'                   => 'http://mi.com/pic.jpg',
        '0'                          => 'http://mi.com/0',
        'cheese//'                   => 'http://mi.com/cheese/',
        'http://mi.com'              => 'http://mi.com',
        'http://mi.com/my_page'      => 'http://mi.com/my_page',
        'http://mi.com/my_page.html' => 'http://mi.com/my_page.html',
        'https://mi.com/'            => 'https://mi.com/',
        'mi.com'                     => 'http://mi.com/mi.com',
        'pic.jpg'                    => 'http://mi.com/pic.jpg',
    );

    foreach my $url ( sort keys %cases ) {
        $case++;
        my $new_url = Cpanel::Sitejet::Publish::_add_url_origin($url);
        is( $new_url, $cases{$url}, "test__add_url_origin Valid case$case : $new_url" );
    }

    return;
}

sub test_fetchUrlsFromSitemap : Tests(4) ($self) {
    $Cpanel::Sitejet::Publish::domain = 'testdom.test';
    my ( $path, $xml, $mime_type );
    $Cpanel::Sitejet::Publish::origin = "http://www.moe.com";
    my @ua_inputs;
    my $etag  = "mockedPreviewEtag50ae26ce3bd3ebc";
    my @mocks = (
        $self->mock_subs(
            'LWP::UserAgent::get' => sub {
                @ua_inputs = @_;
                return HTTP::Response->new(
                    '200', 'OK',
                    HTTP::Headers->new(
                        'content-type'    => 'text/html; charset=utf-8',
                        'x-meta-keywords' => 'Marketing, Copywriting, Social Media, CV, first_load_company, Berlin',
                    ),
                );
            },
            'LWP::UserAgent::ssl_opts' => sub {
                @ua_inputs = @_;
                return;
            },
            'Cpanel::Sitejet::Publish::log_message' => sub {
                return;
            },
            'Cpanel::Sitejet::Publish::_get_etag' => sub {
                return "mockedPreviewEtag50ae26ce3bd3ebc";
            },
            'Cpanel::Sitejet::Publish::_sleep' => 1,
            'Cpanel::XMLParser::XMLin'         => sub {
                return {
                    url => [
                        {
                            loc => 'http://mocked-url01.preview.sitehub.io/',
                        },
                        {
                            loc => 'http://mocked-url01.preview.sitehub.io/menu',
                        },
                        {
                            loc => 'http://mocked-url01.preview.sitehub.io/book-a-table',
                        },
                        {
                            loc => 'http://mocked-url01.preview.sitehub.io/legal-notice',
                        },
                        {
                            loc => 'http://mocked-url01.preview.sitehub.io/privacy',
                        },
                    ]
                };
            },
            'Cpanel::Sitejet::Publish::storeContent' => sub {
                ( $path, $xml, $mime_type ) = @_;
                return;
            },
        )
    );
    my $urls = Cpanel::Sitejet::Publish::fetchUrlsFromSitemap($etag);
    is(
        $urls,
        [
            'http://mocked-url01.preview.sitehub.io/',
            'http://mocked-url01.preview.sitehub.io/menu',
            'http://mocked-url01.preview.sitehub.io/book-a-table',
            'http://mocked-url01.preview.sitehub.io/legal-notice',
            'http://mocked-url01.preview.sitehub.io/privacy',
        ],
        "test_fetchUrlsFromSitemap returns correct URLs"
    ) or diag explain $urls;

    is( $path,      '/sitemap.xml',    "test_fetchUrlsFromSitemap returns correct sitemap path" );
    is( $mime_type, 'application/xml', "test_fetchUrlsFromSitemap returns correct mime_type" );

    $etag = "mockedMismatchEtag50ae26ce3bd3ebc";

    note "etag mismatch causes sleep to occur";
    my ( $error_type, $exception );
    local *Cpanel::Exception::create = sub {
        ( $error_type, $exception ) = @_;
    };
    my $mismatch = dies { Cpanel::Sitejet::Publish::fetchUrlsFromSitemap($etag) };
    like(
        $mismatch,
        qr/The publish process did not update all required files./,
        "Throws exception for etag mismatch"
    );
}

sub test_cleanup : Tests(6) ($self) {

    # Vars in the order they'll get used
    $Cpanel::user    = 'mockuser';
    $Cpanel::homedir = "/home/$Cpanel::user";
    my $cpuser_www_dir = "$Cpanel::homedir/public_html";
    $Cpanel::Sitejet::Publish::document_root = $cpuser_www_dir;
    my $got_cp_user;

    # XXX Missing assertions: This test tests cleanup of public_html, but not cleanup of any subodomains
    my %expected_docrootlist = (
        '/home/mockuser/public_html'                                             => 1,
        '/home/mockuser/notshared-3-outsidepub.test'                             => 1,
        '/home/mockuser/public_html/notshared-2.test'                            => 1,
        '/home/mockuser/public_html/notshared-2.test/notshared-4--nested-1.test' => 1,
        '/home/mockuser/public_html/sub1.test'                                   => 1,
    );

    my @dirs_to_protect = grep { !/^$cpuser_www_dir$/ } ( keys %expected_docrootlist );

    my $got_homedir_in_list_wds;
    my @expected_webdisks = (
        {
            'homedir' => '/home/mockuser/public_html/awebd1',
            'login'   => 'awebd1@mockuser.test',
            'reldir'  => 'public_html/awebd1',
            'user'    => 'awebd1',
        },
        {
            'homedir' => '/home/mockuser/a1',
            'login'   => 'a1@mockuser.test',
            'reldir'  => 'a1',
            'user'    => 'a1',
        },
    );
    my @expected_webdisk_dirs = map { $_->{homedir} } @expected_webdisks;
    push @dirs_to_protect, @expected_webdisk_dirs;

    my $expected_tar_bin = '/bin/mocktar';
    my $saferun_got_tarbin;
    my $saferun_got_target_dir;
    my $saferun_got_document_root;

    my @got_dirs_to_delete;
    my @got_files_to_delete;

    # Mocks
    my @mocks = (
        $self->mock_subs(
            'Cpanel::DomainLookup::getdocrootlist' => sub {
                $got_cp_user = shift;
                return \%expected_docrootlist;
            },
            'Cpanel::WebDisk::api2_listwebdisks' => sub {
                my %in = @_;
                $got_homedir_in_list_wds = $in{homedir};
                return @expected_webdisks;
            },
            'Cpanel::Tar::load_tarcfg' => sub {
                return { 'bin' => $expected_tar_bin };
            },
            'Cpanel::SafeRun::Errors::saferunallerrors' => sub {
                $saferun_got_tarbin        = $_[0];
                $saferun_got_target_dir    = $_[5];
                $saferun_got_document_root = $_[6];
            },
            'Cpanel::SafeDir::RM::safermdir' => sub {
                my $dir = shift;
                push @got_dirs_to_delete, $dir;
            },
            'Cpanel::Autodie::unlink_if_exists' => sub {
                my $file = shift;
                push @got_files_to_delete, $file;
            },
        ),
    );

    # Add an index.html to every dir. Some of these should get deleted, some saved.
    foreach my $dir ( keys %expected_docrootlist, @expected_webdisk_dirs ) {
        push @mocks, (
            Test::MockFile->dir($dir),
            Test::MockFile->file( "$dir/index.html", 'mocked index content' ),
        );
    }

    my @dirs_should_be_removed = qq{$cpuser_www_dir/mocked_custom_dir};
    foreach my $dir (@dirs_should_be_removed) {
        push @mocks, [
            Test::MockFile->dir($dir),
            Test::MockFile->file( "$dir/index.html", 'mocked index content' ),
        ];
    }

    my @expected_files_removed = map { "$cpuser_www_dir/$_" } qw/ index.php .htaccess /;
    foreach my $file (@expected_files_removed) {
        push @mocks, Test::MockFile->file( $file, 'mocked content' );
    }

    my %expected_files_to_remain = map { $_ . '/index.html' => 1 } @dirs_to_protect;

    # Run Cleanup
    Cpanel::Sitejet::Publish::cleanup();

    # html was added to all the dirs, but this is the only one that should be deleted
    push @expected_files_removed, "$cpuser_www_dir/index.html";

    # Assertions
    is( $saferun_got_tarbin, $expected_tar_bin, "test_cleanup - passed tar binary path to run tar" );
    like( $saferun_got_target_dir, qr{/home/mockuser/public_html_.*.gz}, "test_cleanup - passed correct target to run tar (saving gz file in user homedir)" );
    is( $saferun_got_document_root, $cpuser_www_dir, "test_cleanup - passed correct to run tar" );

    # -e didn't work due to mocking.  Don't know why bag didn't work.
    is( [ sort @expected_files_removed ], [ sort @got_files_to_delete ], "test_cleanup - all custom files were sent for deletion" );

    is( [ sort @dirs_should_be_removed ], [ sort @got_dirs_to_delete ], "test_cleanup - all custom dirs were sent for deletion" );

    my @shouldnt_have_deleted = grep { $expected_files_to_remain{$_} } @got_files_to_delete;
    my $failed                = scalar @shouldnt_have_deleted;
    is( $failed, 0, 'test_cleanup - did not delete any files it was not supposed to' );
}

sub test_parseAssets : Tests(21) ($self) {
    my $case = 0;
    $Cpanel::Sitejet::Publish::origin = 'http://mi.com';

    my $responses = [
        bless(
            {
                '_headers' => bless(
                    {
                        'content-type' => 'text/html; charset=utf-8',
                    },
                    'HTTP::Headers'
                ),

                # bit of real looking data
                '_content' =>
                  q{<!DOCTYPE html><html><head><title>A Page</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><h2>Welcome!</h2><img src="images/tiny.jpg"><ul><li><a href="/">Home</a></li><li><a href="/products">Products</a></li><li><a href="/contact">Contact</a></li></ul><img src="images/tiny.png"><div class="ed-element ed-image" id="ed-114361988"><img src="data:image/svg+xml,%3Csvg%20width=\'1000\'%20viewBox=\'0%200%201000%201000\'%20xmlns=\'http://www.w3.org/2000/svg\'%3E%3Crect%20width=\'1000\'%20height=\'1000\'%20style=\'fill:%20%23F7F7F7\'%20/%3E%3C/svg%3E" alt="" data-src="https://cdn1.site-media.eu/images/1920/6688541/unsplash_vmIWr0NnpCQ.jpg" class="ed-lazyload" srcset="data:image/svg+xml,%3Csvg%20width=\'1000\'%20viewBox=\'0%200%201000%201000\'%20xmlns=\'http://www.w3.org/2000/svg\'%3E%3Crect%20width=\'1000\'%20height=\'1000\'%20style=\'fill:%20%23F7F7F7\'%20/%3E%3C/svg%3E 1w" data-srcset="https://cdn1.site-media.eu/images/576/6688541/unsplash_vmIWr0NnpCQ.jpg 576w, https://cdn1.site-media.eu/images/976/6688541/unsplash_vmIWr0NnpCQ.jpg 976w, https://cdn1.site-media.eu/images/1000/6688541/unsplash_vmIWr0NnpCQ.jpg 1000w"></div><script src="/webcard/static/app.bundle.js?t=1694527246"></script><script src="/js/custom.js?v=230926155147"></script></body></html>},
            },
            'HTTP::Response'
        ),
        bless(
            {
                '_headers' => bless(
                    {
                        'content-type' => 'text/html; charset=utf-8',
                    },
                    'HTTP::Headers'
                ),

                # more readable and invented examples
                '_content' => q{
                 <img src="images/tiny.png">
                 <img src="images/0/tiny.png">
                 <img src="images/0,tiny.png">
                 <img src="images/0%2tiny.png">
                 <img src="
                     data:image/svg+xml,%3Csvg%20width=\'1000\'%20viewBox=\'0%200%201000%201000\'%20xmlns=\'http://www.w3.org/2000/svg\'%3E%3Crect%20width=\'1000\'%20height=\'1000\'%20style=\'fill:%20%23F7F7F7\'%20/%3E%3C/svg%3E"
                     data-src="https://cdn1.site-media.eu/images/1920/6688541/unsplash_oooor0NnpCQ.jpg"
                     srcset="data:image/svg+xml,%3Csvg%20width=\'1000\'%20viewBox=\'0%200%201000%201000\'%20xmlns=\'http://www.w3.org/2000/svg\'%3E%3Crect%20width=\'1000\'%20height=\'1000\'%20style=\'fill:%20%23F7F7F7\'%20/%3E%3C/svg%3E 1w"
                     data-srcset="https://cdn1.site-media.eu/images/576/6688541/unsplash_oooor0NnpCQ.jpg 576w,
                     https://cdn1.site-media.eu/images/976/6688541/unsplash_oooor0NnpCQ.jpg 976w,
                     https://cdn1.site-media.eu/images/1000/6688541/unsplash_oooor0NnpCQ.jpg 1000w
                 ">
                 <script src="/webcard/static/app.bundle.js?t=1694527246">
                 <script src="/js/plain.js?v=230926150000">
                 <script src="/images/NorMaL.png">
                 <script src="/g/[brackets].js">
                 <script src="/images/foreign_字符.jpg">
                 <script src="/g/चित्र.png">
                 <script src="/js/スクリプト.js">
                 <script src="/css/スタイル.css">
                 <script src="/images/dottedfiles.with.multiple.dots.jpg">
                 <script src='/images/quotedfile"quotes`quotes.jpg'>
                 <script src='/images/realspace spaces.png'>
                 <script src="/images/spaces%20but%20encoded.jpg">
                 <script src="/images/slashencoded%2Finfilename.jpg">
                  },
            },
            'HTTP::Response'
        ),
        bless(
            {
                '_headers' => bless(
                    {
                        'content-type' => 'image/png',
                    },
                    'HTTP::Headers'
                ),
                '_content' => qq{\357\277\275PNG\n--Not a real image binary, but close--\cZ\c@\c@\c@},
            },
            'HTTP::Response'
        ),
        bless(
            {
                '_headers' => bless(
                    {
                        'content-type' => 'application/octet-stream',
                    },
                    'HTTP::Headers'
                ),
                '_content' => qq{//g/^uètãÕ<9d>öváÑ-ï<9d>ê[^K<90>hb«^X<9d><8e>^R¸^^tK},
            },
            'HTTP::Response'
        ),
    ];

    my @finishedUrls = (
        'http://www.ow.com/js/base.js',
        'http://www.wo.com/js/base.js',
        'http://co.co/notice.html',
        'http://mi.com/js/plain.js?v=230926150000',
    );

    my %expected = (
        '/images/1000/6688541/unsplash_oooor0NnpCQ.jpg' => 1,
        '/images/1000/6688541/unsplash_vmIWr0NnpCQ.jpg' => 1,
        '/images/1920/6688541/unsplash_oooor0NnpCQ.jpg' => 1,
        '/images/1920/6688541/unsplash_vmIWr0NnpCQ.jpg' => 1,
        '/images/576/6688541/unsplash_oooor0NnpCQ.jpg'  => 1,
        '/images/576/6688541/unsplash_vmIWr0NnpCQ.jpg'  => 1,
        '/images/976/6688541/unsplash_oooor0NnpCQ.jpg'  => 1,
        '/images/976/6688541/unsplash_vmIWr0NnpCQ.jpg'  => 1,
        '/js/custom.js?v=230926155147'                  => 1,
        '/js/plain.js?v=230926150000'                   => 1,
        '/webcard/static/1940.js'                       => 1,
        '/webcard/static/app.bundle.js?t=1694527246'    => 1,

        # Made-up examples below. See note in Publish.pm sub parseAssets.
        # left intact
        '/images/NorMaL.png'                         => 1,
        '/g/[brackets].js'                           => 1,
        "/images/dottedfiles.with.multiple.dots.jpg" => 1,
        '/images/foreign_字符.jpg'                     => 1,
        '/g/चित्र.png'                               => 1,
        '/js/スクリプト.js'                               => 1,
        '/css/スタイル.css'                              => 1,

        # partial matches cut off by REGEX_ASSETS
        '/images/quotedfile' => 1,
        '/images/realspace'  => 1,
        "/images/spaces"     => 1,

        # slash gets translated
        "/images/slashencoded/infilename.jpg" => 1,
    );

    my $new_urls = Cpanel::Sitejet::Publish::parseAssets( $responses, \@finishedUrls );
    foreach my $url (@$new_urls) {
        $case++;
        is( $expected{$url}, 1, "test_parseAssets Valid case$case : $url" );
    }

    return;
}

sub test_storeContent : Tests(11) ($self) {
    my $case = 0;
    $Cpanel::Sitejet::Publish::document_root = '/home/mock/public_html';

    my @mocks = (
        $self->mock_subs(
            'File::Path::make_path'                 => sub { return; },
            'Cpanel::Sitejet::Publish::log_message' => sub { return; },
        )
    );

    my @files_info = (
        {
            uri       => 'http://mi.com/webcard/static/app.bundle.js',
            body      => '!function(t,e){if("object"==typeof exports&&"object"==typeof...',
            mime_type => 'application/javascript',
            path      => '/home/mock/public_html/webcard/static/app.bundle.js',
        },
        {
            uri       => 'http://mi.com/webcard/static/fonts/fontawesome-webfont.8b43027f.eot',
            body      => qq{n\357\277\275\cB\c@\357\277\275\357\277\275\cB\c@\cA\c@\cB\c@\c@\c@\c@\c@\c@...},
            mime_type => 'application/vnd.ms-fontobject',
            path      => '/home/mock/public_html/webcard/static/fonts/fontawesome-webfont.8b43027f.eot',
        },
        {
            uri       => 'http://mi.com/webcard/static/fonts/fontawesome-webfont.1e59d233.ttf',
            body      => qq{\c@\cA\c@\c@\c@\n\c@\357\277\275\c@\cC\c@PFFTMk\357\277\275G\357\277\275\c@\cB...},
            mime_type => 'font/ttf',
            path      => '/home/mock/public_html/webcard/static/fonts/fontawesome-webfont.1e59d233.ttf',
        },
        {
            uri       => 'http://mi.com/webcard/static/fonts/fontawesome-webfont.f691f37e.woff',
            body      => qq{wOFF\c@\cA\c@\c@\c@\cA~\357\277\275\c@\n\c@\c@\c@\cB\357\277\275\357\277\275\c@...},
            mime_type => 'font/woff',
            path      => '/home/mock/public_html/webcard/static/fonts/fontawesome-webfont.f691f37e.woff',
        },
        {
            uri       => 'http://mi.com/g/static/s/xyz/v32/Qw3a.woff2',
            body      => qq{wOF2\c@\cA\c@\c@\c@\c@R\254\c@\cT\c@\c@\c@...},
            mime_type => 'font/woff2',
            path      => '/home/mock/public_html/g/static/s/xyz/v32/Qw3a.woff2',
        },
        {
            uri       => 'images/1024/6928471/Rect.jpg',
            body      => qq{\357\277\275\357\277\275\357\277\275\357\277\275\c@\cPJFIF\c@\cA\cA\c@\c@\cA...},
            mime_type => 'image/jpeg',
            path      => '/home/mock/public_html/images/1024/6928471/Rect.jpg',
        },
        {
            uri       => 'http://mi.com/images/dummy.png',
            body      => qq{\357\277\275PNG\n...},
            mime_type => 'image/png',
            path      => '/home/mock/public_html/images/dummy.png',
        },
        {
            uri       => 'http://mi.com/g/fonts.css',
            body      => '@font-face { font-family: ...',
            mime_type => 'text/css',
            path      => '/home/mock/public_html/g/fonts.css',
        },
        {
            uri       => 'http://mi.com/index.html',
            body      => '<html><head>Mi</head><body>...',
            mime_type => 'text/html',
            path      => '/home/mock/public_html/index.html',
        },
        {
            uri       => 'http://mi.com/README.txt',
            body      => 'This file is (C)Copyright XYZ 2029.  All...',
            mime_type => 'text/plain',
            path      => '/home/mock/public_html/README.txt',
        },
        {
            uri       => 'http://mi.com/sitemap.xml',
            body      => qq{<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">},
            mime_type => 'application/xml',
            path      => '/home/mock/public_html/sitemap.xml',
        },
    );

    foreach my $file (@files_info) {
        $case++;
        my ($dir)     = $file->{path} =~ m{(.*)/};
        my $mock_dir  = Test::MockFile->dir($dir);
        my $mock_file = Test::MockFile->file( $file->{path}, $file->{body} );
        my $body      = Cpanel::Sitejet::Publish::storeContent( @{$file}{ 'uri', 'body', 'mime_type' } );
        is( $file->{body}, $body, "test_storeContent Valid case$case : $file->{uri}" );
    }

    return;
}

sub test_parseAndInstallLanguages : Tests(6) ($self) {
    my ( $conn_obj,        $document_root, @languages );
    my ( $got_connector_2, $got_domain_3,  $got_multi_language_hr );
    my @mocks = (
        $self->mock_subs(
            'Cpanel::Sitejet::Connector::installLanguages' => sub {
                ( $conn_obj, $document_root, @languages ) = @_;
                return;
            },
            'Cpanel::Sitejet::Connector::cp_update_sitejet_metadata' => sub {
                ( $got_connector_2, $got_domain_3, $got_multi_language_hr ) = @_;
                return 1;
            },
            'Cpanel::Sitejet::Connector::_check_domain_config' => sub { return; },
        )
    );
    my $connector = Cpanel::Sitejet::Connector->new();
    my %data      = (
        'valid_language' => 'abc<link rel="alternate" hreflang="en"><link rel="alternate" hreflang="es">',
        'empty_language' => 'abc'
    );
    foreach my $case ( sort keys %data ) {
        my $response           = $data{$case};
        my @expected_languages = $case eq 'empty_language' ? () : ( 'en', 'es' );
        ok(
            lives { Cpanel::Sitejet::Publish::parseAndInstallLanguages( $response, $connector ) }
            , "test_parseAndInstallLanguages runs successful as expected for $case."
        );
        is( \@languages,            \@expected_languages,                                   "test_parseAndInstallLanguages returns right languages as expected for $case." );
        is( $got_multi_language_hr, { multi_language => join( ",", @expected_languages ) }, "test_parseAndInstallLanguages updates cp_update_sitejet_metadata as expected for $case." );
    }
}

sub test_cp_remove_unused_directories : Tests(7) ($self) {

    # build mock directory structure with some files here and there, but mostly empty directories.
    # Make sure only the paths that should remain, do remain.

    my $doc_root = '/tmp/mock_doc_root';
    local $Cpanel::Sitejet::Publish::document_root = $doc_root;

    my %mocks = (
        doc_root => Test::MockFile->dir($doc_root),
        dir1     => Test::MockFile->dir("$doc_root/empty_dir1"),
        dir2     => Test::MockFile->dir("$doc_root/not_empty1/not_empty2"),
        file1    => Test::MockFile->file( "$doc_root/not_empty1/not_empty2/file1.txt", 'Nothing to see here.' ),
        dir3     => Test::MockFile->dir("$doc_root/empty_dir2/empty_dir3/empty_dir4"),
        file2    => Test::MockFile->file( "$doc_root/file2.txt", 'Nothing to see here either.' ),
        dir4     => Test::MockFile->dir("$doc_root/not_empty_dir3"),
        file3    => Test::MockFile->file( "$doc_root/not_empty_dir3/file3.txt", 'Nothing to see here also.' ),
        dir5     => Test::MockFile->dir("$doc_root/not_empty_dir3/empty_dir5"),
        dir6     => Test::MockFile->dir("$doc_root/not_empty_dir3/empty_dir5/empty_dir6"),
        dir7     => Test::MockFile->dir("$doc_root/not_empty_dir3/empty_dir5/empty_dir6/empty_dir7"),
    );

    # This is a list of files to be deleted, just like
    # cp_remove_unused_directories() would run into.  It would have the files
    # like file_a, file_b, etc. in the list, but _cp_remove_obsolete_files
    # would already have removed those files and left the empty directories.
    my @deleted_files = (
        "$doc_root/empty_dir1/file_a",
        "$doc_root/empty_dir1/file_b",
        "$doc_root/empty_dir1/file_c",
        "$doc_root/empty_dir1/file_d",
        "$doc_root/empty_dir1/file_e",
        "$doc_root/not_empty1/not_empty2/file_f",
        "$doc_root/not_empty1/not_empty2/file_g",
        "$doc_root/not_empty1/not_empty2/file_h",
        "$doc_root/empty_dir2/empty_dir3/empty_dir4/file_i",
        "$doc_root/empty_dir2/empty_dir3/empty_dir4/file_j",
        "$doc_root/empty_dir2/empty_dir3/empty_dir4/file_k",
        "$doc_root/not_empty_dir3/file_l",
        "$doc_root/not_empty_dir3/file_m",
        "$doc_root/not_empty_dir3/file_n",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_o",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_p",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_q",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_r",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_s",
        "$doc_root/not_empty_dir3/empty_dir5/empty_dir6/file_t",
    );

    Cpanel::Sitejet::Publish::cp_remove_unused_directories( 'not_a_real_domain.tld', @deleted_files );

    # Checking $mocks{<dir>}{has_content} because !-e <dir> does not reliably work with mocked directories.
    is( -e $doc_root && -d $doc_root,                   1, "test_remove_unused_directories document root okay." );
    is( $mocks{dir1}{has_content},                      0, "test_remove_unused_directories empty_dir1 removed, as expected." );
    is( -e "$doc_root/not_empty1/not_empty2/file1.txt", 1, "test_remove_unused_directories not_empty1 directory still here, as expected." );
    is( $mocks{dir3}{has_content},                      0, "test_remove_unused_directories empty_dir2 removed, as expected." );
    is( -e "$doc_root/file2.txt",                       1, "test_remove_unused_directories file2.txt still here, as expected." );
    is( -e "$doc_root/not_empty_dir3/file3.txt",        1, "test_remove_unused_directories file3.txt still here, as expected." );
    is( $mocks{dir5}{has_content},                      0, "test_remove_unused_directories empty_dir5 removed, as expected." );

    return;
}

1;
