zip-download-procs.tcl

Regression tests for ZIP download links and destination-page CSP.

Location:
packages/file-storage/tcl/test/zip-download-procs.tcl

Procedures in this file

Detailed information

_file-storage__file_storage_zip_download_ui (private)

 _file-storage__file_storage_zip_download_ui

Partial Call Graph (max 5 caller/called nodes):
aa_equals aa_equals (public) aa_true aa_true (public) acs_package_root_dir acs_package_root_dir (public) ad_mktmpdir ad_mktmpdir (public) _file-storage__file_storage_zip_download_ui _file-storage__file_storage_zip_download_ui _file-storage__file_storage_zip_download_ui->aa_equals _file-storage__file_storage_zip_download_ui->aa_true _file-storage__file_storage_zip_download_ui->acs_package_root_dir _file-storage__file_storage_zip_download_ui->ad_mktmpdir

Testcases:
No testcase defined.
[ hide source ] | [ make this the default ]

Content File Source

ad_library {
    Regression tests for ZIP download links and destination-page CSP.
}

aa_register_case -cats {api smoke production_safe} -procs {
    security::csp::require security::csp::render
} file_storage_zip_download_ui {
    Execute the ZIP page scripts with isolated request/database dependencies,
    using temporary files and the production CSP collector and renderer.
} {
    set child [interp create]
    set tmp [ad_mktmpdir]
    try {
        set root [acs_package_root_dir file-storage]
        interp eval $child [list set testDir $tmp]
        interp alias $child ns_urlencode {} ns_urlencode
        interp alias $child ns_quotehtml {} ns_quotehtml
        interp eval $child {
            namespace eval auth {}; namespace eval permission {}
            namespace eval fs {}; namespace eval util {}; namespace eval site_node {}
            namespace eval security::csp {}
            proc ad_page_contract args {
                dict for {key value} $::contract {uplevel 1 [list set $key $value]}
            }
            proc ad_include_contract args {
                uplevel 1 {set n_past_days 99999; set folder_id 2781645}
            }
            proc auth::require_login {} {incr ::loginChecks}
            proc permission::require_permission args {lappend ::permissionChecks $args}
            proc permission::permission_p args {return -code error -errorcode FS_TEST_STOP stop}
            proc ad_conn key {
                switch $key {
                    user_id {return $::userId}
                    outputheaders {return headers}
                    default {error "Unexpected ad_conn $key"}
                }
            }
            proc ad_return_url {} {return /storage/index?folder_id=2781645}
            proc db_string args {return 0}
            proc ad_try args {uplevel 1 [list try {*}$args]}
            proc _ key {return $key}
            proc ad_progress_bar_begin args {set ::progressStarted 1}
            proc ad_progress_bar_end args {set ::returnTarget [lindex $args 1]}
            proc ad_mktmpdir {} {
                set path $::testDir/staging
                file mkdir $path
                return $path
            }
            proc ad_tmpnam {} {return $::testDir/archive.zip}
            proc fs::publish_object_to_file_system args {
                set f [open $::testDir/staging/payload w]
                puts $f payload
                close $f
            }
            proc util::zip args {
                set f [open $::testDir/archive.zip wb]
                puts -nonewline $f ZIP-FIXTURE
                close $f
            }
            proc fs::get_file_system_safe_object_name args {return {archive'"&<test>}}
            proc fs::get_file_package_id args {return 123}
            proc fs::get_parent args {return 2781645}
            proc site_node::get_url_from_object_id args {return $::packageUrl}
            proc export_vars {flag base specs} {
                set ::exportBase $base
                set ::exportSpecs $specs
                set pairs {}; set ::exportValues {}
                foreach spec $specs {
                    set name [lindex [split $spec :] 0]
                    set value [uplevel 1 [list set $name]]
                    dict set ::exportValues $name $value
                    lappend pairs "[ns_urlencode -- $name]=[ns_urlencode -- $value]"
                    lappend pairs "[ns_urlencode -- ${name}:sig]=[ns_urlencode -- {7 123 HASH}]"
                }
                set ::generatedUrl "$base?[join $pairs &]"
                return $::generatedUrl
            }
            proc util_user_message args {
                set ::messageArgs $args
                set ::readyMessage [lindex $args [expr {[lsearch -exact $args -message]+1}]]
            }
            proc ns_set {op set name value} {dict set ::responseHeaders $name $value}
            proc ns_returnfile {status type path} {
                set f [open $path rb]
                try {set ::delivered [list $status $type [read $f]]} finally {close $f}
            }
            proc ad_file {op path} {file $op $path}
            proc ns_returnnotfound {} {set ::notFound 1}
            proc ad_script_abort {} {return -code error -errorcode FS_TEST_ABORT abort}
            proc security::csp::nonce {} {return testnonce}
            set userId 143022; set loginChecks 0; set permissionChecks {}
        }
        # Use the actual CSP collector/renderer rather than testing a mock
        # implementation of the destination page's policy requirement.
        set f [open [file join [acs_package_root_dir acs-tcl] tcl security-procs.tcl]]
        try {set text [read $f]} finally {close $f}
        foreach {name arguments defaults} {
            require {directive value} {set force_p 0}
            render {} {}
        } {
            set prefix "ad_proc -public ::security::csp::$name"
            set start [string first "$prefix \{" $text]
            if {$start < 0} {error "Missing declaration: $prefix"}
            set declaration {}
            foreach line [split [string range $text $start end] \n] {
                append declaration $line \n
                if {[info complete $declaration]} break
            }
            interp eval $child [list proc security::csp::$name $arguments "$defaults\n[lindex $declaration end]"]
        }
        foreach packageUrl {/storage/ /storage /demo/storage/ /} {
            interp eval $child [list set packageUrl $packageUrl]
            interp eval $child {
                set contract {object_id 2781645 confirm_p 0 return_url /storage/index?folder_id=2781645}
            }
            interp eval $child [list source [file join $root www download-zip.tcl]]
            aa_equals "ZIP staging files cleaned up ($packageUrl)" [file exists $tmp/staging] 0
            aa_true "Archive retained for the second request ($packageUrl)" [file exists $tmp/archive.zip]
            aa_equals "Download path has exactly one boundary slash ($packageUrl)" \
                [interp eval $child {set exportBase}] "[string trimright $packageUrl /]/download-zip-2"
            set specs [interp eval $child {set exportSpecs}]
            aa_equals "All three signature specs present ($packageUrl)" \
                [join $specs ,] {f:sign(max_age=300),n:sign(max_age=300),u:sign(max_age=300)}
            set message [interp eval $child {set readyMessage}]
            set quoted [ns_quotehtml [interp eval $child {set generatedUrl}]]
            aa_true "Visible localized download link ($packageUrl)" \
                [expr {[string first "<a href='$quoted'>#file-storage.Download_ZIP#</a>" $message]>=0}]
            aa_true "Automatic iframe uses the same escaped URL ($packageUrl)" \
                [expr {[string first "<iframe style='display:none' src='$quoted'></iframe>" $message]>=0}]
            aa_true "Ready message is passed as HTML ($packageUrl)" \
                [expr {[lsearch -exact [interp eval $child {set messageArgs}] -html]>=0}]
            aa_equals "Folder return target is preserved ($packageUrl)" \
                [interp eval $child {set returnTarget}] /storage/index?folder_id=2781645
            # Exercise the actual delivery page, including its ownership check
            # and removal of the completed archive after delivery.
            interp eval $child {set contract $exportValues; set responseHeaders {}}
            interp eval $child [list source [file join $root www download-zip-2.tcl]]
            aa_equals "Archive content is delivered ($packageUrl)" \
                [interp eval $child {set delivered}] {200 application/octet-stream ZIP-FIXTURE}
            aa_equals "ZIP attachment disposition retained ($packageUrl)" \
                [interp eval $child {dict get $responseHeaders Content-Disposition}] \
                {attachment;filename="archive'"&<test>.zip"}
            aa_equals "Archive removed after delivery ($packageUrl)" [file exists $tmp/archive.zip] 0
        }
        # Empty return_url falls back to the file-storage package URL.
        interp eval $child {
            set packageUrl /storage/
            set contract {object_id {2781645 4169041} confirm_p 0 return_url {}}
        }
        interp eval $child [list source [file join $root www download-zip.tcl]]
        aa_equals "Empty return URL falls back to the folder root" \
            [interp eval $child {set returnTarget}] /storage/
        interp eval $child {set contract $exportValues; set userId 0; set notFound 0}
        set code [catch {interp eval $child [list source [file join $root www download-zip-2.tcl]]} result opts]
        aa_equals "Different user cannot collect the archive" [dict get $opts -errorcode] FS_TEST_ABORT
        aa_equals "Ownership failure returns not found" [interp eval $child {set notFound}] 1
        aa_true "Ownership failure does not delete another user's archive" [file exists $tmp/archive.zip]
        interp eval $child {
            unset -nocomplain ::__csp__directive ::__csp__directive_forced
            security::csp::require default-src 'none'
            set userId 143022
        }
        set code [catch {interp eval $child [list source [file join $root www folder-chunk.tcl]]} result opts]
        aa_equals "Folder script reaches the policy setup before data queries" \
            [dict get $opts -errorcode] FS_TEST_STOP
        aa_equals "Destination folder permits only same-origin frames" \
            [interp eval $child {set ::__csp__directive(frame-src)}] {'self'}
        aa_equals "Folder setup preserves restrictive default-src" \
            [interp eval $child {set ::__csp__directive(default-src)}] {'none'}
        set policy [interp eval $child {security::csp::render}]
        aa_true "Rendered policy explicitly permits same-origin frames" \
            [expr {[string first {frame-src 'self';} $policy]>=0}]
        aa_equals "Folder does not add a broad child-src policy" \
            [interp eval $child {info exists ::__csp__directive(child-src)}] 0
    } finally {
        interp delete $child
        file delete -force -- $tmp
    }
}