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