Version : 1.0.0
Date : 2026-06-13
Scope : All 14 tRPC routers (auth, practice, location, gbp, social, content, citation, site, report, lead, billing, skill, admin)
Format : Follows docs/test-plan.md Section 3 — Success Criteria Template
1. Test Data Factories#
1.1 Core Factories#
// src/__tests__/factories/user.ts
import { faker } from "@faker-js/faker";
export const createMockUser = (overrides?: Partial<User>) => ({
id: `usr_${faker.string.nanoid(8)}`,
email: faker.internet.email(),
name: faker.person.fullName(),
image: faker.image.avatar(),
role: "USER",
emailVerified: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockSession = (overrides?: Partial<Session>) => ({
id: `sess_${faker.string.nanoid(8)}`,
token: faker.string.uuid(),
userId: `usr_${faker.string.nanoid(8)}`,
expiresAt: new Date(Date.now() + 86400000),
ipAddress: faker.internet.ip(),
userAgent: faker.internet.userAgent(),
...overrides,
});
// src/__tests__/factories/practice.ts
export const createMockPractice = (overrides?: Partial<Practice>) => ({
id: `prac_${faker.string.nanoid(8)}`,
name: faker.company.name(),
type: faker.helpers.arrayElement([
"CLINIC", "HOSPITAL", "DIAGNOSTIC_CENTER", "DENTAL_CLINIC",
"PHYSIOTHERAPY", "AYURVEDIC_CENTER", "HOMEOPATHY_CLINIC",
"CA", "LAWYER", "WEDDING_PHOTOGRAPHER",
]),
slug: faker.internet.domainWord(),
logoUrl: faker.image.url(),
primaryColor: "#3B82F6",
settings: {},
status: "ACTIVE",
tier: "STANDARD",
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockPracticeMember = (overrides?: Partial<PracticeMember>) => ({
id: `pm_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
userId: `usr_${faker.string.nanoid(8)}`,
role: faker.helpers.arrayElement(["OWNER", "ADMIN", "EDITOR", "VIEWER"]),
joinedAt: new Date(),
...overrides,
});
// src/__tests__/factories/location.ts
export const createMockLocation = (overrides?: Partial<Location>) => ({
id: `loc_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
name: faker.location.streetAddress(),
businessName: faker.company.name(),
address: faker.location.streetAddress(),
city: faker.location.city(),
state: faker.location.state(),
postalCode: faker.location.zipCode(),
country: "IN",
phone: faker.string.numeric(10),
phoneSecondary: faker.string.numeric(10),
email: faker.internet.email(),
website: faker.internet.url(),
category: "Medical Clinic",
services: ["General Checkup", "Vaccination"],
businessHours: { monday: "9:00-18:00" },
targetKeywords: ["doctor near me", "best clinic"],
serviceAreas: [faker.location.city()],
languages: ["English", "Hindi"],
latitude: faker.location.latitude(),
longitude: faker.location.longitude(),
isPrimary: false,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/content.ts
export const createMockContentPiece = (overrides?: Partial<ContentPiece>) => ({
id: `cnt_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
locationId: `loc_${faker.string.nanoid(8)}`,
taskType: "GBP_POST",
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
seoTitle: faker.lorem.sentence(5),
seoDescription: faker.lorem.sentence(10),
focusKeywords: ["keyword1", "keyword2"],
status: faker.helpers.arrayElement(["DRAFT", "PENDING", "APPROVED", "REJECTED", "PUBLISHED"]),
model: "claude-sonnet",
costUsd: 0.002,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/gbp.ts
export const createMockGbpPost = (overrides?: Partial<GbpPost>) => ({
id: `gbp_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
locationId: `loc_${faker.string.nanoid(8)}`,
content: faker.lorem.paragraph(),
mediaUrls: [faker.image.url()],
ctaType: "BOOK",
topicType: "STANDARD",
status: "PUBLISHED",
gbpPostId: faker.string.uuid(),
scheduledFor: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockGbpAccount = (overrides?: Partial<GbpAccount>) => ({
id: `gba_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
googleAccountId: faker.string.uuid(),
email: faker.internet.email(),
accessToken: faker.string.alphanumeric(100),
refreshToken: faker.string.alphanumeric(100),
expiresAt: new Date(Date.now() + 3600000),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/social.ts
export const createMockSocialAccount = (overrides?: Partial<SocialAccount>) => ({
id: `soc_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
platform: faker.helpers.arrayElement(["FACEBOOK", "INSTAGRAM", "LINKEDIN", "TWITTER"]),
accountName: faker.internet.userName(),
accountId: faker.string.uuid(),
accessToken: faker.string.alphanumeric(100),
refreshToken: faker.string.alphanumeric(100),
expiresAt: new Date(Date.now() + 3600000),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockSocialPost = (overrides?: Partial<SocialPost>) => ({
id: `sp_${faker.string.nanoid(8)}`,
socialAccountId: `soc_${faker.string.nanoid(8)}`,
content: faker.lorem.paragraph(),
mediaUrls: [faker.image.url()],
mediaType: "IMAGE",
hashtags: ["#health", "#wellness"],
linkUrl: faker.internet.url(),
status: "SCHEDULED",
scheduledFor: new Date(Date.now() + 3600000),
publishedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/citation.ts
export const createMockCitation = (overrides?: Partial<Citation>) => ({
id: `cit_${faker.string.nanoid(8)}`,
locationId: `loc_${faker.string.nanoid(8)}`,
directoryName: faker.helpers.arrayElement(["justdial", "practo", "lybrate", "google"]),
url: faker.internet.url(),
status: faker.helpers.arrayElement(["PENDING", "SUBMITTED", "VERIFIED", "ERROR"]),
napMatch: null,
lastVerifiedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockCitationDirectory = (overrides?: Partial<CitationDirectory>) => ({
id: `cd_${faker.string.nanoid(8)}`,
name: faker.helpers.arrayElement(["justdial", "practo", "lybrate", "google", "facebook", "yellowpages"]),
category: "medical",
domainAuthority: faker.number.int({ min: 20, max: 90 }),
isFree: faker.datatype.boolean(),
requiresPhoneVerification: faker.datatype.boolean(),
createdAt: new Date(),
...overrides,
});
// src/__tests__/factories/lead.ts
export const createMockLead = (overrides?: Partial<Lead>) => ({
id: `ld_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
locationId: `loc_${faker.string.nanoid(8)}`,
name: faker.person.fullName(),
email: faker.internet.email(),
phone: faker.string.numeric(10),
source: faker.helpers.arrayElement(["GBP", "WEBSITE", "SOCIAL", "REFERRAL"]),
status: faker.helpers.arrayElement(["NEW", "CONTACTED", "CONVERTED", "LOST"]),
message: faker.lorem.sentence(),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/billing.ts
export const createMockInvoice = (overrides?: Partial<Invoice>) => ({
id: `inv_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
amount: faker.number.int({ min: 1000, max: 50000 }),
currency: "INR",
status: faker.helpers.arrayElement(["PENDING", "PAID", "OVERDUE", "REFUNDED"]),
stripeInvoiceId: faker.string.uuid(),
periodStart: new Date(),
periodEnd: new Date(Date.now() + 2592000000),
paidAt: null,
createdAt: new Date(),
...overrides,
});
// src/__tests__/factories/report.ts
export const createMockReport = (overrides?: Partial<Report>) => ({
id: `rep_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
type: "MONTHLY",
periodStart: new Date(Date.now() - 2592000000),
periodEnd: new Date(),
status: faker.helpers.arrayElement(["PENDING", "GENERATING", "READY", "FAILED"]),
pdfUrl: null,
data: {},
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
// src/__tests__/factories/admin.ts
export const createMockJob = (overrides?: Partial<Job>) => ({
id: `job_${faker.string.nanoid(8)}`,
practiceId: `prac_${faker.string.nanoid(8)}`,
type: faker.helpers.arrayElement(["CONTENT_GENERATE", "CITATION_SUBMIT", "REPORT_GENERATE", "SITE_PUBLISH"]),
status: faker.helpers.arrayElement(["PENDING", "RUNNING", "COMPLETED", "FAILED", "RETRYING"]),
payload: {},
result: null,
error: null,
attempts: 0,
maxAttempts: 3,
scheduledFor: new Date(),
startedAt: null,
completedAt: null,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
export const createMockPromptTemplate = (overrides?: Partial<PromptTemplate>) => ({
id: `pt_${faker.string.nanoid(8)}`,
taskType: "GBP_POST",
systemPrompt: faker.lorem.paragraphs(2),
userPromptTemplate: faker.lorem.paragraphs(3),
modelConfig: {
provider: "anthropic",
model: "claude-sonnet-4",
temperature: 0.7,
maxTokens: 2000,
},
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
2. Auth Router#
Spec Reference : docs/specs/backend-api.md Section 2
File : src/server/api/routers/auth.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
updateProfileSchema.valid
{ name: "John Doe", image: "https://example.com/avatar.png" }
Passes validation
src/__tests__/unit/schemas/auth.test.ts
updateProfileSchema.nameTooLong
{ name: "a".repeat(101) }
Zod error: max 100 chars
src/__tests__/unit/schemas/auth.test.ts
updateProfileSchema.invalidUrl
{ image: "not-a-url" }
Zod error: invalid URL
src/__tests__/unit/schemas/auth.test.ts
changePasswordSchema.valid
{ currentPassword: "oldpass123", newPassword: "newpass123" }
Passes validation
src/__tests__/unit/schemas/auth.test.ts
changePasswordSchema.shortPassword
{ newPassword: "short" }
Zod error: min 8 chars
src/__tests__/unit/schemas/auth.test.ts
changePasswordSchema.longPassword
{ newPassword: "a".repeat(101) }
Zod error: max 100 chars
src/__tests__/unit/schemas/auth.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
auth.me.authenticated
Mock session with user
Call auth.me
Returns user object with correct fields
src/__tests__/integration/routers/auth.test.ts
auth.me.unauthenticated
No session
Call auth.me
Throws UNAUTHORIZED (401)
src/__tests__/integration/routers/auth.test.ts
auth.updateProfile.success
Mock user in DB
Call auth.updateProfile with valid name
DB updated, returns updated user
src/__tests__/integration/routers/auth.test.ts
auth.changePassword.success
Mock user with password hash
Call with valid current + new password
Password hash updated, returns { success: true }
src/__tests__/integration/routers/auth.test.ts
auth.changePassword.wrongCurrent
Mock user with password hash
Call with wrong current password
Throws BAD_REQUEST (400)
src/__tests__/integration/routers/auth.test.ts
auth.listSessions
Mock 3 sessions for user
Call auth.listSessions
Returns array of 3 sessions
src/__tests__/integration/routers/auth.test.ts
auth.revokeSession
Mock session token
Call auth.revokeSession with token
Session deleted from DB, returns { success: true }
src/__tests__/integration/routers/auth.test.ts
auth.revokeSession.ownSession
Attempt to revoke own active session
Call with own token
Throws FORBIDDEN (403) or prevents deletion
src/__tests__/integration/routers/auth.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
auth.profileUpdateFlow
1. Login 2. Update profile 3. Refresh page
Profile displays new name
e2e/auth/profile.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User table with at least 1 user, Session table with 3+ sessions for that user
Required env vars : BETTER_AUTH_SECRET configured
Required external mocks : None (pure internal auth)
Verification Commands#
# Run unit tests for auth schemas
pnpm test:unit -- src/__tests__/unit/schemas/auth.test.ts
# Run integration tests for auth router
pnpm test:integration -- src/__tests__/integration/routers/auth.test.ts
# Run auth E2E flow
pnpm test:e2e -- e2e/auth/profile.spec.ts
3. Practice Router#
Spec Reference : docs/specs/backend-api.md Section 3
File : src/server/api/routers/practice.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
createPracticeSchema.valid
{ name: "Smile Dental", type: "DENTAL_CLINIC" }
Passes validation
src/__tests__/unit/schemas/practice.test.ts
createPracticeSchema.nameTooShort
{ name: "A", type: "CLINIC" }
Zod error: min 2 chars
src/__tests__/unit/schemas/practice.test.ts
createPracticeSchema.invalidType
{ name: "Test", type: "INVALID" }
Zod error: invalid enum value
src/__tests__/unit/schemas/practice.test.ts
createPracticeSchema.invalidSlug
{ name: "Test", slug: "invalid_slug!" }
Zod error: regex mismatch
src/__tests__/unit/schemas/practice.test.ts
updatePracticeSchema.validColor
{ id: "prac_123", primaryColor: "#3B82F6" }
Passes validation
src/__tests__/unit/schemas/practice.test.ts
updatePracticeSchema.invalidColor
{ id: "prac_123", primaryColor: "blue" }
Zod error: invalid hex format
src/__tests__/unit/schemas/practice.test.ts
updatePracticeSchema.invalidUrl
{ id: "prac_123", logoUrl: "not-url" }
Zod error: invalid URL
src/__tests__/unit/schemas/practice.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
practice.create.success
Authenticated user, no practice
Call practice.create with valid input
Practice created, user becomes OWNER
src/__tests__/integration/routers/practice.test.ts
practice.create.duplicateSlug
Existing practice with slug "test"
Call with same slug
Throws CONFLICT (409)
src/__tests__/integration/routers/practice.test.ts
practice.get.success
Practice + member relation
Call practice.get with valid ID
Returns practice with correct ID
src/__tests__/integration/routers/practice.test.ts
practice.get.notMember
Practice exists, user not member
Call practice.get
Throws FORBIDDEN (403)
src/__tests__/integration/routers/practice.test.ts
practice.update.success
Practice exists, user is OWNER
Call practice.update with name change
DB updated, returns updated practice
src/__tests__/integration/routers/practice.test.ts
practice.delete.success
Practice exists, user is OWNER
Call practice.delete
Practice soft-deleted or removed
src/__tests__/integration/routers/practice.test.ts
practice.delete.notOwner
Practice exists, user is EDITOR
Call practice.delete
Throws FORBIDDEN (403)
src/__tests__/integration/routers/practice.test.ts
practice.list
User member of 2 practices
Call practice.list
Returns array of 2 practices
src/__tests__/integration/routers/practice.test.ts
practice.inviteMember
Practice exists, user is OWNER
Call practice.inviteMember with email
Invite record created, returns inviteUrl
src/__tests__/integration/routers/practice.test.ts
practice.inviteMember.editorAttempt
Practice exists, user is EDITOR
Call practice.inviteMember
Throws FORBIDDEN (403)
src/__tests__/integration/routers/practice.test.ts
practice.removeMember
Practice with 2 members, OWNER caller
Call practice.removeMember with member userId
Member removed from practice
src/__tests__/integration/routers/practice.test.ts
practice.updateMemberRole
Practice with EDITOR member, OWNER caller
Call with role change to ADMIN
Role updated, returns updated member
src/__tests__/integration/routers/practice.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
practice.onboardingFlow
1. Sign up 2. Create practice 3. Verify dashboard
Practice visible in sidebar
e2e/practice/onboarding.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User table with 2+ users, Practice table with 1+ practice, PracticeMember table with role mappings
Required env vars : RESEND_API_KEY (for invite email mock)
Required external mocks : Resend API mock (200 OK + messageId)
Verification Commands#
# Run unit tests for practice schemas
pnpm test:unit -- src/__tests__/unit/schemas/practice.test.ts
# Run integration tests for practice router
pnpm test:integration -- src/__tests__/integration/routers/practice.test.ts
# Run practice onboarding E2E
pnpm test:e2e -- e2e/practice/onboarding.spec.ts
4. Location Router#
Spec Reference : docs/specs/backend-api.md Section 4
File : src/server/api/routers/location.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
createLocationSchema.valid
Full valid location input
Passes validation
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.shortAddress
{ address: "123" }
Zod error: min 5 chars
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.invalidEmail
{ email: "not-email" }
Zod error: invalid email
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.invalidUrl
{ website: "not-url" }
Zod error: invalid URL
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.phoneTooShort
{ phone: "12345" }
Zod error: min 10 chars
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.defaultCountry
Input without country
Country defaults to "IN"
src/__tests__/unit/schemas/location.test.ts
createLocationSchema.defaultLanguages
Input without languages
Languages defaults to ["English", "Hindi"]
src/__tests__/unit/schemas/location.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
location.create.success
Practice member context
Call location.create with valid input
Location created with practiceId set
src/__tests__/integration/routers/location.test.ts
location.create.invalidPractice
User not in practice
Call location.create
Throws FORBIDDEN (403) or BAD_REQUEST
src/__tests__/integration/routers/location.test.ts
location.get.success
Location exists in practice
Call location.get with valid ID
Returns location with all fields
src/__tests__/integration/routers/location.test.ts
location.get.wrongPractice
Location belongs to other practice
Call location.get
Throws FORBIDDEN (403)
src/__tests__/integration/routers/location.test.ts
location.update.success
Location exists
Call location.update with phone change
DB updated, returns updated location
src/__tests__/integration/routers/location.test.ts
location.delete.success
Location exists
Call location.delete
Location removed from DB
src/__tests__/integration/routers/location.test.ts
location.list
3 locations in practice
Call location.list
Returns array of 3 locations
src/__tests__/integration/routers/location.test.ts
location.setPrimary
2 locations, none primary
Call location.setPrimary on loc2
loc2.isPrimary = true, loc1.isPrimary = false
src/__tests__/integration/routers/location.test.ts
location.setPrimary.unsetPrevious
loc1 is primary, set loc2
Call location.setPrimary on loc2
Previous primary unset, new primary set
src/__tests__/integration/routers/location.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
location.crudFlow
1. Create location 2. Update phone 3. Set primary 4. Delete
Location list reflects changes
e2e/location/crud.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with 1+ member, Location table with 2+ locations for that practice
Required env vars : None
Required external mocks : None
Verification Commands#
# Run unit tests for location schemas
pnpm test:unit -- src/__tests__/unit/schemas/location.test.ts
# Run integration tests for location router
pnpm test:integration -- src/__tests__/integration/routers/location.test.ts
# Run location CRUD E2E
pnpm test:e2e -- e2e/location/crud.spec.ts
5. GBP Router#
Spec Reference : docs/specs/backend-api.md Section 5
File : src/server/api/routers/gbp.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
createGbpPostSchema.valid
{ practiceId, locationId, content, ctaType: "BOOK" }
Passes validation
src/__tests__/unit/schemas/gbp.test.ts
createGbpPostSchema.contentTooLong
{ content: "a".repeat(1501) }
Zod error: max 1500 chars
src/__tests__/unit/schemas/gbp.test.ts
createGbpPostSchema.tooManyMedia
{ mediaUrls: ["url1", ..., "url11"] }
Zod error: max 10 URLs
src/__tests__/unit/schemas/gbp.test.ts
createGbpPostSchema.invalidMediaUrl
{ mediaUrls: ["not-url"] }
Zod error: invalid URL
src/__tests__/unit/schemas/gbp.test.ts
createGbpPostSchema.invalidCta
{ ctaType: "INVALID" }
Zod error: invalid enum
src/__tests__/unit/schemas/gbp.test.ts
replyToReviewSchema.valid
{ reviewId, replyText: "Thank you!" }
Passes validation
src/__tests__/unit/schemas/gbp.test.ts
replyToReviewSchema.emptyReply
{ reviewId, replyText: "" }
Zod error: min 1 char
src/__tests__/unit/schemas/gbp.test.ts
replyToReviewSchema.replyTooLong
{ replyText: "a".repeat(2001) }
Zod error: max 2000 chars
src/__tests__/unit/schemas/gbp.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
gbp.getAuthUrl
Practice with no GBP connection
Call gbp.getAuthUrl
Returns valid Google OAuth URL with scopes
src/__tests__/integration/routers/gbp.test.ts
gbp.listAccounts
Practice with 1 linked GBP account
Call gbp.listAccounts
Returns array with 1 account
src/__tests__/integration/routers/gbp.test.ts
gbp.listAccounts.empty
Practice with no GBP account
Call gbp.listAccounts
Returns empty array
src/__tests__/integration/routers/gbp.test.ts
gbp.listLocations
Mock GBP API with 2 locations
Call gbp.listLocations
Returns 2 locations from mock
src/__tests__/integration/routers/gbp.test.ts
gbp.createPost
Mock GBP API ready
Call gbp.createPost with valid content
Post persisted, GBP API called with correct payload
src/__tests__/integration/routers/gbp.test.ts
gbp.createPost.gbpApiError
Mock GBP API returns 500
Call gbp.createPost
Throws INTERNAL_SERVER_ERROR (500) or handles gracefully
src/__tests__/integration/routers/gbp.test.ts
gbp.listPosts
3 posts in DB for location
Call gbp.listPosts
Returns array of 3 posts
src/__tests__/integration/routers/gbp.test.ts
gbp.listPosts.filtered
2 published, 1 draft
Call with status: "PUBLISHED"
Returns array of 2 posts
src/__tests__/integration/routers/gbp.test.ts
gbp.deletePost
Post exists in DB
Call gbp.deletePost
Post deleted from DB and GBP API
src/__tests__/integration/routers/gbp.test.ts
gbp.listReviews
Mock GBP API with 2 reviews
Call gbp.listReviews
Returns 2 reviews
src/__tests__/integration/routers/gbp.test.ts
gbp.replyToReview
Mock review exists
Call gbp.replyToReview with text
Reply sent to GBP API, review updated
src/__tests__/integration/routers/gbp.test.ts
gbp.replyToReview.aiGenerate
Mock review, generateAI: true
Call with AI flag
AI service called, generated reply sent to GBP
src/__tests__/integration/routers/gbp.test.ts
gbp.getInsights
Mock GBP API with insights data
Call gbp.getInsights with date range
Returns insights array
src/__tests__/integration/routers/gbp.test.ts
gbp.listQA
Mock GBP API with 2 QAs
Call gbp.listQA
Returns 2 QAs
src/__tests__/integration/routers/gbp.test.ts
gbp.answerQuestion
Mock QA exists
Call gbp.answerQuestion with answer
Answer sent to GBP API, QA updated
src/__tests__/integration/routers/gbp.test.ts
gbp.uploadPhoto
Mock GBP API ready
Call gbp.uploadPhoto with URL
Photo uploaded to GBP, record created
src/__tests__/integration/routers/gbp.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
gbp.connectAndPost
1. Connect GBP 2. List locations 3. Create post 4. Verify post
Post visible in GBP list
e2e/gbp/connect-and-post.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with GBP account connection, GbpAccount with valid tokens
Required env vars : GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GBP_API_KEY
Required external mocks : GBP API mock (locations, posts, reviews, insights, Q&A, photos), Google OAuth mock, AI service mock for generateAI
Verification Commands#
# Run unit tests for GBP schemas
pnpm test:unit -- src/__tests__/unit/schemas/gbp.test.ts
# Run integration tests for GBP router
pnpm test:integration -- src/__tests__/integration/routers/gbp.test.ts
# Run GBP connect-and-post E2E
pnpm test:e2e -- e2e/gbp/connect-and-post.spec.ts
6. Social Router#
Spec Reference : docs/specs/backend-api.md Section 6
File : src/server/api/routers/social.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
createSocialPostSchema.valid
{ socialAccountId, content: "Hello!" }
Passes validation
src/__tests__/unit/schemas/social.test.ts
createSocialPostSchema.contentTooLong
{ content: "a".repeat(5001) }
Zod error: max 5000 chars
src/__tests__/unit/schemas/social.test.ts
createSocialPostSchema.tooManyMedia
{ mediaUrls: Array(11).fill("url") }
Zod error: max 10 URLs
src/__tests__/unit/schemas/social.test.ts
createSocialPostSchema.invalidMediaType
{ mediaType: "INVALID" }
Zod error: invalid enum
src/__tests__/unit/schemas/social.test.ts
createSocialPostSchema.invalidLinkUrl
{ linkUrl: "not-url" }
Zod error: invalid URL
src/__tests__/unit/schemas/social.test.ts
schedulePostsSchema.valid
{ posts: [{ platform: "FACEBOOK", content: "Hi", scheduledFor: "2026-01-01T00:00:00Z" }] }
Passes validation
src/__tests__/unit/schemas/social.test.ts
schedulePostsSchema.invalidPlatform
{ posts: [{ platform: "TIKTOK" }] }
Zod error: invalid platform enum
src/__tests__/unit/schemas/social.test.ts
schedulePostsSchema.invalidDate
{ posts: [{ scheduledFor: "not-a-date" }] }
Zod error: invalid datetime
src/__tests__/unit/schemas/social.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
social.getAuthUrl
Platform = "FACEBOOK"
Call social.getAuthUrl
Returns valid platform OAuth URL
src/__tests__/integration/routers/social.test.ts
social.listAccounts
Practice with 2 connected accounts
Call social.listAccounts
Returns 2 accounts
src/__tests__/integration/routers/social.test.ts
social.disconnect
Account exists
Call social.disconnect with accountId
Account marked inactive or deleted
src/__tests__/integration/routers/social.test.ts
social.createPost
Connected account ready
Call social.createPost with content
Post persisted to DB
src/__tests__/integration/routers/social.test.ts
social.listPosts
3 posts in DB
Call social.listPosts
Returns 3 posts
src/__tests__/integration/routers/social.test.ts
social.listPosts.filtered
2 for account1, 1 for account2
Call with accountId
Returns filtered posts
src/__tests__/integration/routers/social.test.ts
social.deletePost
Post exists
Call social.deletePost
Post removed from DB and platform API
src/__tests__/integration/routers/social.test.ts
social.schedulePosts
Multiple posts with future dates
Call social.schedulePosts
Posts created with SCHEDULED status, job queued
src/__tests__/integration/routers/social.test.ts
social.schedulePosts.platformApi
Mock platform API ready
Call with valid posts
Each platform API called with correct payload
src/__tests__/integration/routers/social.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
social.connectAndSchedule
1. Connect Facebook 2. Create post 3. Schedule post
Post appears in scheduled list
e2e/social/connect-and-schedule.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with SocialAccount records for 1+ platforms
Required env vars : FACEBOOK_APP_ID, INSTAGRAM_APP_ID, LINKEDIN_CLIENT_ID, TWITTER_CLIENT_ID
Required external mocks : Platform OAuth mocks (Facebook, Instagram, LinkedIn, Twitter), Platform API mocks for post creation/deletion
Verification Commands#
# Run unit tests for social schemas
pnpm test:unit -- src/__tests__/unit/schemas/social.test.ts
# Run integration tests for social router
pnpm test:integration -- src/__tests__/integration/routers/social.test.ts
# Run social connect-and-schedule E2E
pnpm test:e2e -- e2e/social/connect-and-schedule.spec.ts
7. Content Router#
Spec Reference : docs/specs/backend-api.md Section 7
File : src/server/api/routers/content.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
generateContentSchema.valid
{ taskType: "GBP_POST", variables: {} }
Passes validation
src/__tests__/unit/schemas/content.test.ts
generateContentSchema.invalidTaskType
{ taskType: "INVALID_TASK" }
Zod error: invalid enum
src/__tests__/unit/schemas/content.test.ts
generateContentSchema.validModel
{ model: "claude-sonnet" }
Passes validation
src/__tests__/unit/schemas/content.test.ts
generateContentSchema.invalidModel
{ model: "gpt-3" }
Zod error: invalid model enum
src/__tests__/unit/schemas/content.test.ts
updateContentSchema.valid
{ id: "cnt_123", title: "New Title" }
Passes validation
src/__tests__/unit/schemas/content.test.ts
updateContentSchema.seoFields
{ id: "cnt_123", seoTitle: "Title", seoDescription: "Desc", focusKeywords: ["kw1"] }
Passes validation
src/__tests__/unit/schemas/content.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
content.generate.success
Mock AI service ready
Call content.generate with taskType: "GBP_POST"
ContentPiece created with status "PENDING" or "DRAFT"
src/__tests__/integration/routers/content.test.ts
content.generate.aiService
Mock AI service returns content
Call with valid variables
AI service called with correct prompt template
src/__tests__/integration/routers/content.test.ts
content.generate.rateLimit
5 calls in last minute
6th call to content.generate
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/routers/content.test.ts
content.list
3 content pieces in practice
Call content.list
Returns 3 content pieces
src/__tests__/integration/routers/content.test.ts
content.list.filteredByStatus
2 DRAFT, 1 APPROVED
Call with status: "DRAFT"
Returns 2 DRAFT pieces
src/__tests__/integration/routers/content.test.ts
content.list.filteredByType
2 GBP_POST, 1 BLOG_POST
Call with type: "GBP_POST"
Returns 2 GBP_POST pieces
src/__tests__/integration/routers/content.test.ts
content.get
Content piece exists
Call content.get with ID
Returns content piece with all fields
src/__tests__/integration/routers/content.test.ts
content.update
Content piece exists
Call content.update with title change
DB updated, returns updated piece
src/__tests__/integration/routers/content.test.ts
content.approve
Content in "PENDING" status
Call content.approve
Status changed to "APPROVED"
src/__tests__/integration/routers/content.test.ts
content.reject
Content in "PENDING" status
Call content.reject with reason
Status changed to "REJECTED", reason stored
src/__tests__/integration/routers/content.test.ts
content.delete
Content piece exists
Call content.delete
Content removed from DB
src/__tests__/integration/routers/content.test.ts
content.delete.published
Content in "PUBLISHED" status
Call content.delete
Throws CONFLICT or requires unpublish first
src/__tests__/integration/routers/content.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
content.generateAndApprove
1. Generate content 2. Review 3. Approve 4. Publish
Content status = PUBLISHED
e2e/content/generate-and-approve.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with ContentPiece records, PromptTemplate for each taskType
Required env vars : ANTHROPIC_API_KEY, OPENAI_API_KEY
Required external mocks : Claude API mock, GPT-4o API mock
Verification Commands#
# Run unit tests for content schemas
pnpm test:unit -- src/__tests__/unit/schemas/content.test.ts
# Run integration tests for content router
pnpm test:integration -- src/__tests__/integration/routers/content.test.ts
# Run content generate-and-approve E2E
pnpm test:e2e -- e2e/content/generate-and-approve.spec.ts
8. Citation Router#
Spec Reference : docs/specs/backend-api.md Section 8
File : src/server/api/routers/citation.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
submitCitationsSchema.valid
{ locationId: "loc_123", directoryNames: ["justdial", "practo"] }
Passes validation
src/__tests__/unit/schemas/citation.test.ts
submitCitationsSchema.emptyDirectories
{ directoryNames: [] }
Zod error: min 1 directory
src/__tests__/unit/schemas/citation.test.ts
submitCitationsSchema.tooManyDirectories
{ directoryNames: Array(51).fill("dir") }
Zod error: max 50 directories
src/__tests__/unit/schemas/citation.test.ts
citationActionSchema.valid
{ citationId: "cit_123" }
Passes validation
src/__tests__/unit/schemas/citation.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
citation.listDirectories
5 directories in DB
Call citation.listDirectories
Returns 5 directories
src/__tests__/integration/routers/citation.test.ts
citation.list
3 citations for location
Call citation.list with locationId
Returns 3 citations
src/__tests__/integration/routers/citation.test.ts
citation.submit
Location with valid NAP
Call citation.submit with 2 directories
Job created, returns { jobId }
src/__tests__/integration/routers/citation.test.ts
citation.submit.invalidDirectory
Location valid
Call with non-existent directory name
Returns error or skips invalid directory
src/__tests__/integration/routers/citation.test.ts
citation.verifyNap
Citation with pending status
Call citation.verifyNap
Job created for NAP verification
src/__tests__/integration/routers/citation.test.ts
citation.delete
Citation exists
Call citation.delete
Job created for deletion, citation marked deleting
src/__tests__/integration/routers/citation.test.ts
citation.getSnapshot
Citation with snapshot data
Call citation.getSnapshot
Returns citation with NAP snapshot
src/__tests__/integration/routers/citation.test.ts
citation.getSnapshot.notFound
Non-existent citation
Call citation.getSnapshot
Throws NOT_FOUND (404)
src/__tests__/integration/routers/citation.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
citation.submitAndVerify
1. List directories 2. Submit to 2 dirs 3. Verify NAP
Citations show SUBMITTED/VERIFIED
e2e/citation/submit-and-verify.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with Location, CitationDirectory seed data, Citation records
Required env vars : FIRECRAWL_API_KEY (for NAP verification mock)
Required external mocks : Firecrawl API mock (NAP matching), Directory submission API mocks (Justdial, Practo, etc.)
Verification Commands#
# Run unit tests for citation schemas
pnpm test:unit -- src/__tests__/unit/schemas/citation.test.ts
# Run integration tests for citation router
pnpm test:integration -- src/__tests__/integration/routers/citation.test.ts
# Run citation submit-and-verify E2E
pnpm test:e2e -- e2e/citation/submit-and-verify.spec.ts
9. DirectoryProfile Router#
Spec Reference : docs/specs/backend-api.md Section 9
File : src/server/api/routers/directory-profile.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
updateSectionSchema.valid
{ sectionKey: "hero", content: "Welcome!" }
Passes validation
src/__tests__/unit/schemas/site.test.ts
updateSectionSchema.invalidKey
{ sectionKey: "invalid" }
Zod error: invalid enum
src/__tests__/unit/schemas/site.test.ts
updateSectionSchema.contentTooLong
{ content: "a".repeat(50001) }
Zod error: max 50000 chars
src/__tests__/unit/schemas/site.test.ts
updateSectionSchema.tooManyMedia
{ mediaUrls: Array(21).fill("url") }
Zod error: max 20 URLs
src/__tests__/unit/schemas/site.test.ts
setTemplateSchema.valid
{ templateId: "medical-modern" }
Passes validation
src/__tests__/unit/schemas/site.test.ts
setTemplateSchema.invalid
{ templateId: "invalid-template" }
Zod error: invalid enum
src/__tests__/unit/schemas/site.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
directoryProfile.get
Practice with profile sections
Call directoryProfile.get
Returns practice + all profile sections
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.updateSection
Practice with "hero" section
Call directoryProfile.updateSection with new content
Section updated in DB, returns updated section
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.setVisibility
Section visible = true
Call directoryProfile.setVisibility with isVisible: false
Section visibility toggled
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.reorderSections
3 sections: [hero, about, services]
Call with [about, hero, services]
DB updated with new order, returns reordered sections
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.reorderSections.invalid
3 sections
Call with [hero, about] (missing 1)
Throws BAD_REQUEST (400)
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.setTemplate
Practice on "medical-modern"
Call with templateId: "dental-clean"
Practice template updated
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.publish
Practice with valid sections
Call directoryProfile.publish
Site deployed, returns published URL
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.publish.noPrimaryDomain
Practice with no directory profile URL
Call directoryProfile.publish
Returns directorySlug URL (e.g., practice.rankflow.site)
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.unpublish
Published profile
Call directoryProfile.unpublish
Site taken down, returns { success: true }
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.setDirectoryUrl
Practice with domain "example.com"
Call directoryProfile.setDirectoryUrl
Returns directory profile paths for verification
src/__tests__/integration/routers/directory-profile.test.ts
directoryProfile.regenerate
Practice with AI-generated sections
Call directoryProfile.regenerate
Job created for AI regeneration, returns { jobId }
src/__tests__/integration/routers/directory-profile.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
site.buildAndPublish
1. Update sections 2. Set template 3. Publish 4. Visit URL
Site live at public URL
e2e/profile/profile-publish.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with DirectoryProfileSection records for all 12 sections, DirectoryProfileTemplate seed data
Required env vars : VERCEL_TOKEN or NETLIFY_TOKEN (for deploy mock), ANTHROPIC_API_KEY (for regeneration)
Required external mocks : Vercel/Netlify deploy API mock, AI service mock for regeneration
Verification Commands#
# Run unit tests for site schemas
pnpm test:unit -- src/__tests__/unit/schemas/site.test.ts
# Run integration tests for site router
pnpm test:integration -- src/__tests__/integration/routers/directory-profile.test.ts
# Run site build-and-publish E2E
pnpm test:e2e -- e2e/profile/profile-publish.spec.ts
10. Report Router#
Spec Reference : docs/specs/backend-api.md Section 10
File : src/server/api/routers/report.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
generateReportSchema.valid
{ periodStart: "2026-01-01T00:00:00Z", periodEnd: "2026-01-31T23:59:59Z" }
Passes validation
src/__tests__/unit/schemas/report.test.ts
generateReportSchema.invalidStart
{ periodStart: "not-a-date" }
Zod error: invalid datetime
src/__tests__/unit/schemas/report.test.ts
generateReportSchema.endBeforeStart
{ periodStart: "2026-02-01", periodEnd: "2026-01-01" }
Zod error: end must be after start
src/__tests__/unit/schemas/report.test.ts
emailReportSchema.valid
{ id: "rep_123", recipients: ["a@b.com", "c@d.com"] }
Passes validation
src/__tests__/unit/schemas/report.test.ts
emailReportSchema.invalidEmail
{ recipients: ["not-email"] }
Zod error: invalid email
src/__tests__/unit/schemas/report.test.ts
emailReportSchema.tooManyRecipients
{ recipients: Array(11).fill("a@b.com") }
Zod error: max 10 recipients
src/__tests__/unit/schemas/report.test.ts
emailReportSchema.emptyRecipients
{ recipients: [] }
Zod error: min 1 recipient
src/__tests__/unit/schemas/report.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
report.list
3 reports in practice
Call report.list
Returns array of 3 reports
src/__tests__/integration/routers/report.test.ts
report.get
Report exists
Call report.get with ID
Returns report with data and pdfUrl
src/__tests__/integration/routers/report.test.ts
report.get.notFound
Non-existent report
Call report.get
Throws NOT_FOUND (404)
src/__tests__/integration/routers/report.test.ts
report.generate
Valid date range
Call report.generate
Job created, returns { jobId }
src/__tests__/integration/routers/report.test.ts
report.generate.periodValidation
periodEnd before periodStart
Call with invalid range
Throws BAD_REQUEST (400)
src/__tests__/integration/routers/report.test.ts
report.downloadPdf
Report with pdfUrl
Call report.downloadPdf
Returns signed URL or direct download
src/__tests__/integration/routers/report.test.ts
report.downloadPdf.notReady
Report in "PENDING" status
Call report.downloadPdf
Throws BAD_REQUEST or returns 202
src/__tests__/integration/routers/report.test.ts
report.email
Report ready, 2 valid emails
Call report.email with recipients
Emails sent via Resend mock, returns { success: true }
src/__tests__/integration/routers/report.test.ts
report.email.resendFailure
Mock Resend returns 500
Call report.email
Returns { success: false } or throws
src/__tests__/integration/routers/report.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
report.generateAndDownload
1. Generate report 2. Wait for job 3. Download PDF
PDF file downloaded successfully
e2e/report/generate-and-download.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with Report records, Job records for report generation
Required env vars : RESEND_API_KEY, PDF_GENERATION_SERVICE_URL
Required external mocks : Resend API mock, PDF generation service mock, BullMQ job processor mock
Verification Commands#
# Run unit tests for report schemas
pnpm test:unit -- src/__tests__/unit/schemas/report.test.ts
# Run integration tests for report router
pnpm test:integration -- src/__tests__/integration/routers/report.test.ts
# Run report generate-and-download E2E
pnpm test:e2e -- e2e/report/generate-and-download.spec.ts
11. Lead Router#
Spec Reference : docs/specs/backend-api.md Section 11
File : src/server/api/routers/lead.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
updateLeadStatusSchema.valid
{ id: "ld_123", status: "CONVERTED" }
Passes validation
src/__tests__/unit/schemas/lead.test.ts
updateLeadStatusSchema.invalidStatus
{ status: "INVALID" }
Zod error: invalid enum
src/__tests__/unit/schemas/lead.test.ts
updateLeadStatusSchema.missingId
{ status: "NEW" }
Zod error: required id
src/__tests__/unit/schemas/lead.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
lead.list
4 leads in practice (2 NEW, 1 CONTACTED, 1 CONVERTED)
Call lead.list
Returns 4 leads
src/__tests__/integration/routers/lead.test.ts
lead.list.filtered
4 leads with mixed statuses
Call with status: "NEW"
Returns 2 NEW leads
src/__tests__/integration/routers/lead.test.ts
lead.get
Lead exists
Call lead.get with ID
Returns lead with all fields
src/__tests__/integration/routers/lead.test.ts
lead.get.wrongPractice
Lead belongs to other practice
Call lead.get
Throws FORBIDDEN (403)
src/__tests__/integration/routers/lead.test.ts
lead.updateStatus
Lead in "NEW" status
Call lead.updateStatus with "CONTACTED"
Status updated, returns updated lead
src/__tests__/integration/routers/lead.test.ts
lead.updateStatus.invalid
Lead exists
Call with status "INVALID"
Throws BAD_REQUEST (400)
src/__tests__/integration/routers/lead.test.ts
lead.delete
Lead exists
Call lead.delete
Lead removed from DB
src/__tests__/integration/routers/lead.test.ts
lead.delete.notFound
Non-existent lead
Call lead.delete
Throws NOT_FOUND (404)
src/__tests__/integration/routers/lead.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
lead.managePipeline
1. List leads 2. Update status to CONTACTED 3. Convert lead
Lead status = CONVERTED
e2e/lead/manage-pipeline.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with Lead records (4+ leads with mixed statuses)
Required env vars : None
Required external mocks : None
Verification Commands#
# Run unit tests for lead schemas
pnpm test:unit -- src/__tests__/unit/schemas/lead.test.ts
# Run integration tests for lead router
pnpm test:integration -- src/__tests__/integration/routers/lead.test.ts
# Run lead manage-pipeline E2E
pnpm test:e2e -- e2e/lead/manage-pipeline.spec.ts
12. Billing Router#
Spec Reference : docs/specs/backend-api.md Section 12
File : src/server/api/routers/billing.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
createSubscriptionSchema.valid
{ plan: "STANDARD", paymentMethod: "stripe" }
Passes validation
src/__tests__/unit/schemas/billing.test.ts
createSubscriptionSchema.invalidPlan
{ plan: "FREE" }
Zod error: invalid enum
src/__tests__/unit/schemas/billing.test.ts
createSubscriptionSchema.invalidMethod
{ paymentMethod: "paypal" }
Zod error: invalid enum
src/__tests__/unit/schemas/billing.test.ts
createSubscriptionSchema.lowercasePlan
{ plan: "starter" }
Zod error: invalid enum (case sensitive)
src/__tests__/unit/schemas/billing.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
billing.getPlan
Practice on STANDARD plan
Call billing.getPlan
Returns { tier: "STANDARD", features: [...] }
src/__tests__/integration/routers/billing.test.ts
billing.getPlan.trial
Practice in trial mode
Call billing.getPlan
Returns trial info with expiry date
src/__tests__/integration/routers/billing.test.ts
billing.createSubscription.stripe
Mock Stripe ready
Call with plan: "PREMIUM", paymentMethod: "stripe"
Stripe subscription created, returns clientSecret
src/__tests__/integration/routers/billing.test.ts
billing.createSubscription.razorpay
Mock Razorpay ready
Call with plan: "STANDARD", paymentMethod: "razorpay"
Razorpay subscription created, returns clientSecret
src/__tests__/integration/routers/billing.test.ts
billing.createSubscription.cardDeclined
Mock Stripe returns card_declined
Call with valid plan
Returns error or handles gracefully
src/__tests__/integration/routers/billing.test.ts
billing.cancel
Active subscription
Call billing.cancel
Subscription cancelled, returns { success, effectiveDate }
src/__tests__/integration/routers/billing.test.ts
billing.cancel.alreadyCancelled
Cancelled subscription
Call billing.cancel
Returns error or { success: false }
src/__tests__/integration/routers/billing.test.ts
billing.listInvoices
3 invoices for practice
Call billing.listInvoices
Returns 3 invoices
src/__tests__/integration/routers/billing.test.ts
billing.getInvoice
Invoice exists
Call billing.getInvoice with ID
Returns invoice with line items
src/__tests__/integration/routers/billing.test.ts
billing.updatePaymentMethod
Mock Stripe with new payment method
Call billing.updatePaymentMethod with methodId
Payment method updated, returns { success: true }
src/__tests__/integration/routers/billing.test.ts
billing.updatePaymentMethod.invalid
Invalid payment method ID
Call with invalid ID
Returns { success: false }
src/__tests__/integration/routers/billing.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
billing.subscribeAndCancel
1. Get plan 2. Create subscription 3. Cancel
Subscription status = cancelled
e2e/billing/subscribe-and-cancel.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with Billing record, Invoice records (3+), Subscription record
Required env vars : STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET
Required external mocks : Stripe API mock (subscriptions, payment methods, invoices), Razorpay API mock
Verification Commands#
# Run unit tests for billing schemas
pnpm test:unit -- src/__tests__/unit/schemas/billing.test.ts
# Run integration tests for billing router
pnpm test:integration -- src/__tests__/integration/routers/billing.test.ts
# Run billing subscribe-and-cancel E2E
pnpm test:e2e -- e2e/billing/subscribe-and-cancel.spec.ts
13. Skill Router#
Spec Reference : docs/specs/backend-api.md Section 13
File : src/server/api/routers/skill.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
executeSkillSchema.valid
{ skillId: "sk_123", payload: {}, async: false }
Passes validation
src/__tests__/unit/schemas/skill.test.ts
executeSkillSchema.defaultAsync
{ skillId: "sk_123" }
async defaults to false
src/__tests__/unit/schemas/skill.test.ts
executeSkillSchema.defaultPayload
{ skillId: "sk_123" }
payload defaults to {}
src/__tests__/unit/schemas/skill.test.ts
executeAdminSchema.valid
{ skillId: "sk_123", practiceId: "prac_123", payload: {} }
Passes validation
src/__tests__/unit/schemas/skill.test.ts
executeAdminSchema.missingPracticeId
{ skillId: "sk_123" }
Zod error: required practiceId
src/__tests__/unit/schemas/skill.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
skill.list
5 skills in DB
Call skill.list
Returns 5 skill summaries
src/__tests__/integration/routers/skill.test.ts
skill.list.protected
No auth
Call skill.list without session
Throws UNAUTHORIZED (401)
src/__tests__/integration/routers/skill.test.ts
skill.get
Skill exists
Call skill.get with ID
Returns full skill definition
src/__tests__/integration/routers/skill.test.ts
skill.get.notFound
Non-existent skill
Call skill.get
Throws NOT_FOUND (404)
src/__tests__/integration/routers/skill.test.ts
skill.execute.success
Mock skill processor ready
Call skill.execute with valid skillId
Returns ExecutionResult with status "SUCCESS"
src/__tests__/integration/routers/skill.test.ts
skill.execute.async
Mock skill processor ready
Call skill.execute with async: true
Returns ExecutionResult with status "QUEUED" and jobId
src/__tests__/integration/routers/skill.test.ts
skill.execute.rateLimit
10 calls in last minute
11th call to skill.execute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/routers/skill.test.ts
skill.execute.invalidSkill
Non-existent skillId
Call skill.execute
Throws NOT_FOUND (404)
src/__tests__/integration/routers/skill.test.ts
skill.executeAdmin
Admin user, mock skill ready
Call skill.executeAdmin with practiceId
Returns ExecutionResult with status "SUCCESS"
src/__tests__/integration/routers/skill.test.ts
skill.executeAdmin.nonAdmin
Non-admin user
Call skill.executeAdmin
Throws FORBIDDEN (403)
src/__tests__/integration/routers/skill.test.ts
skill.getStatus
Job in "RUNNING" status
Call skill.getStatus with jobId
Returns ExecutionResult with current status
src/__tests__/integration/routers/skill.test.ts
skill.getStatus.completed
Job completed with data
Call skill.getStatus
Returns ExecutionResult with status "SUCCESS" and data
src/__tests__/integration/routers/skill.test.ts
skill.getStatus.notFound
Non-existent jobId
Call skill.getStatus
Throws NOT_FOUND (404)
src/__tests__/integration/routers/skill.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
skill.executeAndCheckStatus
1. Execute async skill 2. Poll status 3. Verify completion
Status transitions from QUEUED → SUCCESS
e2e/skill/execute-and-check.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Skill table with 5+ skill definitions, Job table with job records
Required env vars : SKILL_EXECUTION_TIMEOUT_MS (default: 30000)
Required external mocks : Skill execution engine mock (Composio or internal)
Verification Commands#
# Run unit tests for skill schemas
pnpm test:unit -- src/__tests__/unit/schemas/skill.test.ts
# Run integration tests for skill router
pnpm test:integration -- src/__tests__/integration/routers/skill.test.ts
# Run skill execute-and-check E2E
pnpm test:e2e -- e2e/skill/execute-and-check.spec.ts
14. Admin Router#
Spec Reference : docs/specs/backend-api.md Section 14
File : src/server/api/routers/admin.ts
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
updateClientSchema.valid
{ id: "prac_123", status: "ACTIVE", tier: "PREMIUM" }
Passes validation
src/__tests__/unit/schemas/admin.test.ts
updateClientSchema.invalidStatus
{ status: "DELETED" }
Zod error: invalid enum
src/__tests__/unit/schemas/admin.test.ts
updateClientSchema.invalidTier
{ tier: "BASIC" }
Zod error: invalid enum
src/__tests__/unit/schemas/admin.test.ts
updateClientSchema.datetime
{ trialEndsAt: "2026-12-31T23:59:59Z" }
Passes validation
src/__tests__/unit/schemas/admin.test.ts
updateClientSchema.invalidDate
{ trialEndsAt: "not-a-date" }
Zod error: invalid datetime
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.valid
{ taskType: "GBP_POST", systemPrompt: "...", userPromptTemplate: "..." }
Passes validation
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.systemTooLong
{ systemPrompt: "a".repeat(10001) }
Zod error: max 10000 chars
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.templateTooLong
{ userPromptTemplate: "a".repeat(20001) }
Zod error: max 20000 chars
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.modelConfig
{ modelConfig: { provider: "anthropic", model: "claude-sonnet", temperature: 0.7, maxTokens: 2000 } }
Passes validation
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.invalidProvider
{ modelConfig: { provider: "google" } }
Zod error: invalid provider enum
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.tempOutOfRange
{ modelConfig: { temperature: 3 } }
Zod error: max 2
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.tokensTooLow
{ modelConfig: { maxTokens: 50 } }
Zod error: min 100
src/__tests__/unit/schemas/admin.test.ts
updatePromptSchema.tokensTooHigh
{ modelConfig: { maxTokens: 9000 } }
Zod error: max 8000
src/__tests__/unit/schemas/admin.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
admin.getKPIs
5 practices, 10 jobs, revenue data
Call admin.getKPIs
Returns KPIs with counts and revenue
src/__tests__/integration/routers/admin.test.ts
admin.getKPIs.nonAdmin
Non-admin user
Call admin.getKPIs
Throws FORBIDDEN (403)
src/__tests__/integration/routers/admin.test.ts
admin.listClients
10 practices in DB
Call admin.listClients
Returns 10 practices
src/__tests__/integration/routers/admin.test.ts
admin.listClients.filtered
5 ACTIVE, 5 TRIAL
Call with status: "ACTIVE"
Returns 5 ACTIVE practices
src/__tests__/integration/routers/admin.test.ts
admin.listClients.tierFilter
3 STARTER, 3 PREMIUM
Call with tier: "PREMIUM"
Returns 3 PREMIUM practices
src/__tests__/integration/routers/admin.test.ts
admin.getClient
Practice with locations, members, content
Call admin.getClient with ID
Returns practice with all relations
src/__tests__/integration/routers/admin.test.ts
admin.updateClient
Practice in TRIAL
Call admin.updateClient with status "ACTIVE"
DB updated, returns updated practice
src/__tests__/integration/routers/admin.test.ts
admin.updateClient.invalidStatus
Practice exists
Call with invalid status
Throws BAD_REQUEST (400)
src/__tests__/integration/routers/admin.test.ts
admin.listJobs
20 jobs in DB
Call admin.listJobs
Returns 20 jobs
src/__tests__/integration/routers/admin.test.ts
admin.listJobs.filtered
10 RUNNING, 10 COMPLETED
Call with status: "RUNNING"
Returns 10 RUNNING jobs
src/__tests__/integration/routers/admin.test.ts
admin.listJobs.practiceFilter
Jobs across 3 practices
Call with practiceId
Returns jobs for that practice only
src/__tests__/integration/routers/admin.test.ts
admin.retryJob
Job in FAILED status
Call admin.retryJob with jobId
Job status reset to PENDING, attempts reset
src/__tests__/integration/routers/admin.test.ts
admin.retryJob.notFailed
Job in RUNNING status
Call admin.retryJob
Returns error or no-op
src/__tests__/integration/routers/admin.test.ts
admin.listDomains
3 directory profile URLs
Call admin.listDomains
Returns domain status array
src/__tests__/integration/routers/admin.test.ts
admin.listSocialConnections
5 social accounts
Call admin.listSocialConnections
Returns all social accounts
src/__tests__/integration/routers/admin.test.ts
admin.listContent
50 content pieces
Call admin.listContent
Returns content array
src/__tests__/integration/routers/admin.test.ts
admin.listContent.filtered
25 PENDING, 25 APPROVED
Call with status: "PENDING"
Returns 25 PENDING
src/__tests__/integration/routers/admin.test.ts
admin.approveContent
Content in PENDING status
Call admin.approveContent with contentId
Status changed to APPROVED
src/__tests__/integration/routers/admin.test.ts
admin.updatePrompt
Existing prompt template
Call admin.updatePrompt with new system prompt
Prompt updated, returns updated template
src/__tests__/integration/routers/admin.test.ts
admin.getRevenue
Revenue data for period
Call admin.getRevenue with period: "2026-01"
Returns revenue breakdown
src/__tests__/integration/routers/admin.test.ts
admin.getSystemHealth
All services healthy
Call admin.getSystemHealth
Returns { status: "healthy", checks: [...] }
src/__tests__/integration/routers/admin.test.ts
admin.getSystemHealth.degraded
DB connection failing
Call admin.getSystemHealth
Returns { status: "degraded", failedChecks: [...] }
src/__tests__/integration/routers/admin.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
admin.dashboardOverview
1. Login as admin 2. View KPIs 3. List clients 4. Check health
All admin endpoints return 200
e2e/admin/dashboard.spec.ts
Success Criteria (Binary)#
admin.getKPIs requires adminProcedure (ADMIN role only)
admin.getKPIs returns aggregate counts (practices, jobs, revenue, active users)
admin.listClients supports filtering by status (TRIAL, ACTIVE, PAST_DUE, SUSPENDED, CANCELLED, EXPIRED)
admin.listClients supports filtering by tier (STARTER, STANDARD, PREMIUM, ENTERPRISE)
admin.getClient returns practice with all relations (locations, members, content, jobs)
admin.getClient returns 404 for non-existent practice ID
admin.updateClient validates status against 6 enum values
admin.updateClient validates tier against 4 enum values
admin.updateClient validates trialEndsAt and subscriptionEndsAt as ISO datetime
admin.updateClient persists changes to database
admin.listJobs supports filtering by status and practiceId
admin.retryJob resets FAILED job to PENDING status
admin.retryJob resets attempt count
admin.retryJob returns error for non-FAILED jobs
admin.listDomains returns all directory profile URL registrations with profile status
admin.listSocialConnections returns all social accounts across all practices
admin.listContent supports filtering by status
admin.approveContent changes content status to APPROVED
admin.updatePrompt validates systemPrompt max 10000 chars
admin.updatePrompt validates userPromptTemplate max 20000 chars
admin.updatePrompt validates modelConfig.provider as "anthropic" or "openai"
admin.updatePrompt validates modelConfig.temperature range 0-2
admin.updatePrompt validates modelConfig.maxTokens range 100-8000
admin.getRevenue returns revenue data for requested period
admin.getSystemHealth checks DB, Redis, external APIs
admin.getSystemHealth returns "healthy" when all checks pass
admin.getSystemHealth returns "degraded" when any check fails
Agent Context (Pre-conditions)#
Required DB state : Practice (10+ records), Job (20+ records), ContentPiece (50+), SocialAccount (5+), PromptTemplate (5+), Domain (3+), User with ADMIN role
Required env vars : All service health check endpoints configured
Required external mocks : None for admin router (internal aggregation)
Verification Commands#
# Run unit tests for admin schemas
pnpm test:unit -- src/__tests__/unit/schemas/admin.test.ts
# Run integration tests for admin router
pnpm test:integration -- src/__tests__/integration/routers/admin.test.ts
# Run admin dashboard E2E
pnpm test:e2e -- e2e/admin/dashboard.spec.ts
15. Procedure & Auth Tests#
Spec Reference : docs/specs/backend-api.md Section 15 — Middleware & Procedures
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
publicProcedure.allowsUnauthenticated
No session
Call public endpoint
Request allowed, handler executes
protectedProcedure.rejectsUnauthenticated
No session
Call protected endpoint
Throws UNAUTHORIZED (401)
protectedProcedure.allowsAuthenticated
Valid session
Call protected endpoint
Request allowed, ctx.session present
practiceProcedure.rejectsNoPractice
Valid session, no practice context
Call practice endpoint
Throws BAD_REQUEST (400) — missing practice ID
practiceProcedure.rejectsNonMember
Valid session, user not in practice
Call practice endpoint
Throws FORBIDDEN (403)
practiceProcedure.allowsViewer
Valid session, user is VIEWER
Call practice endpoint
Request allowed
practiceProcedure.rejectsEditorForOwnerOps
Valid session, user is EDITOR
Call owner-only endpoint (e.g., delete practice)
Throws FORBIDDEN (403)
adminProcedure.rejectsNonAdmin
Valid session, role = USER
Call admin endpoint
Throws FORBIDDEN (403)
adminProcedure.allowsAdmin
Valid session, role = ADMIN
Call admin endpoint
Request allowed
requestId.injection
Any request
Call any endpoint
Response includes x-request-id header
requestId.propagation
Request with ID
Call endpoint that triggers external API
External call includes X-RankFlow-Request-ID
Integration Tests#
Test
Setup
Action
Assertion
File
procedure.chain.public
No auth
Call health.check (publicProcedure)
Returns 200 OK
src/__tests__/integration/trpc/procedures.test.ts
procedure.chain.protected
Valid session
Call auth.me (protectedProcedure)
Returns user data
src/__tests__/integration/trpc/procedures.test.ts
procedure.chain.practice
Valid session + practice member
Call practice.get (practiceProcedure)
Returns practice data
src/__tests__/integration/trpc/procedures.test.ts
procedure.chain.admin
Admin session
Call admin.getKPIs (adminProcedure)
Returns KPI data
src/__tests__/integration/trpc/procedures.test.ts
procedure.chain.middlewareOrder
Any request
Call any endpoint
Middleware executes in order: Rate Limit → Request ID → Auth → Practice Resolve → Role Check
src/__tests__/integration/trpc/procedures.test.ts
context.dbInjection
Any request
Call any endpoint
ctx.db is Prisma client with correct connection
src/__tests__/integration/trpc/procedures.test.ts
context.redisInjection
Any request
Call any endpoint
ctx.redis is Redis client with correct connection
src/__tests__/integration/trpc/procedures.test.ts
context.sessionInjection
Authenticated request
Call protected endpoint
ctx.session contains user ID, email, role
src/__tests__/integration/trpc/procedures.test.ts
zodError.formatting
Invalid input to endpoint
Call with bad input
Response includes zodError.fieldErrors with field names
src/__tests__/integration/trpc/procedures.test.ts
zodError.flatten
Invalid input to endpoint
Call with bad input
zodError.formErrors is empty array, fieldErrors populated
src/__tests__/integration/trpc/procedures.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User (regular + admin), Practice with PracticeMember records for all roles
Required env vars : BETTER_AUTH_SECRET, REDIS_URL, DATABASE_URL
Required external mocks : None (internal middleware)
Verification Commands#
# Run procedure unit tests
pnpm test:unit -- src/__tests__/unit/trpc/procedures.test.ts
# Run procedure integration tests
pnpm test:integration -- src/__tests__/integration/trpc/procedures.test.ts
# Run all auth/procedure tests
pnpm test:unit -- src/__tests__/unit/trpc/
16. Rate Limiting Tests#
Spec Reference : docs/specs/backend-api.md Section 15 — Rate Limiting
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
rateLimit.config.skillExecute
{ route: "skill.execute", limit: 10, window: 60000 }
Config matches spec
src/__tests__/unit/rate-limit.test.ts
rateLimit.config.contentGenerate
{ route: "content.generate", limit: 5, window: 60000 }
Config matches spec
src/__tests__/unit/rate-limit.test.ts
rateLimit.config.gbpCreatePost
{ route: "gbp.createPost", limit: 30, window: 60000 }
Config matches spec
src/__tests__/unit/rate-limit.test.ts
rateLimit.config.socialCreatePost
{ route: "social.createPost", limit: 30, window: 60000 }
Config matches spec
src/__tests__/unit/rate-limit.test.ts
rateLimit.config.default
{ route: "any.other", limit: 100, window: 60000 }
Config matches spec
src/__tests__/unit/rate-limit.test.ts
rateLimit.keyGeneration
Request with user ID usr_123
Key includes usr_123 and route name
src/__tests__/unit/rate-limit.test.ts
rateLimit.keyAnonymous
Request with no session
Key includes IP address and route name
src/__tests__/unit/rate-limit.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
rateLimit.skillExecute.10req
Authenticated user
Make 10 calls to skill.execute within 1 minute
All 10 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.skillExecute.11req
Authenticated user
Make 11th call to skill.execute within 1 minute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.skillExecute.reset
Authenticated user, rate limited
Wait 1 minute, call skill.execute again
Request succeeds after window reset
src/__tests__/integration/rate-limit.test.ts
rateLimit.contentGenerate.5req
Authenticated user
Make 5 calls to content.generate within 1 minute
All 5 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.contentGenerate.6req
Authenticated user
Make 6th call to content.generate within 1 minute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.gbpCreatePost.30req
Authenticated user
Make 30 calls to gbp.createPost within 1 minute
All 30 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.gbpCreatePost.31req
Authenticated user
Make 31st call to gbp.createPost within 1 minute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.socialCreatePost.30req
Authenticated user
Make 30 calls to social.createPost within 1 minute
All 30 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.socialCreatePost.31req
Authenticated user
Make 31st call to social.createPost within 1 minute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.default.100req
Authenticated user
Make 100 calls to practice.get within 1 minute
All 100 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.default.101req
Authenticated user
Make 101st call to practice.get within 1 minute
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.responseHeaders
Rate limited request
Call skill.execute over limit
Response includes Retry-After header
src/__tests__/integration/rate-limit.test.ts
rateLimit.anonymousRequest
No session
Make 100 calls from same IP
All 100 succeed
src/__tests__/integration/rate-limit.test.ts
rateLimit.anonymousExceeded
No session
Make 101st call from same IP
Throws TOO_MANY_REQUESTS (429)
src/__tests__/integration/rate-limit.test.ts
rateLimit.perUserIsolation
User A at limit, User B fresh
User B makes request to skill.execute
User B succeeds despite User A being rate limited
src/__tests__/integration/rate-limit.test.ts
rateLimit.perRouteIsolation
skill.execute at limit
Call content.generate (different route)
Request succeeds (limits are per-route)
src/__tests__/integration/rate-limit.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User (2+ users for isolation tests)
Required env vars : REDIS_URL, RATE_LIMIT_ENABLED=true
Required external mocks : Redis instance (test container or mock)
Verification Commands#
# Run rate limiting unit tests
pnpm test:unit -- src/__tests__/unit/rate-limit.test.ts
# Run rate limiting integration tests
pnpm test:integration -- src/__tests__/integration/rate-limit.test.ts
# Run rate limit stress test
pnpm test:integration -- src/__tests__/integration/rate-limit.test.ts --grep "stress"
17. Global Verification Commands#
Run All Backend API Tests#
# 1. Run all unit tests (schemas, utilities, procedures)
pnpm test:unit
# 2. Run all integration tests (routers, DB, middleware)
pnpm test:integration
# 3. Run all contract tests (external API mocks)
pnpm test:contract
# 4. Run all E2E tests (critical flows)
pnpm test:e2e
# 5. Run router-specific integration tests
pnpm test:integration -- src/__tests__/integration/routers/auth.test.ts
pnpm test:integration -- src/__tests__/integration/routers/practice.test.ts
pnpm test:integration -- src/__tests__/integration/routers/location.test.ts
pnpm test:integration -- src/__tests__/integration/routers/gbp.test.ts
pnpm test:integration -- src/__tests__/integration/routers/social.test.ts
pnpm test:integration -- src/__tests__/integration/routers/content.test.ts
pnpm test:integration -- src/__tests__/integration/routers/citation.test.ts
pnpm test:integration -- src/__tests__/integration/routers/directory-profile.test.ts
pnpm test:integration -- src/__tests__/integration/routers/report.test.ts
pnpm test:integration -- src/__tests__/integration/routers/lead.test.ts
pnpm test:integration -- src/__tests__/integration/routers/billing.test.ts
pnpm test:integration -- src/__tests__/integration/routers/skill.test.ts
pnpm test:integration -- src/__tests__/integration/routers/admin.test.ts
# 6. Run all schema unit tests
pnpm test:unit -- src/__tests__/unit/schemas/
# 7. Run procedure and middleware tests
pnpm test:unit -- src/__tests__/unit/trpc/
pnpm test:integration -- src/__tests__/integration/trpc/
# 8. Run rate limiting tests
pnpm test:integration -- src/__tests__/integration/rate-limit.test.ts
# 9. Full backend API test suite
pnpm test -- src/__tests__/unit/schemas/ src/__tests__/unit/trpc/ src/__tests__/integration/routers/ src/__tests__/integration/trpc/ src/__tests__/integration/rate-limit.test.ts
Coverage Requirements#
Category
Minimum Coverage
Target Coverage
Zod Schemas
95%
100%
tRPC Procedures
90%
95%
Router Integration
85%
90%
Auth Middleware
95%
100%
Rate Limiting
90%
95%
Error Handling
85%
90%
End of Backend API Test Specification — RankFlow AI v1.0.0