# SPDX-FileCopyrightText: 2024 Redict Contributors
# SPDX-FileCopyrightText: 2024 Salvatore Sanfilippo <antirez at gmail dot com>
#
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-License-Identifier: LGPL-3.0-only

set testmodule [file normalize tests/modules/usercall.so]

set test_script_set "#!lua
redict.call('set','x',1)
return 1"

set test_script_get "#!lua
redict.call('get','x')
return 1"

start_server {tags {"modules usercall"}} {
    r module load $testmodule

    # baseline test that module isn't doing anything weird
    test {test module check regular redict command without user/acl} {
        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
        assert_equal [r usercall.call_without_user set x 5] OK
        assert_equal [r usercall.reset_user] OK
    }

    # call with user with acl set on it, but without testing the acl
    test {test module check regular redict command with user} {
        assert_equal [r set x 5] OK

        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
        # off and sanitize-payload because module user / default value
        assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"

        # doesn't fail for regular commands as just testing acl here
        assert_equal [r usercall.call_with_user_flag {} set x 10] OK

        assert_equal [r get x] 10
        assert_equal [r usercall.reset_user] OK
    }

    # call with user with acl set on it, but with testing the acl in rm_call (for cmd itself)
    test {test module check regular redict command with user and acl} {
        assert_equal [r set x 5] OK

        r ACL LOG RESET
        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
        # off and sanitize-payload because module user / default value
        assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"

        # fails here as testing acl in rm call
        assert_error {*NOPERM User module_user has no permissions*} {r usercall.call_with_user_flag C set x 10}

        assert_equal [r usercall.call_with_user_flag C get x] 5

        # verify that new log entry added
        set entry [lindex [r ACL LOG] 0]
        assert_equal [dict get $entry username] {module_user}
        assert_equal [dict get $entry context] {module}
        assert_equal [dict get $entry object] {set}
        assert_equal [dict get $entry reason] {command}
        assert_match {*cmd=usercall.call_with_user_flag*} [dict get $entry client-info]

        assert_equal [r usercall.reset_user] OK
    }

    # call with user with acl set on it, but with testing the acl in rm_call (for cmd itself)
    test {test module check regular redict command with user and acl from blocked background thread} {
        assert_equal [r set x 5] OK

        r ACL LOG RESET
        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK

        # fails here as testing acl in rm call from a background thread
        assert_error {*NOPERM User module_user has no permissions*} {r usercall.call_with_user_bg C set x 10}

        assert_equal [r usercall.call_with_user_bg C get x] 5

        # verify that new log entry added
        set entry [lindex [r ACL LOG] 0]
        assert_equal [dict get $entry username] {module_user}
        assert_equal [dict get $entry context] {module}
        assert_equal [dict get $entry object] {set}
        assert_equal [dict get $entry reason] {command}
        assert_match {*cmd=NULL*} [dict get $entry client-info]

        assert_equal [r usercall.reset_user] OK
    }

    # baseline script test, call without user on script
    test {test module check eval script without user} {
        set sha_set [r script load $test_script_set]
        set sha_get [r script load $test_script_get]

        assert_equal [r usercall.call_without_user evalsha $sha_set 0] 1
        assert_equal [r usercall.call_without_user evalsha $sha_get 0] 1
    }

    # baseline script test, call without user on script
    test {test module check eval script with user being set, but not acl testing} {
        set sha_set [r script load $test_script_set]
        set sha_get [r script load $test_script_get]

        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
        # off and sanitize-payload because module user / default value
        assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"

        # passes as not checking ACL
        assert_equal [r usercall.call_with_user_flag {} evalsha $sha_set 0] 1
        assert_equal [r usercall.call_with_user_flag {} evalsha $sha_get 0] 1
    }

    # call with user on script (without rm_call acl check) to ensure user carries through to script execution
    # we already tested the check in rm_call above, here we are checking the script itself will enforce ACL
    test {test module check eval script with user and acl} {
        set sha_set [r script load $test_script_set]
        set sha_get [r script load $test_script_get]

        r ACL LOG RESET
        assert_equal [r usercall.reset_user] OK
        assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK

        # fails here in script, as rm_call will permit the eval call
        catch {r usercall.call_with_user_flag C evalsha $sha_set 0} e
        assert_match {*ERR ACL failure in script*} $e

        assert_equal [r usercall.call_with_user_flag C evalsha $sha_get 0] 1

        # verify that new log entry added
        set entry [lindex [r ACL LOG] 0]
        assert_equal [dict get $entry username] {module_user}
        assert_equal [dict get $entry context] {lua}
        assert_equal [dict get $entry object] {set}
        assert_equal [dict get $entry reason] {command}
        assert_match {*cmd=usercall.call_with_user_flag*} [dict get $entry client-info]
    }

    start_server {tags {"wait aof network external:skip"}} {
        set slave [srv 0 client]
        set slave_host [srv 0 host]
        set slave_port [srv 0 port]
        set slave_pid [srv 0 pid]
        set master [srv -1 client]
        set master_host [srv -1 host]
        set master_port [srv -1 port]

        $master config set appendonly yes
        $master config set appendfsync everysec
        $slave config set appendonly yes
        $slave config set appendfsync everysec

        test {Setup slave} {
            $slave slaveof $master_host $master_port
            wait_for_condition 50 100 {
                [s 0 master_link_status] eq {up}
            } else {
                fail "Replication not started."
            }
        }

        test {test module replicate only to replicas and WAITAOF} {
            $master set x 1
            assert_equal [$master waitaof 1 1 10000] {1 1}
            $master usercall.call_with_user_flag A! config set loglevel notice
            # Make sure WAITAOF doesn't hang
            assert_equal [$master waitaof 1 1 10000] {1 1}
        }
    }
}
