From ae8f4dc3793634a9ebc6b15cf19d35068404baa8 Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 13:44:39 -0300 Subject: [PATCH 1/8] feat(db): adiciona campos de tecnologias e nivel ao schema de usuario --- backend/drizzle/0005_heavy_la_nuit.sql | 12 + backend/drizzle/meta/0005_snapshot.json | 611 ++++++++++++++++++++++++ backend/drizzle/meta/_journal.json | 7 + backend/src/db/schema/users.ts | 5 + 4 files changed, 635 insertions(+) create mode 100644 backend/drizzle/0005_heavy_la_nuit.sql create mode 100644 backend/drizzle/meta/0005_snapshot.json diff --git a/backend/drizzle/0005_heavy_la_nuit.sql b/backend/drizzle/0005_heavy_la_nuit.sql new file mode 100644 index 0000000..85bb58c --- /dev/null +++ b/backend/drizzle/0005_heavy_la_nuit.sql @@ -0,0 +1,12 @@ +CREATE TABLE "keywords" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "keyword" text NOT NULL, + "source" text DEFAULT 'user' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "phone" varchar(20);--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "cpf" varchar(14);--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "technologies" text[] DEFAULT '{}';--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "level" varchar(50);--> statement-breakpoint +CREATE UNIQUE INDEX "keywords_keyword_unique" ON "keywords" USING btree ("keyword"); \ No newline at end of file diff --git a/backend/drizzle/meta/0005_snapshot.json b/backend/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..09d6839 --- /dev/null +++ b/backend/drizzle/meta/0005_snapshot.json @@ -0,0 +1,611 @@ +{ + "id": "d46d415d-9787-44ad-962a-a2405d5896e8", + "prevId": "5f2483f7-3b14-415f-93bc-c9a77d90b2b1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_unique": { + "name": "accounts_provider_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credentials_user_id_unique": { + "name": "credentials_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "credentials_email_unique": { + "name": "credentials_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keywords": { + "name": "keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keywords_keyword_unique": { + "name": "keywords_keyword_unique", + "columns": [ + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_jobs": { + "name": "saved_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_link": { + "name": "job_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'saved'" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "saved_jobs_user_id_users_id_fk": { + "name": "saved_jobs_user_id_users_id_fk", + "tableFrom": "saved_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keywords": { + "name": "keywords", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "search_location": { + "name": "search_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "search_language": { + "name": "search_language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "remote_only": { + "name": "remote_only", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "job_types": { + "name": "job_types", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(14)", + "primaryKey": false, + "notNull": false + }, + "technologies": { + "name": "technologies", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "level": { + "name": "level", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index 10b0317..a69659f 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1780927293836, "tag": "0004_small_psylocke", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1781886747704, + "tag": "0005_heavy_la_nuit", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/schema/users.ts b/backend/src/db/schema/users.ts index 6a73c44..26d917a 100644 --- a/backend/src/db/schema/users.ts +++ b/backend/src/db/schema/users.ts @@ -5,6 +5,7 @@ import { timestamp, uniqueIndex, uuid, + varchar, } from "drizzle-orm/pg-core"; import { InferInsertModel, InferSelectModel } from "drizzle-orm"; @@ -24,6 +25,10 @@ export const users = pgTable( emailVerified: boolean("email_verified").default(false).notNull(), avatarUrl: text("avatar_url"), + phone: varchar("phone", { length: 20 }), + cpf: varchar("cpf", { length: 14 }), + technologies: text("technologies").array().default([]), + level: varchar("level", { length: 50 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), From f739b45be98c3b2eaa36252c1763f50f5a13ce63 Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 13:44:40 -0300 Subject: [PATCH 2/8] feat(api): processa campos de tecnologias e nivel no registro --- backend/src/modules/auth/credentials.service.ts | 13 +++++++++++-- backend/src/modules/types/credentials.types.ts | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/auth/credentials.service.ts b/backend/src/modules/auth/credentials.service.ts index ca69b73..1375770 100644 --- a/backend/src/modules/auth/credentials.service.ts +++ b/backend/src/modules/auth/credentials.service.ts @@ -32,7 +32,7 @@ export class CredentialsService { async register( input: RegisterInput, ): Promise<{ user: User; session: Session }> { - const { email, password, name } = RegisterSchema.parse(input); + const { email, password, name, phone, cpf, technologies, level } = RegisterSchema.parse(input); const existingCredential = await db.query.credentials.findFirst({ where: eq(credentials.email, email), @@ -49,7 +49,16 @@ export class CredentialsService { const [user] = await db .insert(users) - .values({ email, displayName: name, username, emailVerified: false }) + .values({ + email, + displayName: name, + username, + emailVerified: false, + phone, + cpf, + technologies, + level + }) .returning(); await db.insert(credentials).values({ userId: user.id, email, passwordHash }); diff --git a/backend/src/modules/types/credentials.types.ts b/backend/src/modules/types/credentials.types.ts index 92717e5..1ba8f15 100644 --- a/backend/src/modules/types/credentials.types.ts +++ b/backend/src/modules/types/credentials.types.ts @@ -4,6 +4,10 @@ export const RegisterSchema = z.object({ email: z.string().email("Email inválido"), password: z.string().min(8, "Senha deve ter no mínimo 8 caracteres"), name: z.string().min(1, "Nome é obrigatório").optional(), + phone: z.string().optional(), + cpf: z.string().optional(), + technologies: z.array(z.string()).optional(), + level: z.string().optional(), }); export const LoginSchema = z.object({ From 1f97fba483dd7bb1819f06e920df48c7ee5922dd Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 13:44:41 -0300 Subject: [PATCH 3/8] feat(ui): adiciona selecao de nivel e tags de tecnologias no registro --- .../src/components/login/RegisterSide.tsx | 68 +++++++++++++++++++ frontend/src/services/authService.ts | 4 ++ 2 files changed, 72 insertions(+) diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index 911cf9d..cc8e457 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { register, getGoogleAuthUrl } from "@/services/authService"; +import { Badge } from "@/components/ui/badge"; import { Image } from "@unpic/react"; import { motion } from "framer-motion"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; @@ -19,6 +20,13 @@ const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => { }; }); +const POPULAR_TECHNOLOGIES = [ + "React", "Node.js", "Python", "Java", "TypeScript", + "Vue.js", "Angular", "Go", "Ruby", "C#", + "PHP", "Docker", "AWS", "SQL", "MongoDB", + "Flutter", "React Native" +]; + function StarsBackground() { return (
@@ -55,12 +63,25 @@ export default function RegisterSide() { const [telefone, setTelefone] = useState(""); const [password, setPassword] = useState(""); const [cpf, setCpf] = useState(""); + const [level, setLevel] = useState(""); + const [selectedTechnologies, setSelectedTechnologies] = useState([]); + + const toggleTechnology = (tech: string) => { + if (isLoading) return; + setSelectedTechnologies(prev => + prev.includes(tech) + ? prev.filter(t => t !== tech) + : [...prev, tech] + ); + }; const [nomeError, setNomeError] = useState(""); const [emailError, setEmailError] = useState(""); const [telefoneError, setTelefoneError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [cpfError, setCpfError] = useState(""); + const [levelError, setLevelError] = useState(""); + const [technologiesError, setTechnologiesError] = useState(""); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(""); @@ -92,6 +113,8 @@ export default function RegisterSide() { setTelefoneError(""); setPasswordError(""); setCpfError(""); + setLevelError(""); + setTechnologiesError(""); setApiError(""); let isValid = true; @@ -130,6 +153,16 @@ export default function RegisterSide() { isValid = false; } + if (!level) { + setLevelError("A seleção de nível é obrigatória."); + isValid = false; + } + + if (selectedTechnologies.length === 0) { + setTechnologiesError("Selecione ao menos uma tecnologia."); + isValid = false; + } + if (isValid) { setIsLoading(true); try { @@ -137,6 +170,10 @@ export default function RegisterSide() { email: email, password: password, name: nome, + phone: telefone, + cpf: cpf, + level: level, + technologies: selectedTechnologies, }); window.location.href = "/login?registered=true"; } catch (error: any) { @@ -213,6 +250,37 @@ export default function RegisterSide() { setCpf(formatCpf(e.target.value))} placeholder="091.000.000-00" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {cpfError &&

{cpfError}

}
+
+ + + {levelError &&

{levelError}

} +
+
+ +
+ {POPULAR_TECHNOLOGIES.map((tech) => { + const isSelected = selectedTechnologies.includes(tech); + return ( + toggleTechnology(tech)} + > + {tech} + + ); + })} +
+ {technologiesError &&

{technologiesError}

} +
{isLoading ? "Cadastrando..." : "Cadastrar"} diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 7c8365e..5281c39 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -53,6 +53,8 @@ export interface RegisterData { name: string; phone?: string; cpf?: string; + technologies?: string[]; + level?: string; } export async function login(credentials: LoginCredentials) { @@ -82,6 +84,8 @@ export async function register(userData: RegisterData) { name: userData.name, phone: userData.phone, cpf: userData.cpf, + technologies: userData.technologies, + level: userData.level, }), credentials: "include", }); From eaa6229fb52cab20f930089df6b1cdfa6ff5992c Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 13:44:43 -0300 Subject: [PATCH 4/8] test(ui): atualiza testes de registro com os novos campos obrigatorios --- .../unit/components/login/RegisterSide.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/tests/unit/components/login/RegisterSide.test.tsx b/frontend/tests/unit/components/login/RegisterSide.test.tsx index 184be00..af01eb9 100644 --- a/frontend/tests/unit/components/login/RegisterSide.test.tsx +++ b/frontend/tests/unit/components/login/RegisterSide.test.tsx @@ -67,6 +67,8 @@ describe("RegisterSide", () => { expect(await screen.findByText(/campo de e-mail é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de telefone é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de senha é obrigatório/i)).toBeInTheDocument(); + expect(await screen.findByText(/seleção de nível é obrigatória/i)).toBeInTheDocument(); + expect(await screen.findByText(/selecione ao menos uma tecnologia/i)).toBeInTheDocument(); }); it("valida CPF inválido quando preenchido", async () => { @@ -76,6 +78,8 @@ describe("RegisterSide", () => { fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "123" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/cpf inválido/i)).toBeInTheDocument(); }); @@ -87,12 +91,18 @@ describe("RegisterSide", () => { fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); await waitFor(() => { expect(mockRegister).toHaveBeenCalledWith({ email: "bene@teste.com", password: "123456", name: "Bene", + phone: "+5534999999999", + cpf: "", + level: "Júnior", + technologies: ["React"], }); }); expect(window.location.href).toBe("/login?registered=true"); @@ -105,6 +115,8 @@ describe("RegisterSide", () => { fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/Email já cadastrado/i)).toBeInTheDocument(); }); @@ -116,6 +128,8 @@ describe("RegisterSide", () => { fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByRole("button", { name: /cadastrando\.\.\./i })).toBeDisabled(); }); @@ -140,12 +154,15 @@ describe("RegisterSide", () => { fireEvent.change(emailInput, { target: { value: "bene@teste.com" } }); fireEvent.change(telefoneInput, { target: { value: "+5534999999999" } }); fireEvent.change(passwordInput, { target: { value: "123456" } }); + fireEvent.change(screen.getByLabelText(/nível/i), { target: { value: "Júnior" } }); + fireEvent.click(screen.getByText("React")); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); await waitFor(() => { expect(nomeInput).toBeDisabled(); expect(emailInput).toBeDisabled(); expect(telefoneInput).toBeDisabled(); expect(passwordInput).toBeDisabled(); + expect(screen.getByLabelText(/nível/i)).toBeDisabled(); }); }); }); \ No newline at end of file From 382d3218f8ada90d3e797e197570866f96a724db Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 16:12:23 -0300 Subject: [PATCH 5/8] feat(db): adiciona campo de role ao schema de usuario --- backend/drizzle/0006_happy_gateway.sql | 1 + backend/drizzle/meta/0006_snapshot.json | 618 ++++++++++++++++++++++++ backend/drizzle/meta/_journal.json | 7 + backend/src/db/schema/users.ts | 1 + 4 files changed, 627 insertions(+) create mode 100644 backend/drizzle/0006_happy_gateway.sql create mode 100644 backend/drizzle/meta/0006_snapshot.json diff --git a/backend/drizzle/0006_happy_gateway.sql b/backend/drizzle/0006_happy_gateway.sql new file mode 100644 index 0000000..89c1853 --- /dev/null +++ b/backend/drizzle/0006_happy_gateway.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "role" varchar(20) DEFAULT 'user' NOT NULL; \ No newline at end of file diff --git a/backend/drizzle/meta/0006_snapshot.json b/backend/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..397c618 --- /dev/null +++ b/backend/drizzle/meta/0006_snapshot.json @@ -0,0 +1,618 @@ +{ + "id": "facfeeea-cf12-4191-bc05-188150aaceb9", + "prevId": "d46d415d-9787-44ad-962a-a2405d5896e8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_unique": { + "name": "accounts_provider_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credentials_user_id_unique": { + "name": "credentials_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "credentials_email_unique": { + "name": "credentials_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keywords": { + "name": "keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keywords_keyword_unique": { + "name": "keywords_keyword_unique", + "columns": [ + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_jobs": { + "name": "saved_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_link": { + "name": "job_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'saved'" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "saved_jobs_user_id_users_id_fk": { + "name": "saved_jobs_user_id_users_id_fk", + "tableFrom": "saved_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keywords": { + "name": "keywords", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "search_location": { + "name": "search_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "search_language": { + "name": "search_language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "remote_only": { + "name": "remote_only", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "job_types": { + "name": "job_types", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_unique": { + "name": "user_preferences_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(14)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "technologies": { + "name": "technologies", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "level": { + "name": "level", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index a69659f..cfae5aa 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1781886747704, "tag": "0005_heavy_la_nuit", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1781892324992, + "tag": "0006_happy_gateway", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/schema/users.ts b/backend/src/db/schema/users.ts index 26d917a..bc5b617 100644 --- a/backend/src/db/schema/users.ts +++ b/backend/src/db/schema/users.ts @@ -27,6 +27,7 @@ export const users = pgTable( avatarUrl: text("avatar_url"), phone: varchar("phone", { length: 20 }), cpf: varchar("cpf", { length: 14 }), + role: varchar("role", { length: 20 }).default("user").notNull(), technologies: text("technologies").array().default([]), level: varchar("level", { length: 50 }), From 62a380d83d2714698e236903bb69d5e4354ec780 Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 16:12:24 -0300 Subject: [PATCH 6/8] feat(api): adiciona e persiste role de usuario no registro --- backend/src/modules/auth/credentials.service.ts | 5 +++-- backend/src/modules/types/credentials.types.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/auth/credentials.service.ts b/backend/src/modules/auth/credentials.service.ts index 1375770..3cd8c38 100644 --- a/backend/src/modules/auth/credentials.service.ts +++ b/backend/src/modules/auth/credentials.service.ts @@ -32,7 +32,7 @@ export class CredentialsService { async register( input: RegisterInput, ): Promise<{ user: User; session: Session }> { - const { email, password, name, phone, cpf, technologies, level } = RegisterSchema.parse(input); + const { email, password, name, phone, cpf, technologies, level, role } = RegisterSchema.parse(input); const existingCredential = await db.query.credentials.findFirst({ where: eq(credentials.email, email), @@ -57,7 +57,8 @@ export class CredentialsService { phone, cpf, technologies, - level + level, + role }) .returning(); diff --git a/backend/src/modules/types/credentials.types.ts b/backend/src/modules/types/credentials.types.ts index 1ba8f15..55cecf4 100644 --- a/backend/src/modules/types/credentials.types.ts +++ b/backend/src/modules/types/credentials.types.ts @@ -8,6 +8,7 @@ export const RegisterSchema = z.object({ cpf: z.string().optional(), technologies: z.array(z.string()).optional(), level: z.string().optional(), + role: z.enum(["user", "admin"]).optional().default("user"), }); export const LoginSchema = z.object({ From 34e0dd53c0967f470e1aa96bacb8059209d11d25 Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 16:12:25 -0300 Subject: [PATCH 7/8] feat(ui): unifica formulario com abas de admin e usuario --- .../src/components/login/RegisterSide.tsx | 104 +++++++++++------- frontend/src/services/authService.ts | 2 + 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index cc8e457..6be5045 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -57,6 +57,7 @@ function StarsBackground() { } export default function RegisterSide() { + const [role, setRole] = useState<"user" | "admin">("user"); const [showPassword, setShowPassword] = useState(false); const [nome, setNome] = useState(""); const [email, setEmail] = useState(""); @@ -153,14 +154,16 @@ export default function RegisterSide() { isValid = false; } - if (!level) { - setLevelError("A seleção de nível é obrigatória."); - isValid = false; - } + if (role === "admin") { + if (!level) { + setLevelError("A seleção de nível é obrigatória."); + isValid = false; + } - if (selectedTechnologies.length === 0) { - setTechnologiesError("Selecione ao menos uma tecnologia."); - isValid = false; + if (selectedTechnologies.length === 0) { + setTechnologiesError("Selecione ao menos uma tecnologia."); + isValid = false; + } } if (isValid) { @@ -172,8 +175,9 @@ export default function RegisterSide() { name: nome, phone: telefone, cpf: cpf, - level: level, - technologies: selectedTechnologies, + level: role === "admin" ? level : undefined, + technologies: role === "admin" ? selectedTechnologies : undefined, + role: role, }); window.location.href = "/login?registered=true"; } catch (error: any) { @@ -213,6 +217,22 @@ export default function RegisterSide() {
+
+ + +
{apiError && (

{apiError}

@@ -250,37 +270,41 @@ export default function RegisterSide() { setCpf(formatCpf(e.target.value))} placeholder="091.000.000-00" disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {cpfError &&

{cpfError}

}
-
- - - {levelError &&

{levelError}

} -
-
- -
- {POPULAR_TECHNOLOGIES.map((tech) => { - const isSelected = selectedTechnologies.includes(tech); - return ( - toggleTechnology(tech)} - > - {tech} - - ); - })} -
- {technologiesError &&

{technologiesError}

} -
+ {role === "admin" && ( + <> +
+ + + {levelError &&

{levelError}

} +
+
+ +
+ {POPULAR_TECHNOLOGIES.map((tech) => { + const isSelected = selectedTechnologies.includes(tech); + return ( + toggleTechnology(tech)} + > + {tech} + + ); + })} +
+ {technologiesError &&

{technologiesError}

} +
+ + )} {isLoading ? "Cadastrando..." : "Cadastrar"} diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 5281c39..13a7ea0 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -55,6 +55,7 @@ export interface RegisterData { cpf?: string; technologies?: string[]; level?: string; + role?: "user" | "admin"; } export async function login(credentials: LoginCredentials) { @@ -86,6 +87,7 @@ export async function register(userData: RegisterData) { cpf: userData.cpf, technologies: userData.technologies, level: userData.level, + role: userData.role, }), credentials: "include", }); From 26ad9fa65f3aaf6168d73a3ad98ac767e32e1f4d Mon Sep 17 00:00:00 2001 From: Jovinull Date: Fri, 19 Jun 2026 16:12:27 -0300 Subject: [PATCH 8/8] test(ui): adapta testes de registro para lidar com abas de perfis --- .../components/login/RegisterSide.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/frontend/tests/unit/components/login/RegisterSide.test.tsx b/frontend/tests/unit/components/login/RegisterSide.test.tsx index af01eb9..96e708e 100644 --- a/frontend/tests/unit/components/login/RegisterSide.test.tsx +++ b/frontend/tests/unit/components/login/RegisterSide.test.tsx @@ -62,6 +62,7 @@ describe("RegisterSide", () => { it("mostra erros obrigatórios ao submeter vazio", async () => { render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); expect(await screen.findByText(/campo de nome é obrigatório/i)).toBeInTheDocument(); expect(await screen.findByText(/campo de e-mail é obrigatório/i)).toBeInTheDocument(); @@ -73,6 +74,7 @@ describe("RegisterSide", () => { it("valida CPF inválido quando preenchido", async () => { render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Usuário" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); @@ -87,6 +89,7 @@ describe("RegisterSide", () => { it("envia formulário válido sem CPF", async () => { mockRegister.mockResolvedValueOnce({ message: "Usuário criado" }); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); @@ -103,14 +106,38 @@ describe("RegisterSide", () => { cpf: "", level: "Júnior", technologies: ["React"], + role: "admin", }); }); expect(window.location.href).toBe("/login?registered=true"); }); + it("envia formulário válido como usuário (sem tecnologias/nível)", async () => { + mockRegister.mockResolvedValueOnce({ message: "Usuário criado" }); + render(); + fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + await waitFor(() => { + expect(mockRegister).toHaveBeenCalledWith({ + email: "bene@teste.com", + password: "123456", + name: "Bene", + phone: "+5534999999999", + cpf: "", + level: undefined, + technologies: undefined, + role: "user" + }); + }); + }); + it("exibe erro da API", async () => { mockRegister.mockRejectedValueOnce(new Error("Email já cadastrado")); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); @@ -124,6 +151,7 @@ describe("RegisterSide", () => { it("mostra loading durante requisição", async () => { mockRegister.mockImplementation(() => new Promise(() => {})); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { target: { value: "+5534999999999" } }); @@ -146,6 +174,7 @@ describe("RegisterSide", () => { it("desabilita inputs durante loading", async () => { mockRegister.mockImplementation(() => new Promise(() => {})); render(); + fireEvent.click(screen.getByRole("button", { name: /sou admin/i })); const nomeInput = screen.getByLabelText(/nome/i); const emailInput = screen.getByLabelText(/email/i); const telefoneInput = screen.getByPlaceholderText(/\(34\)/i);