#! /usr/bin/perl -Idebian/tests/lib

# check first/last uid/gid functionality

use diagnostics;
use strict;
use warnings;

use AdduserTestsCommon;
use Test::More;

my @quiet=('--stdoutmsglevel=error', '--stderrmsglevel=error');
my $gidcount;
my $uidcount;

my @unames = ( 'flugu01', 'flugu02', 'flugu03', 'flugu04' );
my $unamex = 'flugu05';
my @gnames = ( 'flugg01', 'flugg02', 'flugg03', 'flugg04' );
my $gnamex = 'flugg05';
my $funame = 'fixedu01';
my $fgname = 'fixedg01';
my $user;
my $userbase;
my $group;
my $groupbase;
my $prefix;

sub cleanup {
    my $prefix=$_[0] || '';
    foreach $userbase( @unames ) {
        $user=$prefix.$userbase;
        system("/usr/sbin/deluser @quiet --remove-home $user 2>/dev/null");
        assert_user_does_not_exist($user);
    }
    system("/usr/sbin/deluser @quiet --remove-home $prefix$unamex 2>/dev/null");
    assert_user_does_not_exist($prefix.$unamex);
    system("/usr/sbin/deluser @quiet --remove-home $prefix$funame 2>/dev/null");
    assert_user_does_not_exist($prefix.$funame);
    $uidcount=0;
    foreach $groupbase( @gnames ) {
        $group=$prefix.$groupbase;
        system("/usr/sbin/delgroup @quiet $group 2>/dev/null");
        assert_group_does_not_exist($group);
    }
    system("/usr/sbin/delgroup @quiet $prefix$gnamex 2>/dev/null");
    assert_group_does_not_exist($prefix.$gnamex);
    system("/usr/sbin/delgroup @quiet $prefix$fgname 2>/dev/null");
    assert_user_does_not_exist($prefix.$fgname);
    $gidcount=0;
}
cleanup();

# ------------------------------------------------------------
# Helpers for the UID/GID intersection-search and --ingroup/--gid
# preferred-uid test groups (U5A onwards). These blocks all follow
# one of three shapes; pulling each shape into a function removes
# the need to retype the same addgroup/adduser/assert sequence with
# only the numbers and a couple of flags changed.
# ------------------------------------------------------------

# assert_intersection_search_success: create @unames as a usergroup
# (one adduser call per name, each creating its own same-named
# group) using the given uid/gid ranges, optionally as system users
# with --group, and assert the resulting uids/gids are sequential
# starting at expect_first. Used by the U5A/U6A/S5A/S6A-style
# "the combined search picks the right intersection floor" tests.
#
# Args (hashref): prefix, system (bool), firstuid, lastuid,
#   firstgid, lastgid, expect_first.
sub assert_intersection_search_success {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my @sysopt = $a->{system} ? ('--system', '--group') : ();
    my $uidcount = $a->{expect_first};
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
            '--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid},
            $user);
        assert_user_exists($user);
        assert_group_exists($user);
        assert_primary_group_membership_exists($user, $user);
        assert_user_has_uid($user, $uidcount);
        assert_group_has_gid($user, $uidcount);
        $uidcount++;
    }
    cleanup($prefix);
}

# assert_intersection_search_failure: same usergroup setup, but with
# uid/gid ranges that do not overlap at all, so adduser must fail
# outright and create neither the user nor the group. Used by the
# U7A/S6A-style "non-overlapping ranges must fail" tests.
#
# Args (hashref): prefix, system (bool), firstuid, lastuid,
#   firstgid, lastgid.
sub assert_intersection_search_failure {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my @sysopt = $a->{system} ? ('--system', '--group') : ();
    assert_command_failure_silent('/usr/sbin/adduser', @quiet,
        @sysopt,
        '--comment', '""', '--disabled-password', '--no-create-home',
        '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
        '--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid},
        $prefix.$unamex);
    assert_user_does_not_exist($prefix.$unamex);
    assert_group_does_not_exist($prefix.$unamex);
    cleanup($prefix);
}

# assert_preferred_uid: create a group with a fixed gid (system or
# not), optionally pre-occupy that gid as a uid via a separate fixed
# user (to provoke the fallback path), then create one new user
# joining that group either by name (--ingroup) or by numeric id
# (--gid), within the given uid range, and assert the uid it ends
# up with. Used by the U8A/U9A/U10A/S7A/S8A/S9A/S10A-style
# preferred-uid hit/fallback/out-of-range tests.
#
# Args (hashref): prefix, system (bool), firstuid, lastuid, fgid,
#   join_by_gid (bool: false to join with --ingroup GROUPNAME, true
#     to join with --gid GID -- only meaningful in combination with
#     system, since only adduser --system supports the --gid form
#     of this feature),
#   occupy_gid_as_uid (bool: if true, first create a fixed user
#     occupying the group's gid as a uid, to provoke the fallback
#     path),
#   expect_uid (the uid the new user is expected to end up with).
sub assert_preferred_uid {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my @sysopt = $a->{system} ? ('--system') : ();
    assert_command_success('/usr/sbin/addgroup', @quiet,
        @sysopt,
        '--gid', $a->{fgid},
        $prefix.$fgname);
    assert_group_exists($prefix.$fgname);
    assert_group_has_gid($prefix.$fgname, $a->{fgid});
    if ($a->{occupy_gid_as_uid}) {
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            '--uid', $a->{fgid}, '--gid', $a->{fgid},
            $prefix.$funame);
        assert_user_exists($prefix.$funame);
        assert_user_has_uid($prefix.$funame, $a->{fgid});
    }
    my @joinopt = $a->{join_by_gid} ? ('--gid', $a->{fgid}) : ('--ingroup', $prefix.$fgname);
    assert_command_success('/usr/sbin/adduser', @quiet,
        @sysopt,
        @joinopt,
        '--comment', '""', '--disabled-password', '--no-create-home',
        '--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid},
        $prefix.$unames[0]);
    assert_user_exists($prefix.$unames[0]);
    assert_primary_group_membership_exists($prefix.$unames[0], $prefix.$fgname);
    assert_user_has_uid($prefix.$unames[0], $a->{expect_uid});
    cleanup($prefix);
}

# assert_usergroup_loop: create @gnames as plain groups using
# --firstgid (the last one's gid is captured as $gidcount and
# returned), plus one fixed-gid group; then create @unames joining
# the first of those groups via --ingroup, using --firstuid; the
# first such user may get the group's gid preferred as its uid (if
# it is free and in range -- it always is here, since no
# --lastuid/--lastgid is given), the rest get sequential uids from
# firstuid; finally create one fixed-uid user outside any group
# loop. Used by the U2A/U3A/U4A/S2A/S3A/S4A-style tests, which only
# differ in whether the ranges come from the command line, the
# config file, or both, and in whether --system is used throughout.
#
# Args (hashref): prefix, system (bool), firstuid, firstgid, fuid,
#   fgid, via_cli (bool, defaults to true: whether --firstuid/
#   --firstgid are actually passed on the command line, vs. relying
#   on a value already present in the config file).
sub assert_usergroup_loop {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my $firstuid = $a->{firstuid};
    my $firstgid = $a->{firstgid};
    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
    my @sysopt = $a->{system} ? ('--system') : ();
    my @gidopt = $via_cli ? ('--firstgid', $firstgid) : ();
    my @uidopt = $via_cli ? ('--firstuid', $firstuid) : ();
    my $gidcount = $firstgid;
    foreach my $groupbase ( @gnames ) {
        my $group = $prefix.$groupbase;
        assert_command_success('/usr/sbin/addgroup', @quiet,
            @sysopt,
            @gidopt,
            $group);
        assert_group_exists($group);
        assert_group_has_gid($group, $gidcount);
        $gidcount++;
    }
    assert_group_does_not_exist($prefix.$fgname);
    assert_command_success('/usr/sbin/addgroup', @quiet,
        @sysopt,
        @gidopt,
        '--gid', $a->{fgid},
        $prefix.$fgname);
    assert_group_exists($prefix.$fgname);
    assert_group_has_gid($prefix.$fgname, $a->{fgid});

    my $uidcount = $firstuid;
    my $user_index = 0;
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--ingroup', $prefix.$gnames[0],
            '--comment', '""', '--disabled-password', '--no-create-home',
            @uidopt,
            $user);
        assert_user_exists($user);
        assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
        if ($user_index == 0) {
            # The group's GID is free as a UID and inside the search
            # range, so adduser prefers it as the new user's UID.
            # Query the group's actual GID to be robust against it
            # having landed somewhere other than $firstgid.
            my $group_gid = (getgrnam($prefix.$gnames[0]))[2];
            assert_user_has_uid($user, $group_gid);
            # Seed $uidcount for subsequent users: the preferred GID
            # is now taken, so adduser falls back to scanning from
            # $firstuid, skipping anything already occupied.
            $uidcount = $firstuid;
            while (defined(getpwuid($uidcount))) {
                $uidcount++;
            }
        } else {
            assert_user_has_uid($user, $uidcount);
            $uidcount++;
            while (defined(getpwuid($uidcount))) {
                $uidcount++;
            }
        }
        $user_index++;
    }
    assert_command_success('/usr/sbin/adduser', @quiet,
        @sysopt,
        '--comment', '""', '--disabled-password', '--no-create-home',
        @uidopt,
        '--uid', $a->{fuid},
        $prefix.$funame);
    assert_user_exists($prefix.$funame);
    assert_user_has_uid($prefix.$funame, $a->{fuid});
    cleanup($prefix);
}

# assert_usergroup_loop_to_exhaustion: same shape as
# assert_usergroup_loop, but with a uid range and a gid range each
# exactly as wide as @gnames/@unames, so the range fills up exactly
# and one further attempt (creating $gnamex / $unamex) must fail.
# Because the gid range and uid range given here are always
# disjoint by construction, the group's gid is never a candidate
# for the uid search, so (unlike assert_usergroup_loop) every user
# in the loop gets a plain sequential uid, with no preferred-uid
# special case needed for the first one. Used by the
# U2L/U3L/U4L/S2L/S3L/S4L-style tests.
#
# Args (hashref): prefix, system (bool), firstuid, lastuid,
#   firstgid, lastgid, fuid, fgid, via_cli (bool, defaults to true:
#   whether --firstuid/--lastuid/--firstgid/--lastgid are actually
#   passed on the command line, vs. relying on values already
#   present in the config file).
sub assert_usergroup_loop_to_exhaustion {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my $system = $a->{system};
    my $fuid = $a->{fuid};
    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
    my @sysopt = $system ? ('--system') : ();
    my @uidrange = $via_cli ? ('--firstuid', $a->{firstuid}, '--lastuid', $a->{lastuid}) : ();
    my @gidrange = $via_cli ? ('--firstgid', $a->{firstgid}, '--lastgid', $a->{lastgid}) : ();

    my $gidcount = $a->{firstgid};
    foreach my $groupbase ( @gnames ) {
        my $group = $prefix.$groupbase;
        assert_command_success('/usr/sbin/addgroup', @quiet,
            @sysopt, @gidrange,
            $group);
        assert_group_exists($group);
        assert_group_has_gid($group, $gidcount);
        $gidcount++;
    }
    assert_command_failure_silent('/usr/sbin/addgroup', @quiet,
        @sysopt, @gidrange,
        $prefix.$gnamex);
    assert_group_does_not_exist($prefix.$gnamex);
    assert_command_success('/usr/sbin/addgroup', @quiet,
        @sysopt, @gidrange,
        '--gid', $a->{fgid},
        $prefix.$fgname);
    assert_group_exists($prefix.$fgname);
    assert_group_has_gid($prefix.$fgname, $a->{fgid});

    my $uidcount = $a->{firstuid};
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--ingroup', $prefix.$gnames[0],
            '--comment', '""', '--disabled-password', '--no-create-home',
            @uidrange,
            $user);
        assert_user_exists($user);
        assert_primary_group_membership_exists($user, $prefix.$gnames[0]);
        assert_user_has_uid($user, $uidcount);
        $uidcount++;
    }
    assert_command_failure_silent('/usr/sbin/adduser', @quiet,
        @sysopt,
        '--ingroup', $prefix.$gnames[0],
        '--comment', '""', '--disabled-password', '--no-create-home',
        @uidrange,
        $prefix.$unamex);
    assert_user_does_not_exist($prefix.$unamex);
    if ($system) {
        assert_command_success_silent('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            @uidrange,
            '--uid', $fuid,
            $prefix.$funame);
    } else {
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            @uidrange,
            '--uid', $fuid,
            $prefix.$funame);
    }
    assert_user_exists($prefix.$funame);
    assert_user_has_uid($prefix.$funame, $fuid);
    cleanup($prefix);
}

# assert_plain_usergroup_loop: create @unames as plain usergroups
# (one adduser call per name, no --ingroup, no group loop), and
# assert sequential matching uid==gid for each, starting at
# firstuid. By default firstuid is passed via --firstuid on the
# command line; pass a false via_cli to omit the flag and rely on
# whatever FIRST_UID is already in the config file instead (used by
# the U3A2/S3A2-style tests, which deliberately test that a
# leftover config value, with no command-line override at all,
# still produces the expected uid). Used by the
# U2A2/U3A2/U4A2/S2A2/S3A2/S4A2-style tests.
#
# Args (hashref): prefix, system (bool), firstuid, via_cli (bool,
#   defaults to true).
sub assert_plain_usergroup_loop {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my $system = $a->{system};
    my $firstuid = $a->{firstuid};
    my $via_cli = exists($a->{via_cli}) ? $a->{via_cli} : 1;
    my @sysopt = $system ? ('--system') : ();
    my @uidopt = $via_cli ? ('--firstuid', $firstuid) : ();
    my $expected_uidgid = exists($a->{expected_uidgid}) ? $a->{expected_uidgid} : $firstuid;
    assert_command_success("true plain_usergroup_loop firstuid $firstuid expected_uidgid $expected_uidgid");
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            @uidopt,
            $user);
        assert_user_exists($user);
        if ($system) {
            assert_primary_group_membership_exists($user, 'nogroup');
        } else {
            assert_group_exists($user);
            assert_primary_group_membership_exists($user, $user);
            assert_group_has_gid($user, $expected_uidgid);
        }
        assert_user_has_uid($user, $expected_uidgid);
        $expected_uidgid++;
    }
    cleanup($prefix);
}

# assert_usergroup_loop_dynamic: same overall shape as
# assert_usergroup_loop (group loop + --ingroup user loop + one
# fixed-uid user), but for the case where no --firstuid/--firstgid
# is given at all and the config is left at its default: the actual
# first uid/gid assigned is whatever the system picks, captured
# from the first created group/user rather than asserted up front.
# For system users, the default system id ranges are typically
# densely populated by real system accounts, so each subsequent id
# is found by skipping forward past any id that turns out to
# already be taken (mirroring what adduser's own search would do),
# rather than assuming a plain ++ always lands on a free slot. Used
# by the U1A/S1A-style tests.
#
# Args (hashref): prefix, system (bool), fuid, fgid.
sub assert_usergroup_loop_dynamic {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my $system = $a->{system};
    my @sysopt = $system ? ('--system') : ();
    my $gidcount = 0;
    foreach my $groupbase ( @gnames ) {
        my $group = $prefix.$groupbase;
        assert_command_success('/usr/sbin/addgroup', @quiet, @sysopt, $group);
        assert_group_exists($group);
        if ($gidcount == 0) {
            $gidcount = ((getgrnam($group))[2]);
        }
        assert_group_has_gid($group, $gidcount);
        if ($system) {
            while (defined(getgrgid($gidcount))) {
                $gidcount++;
            }
        } else {
            $gidcount++;
        }
    }
    # this is optimistcally assuming that fgid is also free as a uid.
    # we error out if we were too optimistic.
    assert_command_success('/usr/sbin/addgroup', @quiet,
        @sysopt,
        '--gid', $a->{fgid},
        $prefix.$fgname);
    assert_group_exists($prefix.$fgname);
    assert_group_has_gid($prefix.$fgname, $a->{fgid});
    assert_uid_does_not_exist($a->{fgid});

    my $uidcount = 0;
    my $first_group_gid = ((getgrnam($prefix.$gnames[0]))[2]);
    my $user_index = 0;
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--ingroup', $prefix.$fgname,
            '--comment', '""', '--disabled-password', '--no-create-home',
            $user);
        assert_user_exists($user);
        assert_primary_group_membership_exists($user, $prefix.$fgname);
        if ($user_index == 0) {
            # the group's gid is free as a uid and inside the default
            # search range, so it is preferred over the lowest free uid
            assert_user_has_uid($user, $a->{fgid});
        } else {
            if ($uidcount == 0) {
                # capture the second user's real uid: after the first
                # user took the preferred gid, the fallback scan
                # restarts from FIRST_UID, which may not be sequential
                # with respect to the first user's uid at all
                $uidcount = ((getpwnam($user))[2]);
            }
            assert_user_has_uid($user, $uidcount);
            if ($system) {
                while (defined(getpwuid($uidcount))) {
                    $uidcount++;
                }
            } else {
                $uidcount++;
            }
        }
        $user_index++;
    }
    assert_command_success('/usr/sbin/adduser', @quiet,
        @sysopt,
        '--comment', '""', '--disabled-password', '--no-create-home',
        '--uid', $a->{fuid},
        $prefix.$funame);
    assert_user_exists($prefix.$funame);
    assert_user_has_uid($prefix.$funame, $a->{fuid});
    cleanup($prefix);
}


# assert_plain_usergroup_loop_dynamic: dynamic-capture counterpart
# of assert_plain_usergroup_loop, for the case where no --firstuid
# is given at all. Captures the uid and gid of the first created
# user independently (rather than assuming they are equal from the
# start, the way assert_plain_usergroup_loop does), since this
# predates the combined uid/gid search guarantee and was written
# without relying on it. Used by the U1A2/S1A2-style tests.
#
# Args (hashref): prefix, system (bool).
sub assert_plain_usergroup_loop_dynamic {
    my ($a) = @_;
    my $prefix = $a->{prefix};
    my $system = $a->{system};
    my @sysopt = $system ? ('--system') : ();
    my $uidcount = 0;
    my $gidcount = 0;
    foreach my $userbase ( @unames ) {
        my $user = $prefix.$userbase;
        assert_command_success('/usr/sbin/adduser', @quiet,
            @sysopt,
            '--comment', '""', '--disabled-password', '--no-create-home',
            $user);
        assert_user_exists($user);
        if ($uidcount == 0) {
            $uidcount = ((getpwnam($user))[2]);
        }
        if ($system) {
            assert_primary_group_membership_exists($user, 'nogroup');
            assert_user_has_uid($user, $uidcount);
            while (defined(getpwuid($uidcount))) {
                $uidcount++;
            }
        } else {
            assert_group_exists($user);
            if ($gidcount == 0) {
                $gidcount = ((getgrnam($user))[2]);
            }
            assert_primary_group_membership_exists($user, $user);
            assert_user_has_uid($user, $uidcount);
            assert_group_has_gid($user, $gidcount);
            $uidcount++;
            $gidcount++;
        }
    }
    cleanup($prefix);
}

# test group U1A: default config, no command line

my %confhash=();
apply_config_hash(\%confhash);
assert_command_success("true u1a: empty config file");
assert_usergroup_loop_dynamic({prefix => 'u1a', system => 0, fuid => 3750, fgid => 3761});

assert_command_success("true u1a2: empty config file");
assert_plain_usergroup_loop_dynamic({prefix => 'u1a2', system => 0});

# test group U2A: default config, uid/gid range requested on command line

assert_command_success("true u2a: empty config file");
assert_usergroup_loop({prefix => 'u2a', system => 0, firstuid => 6000, firstgid => 6050, fuid => 6900, fgid => 6910});

assert_command_success("true u2a2: empty config file");
assert_plain_usergroup_loop({prefix => 'u2a2', system => 0, firstuid => 2000});

# test group U3A: uid range requested by config, no command line

%confhash=();
$confhash{'FIRST_UID'}="6100";
$confhash{'FIRST_GID'}="6150";
apply_config_hash(\%confhash);
assert_command_success("true u3a: FIRST_UID=6100 FIRST_GID=6150");
assert_usergroup_loop({prefix => 'u3a', system => 0, firstuid => 6100, firstgid => 6150, fuid => 6920, fgid => 6930, via_cli => 0});

assert_command_success("true u3a2: FIRST_UID=6100 FIRST_GID=6150");
assert_plain_usergroup_loop({prefix => 'u3a2', system => 0, firstuid => 6100, expected_uidgid => 6150, via_cli => 0});

# test group U4A: ranges requested by config, overriden by command line

assert_usergroup_loop({prefix => 'u4a', system => 0, firstuid => 6200, firstgid => 6250, fuid => 6940, fgid => 6950});

assert_plain_usergroup_loop({prefix => 'u4a2', system => 0, firstuid => 6200});

%confhash=();
apply_config_hash(\%confhash);

# test group S1A: default config, no command line

%confhash=();
apply_config_hash(\%confhash);
assert_usergroup_loop_dynamic({prefix => 's1a', system => 1, fuid => 200, fgid => 210});

assert_plain_usergroup_loop_dynamic({prefix => 's1a2', system => 1});

# test group S2A: default config, uid/gid range requested on command line

assert_usergroup_loop({prefix => 's2a', system => 1, firstuid => 700, firstgid => 720, fuid => 850, fgid => 860});

assert_plain_usergroup_loop({prefix => 's2a2', system => 1, firstuid => 700});

# test group S3A: uid range requested by config, no command line

%confhash=();
$confhash{'FIRST_SYSTEM_UID'}="730";
$confhash{'FIRST_SYSTEM_GID'}="750";
apply_config_hash(\%confhash);
assert_command_success("true firstsysuid=730 firstsysgid=750");
assert_usergroup_loop({prefix => 's3a', system => 1, firstuid => 730, firstgid => 750, fuid => 870, fgid => 880, via_cli => 0});

assert_plain_usergroup_loop({prefix => 's3a2', system => 1, firstuid => 730, via_cli => 0});

# test group S4A: ranges requested by config, overriden by command line

assert_usergroup_loop({prefix => 's4a', system => 1, firstuid => 760, firstgid => 780, fuid => 890, fgid => 900});

assert_plain_usergroup_loop({prefix => 's4a2', system => 1, firstuid => 760});

%confhash=();
apply_config_hash(\%confhash);


# tests with last_ options: we set a range and fill it, last creation must fail

# test group U1L: not applicable
# test group U2L: default config, uid/gid range requested on command line

assert_usergroup_loop_to_exhaustion({prefix => 'u2l', system => 0, firstuid => 2090, lastuid => 2093, firstgid => 2190, lastgid => 2193, fuid => 3750, fgid => 3761});

# test group U3L: uid range requested by config, no command line

%confhash=();
$confhash{'FIRST_UID'}="2090";
$confhash{'FIRST_GID'}="2190";
$confhash{'LAST_UID'}="2093";
$confhash{'LAST_GID'}="2193";
apply_config_hash(\%confhash);
assert_command_success("true firstuid=2090 firstgid=2190 lastuid=2093 lastgid=2193");
assert_usergroup_loop_to_exhaustion({prefix => 'u3l', system => 0, firstuid => 2090, lastuid => 2093, firstgid => 2190, lastgid => 2193, fuid => 3750, fgid => 3761, via_cli => 0});

# test group U4L: ranges requested by config, overriden by command line

assert_usergroup_loop_to_exhaustion({prefix => 'u4l', system => 0, firstuid => 2290, lastuid => 2293, firstgid => 2390, lastgid => 2393, fuid => 3750, fgid => 3761});

%confhash=();
apply_config_hash(\%confhash);

# test group S1L: not applicable
# test group S2L: default config, uid/gid range requested on command line

assert_usergroup_loop_to_exhaustion({prefix => 's2l', system => 1, firstuid => 400, lastuid => 403, firstgid => 500, lastgid => 503, fuid => 700, fgid => 750});

# test group S3L: uid range requested by config, no command line

%confhash=();
$confhash{'FIRST_SYSTEM_UID'}="410";
$confhash{'FIRST_SYSTEM_GID'}="510";
$confhash{'LAST_SYSTEM_UID'}="413";
$confhash{'LAST_SYSTEM_GID'}="513";
apply_config_hash(\%confhash);
assert_command_success("true firstsysuid=410 firstsysgid=510 lastsysuid=413 lastsysgid=513");
assert_usergroup_loop_to_exhaustion({prefix => 's3l', system => 1, firstuid => 410, lastuid => 413, firstgid => 510, lastgid => 513, fuid => 710, fgid => 760, via_cli => 0});

# test group S4L: ranges requested by config, overriden by command line

assert_usergroup_loop_to_exhaustion({prefix => 's4l', system => 1, firstuid => 320, lastuid => 323, firstgid => 520, lastgid => 523, fuid => 720, fgid => 770});

# ============================================================
# Regression tests for the UID/GID range-intersection bugfix.
#
# first_avail_uid_gid() must search the true intersection of the
# UID and GID ranges (the higher of the two starts, the lower of
# the two ends). Before the fix, the expression computing this
# intersection used ($a, $b)[$a > $b] as a max()/min() idiom; that
# idiom actually indexes element 0 (i.e. returns $a) whenever
# "$a > $b" is false, which is the *lower*, not the higher, value
# in the common case -- so the floor of the search range could end
# up lower than intended, and the ceiling could end up higher than
# intended, whenever the configured UID and GID ranges genuinely
# differed (they are usually equal by default, which is why this
# went unnoticed). This also affected addsysuser, which used two
# completely independent first_avail_uid()/first_avail_gid() calls
# and so never even attempted a combined search.
# ============================================================

# test group U5A: adduser (regular, usergroup case), asymmetric
# uid/gid ranges where the uid range starts lower than the gid
# range and ends lower too. The true intersection floor is
# firstgid (the higher start); a regression of the original bug
# would instead start searching at firstuid, which is lower and
# would be free, producing a UID lower than the asserted value
# (and, since the ceiling logic is also affected, the wrong end
# of the range in general).

%confhash=();
apply_config_hash(\%confhash);
assert_intersection_search_success({prefix => 'u5a', system => 0, firstuid => 4000, lastuid => 4100, firstgid => 4050, lastgid => 4150, expect_first => 4050});

# test group U6A: same as U5A but with the gid range starting
# lower than the uid range, to also exercise the mirror case (the
# bug was not symmetric between the two halves of the expression:
# one half picked the lower start instead of the higher one, the
# other picked the higher end instead of the lower one).

assert_intersection_search_success({prefix => 'u6a', system => 0, firstuid => 4250, lastuid => 4350, firstgid => 4200, lastgid => 4300, expect_first => 4250});

# test group U7A: uid and gid ranges that do not overlap at all
# must fail outright, never silently assign mismatched uid/gid.
# A regression of the original bug would compute a search range
# wider than the true (empty) overlap and could succeed with a
# mismatched uid/gid pair instead of failing.

assert_intersection_search_failure({prefix => 'u7a', system => 0, firstuid => 4400, lastuid => 4410, firstgid => 4500, lastgid => 4510});

# test group S5A: adduser --system --group must apply the same
# combined uid/gid intersection search as the regular usergroup
# case. This was missing entirely before the fix: --system used
# two fully independent searches and could create a system user
# whose uid and gid did not match at all.

assert_intersection_search_success({prefix => 's5a', system => 1, firstuid => 920, lastuid => 930, firstgid => 925, lastgid => 935, expect_first => 925});

# test group S6A: --system --group with non-overlapping uid/gid
# ranges must fail outright, same as the regular-user case.

assert_intersection_search_failure({prefix => 's6a', system => 1, firstuid => 936, lastuid => 940, firstgid => 950, lastgid => 955});

# ============================================================
# Feature tests: --ingroup now prefers the requested group's GID
# as the new user's UID, if that number is also free as a UID,
# instead of unconditionally taking the lowest free UID in range.
# If the GID is not free as a UID (or falls outside the UID
# range), behavior falls back to the previous lowest-free-uid
# scan. This applies to both adduser and adduser --system.
# ============================================================

# test group U8A: adduser --ingroup GROUP, where GROUP's gid is
# free as a uid and is *not* the lowest free uid in the requested
# range (firstuid is deliberately lower and also free) -- so a
# successful match on the gid proves the preferred-id logic fired,
# rather than the scan simply reaching that number first anyway.

assert_preferred_uid({prefix => 'u8a', system => 0, firstuid => 4900, lastuid => 4999, fgid => 4950, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 4950});

# test group U9A: same setup, but the group's gid is already
# taken as a uid by another user, so adduser --ingroup must fall
# back to the lowest free uid in the requested range instead
# (firstuid, since that is free and lower than the occupied gid).

assert_preferred_uid({prefix => 'u9a', system => 0, firstuid => 5000, lastuid => 5099, fgid => 5050, join_by_gid => 0, occupy_gid_as_uid => 1, expect_uid => 5000});

# test group S7A: same as U8A, but for adduser --system --ingroup.

assert_preferred_uid({prefix => 's7a', system => 1, firstuid => 941, lastuid => 950, fgid => 945, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 945});

# test group S8A: same as U9A, but for adduser --system --ingroup.

assert_preferred_uid({prefix => 's8a', system => 1, firstuid => 951, lastuid => 960, fgid => 955, join_by_gid => 0, occupy_gid_as_uid => 1, expect_uid => 951});

# test group S9A: same hit-case as S7A, but using --gid GID (a
# numeric primary group) instead of --ingroup GROUPNAME. addsysuser
# treats $gid_option and $ingroup_name uniformly for the preferred-
# uid branch, but this had no dedicated test: the group's gid is
# free as a uid and inside the search range, and is not the lowest
# free uid in that range, so a match proves the preferred-id logic
# fired for the --gid path specifically, not just --ingroup.

assert_preferred_uid({prefix => 's9a', system => 1, firstuid => 961, lastuid => 970, fgid => 965, join_by_gid => 1, occupy_gid_as_uid => 0, expect_uid => 965});

# test group S10A: same fallback-case as S8A, but using --gid GID
# instead of --ingroup GROUPNAME: the group's gid is already taken
# as a uid by another user, so adduser --system --gid must fall
# back to the lowest free uid in the requested range instead.

assert_preferred_uid({prefix => 's10a', system => 1, firstuid => 971, lastuid => 980, fgid => 975, join_by_gid => 1, occupy_gid_as_uid => 1, expect_uid => 971});

# test group U10A: --ingroup preferred-uid logic must respect the
# requested --firstuid/--lastuid range: if the group's gid lies
# outside that range, it must not be used even though it is free,
# and the lowest free uid inside the range must be picked instead.

assert_preferred_uid({prefix => 'u10a', system => 0, firstuid => 5300, lastuid => 5399, fgid => 5450, join_by_gid => 0, occupy_gid_as_uid => 0, expect_uid => 5300});

done_testing();

# vim: tabstop=4 shiftwidth=4 expandtab
