Skip to content

Commit 1aa61b4

Browse files
committed
Expand test suite and injected fault set
1 parent 8bed472 commit 1aa61b4

10 files changed

Lines changed: 296 additions & 26 deletions

File tree

‎app/(dashboard)/dashboard/activity/page.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ function formatAction(action: ActivityType): string {
7171

7272
export default async function ActivityPage() {
7373
const logs = await getActivityLogs();
74-
const visibleLogs = (await isFaultActive("activity-missing-create-team"))
74+
const filteredLogs = (await isFaultActive("activity-missing-create-team"))
7575
? logs.filter((log) => log.action !== ActivityType.CREATE_TEAM)
7676
: logs;
77+
const visibleLogs = (await isFaultActive("activity-order-inverted"))
78+
? [...filteredLogs].reverse()
79+
: filteredLogs;
7780

7881
return (
7982
<section className="flex-1 p-4 lg:p-8">

‎app/(login)/actions.ts‎

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type User,
2323
users,
2424
} from "@/lib/db/schema";
25+
import { isFaultActive } from "@/lib/faults";
2526

2627
const signInSchema = z.object({
2728
email: z.string().email().min(3).max(255),
@@ -110,8 +111,12 @@ export const signUp = validatedAction(signUpSchema, async (data, formData) => {
110111
export async function signOut() {
111112
const user = (await getUser()) as User;
112113
const userWithTeam = await getUserWithTeam(user.id);
113-
await logActivity(userWithTeam?.teamId, user.id, ActivityType.SIGN_OUT);
114-
(await cookies()).delete("session");
114+
if (!(await isFaultActive("signout-activity-log-missing"))) {
115+
await logActivity(userWithTeam?.teamId, user.id, ActivityType.SIGN_OUT);
116+
}
117+
if (!(await isFaultActive("signout-cookie-not-cleared"))) {
118+
(await cookies()).delete("session");
119+
}
115120
}
116121

117122
const updatePasswordSchema = z.object({
@@ -159,12 +164,19 @@ export const updatePassword = validatedActionWithUser(
159164

160165
const newPasswordHash = await hashPassword(newPassword);
161166
const userWithTeam = await getUserWithTeam(user.id);
167+
const skipPasswordUpdate = await isFaultActive(
168+
"password-hash-update-skipped",
169+
);
162170

163171
await Promise.all([
164-
db
165-
.update(users)
166-
.set({ passwordHash: newPasswordHash })
167-
.where(eq(users.id, user.id)),
172+
...(skipPasswordUpdate
173+
? []
174+
: [
175+
db
176+
.update(users)
177+
.set({ passwordHash: newPasswordHash })
178+
.where(eq(users.id, user.id)),
179+
]),
168180
logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_PASSWORD),
169181
]);
170182

@@ -234,10 +246,23 @@ export const updateAccount = validatedActionWithUser(
234246
async (data, _, user) => {
235247
const { name, email } = data;
236248
const userWithTeam = await getUserWithTeam(user.id);
249+
const skipAccountUpdate = await isFaultActive(
250+
"account-update-db-write-skipped",
251+
);
252+
const skipActivityLog = await isFaultActive("activity-update-log-missing");
253+
const activityType = (await isFaultActive(
254+
"activity-update-log-mislabelled",
255+
))
256+
? ActivityType.UPDATE_PASSWORD
257+
: ActivityType.UPDATE_ACCOUNT;
237258

238259
await Promise.all([
239-
db.update(users).set({ name, email }).where(eq(users.id, user.id)),
240-
logActivity(userWithTeam?.teamId, user.id, ActivityType.UPDATE_ACCOUNT),
260+
...(skipAccountUpdate
261+
? []
262+
: [db.update(users).set({ name, email }).where(eq(users.id, user.id))]),
263+
...(skipActivityLog
264+
? []
265+
: [logActivity(userWithTeam?.teamId, user.id, activityType)]),
241266
]);
242267

243268
return { name, success: "Account updated successfully." };
@@ -321,7 +346,10 @@ export const inviteTeamMember = validatedActionWithUser(
321346
)
322347
.limit(1);
323348

324-
if (existingInvitation.length > 0) {
349+
if (
350+
existingInvitation.length > 0 &&
351+
!(await isFaultActive("duplicate-pending-invite-allowed"))
352+
) {
325353
return { error: "An invitation has already been sent to this email" };
326354
}
327355

‎app/api/payment/route.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { desc } from "drizzle-orm";
1+
import { desc, eq } from "drizzle-orm";
22
import { type NextRequest, NextResponse } from "next/server";
33
import { db } from "@/lib/db/drizzle";
44
import { getUser } from "@/lib/db/queries";
5-
import { payments } from "@/lib/db/schema";
5+
import { payments, teamMembers } from "@/lib/db/schema";
66

77
export async function GET(_request: NextRequest) {
88
const user = await getUser();
@@ -12,9 +12,20 @@ export async function GET(_request: NextRequest) {
1212
}
1313

1414
try {
15+
const userTeam = await db
16+
.select({ teamId: teamMembers.teamId })
17+
.from(teamMembers)
18+
.where(eq(teamMembers.userId, user.id))
19+
.limit(1);
20+
21+
if (userTeam.length === 0) {
22+
return NextResponse.json([]);
23+
}
24+
1525
const userPayments = await db
1626
.select()
1727
.from(payments)
28+
.where(eq(payments.teamId, userTeam[0].teamId))
1829
.orderBy(desc(payments.createdAt))
1930
.limit(10);
2031

‎lib/auth/login.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ export async function signup(
7171

7272
if (invitation) {
7373
teamId = invitation.teamId;
74-
userRole = invitation.role;
74+
userRole = (await isFaultActive("invite-role-drift"))
75+
? "owner"
76+
: invitation.role;
7577

7678
await db
7779
.update(invitations)

‎lib/payments/actions.ts‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,31 @@ export async function processPayment(formData: FormData) {
6868
if (await isFaultActive("payment-server-error")) {
6969
throw new Error("Fault injected: payment-server-error");
7070
}
71-
72-
// Store payment information in database
73-
await db.insert(payments).values({
71+
const paymentData = {
7472
teamId: userTeam[0].teamId,
7573
...validatedData,
76-
});
74+
amount: (await isFaultActive("payment-wrong-amount"))
75+
? 800
76+
: validatedData.amount,
77+
};
78+
79+
if (!(await isFaultActive("payment-row-missing"))) {
80+
await db.insert(payments).values(paymentData);
81+
if (await isFaultActive("payment-duplicate-charge")) {
82+
await db.insert(payments).values(paymentData);
83+
}
84+
}
7785

78-
// Update team subscription status
79-
await db
80-
.update(teams)
81-
.set({
82-
planName: validatedData.planName,
83-
subscriptionStatus: "active",
84-
updatedAt: new Date(),
85-
})
86-
.where(eq(teams.id, userTeam[0].teamId));
86+
if (!(await isFaultActive("payment-subscription-update-skipped"))) {
87+
await db
88+
.update(teams)
89+
.set({
90+
planName: validatedData.planName,
91+
subscriptionStatus: "active",
92+
updatedAt: new Date(),
93+
})
94+
.where(eq(teams.id, userTeam[0].teamId));
95+
}
8796

8897
redirect("/dashboard?payment=success");
8998
} catch (error) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect, test } from "./test";
2+
import { testWithNewUser } from "./test-with-new-user";
3+
4+
test.describe("Activity after account update", () => {
5+
testWithNewUser(
6+
"should record an account update in the activity log",
7+
async ({ page }) => {
8+
await test.step("update the account name", async () => {
9+
await page.goto("/dashboard/general");
10+
await expect(
11+
page.getByRole("heading", { name: "General Settings" }),
12+
).toBeVisible();
13+
await expect(page.getByRole("textbox", { name: "Email" })).toHaveValue(
14+
/.+@example\.com/,
15+
);
16+
17+
const nameInput = page.getByRole("textbox", { name: "Name" });
18+
await nameInput.clear();
19+
await nameInput.fill("Activity User");
20+
await page.getByRole("button", { name: "Save Changes" }).click();
21+
await expect(
22+
page.getByText("Account updated successfully."),
23+
).toBeVisible();
24+
});
25+
26+
await test.step("verify the account update appears in activity", async () => {
27+
await page.getByRole("link", { name: "Activity" }).click();
28+
await expect(page).toHaveURL("/dashboard/activity");
29+
await expect(
30+
page.getByRole("heading", { name: "Activity Log" }),
31+
).toBeVisible();
32+
await expect(page.getByText("You updated your account")).toBeVisible();
33+
});
34+
},
35+
);
36+
});

‎tests/activity-order.spec.ts‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { expect, test } from "./test";
2+
import { testWithNewUser } from "./test-with-new-user";
3+
4+
test.describe("Activity ordering", () => {
5+
testWithNewUser(
6+
"should show newer account activity before older activity",
7+
async ({ page }) => {
8+
await test.step("create an account update activity", async () => {
9+
await page.goto("/dashboard/general");
10+
await expect(
11+
page.getByRole("heading", { name: "General Settings" }),
12+
).toBeVisible();
13+
await expect(page.getByRole("textbox", { name: "Email" })).toHaveValue(
14+
/.+@example\.com/,
15+
);
16+
17+
const nameInput = page.getByRole("textbox", { name: "Name" });
18+
await nameInput.clear();
19+
await nameInput.fill("Ordered Activity User");
20+
await page.getByRole("button", { name: "Save Changes" }).click();
21+
await expect(
22+
page.getByText("Account updated successfully."),
23+
).toBeVisible();
24+
await page.waitForTimeout(1100);
25+
});
26+
27+
await test.step("create a newer password update activity", async () => {
28+
await page.getByRole("link", { name: "Security" }).click();
29+
await expect(
30+
page.getByRole("heading", { name: "Security Settings" }),
31+
).toBeVisible();
32+
33+
await page
34+
.getByRole("textbox", { name: "Current Password" })
35+
.fill("testpassword123");
36+
await page
37+
.getByRole("textbox", { name: "New Password", exact: true })
38+
.fill("newpassword123");
39+
await page
40+
.getByRole("textbox", { name: "Confirm New Password" })
41+
.fill("newpassword123");
42+
await page.getByRole("button", { name: "Update Password" }).click();
43+
await expect(
44+
page.getByText("Password updated successfully."),
45+
).toBeVisible();
46+
});
47+
48+
await test.step("verify newest activity is first", async () => {
49+
await page.getByRole("link", { name: "Activity" }).click();
50+
await expect(page).toHaveURL("/dashboard/activity");
51+
52+
const activities = page.getByRole("listitem");
53+
await expect(activities.first()).toContainText(
54+
"You changed your password",
55+
);
56+
await expect(activities.nth(1)).toContainText(
57+
"You updated your account",
58+
);
59+
});
60+
},
61+
);
62+
});

‎tests/duplicate-invite.spec.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from "./test";
2+
import { testWithNewUser } from "./test-with-new-user";
3+
4+
test.describe("Duplicate team invitation", () => {
5+
testWithNewUser(
6+
"should reject a duplicate pending team invitation",
7+
async ({ page }) => {
8+
const inviteEmail = `duplicate-${Date.now()}@example.com`;
9+
10+
await test.step("send the first invitation", async () => {
11+
await page.goto("/dashboard");
12+
await expect(
13+
page.getByRole("heading", { name: "Team Settings" }),
14+
).toBeVisible();
15+
await page.getByRole("textbox", { name: "Email" }).fill(inviteEmail);
16+
await page.getByRole("button", { name: "Invite Member" }).click();
17+
await expect(
18+
page.getByText("Invitation sent successfully"),
19+
).toBeVisible();
20+
});
21+
22+
await test.step("try to send the same invitation again", async () => {
23+
await page.getByRole("textbox", { name: "Email" }).fill(inviteEmail);
24+
await page.getByRole("button", { name: "Invite Member" }).click();
25+
await expect(
26+
page.getByText("An invitation has already been sent to this email"),
27+
).toBeVisible();
28+
});
29+
},
30+
);
31+
});

‎tests/payment-history.spec.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { expect, test } from "./test";
2+
import { testWithNewUser } from "./test-with-new-user";
3+
4+
type PaymentRow = {
5+
planName: string;
6+
amount: number;
7+
currency: string;
8+
};
9+
10+
test.describe("Payment history", () => {
11+
testWithNewUser(
12+
"should record exactly one payment after upgrading to Plus",
13+
async ({ page }) => {
14+
await test.step("complete a Plus plan checkout", async () => {
15+
await page.goto("/dashboard");
16+
await page.getByRole("link", { name: "Change Plan" }).click();
17+
await expect(page).toHaveURL(/.*\/pricing$/);
18+
await page.getByRole("button", { name: "Get Started" }).nth(1).click();
19+
await expect(page).toHaveURL(
20+
/.*\/pricing\/checkout\?plan=Plus&amount=1200$/,
21+
);
22+
23+
await page
24+
.getByRole("textbox", { name: "Card Holder Name" })
25+
.fill("John Doe");
26+
await page
27+
.getByRole("textbox", { name: "Card Number (8 digits only)" })
28+
.fill("12345678");
29+
await page.getByRole("textbox", { name: "Expiry Date" }).fill("12/25");
30+
await page.getByRole("textbox", { name: "CVV" }).fill("123");
31+
await page
32+
.getByRole("textbox", { name: "Street Address" })
33+
.fill("123 Main Street");
34+
await page.getByRole("textbox", { name: "City" }).fill("New York");
35+
await page.getByRole("textbox", { name: "State" }).fill("NY");
36+
await page.getByRole("textbox", { name: "ZIP Code" }).fill("10001");
37+
await page
38+
.getByRole("textbox", { name: "Country" })
39+
.fill("United States");
40+
await page
41+
.getByRole("button", { name: "Complete Purchase - $12/month" })
42+
.click();
43+
await expect(page).toHaveURL(/.*\/dashboard\?payment=success$/);
44+
});
45+
46+
await test.step("verify one expected payment was recorded", async () => {
47+
const payments = await page.evaluate(async () => {
48+
const response = await fetch("/api/payment");
49+
return (await response.json()) as PaymentRow[];
50+
});
51+
52+
expect(payments).toHaveLength(1);
53+
expect(payments[0]).toMatchObject({
54+
planName: "Plus",
55+
amount: 1200,
56+
currency: "USD",
57+
});
58+
});
59+
},
60+
);
61+
});

0 commit comments

Comments
 (0)