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

daycry / auth / 16344800892

17 Jul 2025 12:13PM UTC coverage: 66.494% (+6.6%) from 59.854%
16344800892

push

github

daycry
Add comprehensive unit tests for helpers and services

This commit introduces extensive unit tests for helper functions (auth, checkEndpoint, checkIp, email), libraries (CheckIpInRange, Logger), and service classes (AttemptHandler, ExceptionHandler, RequestLogger). Also fixes ReflectionProperty usage in ExceptionHandler to correctly pass the exception object. These tests improve code coverage and ensure reliability of authentication, endpoint, IP checking, email, logging, and exception handling features.

1 of 1 new or added line in 1 file covered. (100.0%)

136 existing lines in 8 files now uncovered.

2177 of 3274 relevant lines covered (66.49%)

32.78 hits per line

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

0.0
/src/Controllers/MagicLinkController.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\Controllers;
15

16
use CodeIgniter\Events\Events;
17
use CodeIgniter\HTTP\IncomingRequest;
18
use CodeIgniter\HTTP\RedirectResponse;
19
use CodeIgniter\HTTP\ResponseInterface;
20
use CodeIgniter\I18n\Time;
21
use Daycry\Auth\Authentication\Authenticators\Session;
22
use Daycry\Auth\Models\LoginModel;
23
use Daycry\Auth\Models\UserIdentityModel;
24
use Daycry\Auth\Models\UserModel;
25

26
/**
27
 * Handles "Magic Link" logins - an email-based
28
 * no-password login protocol. This works much
29
 * like password reset would, but Shield provides
30
 * this in place of password reset. It can also
31
 * be used on it's own without an email/password
32
 * login strategy.
33
 */
34
class MagicLinkController extends BaseAuthController
35
{
36
    /**
37
     * @var UserModel
38
     */
39
    protected $provider;
40

41
    public function __construct()
42
    {
43
        /** @var class-string<UserModel> $providerClass */
UNCOV
44
        $providerClass = setting('Auth.userProvider');
×
45

UNCOV
46
        $this->provider = new $providerClass();
×
47
    }
48

49
    /**
50
     * Displays the view to enter their email address
51
     * so an email can be sent to them.
52
     */
53
    public function loginView(): ResponseInterface
54
    {
UNCOV
55
        if (! setting('Auth.allowMagicLinkLogins')) {
×
UNCOV
56
            return $this->handleError(
×
UNCOV
57
                config('Auth')->loginRoute(),
×
UNCOV
58
                lang('Auth.magicLinkDisabled'),
×
UNCOV
59
            );
×
60
        }
61

UNCOV
62
        if ($redirect = $this->redirectIfLoggedIn()) {
×
63
            return $redirect;
×
64
        }
65

UNCOV
66
        $content = $this->view(setting('Auth.views')['magic-link-login']);
×
67

68
        return $this->response->setBody($content);
×
69
    }
70

71
    /**
72
     * Receives the email from the user, creates the hash
73
     * to a user identity, and sends an email to the given
74
     * email address.
75
     */
76
    public function loginAction(): RedirectResponse
77
    {
UNCOV
78
        if (! setting('Auth.allowMagicLinkLogins')) {
×
UNCOV
79
            return $this->handleError(
×
UNCOV
80
                config('Auth')->loginRoute(),
×
UNCOV
81
                lang('Auth.magicLinkDisabled'),
×
UNCOV
82
            );
×
83
        }
84

85
        // Validate email format
UNCOV
86
        $rules    = $this->getValidationRules();
×
UNCOV
87
        $postData = $this->request->getPost();
×
88

89
        if (! $this->validateRequest($postData, $rules)) {
×
90
            return $this->handleValidationError('magic-link');
×
91
        }
92

93
        // Check if the user exists
94
        $email = $this->request->getPost('email');
×
95
        $user  = $this->provider->findByCredentials(['email' => $email]);
×
96

97
        if ($user === null) {
×
98
            return $this->handleError('magic-link', lang('Auth.invalidEmail'));
×
99
        }
100

101
        /** @var UserIdentityModel $identityModel */
102
        $identityModel = model(UserIdentityModel::class);
×
103

104
        // Delete any previous magic-link identities
105
        $identityModel->deleteIdentitiesByType($user, Session::ID_TYPE_MAGIC_LINK);
×
106

107
        // Generate the code and save it as an identity
108
        helper('text');
×
109
        $token = random_string('crypto', 20);
×
110

111
        $identityModel->insert([
×
112
            'user_id' => $user->id,
×
113
            'type'    => Session::ID_TYPE_MAGIC_LINK,
×
114
            'secret'  => $token,
×
115
            'expires' => Time::now()->addSeconds(setting('Auth.magicLinkLifetime'))->format('Y-m-d H:i:s'),
×
116
        ]);
×
117

118
        /** @var IncomingRequest $request */
119
        $request = service('request');
×
120

121
        $ipAddress = $request->getIPAddress();
×
122
        $userAgent = (string) $request->getUserAgent();
×
123
        $date      = Time::now()->toDateTimeString();
×
124

125
        // Send the user an email with the code
126
        helper('email');
×
127
        $email = emailer()->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
×
128
        $email->setTo($user->email);
×
129
        $email->setSubject(lang('Auth.magicLinkSubject'));
×
130
        $email->setMessage($this->view(setting('Auth.views')['magic-link-email'], [
×
UNCOV
131
            'token'     => $token,
×
132
            'ipAddress' => $ipAddress,
×
133
            'userAgent' => $userAgent,
×
UNCOV
134
            'date'      => $date,
×
135
        ]));
×
136

UNCOV
137
        if ($email->send(false) === false) {
×
UNCOV
138
            log_message('error', $email->printDebugger(['headers']));
×
139

UNCOV
140
            return $this->handleError('magic-link', lang('Auth.unableSendEmailToUser', [$user->email]));
×
141
        }
142

143
        // Clear the email
UNCOV
144
        $email->clear();
×
145

146
        // Redirect to message page instead of returning the view directly
UNCOV
147
        return redirect()->route('magic-link-message');
×
148
    }
149

150
    /**
151
     * Display the "What's happening/next" message to the user.
152
     */
153
    protected function displayMessage(): string
154
    {
UNCOV
155
        return $this->view(setting('Auth.views')['magic-link-message']);
×
156
    }
157

158
    /**
159
     * Shows the message view (public route)
160
     */
161
    public function messageView(): ResponseInterface
162
    {
UNCOV
163
        $content = $this->view(setting('Auth.views')['magic-link-message']);
×
164

UNCOV
165
        return $this->response->setBody($content);
×
166
    }
167

168
    /**
169
     * Handles the GET request from the email
170
     */
171
    public function verify(): RedirectResponse
172
    {
UNCOV
173
        if (! setting('Auth.allowMagicLinkLogins')) {
×
174
            return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled'));
×
175
        }
176

177
        $token = $this->request->getGet('token');
×
178

179
        /** @var UserIdentityModel $identityModel */
UNCOV
180
        $identityModel = model(UserIdentityModel::class);
×
181

UNCOV
182
        $identity = $identityModel->getIdentityBySecret(Session::ID_TYPE_MAGIC_LINK, $token);
×
183

184
        $identifier = $token ?? '';
×
185

186
        // No token found?
187
        if ($identity === null) {
×
188
            $this->recordLoginAttempt($identifier, false);
×
189

190
            $credentials = ['magicLinkToken' => $token];
×
UNCOV
191
            Events::trigger('failedLogin', $credentials);
×
192

UNCOV
193
            return redirect()->route('magic-link')->with('error', lang('Auth.magicTokenNotFound'));
×
194
        }
195

196
        // Delete the db entry so it cannot be used again.
197
        $identityModel->delete($identity->id);
×
198

199
        // Token expired?
UNCOV
200
        if (Time::now()->isAfter($identity->expires)) {
×
UNCOV
201
            $this->recordLoginAttempt($identifier, false);
×
202

UNCOV
203
            $credentials = ['magicLinkToken' => $token];
×
204
            Events::trigger('failedLogin', $credentials);
×
205

206
            return redirect()->route('magic-link')->with('error', lang('Auth.magicLinkExpired'));
×
207
        }
208

209
        /** @var Session $authenticator */
210
        $authenticator = auth('session')->getAuthenticator();
×
211

212
        // If an action has been defined
UNCOV
213
        if ($authenticator->hasAction($identity->user_id)) {
×
UNCOV
214
            return redirect()->route('auth-action-show')->with('error', lang('Auth.needActivate'));
×
215
        }
216

217
        // Log the user in
UNCOV
218
        $authenticator->loginById($identity->user_id);
×
219

UNCOV
220
        $user = $authenticator->getUser();
×
221

UNCOV
222
        $this->recordLoginAttempt($identifier, true, $user->id);
×
223

224
        // Give the developer a way to know the user
225
        // logged in via a magic link.
UNCOV
226
        session()->setTempdata('magicLogin', true);
×
227

UNCOV
228
        Events::trigger('magicLogin');
×
229

230
        // Get our login redirect url
231
        return redirect()->to(config('Auth')->loginRedirect());
×
232
    }
233

234
    /**
235
     * @param int|string|null $userId
236
     */
237
    private function recordLoginAttempt(
238
        string $identifier,
239
        bool $success,
240
        $userId = null,
241
    ): void {
242
        /** @var LoginModel $loginModel */
UNCOV
243
        $loginModel = model(LoginModel::class);
×
244

UNCOV
245
        $loginModel->recordLoginAttempt(
×
UNCOV
246
            Session::ID_TYPE_MAGIC_LINK,
×
247
            $identifier,
×
248
            $success,
×
249
            $this->request->getIPAddress(),
×
UNCOV
250
            (string) $this->request->getUserAgent(),
×
UNCOV
251
            $userId,
×
UNCOV
252
        );
×
253
    }
254

255
    /**
256
     * Returns the rules that should be used for validation.
257
     *
258
     * @return         array<string, array<string, list<string>|string>>
259
     * @phpstan-return array<string, array<string, string|list<string>>>
260
     */
261
    protected function getValidationRules(): array
262
    {
UNCOV
263
        return [
×
UNCOV
264
            'email' => config('Auth')->emailValidationRules,
×
UNCOV
265
        ];
×
266
    }
267
}
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