| 1 | import test from "ava";
|
| 2 | import { PrismaClient } from '@prisma/client';
|
| 3 | import { PrismaPg } from '@prisma/adapter-pg';
|
| 4 | import bcrypt from 'bcryptjs';
|
| 5 | import 'dotenv/config';
|
| 6 |
|
| 7 | let prisma: PrismaClient;
|
| 8 |
|
| 9 | test.before(() => {
|
| 10 | process.env.NODE_ENV = 'test';
|
| 11 |
|
| 12 | const connectionString = process.env.DATABASE_URL || 'postgres://user:password@orbit_db:5432/orbit_test';
|
| 13 | const adapter = new PrismaPg({ connectionString });
|
| 14 | prisma = new PrismaClient({ adapter });
|
| 15 | });
|
| 16 |
|
| 17 | test.beforeEach(async () => {
|
| 18 | // Clean database before each test
|
| 19 | try {
|
| 20 | await prisma.user.deleteMany({});
|
| 21 | } catch (err) {
|
| 22 | console.error('Error cleaning test DB:', err);
|
| 23 | }
|
| 24 | });
|
| 25 |
|
| 26 | test.after.always(async () => {
|
| 27 | const deleteUsers = prisma.user.deleteMany();
|
| 28 |
|
| 29 | await prisma.$transaction([
|
| 30 | deleteUsers
|
| 31 | ])
|
| 32 |
|
| 33 | await prisma?.$disconnect();
|
| 34 | });
|
| 35 |
|
| 36 | // Basic database connectivity test
|
| 37 | test("database connection works", async (t) => {
|
| 38 | const result = await prisma.$queryRaw`SELECT 1 as test`;
|
| 39 | t.truthy(result);
|
| 40 | });
|
| 41 |
|
| 42 | // User creation and authentication tests
|
| 43 | test("can create user with encrypted password", async (t) => {
|
| 44 | const encryptedPassword = bcrypt.hashSync("testPassword123", 10);
|
| 45 |
|
| 46 | const user = await prisma.user.create({
|
| 47 | data: {
|
| 48 | email: "test@example.com",
|
| 49 | firstName: "Test",
|
| 50 | lastName: "User",
|
| 51 | encryptedPassword: encryptedPassword
|
| 52 | }
|
| 53 | });
|
| 54 |
|
| 55 | t.is(user.email, "test@example.com");
|
| 56 | t.is(user.firstName, "Test");
|
| 57 | t.truthy(user.id);
|
| 58 |
|
| 59 | // Verify password can be validated
|
| 60 | t.true(bcrypt.compareSync("testPassword123", user.encryptedPassword));
|
| 61 | t.false(bcrypt.compareSync("wrongPassword", user.encryptedPassword));
|
| 62 | });
|
| 63 |
|
| 64 | test("can find user by email for authentication", async (t) => {
|
| 65 | // Create user
|
| 66 | await prisma.user.create({
|
| 67 | data: {
|
| 68 | email: "login@example.com",
|
| 69 | firstName: "Login",
|
| 70 | lastName: "User",
|
| 71 | encryptedPassword: bcrypt.hashSync("password123", 10)
|
| 72 | }
|
| 73 | });
|
| 74 |
|
| 75 | // Simulate login lookup
|
| 76 | const foundUser = await prisma.user.findUnique({
|
| 77 | where: { email: "login@example.com" }
|
| 78 | });
|
| 79 |
|
| 80 | t.truthy(foundUser);
|
| 81 | t.is(foundUser!.email, "login@example.com");
|
| 82 | t.true(bcrypt.compareSync("password123", foundUser!.encryptedPassword));
|
| 83 | t.false(bcrypt.compareSync("wrongPassword", foundUser!.encryptedPassword));
|
| 84 | });
|
| 85 |
|
| 86 | test("email uniqueness constraint prevents duplicate registrations", async (t) => {
|
| 87 | const email = "duplicate@example.com";
|
| 88 |
|
| 89 | // Create first user
|
| 90 | await prisma.user.create({
|
| 91 | data: {
|
| 92 | email,
|
| 93 | firstName: "First",
|
| 94 | lastName: "User",
|
| 95 | encryptedPassword: bcrypt.hashSync("password", 10)
|
| 96 | }
|
| 97 | });
|
| 98 |
|
| 99 | // Try to create second user with same email - should fail
|
| 100 | await t.throwsAsync(
|
| 101 | async () => {
|
| 102 | await prisma.user.create({
|
| 103 | data: {
|
| 104 | email,
|
| 105 | firstName: "Second",
|
| 106 | lastName: "User",
|
| 107 | encryptedPassword: bcrypt.hashSync("password", 10)
|
| 108 | }
|
| 109 | });
|
| 110 | }
|
| 111 | );
|
| 112 | });
|
| 113 |
|
| 114 | test("password reset flow works with database", async (t) => {
|
| 115 | const resetToken = "reset-token-123";
|
| 116 | const resetExpires = new Date(Date.now() + 3600000); // 1 hour from now
|
| 117 |
|
| 118 | // Create user
|
| 119 | const user = await prisma.user.create({
|
| 120 | data: {
|
| 121 | email: "reset@example.com",
|
| 122 | firstName: "Reset",
|
| 123 | lastName: "User",
|
| 124 | encryptedPassword: bcrypt.hashSync("oldPassword", 10)
|
| 125 | }
|
| 126 | });
|
| 127 |
|
| 128 | // Step 1: Set reset token (forgot password)
|
| 129 | await prisma.user.update({
|
| 130 | where: { id: user.id },
|
| 131 | data: {
|
| 132 | passwordResetToken: resetToken,
|
| 133 | passwordResetExpires: resetExpires
|
| 134 | }
|
| 135 | });
|
| 136 |
|
| 137 | // Step 2: Find user by reset token
|
| 138 | const userWithToken = await prisma.user.findFirst({
|
| 139 | where: {
|
| 140 | passwordResetToken: resetToken,
|
| 141 | passwordResetExpires: { gte: new Date() } // Token not expired
|
| 142 | }
|
| 143 | });
|
| 144 |
|
| 145 | t.truthy(userWithToken);
|
| 146 | t.is(userWithToken!.email, "reset@example.com");
|
| 147 |
|
| 148 | // Step 3: Reset password
|
| 149 | const newPassword = bcrypt.hashSync("newPassword123", 10);
|
| 150 | const updatedUser = await prisma.user.update({
|
| 151 | where: { id: user.id },
|
| 152 | data: {
|
| 153 | encryptedPassword: newPassword,
|
| 154 | passwordResetToken: null,
|
| 155 | passwordResetExpires: null
|
| 156 | }
|
| 157 | });
|
| 158 |
|
| 159 | // Verify new password works and reset fields are cleared
|
| 160 | t.true(bcrypt.compareSync("newPassword123", updatedUser.encryptedPassword));
|
| 161 | t.false(bcrypt.compareSync("oldPassword", updatedUser.encryptedPassword));
|
| 162 | t.is(updatedUser.passwordResetToken, null);
|
| 163 | t.is(updatedUser.passwordResetExpires, null);
|
| 164 | });
|
| 165 |
|
| 166 | test("expired reset tokens are not found", async (t) => {
|
| 167 | const expiredResetToken = "expired-token";
|
| 168 | const pastDate = new Date(Date.now() - 3600000); // 1 hour ago
|
| 169 |
|
| 170 | // Create user with expired token
|
| 171 | const user = await prisma.user.create({
|
| 172 | data: {
|
| 173 | email: "expired@example.com",
|
| 174 | firstName: "Expired",
|
| 175 | lastName: "User",
|
| 176 | encryptedPassword: bcrypt.hashSync("password", 10),
|
| 177 | passwordResetToken: expiredResetToken,
|
| 178 | passwordResetExpires: pastDate
|
| 179 | }
|
| 180 | });
|
| 181 |
|
| 182 | // Try to find user by expired token
|
| 183 | const foundUser = await prisma.user.findFirst({
|
| 184 | where: {
|
| 185 | passwordResetToken: expiredResetToken,
|
| 186 | passwordResetExpires: { gte: new Date() } // Token not expired
|
| 187 | }
|
| 188 | });
|
| 189 |
|
| 190 | // Should not find the user because token is expired
|
| 191 | t.is(foundUser, null);
|
| 192 |
|
| 193 | // But user still exists
|
| 194 | const stillExists = await prisma.user.findUnique({ where: { id: user.id } });
|
| 195 | t.truthy(stillExists);
|
| 196 | });
|
| 197 |
|
| 198 | test("user profile update works", async (t) => {
|
| 199 | // Create user
|
| 200 | const user = await prisma.user.create({
|
| 201 | data: {
|
| 202 | email: "profile@example.com",
|
| 203 | firstName: "Original",
|
| 204 | lastName: "Name",
|
| 205 | encryptedPassword: bcrypt.hashSync("password", 10)
|
| 206 | }
|
| 207 | });
|
| 208 |
|
| 209 | // Update profile
|
| 210 | const updatedUser = await prisma.user.update({
|
| 211 | where: { id: user.id },
|
| 212 | data: {
|
| 213 | firstName: "Updated",
|
| 214 | lastName: "Profile"
|
| 215 | }
|
| 216 | });
|
| 217 |
|
| 218 | t.is(updatedUser.firstName, "Updated");
|
| 219 | t.is(updatedUser.lastName, "Profile");
|
| 220 | t.is(updatedUser.email, "profile@example.com"); // Should remain unchanged
|
| 221 | t.true(updatedUser.updatedAt > user.updatedAt); // updatedAt should be newer
|
| 222 | });
|
| 223 |
|
| 224 | test("timestamps are automatically managed", async (t) => {
|
| 225 | const beforeCreate = new Date();
|
| 226 |
|
| 227 | const user = await prisma.user.create({
|
| 228 | data: {
|
| 229 | email: "timestamps@example.com",
|
| 230 | firstName: "Time",
|
| 231 | lastName: "Stamps",
|
| 232 | encryptedPassword: bcrypt.hashSync("password", 10)
|
| 233 | }
|
| 234 | });
|
| 235 |
|
| 236 | const afterCreate = new Date();
|
| 237 |
|
| 238 | // Check creation timestamps
|
| 239 | t.true(user.createdAt >= beforeCreate);
|
| 240 | t.true(user.createdAt <= afterCreate);
|
| 241 | t.true(user.updatedAt >= beforeCreate);
|
| 242 | t.true(user.updatedAt <= afterCreate);
|
| 243 |
|
| 244 | // Wait and update to test updatedAt behavior
|
| 245 | await new Promise(resolve => setTimeout(resolve, 100));
|
| 246 | const beforeUpdate = new Date();
|
| 247 |
|
| 248 | const updatedUser = await prisma.user.update({
|
| 249 | where: { id: user.id },
|
| 250 | data: { firstName: "Updated" }
|
| 251 | });
|
| 252 |
|
| 253 | const afterUpdate = new Date();
|
| 254 |
|
| 255 | // createdAt should remain the same, updatedAt should be newer
|
| 256 | t.deepEqual(updatedUser.createdAt, user.createdAt);
|
| 257 | t.true(updatedUser.updatedAt >= beforeUpdate);
|
| 258 | t.true(updatedUser.updatedAt <= afterUpdate);
|
| 259 | t.true(updatedUser.updatedAt > user.updatedAt);
|
| 260 | });
|