adonisjs / lucid

AdonisJS SQL ORM. Supports PostgreSQL, MySQL, MSSQL, Redshift, SQLite and many more
https://lucid.adonisjs.com/
MIT License
1.08k stars 195 forks source link

`database.exists` error when login using user created from factory #1044

Closed fzhnf closed 4 months ago

fzhnf commented 4 months ago

Package version

21.1.0

Describe the bug

SUMMARY

Using a factory-created user leads to both validation, unique() and exitst(), works oppositely. In contrast, using user created manually using controller works just fine as expected, register validator has method unique() for email field, and will return false if my argument for email is exist in database. login validator has method exists() for email field, and will return false if my argument for email is not exist in database, but both method is not working as it supposed to if the user is created using factory.

STEPS TO REPRODUCE

  1. run node ace migration:refresh --seed

  2. copy password from factory

  3. copy the generated email from db or print the user value from seeder file

    const user = await UserFactory.createMany(3)
    console.log(user)
  4. use http method to login using created email and password, example using httpie from cli
    http POST localhost:3333/api/auth/login email=Oda_Zemlak@gmail.com password=12341234 for login, try this first http POST localhost:3333/api/auth/register fullName=fzhnf email=Oda_Zemlak@gmail.com password=12341234 password_confirmation=12341234 for register, try this second

OBSERVED RESULT

login

HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Date: Fri, 19 Jul 2024 20:00:01 GMT
Keep-Alive: timeout=5
content-length: 97
content-type: application/json; charset=utf-8

{
    "errors": [
        {
            "field": "email",
            "message": "The selected email is invalid",
            "rule": "database.exists"
        }
    ]
}

in result, the user do not get the authentication token event though the email with the same value is exist in the database.


register

HTTP/1.1 200 OK
Connection: keep-alive
Date: Fri, 19 Jul 2024 20:07:19 GMT
Keep-Alive: timeout=5
content-length: 186
content-type: application/json; charset=utf-8

{
    "abilities": [
        "*"
    ],
    "expiresAt": "2024-07-20T20:07:19.528Z",
    "lastUsedAt": null,
    "name": null,
    "token": "oat_MQ.R2p3amFzNU13Umtwc3dQWU4tUmxnTVVGMTVnUUtURGF4b0tQTkNZUjI0NzA0MTQy",
    "type": "bearer"
}

in result, the email is inserted into database, making the column has two email with same exact value

EXPECTED RESULT

login

❯ http POST localhost:3333/api/auth/login email=tima@gmail.com password=12341234
HTTP/1.1 200 OK
Connection: keep-alive
Date: Fri, 19 Jul 2024 20:11:09 GMT
Keep-Alive: timeout=5
content-length: 186
content-type: application/json; charset=utf-8

{
    "abilities": [
        "*"
    ],
    "expiresAt": "2024-07-20T20:11:09.677Z",
    "lastUsedAt": null,
    "name": null,
    "token": "oat_NA.a1M1YW5mZW1rLUVyaWRGdmJxdi1CWktNTmkzTFE0S3pVcEtJUGJUSTk5OTkzODY1",
    "type": "bearer"
}

in result, the user is get the authentication token because the email with the same value is exist in the database.


register

❯ http POST localhost:3333/api/auth/register fullName=fzhnf email=tima@gmail.com password=12341234 password_confirmation
=12341234
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Date: Fri, 19 Jul 2024 20:10:45 GMT
Keep-Alive: timeout=5
content-length: 97
content-type: application/json; charset=utf-8

{
    "errors": [
        {
            "field": "email",
            "message": "The selected email is invalid",
            "rule": "database.exists"
        }
    ]
}

in result, the user is not inserted into database because the email with the same value is exist in the database.

Reproduction repo

https://github.com/fzhnf/test-adonis

fzhnf commented 4 months ago

i figured the problem was that i don't implement vinejs normalizeEmail to faker.internet.email(), but i don't know how to do that

fzhnf commented 4 months ago

I finally figured out what's the real problem and was able to solve it myself after 5 days, lol. I'm sorry for my lack of knowledge.

TL;DR

Here is my solution: faker emails will be modified by the validator's normalizeEmail method inside userFactory.

import User from "#models/user";
import factory from "@adonisjs/lucid/factories";
import validator from "validator";

export const UserFactory = factory
    .define(User, async ({ faker }) => {
        const email = faker.internet.email();
        const normalizedEmail = validator.normalizeEmail(email) || email; // Fallback to original email if normalization fails
        return {
            username: faker.internet.userName(),
            fullName: faker.person.fullName(),
            email: normalizedEmail,
            password: "12341234",
        };
    })
    .build();
Details My problem was because the email generated from faker doesn't meet the vinejs `normalizeEmail()` rule standard, which if you are not normalizing it first, you will be dealing with two versions of the same email in your database, and that's the case in my development because I don't use the vinejs normalizeEmail method on my seeder. Making the email with all the features that don't meet the `normalizeEmail` rules is just not going to be found by my validator that is using the `normalizeEmail` method.  #### Here's an example: 1. If I'm seeding with an email like this: `gabe.bEAhan@hotmail.com` Since I don't normalize it before running the seeder, then it will be saved inside my database just like so. 2. Then, when I'm trying to login with the same email, my email value will be modified by the validator into something like this: `gabe.beahan@hotmail.com` which is not the same inside the database. 3. And so if I'm trying to login, I cannot access the email because the query value doesn't match, and when I register, it will be creating a new user with the same email but with the normalized version. ### Resolution My close-to-solution attempt for this is figuring out how to get the email normalized in the factory. At first, I found out that inside vinejs's `normalizeEmail` method is using the `normalizeEmail` method from the validator.js library as described on the [docs](https://vinejs.dev/docs/types/string#normalizeemail). The method from validator returns the type'string | undefined`, which means I can use it for my user factory that is also getting string from faker. ```ts import validator from "validator"; // <-- add validator export const UserFactory = factory     .define(User, async ({ faker }) => {         return {             username: faker.internet.userName(),             fullName: faker.person.fullName(),             email: validator.normalizeEmail(faker.internet.email(),             password: "12341234",         };     })     .build(); ```    but it doesn't work and so many errors coming from my lsp, and so i think it's because `faker.internet.email()` returns a string, but `validator.normalizeEmail()` returns `string|undefined`, and to remove that undefined, i'm returning the original email as a fallback so that the method always returns a string. Here is my best solution:   ```typescript import User from "#models/user"; import factory from "@adonisjs/lucid/factories"; import validator from "validator"; export const UserFactory = factory     .define(User, async ({ faker }) => {         const email = faker.internet.email();         const normalizedEmail = validator.normalizeEmail(email) || email; // Fallback to original email if normalization fails         return {             username: faker.internet.userName(),             fullName: faker.person.fullName(),             email: normalizedEmail,             password: "12341234",         };     })     .build(); ```