#!/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(28) ($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' ],
    );
    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(12) ($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 = (
        etag_mismatch => {
            desc            => 'ETag mismatch',
            dies            => 'InvalidParameter',
            etag            => 'invalid_etag',
            expected_result => '',
            log_msg         => 'etag mismatch found',
            response        => HTTP::Response->new(200),
        },
        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 = ();
        if ( $cases{$case_key}->{dies} ) {
            like(
                dies {
                    $got_content = Cpanel::Sitejet::Publish::processResponse( $case_hr->{response}, $expected_uri, $case_hr->{etag}, $checkEtag );
                },
                qr/$cases{$case_key}->{dies}/,
                "Got expected exception for $cases{$case_key}->{desc}"
            );
            like( $logged[1], qr/$case_hr->{log_msg}/, "Exception properly logged for $case_key" );
        }
        else {
            ok(
                lives {
                    $got_content = Cpanel::Sitejet::Publish::processResponse( $case_hr->{response}, $expected_uri, $case_hr->{etag}, $checkEtag );
                },
                "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(57) ($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',
            '/robots.txt'
        ],
        '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_urls, $got_etag, $got_processed_sitemap_hr );
    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 @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::_ensure_sitemap_etags_are_updated' => sub {
                ( $got_urls, $got_etag, $got_processed_sitemap_hr ) = @_;
            },
            '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, $got_etag_2, $got_check_etag ) = @_;
                push @got_responses,   $got_response;
                push @got_urls,        $got_url;
                push @got_etags,       $got_etag_2;
                push @got_check_etags, $got_check_etag;
                return 'a processed response';
            },
            'Cpanel::Sitejet::Publish::parseAssets' => sub {
                ( $got_responses_ar, $got_finishedUrls_ar ) = @_;
                $parseAssets_called++;

                # careful for infinite loop
                if ( $parseAssets_called > 1 ) {
                    return [];
                }
                else {
                    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 };
            },
        ),
        Test::MockFile->file( $user_log_file,                     'fake content' ),
        Test::MockFile->file( $this_file_name,                    'fake content' ),
        Test::MockFile->file( "$Cpanel::homedir/sitejet/$domain", '{"fake":"json"}' ),
    );

    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' );

    is( $got_urls,                 $expected_urls_ar, 'test_publish - _ensure_sitemap_etags_are_updated is sent the url list' );
    is( $got_etag,                 $expected_etag,    'test_publish - _ensure_sitemap_etags_are_updated is sent the etag' );
    is( $got_processed_sitemap_hr, {},                'test_publish - _ensure_sitemap_etags_are_updated is sent an empty hash for the processed sitemaps' );

    is( $add_url_origin_url, '/robots.txt', 'test_publish - _add_url_origin is sent the url' );
    is( $exec_reqs_url,      '/robots.txt', 'test_publish - executeRequests is sent the url' );

    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 $got_check_tag (@got_check_etags) {
        ok( $got_check_tag eq $expected_check_etag, 'test_publish - processResponse got the expected check etag values' );
    }

    for my $parseAssets_got_resp (@$got_responses_ar) {
        ok( $parseAssets_got_resp eq $expected_response, 'test_publish - parseAssets got the expected responses' );
    }
    like( $got_finishedUrls_ar, $expected_urls_ar, 'test_publish - parseAssets is sent the finishedUrls' );
    is( $parseAssets_called, 2, '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'
    );

    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',
                '/robots.txt'
            ],
            'Cpanel::Chdir::new'                    => 1,
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @logged, @msgs;
            },
        ),
    );

    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',
            '/robots.txt'
        ],
        'cp_process_sitemap found and returned the proper urls, adding robots.txt, 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(2) ($self) {
    $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',
                        },
                    ]
                };
            }
        )
    );
    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',
            '/robots.txt'
        ],
        "test_fetchUrlsFromSitemap returns correct URLs"
    ) or diag explain $urls;

    $etag = "mockedMismatchEtag50ae26ce3bd3ebc";
    note "etag mismatch causes sleep to occur";
    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(11) ($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'
                ),
                '_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'
                ),
                '_content' =>
                  q{<!DOCTYPE html><html><head><title>&quot;Another Page&quot;</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><h2>You are not welcome!</h2><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"><img src="images/0/tiny.png"><img src="images/0,tiny.png"><img src="images/0%2tiny.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_oooor0NnpCQ.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_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"></div><script>(document.documentElement.lang),n.e(1940).then(n.t.bind(n,21940,23))]);</script><script src="/webcard/static/app.bundle.js?t=1694527246"></script><script src="/js/plain.js?v=230926150000"></script></body></html>},
            },
            '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'
        ),
    ];
    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,
    );

    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(10) ($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',
        },
    );

    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__ensure_sitemap_etags_are_updated : Tests(2) ($self) {
    my $sitemap_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',
        '/robots.txt'
    ];
    $Cpanel::Sitejet::Publish::origin = 'http://mi.com';
    my $etag = "mockedPreviewEtag50ae26ce3bd3ebc";
    my %processed_sitemap;
    my @logged;
    my @mocks = (
        $self->mock_subs(
            'Cpanel::Sitejet::Publish::log_message' => sub {
                my @msgs = @_;
                push @logged, @msgs;
            },
            'LWP::UserAgent::get'                 => HTTP::Response->new(200),
            'LWP::UserAgent::ssl_opts'            => 1,
            'Cpanel::Sitejet::Publish::_get_etag' => 'mockedPreviewEtag50ae26ce3bd3ebc',
            'Cpanel::Sitejet::Publish::_sleep'    => 1,
        )
    );

    my $ensured_etags = lives { Cpanel::Sitejet::Publish::_ensure_sitemap_etags_are_updated( $sitemap_urls, $etag, \%processed_sitemap ) };
    ok( $ensured_etags, "_ensure_sitemap_etags_are_updated doesn't return an error", diag explain $ensured_etags);

    $etag = "mockedMismatchEtag50ae26ce3bd3ebc";
    note "etag mismatch causes sleep to occur";
    my $mismatch = dies { Cpanel::Sitejet::Publish::_ensure_sitemap_etags_are_updated( $sitemap_urls, $etag, \%processed_sitemap ) };
    like(
        $mismatch,
        qr/The publish process did not update all required files./,
        "Throws exception for etag mismatch"
    );
}
