• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 25552335000

08 May 2026 11:01AM UTC coverage: 71.592% (+12.6%) from 59.035%
25552335000

push

github

daycry
tests: bring coverage from 58% to 71% with 180 new tests

Adds 22 new test files covering services, models, filters, traits, commands,
exceptions, and password validators. Test count goes from 732 to 912.

Bug fixes uncovered while writing tests:
- Totp2FA::verifyCodeForUser: was passing the encrypted+base64 secret to
  TOTP::verify, which then tried to base32-decode binary garbage. Now uses
  the user's getTotpSecret() so the decrypted base32 secret reaches verify.
- HasAccessTokens trait: accessTokens() and revokeAllAccessTokens() called
  non-existent plural method names on UserIdentityModel; refactored the
  trait to delegate to AccessTokenRepository (the architectural direction).
- PasswordChangeRecorder::stampChangedAt: silently dropped the timestamp
  because password_changed_at isn't in UserModel::$allowedFields. Switched
  to a direct query builder update.
- Five admin commands (audit, sessions, tokens, totp, gdpr): used the static
  CLI::write/error/getOption helpers, which bypass the MockInputOutput
  interceptor used by tests. Switched to instance helpers and \$params.

Baseline regenerated to absorb the new (and legitimate) deprecated-method
and internal-helper warnings raised by tests that intentionally exercise
deprecated APIs and the BaseCommand setInputOutput hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

59 of 69 new or added lines in 8 files covered. (85.51%)

1 existing line in 1 file now uncovered.

4453 of 6220 relevant lines covered (71.59%)

62.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.24
/src/Commands/AuditCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Commands;
15

16
use CodeIgniter\CLI\CLI;
17
use CodeIgniter\I18n\Time;
18
use Daycry\Auth\Models\AuditLogModel;
19
use Daycry\Auth\Models\UserModel;
20
use InvalidArgumentException;
21
use Throwable;
22

23
/**
24
 * Reads recent entries from the audit log table.
25
 *
26
 * Usage:
27
 *   php spark auth:audit
28
 *   php spark auth:audit --since=24h
29
 *   php spark auth:audit --user=user@example.com
30
 *   php spark auth:audit --type=totp.enabled
31
 *   php spark auth:audit --limit=50
32
 */
33
class AuditCommand extends BaseCommand
34
{
35
    protected $name        = 'auth:audit';
36
    protected $description = 'Show recent entries from the audit log.';
37
    protected $usage       = 'auth:audit [--since=24h] [--user=<email>] [--type=<event_type>] [--limit=<n>]';
38

39
    /**
40
     * @var array<string, string>
41
     */
42
    protected $options = [
43
        '--since' => 'Time window, e.g. 24h, 7d, 30d (default: 7d).',
44
        '--user'  => 'Filter by user email.',
45
        '--type'  => 'Filter by event_type (e.g. totp.enabled).',
46
        '--limit' => 'Maximum number of rows to display (default: 100, max: 500).',
47
    ];
48

49
    public function run(array $params): int
7✔
50
    {
51
        $since = (string) ($params['since'] ?? '7d');
7✔
52
        $email = (string) ($params['user'] ?? '');
7✔
53
        $type  = (string) ($params['type'] ?? '');
7✔
54
        $limit = max(1, min(500, (int) ($params['limit'] ?? 100)));
7✔
55

56
        try {
57
            $cutoff = $this->parseSince($since);
7✔
58
        } catch (Throwable $e) {
1✔
59
            $this->error('Invalid --since value: ' . $e->getMessage());
1✔
60

61
            return 1;
1✔
62
        }
63

64
        try {
65
            /** @var AuditLogModel $auditModel */
66
            $auditModel = model(AuditLogModel::class);
6✔
67

68
            $builder = $auditModel
6✔
69
                ->where('created_at >=', $cutoff->toDateTimeString())
6✔
70
                ->orderBy('id', 'DESC')
6✔
71
                ->limit($limit);
6✔
72

73
            if ($type !== '') {
6✔
74
                $builder = $builder->where('event_type', $type);
1✔
75
            }
76

77
            if ($email !== '') {
6✔
78
                /** @var UserModel $userModel */
79
                $userModel = model(UserModel::class);
2✔
80
                $user      = $userModel->findByCredentials(['email' => $email]);
2✔
81

82
                if ($user === null) {
2✔
83
                    $this->error('User not found: ' . $email);
1✔
84

85
                    return 1;
1✔
86
                }
87

88
                $builder = $builder->where('user_id', $user->id);
1✔
89
            }
90

91
            $rows = $builder->find();
5✔
92
        } catch (Throwable $e) {
×
NEW
93
            $this->error('Audit query failed: ' . $e->getMessage());
×
94

95
            return 1;
×
96
        }
97

98
        if ($rows === []) {
5✔
99
            $this->write('No audit entries match the filters.', 'yellow');
1✔
100

101
            return 0;
1✔
102
        }
103

104
        $head = ['ID', 'When', 'Event', 'User', 'IP', 'Metadata'];
4✔
105
        $body = [];
4✔
106

107
        foreach ($rows as $row) {
4✔
108
            $userId  = is_object($row) ? ($row->user_id ?? null) : ($row['user_id'] ?? null);
4✔
109
            $event   = is_object($row) ? ($row->event_type ?? '') : ($row['event_type'] ?? '');
4✔
110
            $created = is_object($row) ? ($row->created_at ?? '') : ($row['created_at'] ?? '');
4✔
111
            $ip      = is_object($row) ? ($row->ip_address ?? '') : ($row['ip_address'] ?? '');
4✔
112
            $rawMeta = is_object($row) ? ($row->metadata ?? null) : ($row['metadata'] ?? null);
4✔
113
            $rowId   = is_object($row) ? ($row->id ?? '') : ($row['id'] ?? '');
4✔
114

115
            $body[] = [
4✔
116
                (string) $rowId,
4✔
117
                (string) $created,
4✔
118
                (string) $event,
4✔
119
                $userId === null ? '' : (string) $userId,
4✔
120
                (string) $ip,
4✔
121
                is_string($rawMeta) ? $this->shortMeta($rawMeta) : '',
4✔
122
            ];
4✔
123
        }
124

125
        CLI::table($body, $head);
4✔
126

127
        return 0;
4✔
128
    }
129

130
    /**
131
     * Parses values like `24h`, `7d`, `30d`, `1w` into an absolute Time.
132
     */
133
    private function parseSince(string $expr): Time
7✔
134
    {
135
        $expr = strtolower(trim($expr));
7✔
136

137
        if ($expr === '' || ! preg_match('/^(\d+)([smhdw])$/', $expr, $m)) {
7✔
138
            throw new InvalidArgumentException("expected NNs|m|h|d|w, got '{$expr}'");
1✔
139
        }
140

141
        $n   = (int) $m[1];
6✔
142
        $now = Time::now();
6✔
143

144
        return match ($m[2]) {
6✔
145
            's'     => $now->subSeconds($n),
1✔
146
            'm'     => $now->subMinutes($n),
1✔
147
            'h'     => $now->subHours($n),
1✔
148
            'd'     => $now->subDays($n),
6✔
149
            'w'     => $now->subDays($n * 7),
1✔
150
            default => $now->subDays(7),
6✔
151
        };
6✔
152
    }
153

154
    /**
155
     * Truncates a metadata JSON string to a single line for tabular display.
156
     */
157
    private function shortMeta(string $json): string
×
158
    {
159
        $compact = preg_replace('/\s+/', ' ', $json) ?? $json;
×
160

161
        return mb_strlen($compact) > 60
×
162
            ? mb_substr($compact, 0, 57) . '...'
×
163
            : $compact;
×
164
    }
165
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc