Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion apps/backend/src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { supabaseAdmin } from "../config/supabase.js";
import { prisma } from "../config/database.js";
import { AppError } from "../utils/AppError.js";
import { logger } from "../utils/logger.js";
import type { SignupInput, LoginInput, RefreshInput } from "../schemas/auth.schema.js";
Expand Down Expand Up @@ -42,6 +43,32 @@ export class AuthService {
throw new AppError(400, "SIGNUP_FAILED", "Failed to create account");
}
const userId = data.user.id;
const email = data.user.email!;

try {
await prisma.user.create({
data: {
id: userId,
email,
displayName: input.displayName,
},
});
} catch (error) {
logger.error({ error, userId }, "Failed to create application user after signup");
try {
await supabaseAdmin.auth.admin.deleteUser(userId);
} catch (cleanupError) {
logger.warn(
{ cleanupError, userId },
"Failed to roll back Supabase user after signup profile creation failed",
);
}
throw new AppError(
500,
"SIGNUP_PROFILE_FAILED",
"Failed to create account profile",
);
}

// Warm up a per-user model in the background; auth should not fail if ML training is down.
void mlService.trainUserModel(userId).catch((error) => {
Expand All @@ -52,7 +79,7 @@ export class AuthService {
return {
user: {
id: userId,
email: data.user.email!,
email,
},
tokens: {
accessToken: data.session.access_token,
Expand Down
116 changes: 116 additions & 0 deletions apps/backend/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("../src/config/database.js", () => ({
isDatabaseHealthy: vi.fn().mockResolvedValue(true),
prisma: {
user: {
create: vi.fn(),
},
},
}));

vi.mock("../src/config/supabase.js", () => ({
supabaseAdmin: {
auth: {
signUp: vi.fn(),
admin: {
deleteUser: vi.fn(),
},
},
},
}));

vi.mock("../src/services/ml.service.js", () => ({
mlService: {
trainUserModel: vi.fn().mockResolvedValue({
user_id: "550e8400-e29b-41d4-a716-446655440000",
interaction_count: 0,
is_cold_start: true,
message: "queued",
}),
},
}));

vi.mock("../src/config/sentry.js", () => ({
captureMlFailure: vi.fn(),
}));

vi.mock("../src/utils/logger.js", () => ({
logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));

import { prisma } from "../src/config/database.js";
import { supabaseAdmin } from "../src/config/supabase.js";
import { mlService } from "../src/services/ml.service.js";
import { authService } from "../src/services/auth.service.js";

const USER_ID = "550e8400-e29b-41d4-a716-446655440000";
const EMAIL = "new-user@snacktrack.dev";
const SIGNUP_INPUT = {
email: EMAIL,
password: "Password123",
displayName: "New User",
};

function mockSuccessfulSupabaseSignup() {
vi.mocked(supabaseAdmin.auth.signUp).mockResolvedValue({
data: {
user: {
id: USER_ID,
email: EMAIL,
},
session: {
access_token: "access-token",
refresh_token: "refresh-token",
expires_in: 3600,
expires_at: 1234567890,
},
},
error: null,
} as never);
}

describe("AuthService", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSuccessfulSupabaseSignup();
vi.mocked(prisma.user.create).mockResolvedValue({
id: USER_ID,
email: EMAIL,
displayName: SIGNUP_INPUT.displayName,
} as never);
vi.mocked(supabaseAdmin.auth.admin.deleteUser).mockResolvedValue({
data: null,
error: null,
} as never);
});

describe("signUp", () => {
it("creates the application user row before returning signup success", async () => {
const result = await authService.signUp(SIGNUP_INPUT);

expect(prisma.user.create).toHaveBeenCalledWith({
data: {
id: USER_ID,
email: EMAIL,
displayName: SIGNUP_INPUT.displayName,
},
});
expect(result.user).toEqual({ id: USER_ID, email: EMAIL });
expect(result.tokens.accessToken).toBe("access-token");
expect(mlService.trainUserModel).toHaveBeenCalledWith(USER_ID);
});

it("rolls back the Supabase auth user when application user creation fails", async () => {
vi.mocked(prisma.user.create).mockRejectedValue(new Error("database unavailable"));

await expect(authService.signUp(SIGNUP_INPUT)).rejects.toMatchObject({
statusCode: 500,
code: "SIGNUP_PROFILE_FAILED",
});

expect(supabaseAdmin.auth.admin.deleteUser).toHaveBeenCalledWith(USER_ID);
expect(mlService.trainUserModel).not.toHaveBeenCalled();
});
});
});
Loading