diff --git a/src/tests/fixtures.test.ts b/src/tests/fixtures.test.ts index 83e3a9f..ef31016 100644 --- a/src/tests/fixtures.test.ts +++ b/src/tests/fixtures.test.ts @@ -9,7 +9,53 @@ import { import { arbitraryParsedEvent, arbitraryVaultCreatedEvent, - arbitraryMilestoneCreatedEvent + arbitraryMilestoneCreatedEvent, + arbitraryVaultStatus, + arbitraryValidationResult, + arbitraryVaultStatusEvent, + arbitraryEventWithVaultId, + arbitraryEventWithMilestoneId, + arbitraryConsistentEventId, + arbitraryVaultCompletedEvent, + arbitraryVaultFailedEvent, + arbitraryVaultCancelledEvent, + arbitraryMilestoneValidatedEvent, + arbitraryStellarAddress, + arbitraryTransactionHash, + arbitraryVaultId, + arbitraryMilestoneId, + arbitraryValidationId, + arbitraryAmount, + arbitraryEventId, + arbitraryLedgerNumber, + arbitraryEventIndex, + arbitraryFutureDate, + arbitraryPastDate, + arbitraryEvidenceHash, + arbitraryVaultCreatedPayload, + arbitraryVaultStatusPayload, + arbitraryMilestoneCreatedPayload, + arbitraryValidationPayload, + arbitraryProcessedEvent, + arbitraryFailedEvent, + arbitraryListenerState, + arbitraryMilestone, + arbitraryValidation, + arbitraryOrganizationId, + arbitraryTeamId, + arbitraryUserId, + arbitraryContractAddress, + arbitraryHorizonUrl, + arbitraryMilestoneStatus, + arbitraryUniqueVaultId, + arbitraryEventSequence, + arbitraryVaultEventSequence, + arbitraryEdgeCaseAmount, + arbitraryEdgeCaseString, + arbitrarySafeStellarAddress, + arbitraryInvalidStatusTransition, + setArbLogEnabled, + logArbGeneration } from './fixtures/arbitraries.js' import fc from 'fast-check' @@ -94,5 +140,472 @@ describe('Test Fixtures and Helpers', () => { { numRuns: 10 } ) }) + + it('should generate valid vault status', () => { + fc.assert( + fc.property(arbitraryVaultStatus(), (status: any) => { + expect(['active', 'completed', 'failed', 'cancelled']).toContain(status) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid validation result', () => { + fc.assert( + fc.property(arbitraryValidationResult(), (result: any) => { + expect(['approved', 'rejected', 'pending_review']).toContain(result) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault status events', () => { + fc.assert( + fc.property(arbitraryVaultStatusEvent(), (event: any) => { + expect(['vault_completed', 'vault_failed', 'vault_cancelled']).toContain(event.eventType) + expect(event.payload).toHaveProperty('vaultId') + expect(event.payload).toHaveProperty('status') + }), + { numRuns: 10 } + ) + }) + + it('should generate event with specific vault ID', () => { + const testVaultId = 'test-vault-123' + fc.assert( + fc.property(arbitraryEventWithVaultId(testVaultId), (event: any) => { + expect(event.payload.vaultId).toBe(testVaultId) + }), + { numRuns: 10 } + ) + }) + + it('should generate event with specific milestone ID', () => { + const testMilestoneId = 'test-milestone-456' + fc.assert( + fc.property(arbitraryEventWithMilestoneId(testMilestoneId), (event: any) => { + expect(event.payload.milestoneId).toBe(testMilestoneId) + }), + { numRuns: 10 } + ) + }) + + it('should generate consistent event ID from transaction hash and index', () => { + fc.assert( + fc.property(arbitraryConsistentEventId(), (result: any) => { + expect(result.eventId).toBe(`${result.transactionHash}:${result.eventIndex}`) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault completed events', () => { + fc.assert( + fc.property(arbitraryVaultCompletedEvent(), (event: any) => { + expect(event.eventType).toBe('vault_completed') + expect(event.payload.status).toBe('completed') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault failed events', () => { + fc.assert( + fc.property(arbitraryVaultFailedEvent(), (event: any) => { + expect(event.eventType).toBe('vault_failed') + expect(event.payload.status).toBe('failed') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault cancelled events', () => { + fc.assert( + fc.property(arbitraryVaultCancelledEvent(), (event: any) => { + expect(event.eventType).toBe('vault_cancelled') + expect(event.payload.status).toBe('cancelled') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid milestone validated events', () => { + fc.assert( + fc.property(arbitraryMilestoneValidatedEvent(), (event: any) => { + expect(event.eventType).toBe('milestone_validated') + expect(event.payload).toHaveProperty('validationId') + expect(event.payload).toHaveProperty('milestoneId') + expect(event.payload).toHaveProperty('validatorAddress') + expect(['approved', 'rejected', 'pending_review']).toContain(event.payload.validationResult) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid Stellar addresses', () => { + fc.assert( + fc.property(arbitraryStellarAddress(), (addr: string) => { + expect(addr).toMatch(/^G[A-Z0-9]{55}$/) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid transaction hashes', () => { + fc.assert( + fc.property(arbitraryTransactionHash(), (hash: string) => { + expect(hash).toMatch(/^[a-f0-9]{64}$/) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault IDs', () => { + fc.assert( + fc.property(arbitraryVaultId(), (id: string) => { + expect(id).toMatch(/^vault-/) + expect(id.length).toBeGreaterThanOrEqual(10) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid milestone IDs', () => { + fc.assert( + fc.property(arbitraryMilestoneId(), (id: string) => { + expect(id).toMatch(/^milestone-/) + expect(id.length).toBeGreaterThanOrEqual(10) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid validation IDs', () => { + fc.assert( + fc.property(arbitraryValidationId(), (id: string) => { + expect(id).toMatch(/^validation-/) + expect(id.length).toBeGreaterThanOrEqual(10) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid amounts', () => { + fc.assert( + fc.property(arbitraryAmount(), (amount: string) => { + expect(amount).toMatch(/^\d+\.\d{7}$/) + expect(parseFloat(amount)).toBeGreaterThan(0) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid event IDs', () => { + fc.assert( + fc.property(arbitraryEventId(), (id: string) => { + const parts = id.split(':') + expect(parts).toHaveLength(2) + expect(parts[0]).toMatch(/^[a-f0-9]{64}$/) + expect(parseInt(parts[1], 10)).toBeGreaterThanOrEqual(0) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid ledger numbers', () => { + fc.assert( + fc.property(arbitraryLedgerNumber(), (num: number) => { + expect(num).toBeGreaterThan(0) + expect(num).toBeLessThanOrEqual(10000000) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid event indices', () => { + fc.assert( + fc.property(arbitraryEventIndex(), (idx: number) => { + expect(idx).toBeGreaterThanOrEqual(0) + expect(idx).toBeLessThanOrEqual(100) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid future dates', () => { + fc.assert( + fc.property(arbitraryFutureDate(), (date: Date) => { + expect(date.getTime()).toBeGreaterThan(Date.now()) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid past dates', () => { + fc.assert( + fc.property(arbitraryPastDate(), (date: Date) => { + expect(date.getTime()).toBeLessThanOrEqual(Date.now()) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid evidence hashes', () => { + fc.assert( + fc.property(arbitraryEvidenceHash(), (hash: string) => { + expect(hash).toMatch(/^hash-/) + expect(hash.length).toBeGreaterThanOrEqual(37) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault created payloads', () => { + fc.assert( + fc.property(arbitraryVaultCreatedPayload(), (payload: any) => { + expect(payload).toHaveProperty('vaultId') + expect(payload).toHaveProperty('creator') + expect(payload).toHaveProperty('amount') + expect(payload).toHaveProperty('status') + expect(payload.status).toBe('active') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault status payloads', () => { + fc.assert( + fc.property(arbitraryVaultStatusPayload('completed'), (payload: any) => { + expect(payload).toHaveProperty('vaultId') + expect(payload).toHaveProperty('status') + expect(payload.status).toBe('completed') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid milestone created payloads', () => { + fc.assert( + fc.property(arbitraryMilestoneCreatedPayload(), (payload: any) => { + expect(payload).toHaveProperty('milestoneId') + expect(payload).toHaveProperty('vaultId') + expect(payload).toHaveProperty('title') + expect(payload).toHaveProperty('targetAmount') + expect(payload).toHaveProperty('deadline') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid validation payloads', () => { + fc.assert( + fc.property(arbitraryValidationPayload(), (payload: any) => { + expect(payload).toHaveProperty('validationId') + expect(payload).toHaveProperty('milestoneId') + expect(payload).toHaveProperty('validatorAddress') + expect(payload).toHaveProperty('validationResult') + expect(payload).toHaveProperty('evidenceHash') + expect(payload).toHaveProperty('validatedAt') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid ProcessedEvent', () => { + fc.assert( + fc.property(arbitraryProcessedEvent(), (event: any) => { + expect(event).toHaveProperty('eventId') + expect(event).toHaveProperty('transactionHash') + expect(event).toHaveProperty('eventIndex') + expect(event).toHaveProperty('ledgerNumber') + expect(event).toHaveProperty('processedAt') + expect(event).toHaveProperty('createdAt') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid FailedEvent', () => { + fc.assert( + fc.property(arbitraryFailedEvent(), (event: any) => { + expect(event).toHaveProperty('id') + expect(event).toHaveProperty('eventId') + expect(event).toHaveProperty('eventPayload') + expect(event).toHaveProperty('errorMessage') + expect(event).toHaveProperty('retryCount') + expect(event.retryCount).toBeGreaterThanOrEqual(0) + expect(event.retryCount).toBeLessThanOrEqual(5) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid ListenerState', () => { + fc.assert( + fc.property(arbitraryListenerState(), (state: any) => { + expect(state).toHaveProperty('id') + expect(state).toHaveProperty('serviceName') + expect(state).toHaveProperty('lastProcessedLedger') + expect(state).toHaveProperty('lastProcessedAt') + expect(state).toHaveProperty('createdAt') + expect(state).toHaveProperty('updatedAt') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid Milestone entity', () => { + fc.assert( + fc.property(arbitraryMilestone(), (milestone: any) => { + expect(milestone).toHaveProperty('id') + expect(milestone).toHaveProperty('vaultId') + expect(milestone).toHaveProperty('title') + expect(milestone).toHaveProperty('targetAmount') + expect(milestone).toHaveProperty('currentAmount') + expect(milestone).toHaveProperty('deadline') + expect(milestone).toHaveProperty('status') + expect(['pending', 'in_progress', 'completed', 'failed']).toContain(milestone.status) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid Validation entity', () => { + fc.assert( + fc.property(arbitraryValidation(), (validation: any) => { + expect(validation).toHaveProperty('id') + expect(validation).toHaveProperty('milestoneId') + expect(validation).toHaveProperty('validatorAddress') + expect(validation).toHaveProperty('validationResult') + expect(validation).toHaveProperty('validatedAt') + expect(validation).toHaveProperty('createdAt') + }), + { numRuns: 10 } + ) + }) + + it('should generate valid organization ID', () => { + fc.assert( + fc.property(arbitraryOrganizationId(), (id: string) => { + expect(id).toMatch(/^org-/) + expect(id.length).toBeGreaterThanOrEqual(14) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid team ID', () => { + fc.assert( + fc.property(arbitraryTeamId(), (id: string) => { + expect(id).toMatch(/^team-/) + expect(id.length).toBeGreaterThanOrEqual(15) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid user ID', () => { + fc.assert( + fc.property(arbitraryUserId(), (id: string) => { + expect(id).toMatch(/^user-/) + expect(id.length).toBeGreaterThanOrEqual(15) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid contract addresses', () => { + fc.assert( + fc.property(arbitraryContractAddress(), (addr: string) => { + expect(addr).toMatch(/^C[A-Z0-9]{54}$/) + expect(addr.length).toBe(55) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid Horizon URLs', () => { + fc.assert( + fc.property(arbitraryHorizonUrl(), (url: string) => { + expect(url).toMatch(/^https?:\/\//) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid milestone statuses', () => { + fc.assert( + fc.property(arbitraryMilestoneStatus(), (status: string) => { + expect(['pending', 'in_progress', 'completed', 'failed']).toContain(status) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid event sequences', () => { + fc.assert( + fc.property(arbitraryEventSequence(2, 5), (events: any[]) => { + expect(events.length).toBeGreaterThanOrEqual(2) + expect(events.length).toBeLessThanOrEqual(5) + events.forEach(e => { + expect(e).toHaveProperty('eventId') + expect(e).toHaveProperty('eventType') + }) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid vault event sequences', () => { + fc.assert( + fc.property(arbitraryVaultEventSequence(), (seq: any) => { + expect(seq.created.eventType).toBe('vault_created') + expect(seq.completed.eventType).toBe('vault_completed') + expect(seq.milestones.length).toBeGreaterThanOrEqual(1) + expect(seq.milestones.length).toBeLessThanOrEqual(5) + }), + { numRuns: 10 } + ) + }) + + it('should generate edge case amounts', () => { + fc.assert( + fc.property(arbitraryEdgeCaseAmount(), (amount: string) => { + expect(amount).toMatch(/^\d+\.\d{7}$/) + }), + { numRuns: 10 } + ) + }) + + it('should generate safe Stellar addresses without special chars', () => { + fc.assert( + fc.property(arbitrarySafeStellarAddress(), (addr: string) => { + expect(addr).toMatch(/^G[A-Z0-9]+$/) + }), + { numRuns: 10 } + ) + }) + + it('should generate valid status transitions with validity flag', () => { + fc.assert( + fc.property(arbitraryInvalidStatusTransition(), (trans: any) => { + expect(trans).toHaveProperty('fromStatus') + expect(trans).toHaveProperty('toStatus') + expect(trans).toHaveProperty('isValid') + }), + { numRuns: 10 } + ) + }) + + it('should enable arb logging when configured', () => { + setArbLogEnabled(true) + expect(() => setArbLogEnabled(false)).not.toThrow() + }) + + it('should log arb generation when enabled', () => { + setArbLogEnabled(true) + expect(() => logArbGeneration('test', 10)).not.toThrow() + setArbLogEnabled(false) + }) }) }) diff --git a/src/tests/fixtures/arbitraries.ts b/src/tests/fixtures/arbitraries.ts index 63a3e38..54cbbf3 100644 --- a/src/tests/fixtures/arbitraries.ts +++ b/src/tests/fixtures/arbitraries.ts @@ -1,20 +1,31 @@ import fc from 'fast-check' import { ParsedEvent, - EventType, VaultEventPayload, MilestoneEventPayload, ValidationEventPayload } from '../../types/horizonSync.js' -/** - * Fast-check arbitraries for property-based testing - * These generators create random valid events for testing universal properties - */ +let arbLoggingEnabled = false + +export const setArbLogEnabled = (enabled: boolean) => { + arbLoggingEnabled = enabled + if (enabled && process.env.NODE_ENV !== 'test') { + console.log('[arbitraries] Logging enabled') + } +} + +export const logArbGeneration = (arbName: string, numRuns: number) => { + if (arbLoggingEnabled && process.env.NODE_ENV !== 'test') { + console.log(`[arbitraries] Generated ${numRuns} samples for ${arbName}`) + } +} // Generate a valid Stellar address (56 characters starting with G) export const arbitraryStellarAddress = (): fc.Arbitrary => - fc.string({ minLength: 55, maxLength: 55 }).map(s => 'G' + s.toUpperCase()) + fc.string({ minLength: 55, maxLength: 55 }).map(s => + 'G' + s.split('').map(c => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.abs(c.charCodeAt(0)) % 36]).join('') + ) // Generate a valid transaction hash (64 character hex string) export const arbitraryTransactionHash = (): fc.Arbitrary => @@ -237,3 +248,165 @@ export const arbitraryConsistentEventId = (): fc.Arbitrary<{ transactionHash: hash, eventIndex: index })) + +// Generate a ProcessedEvent +export const arbitraryProcessedEvent = (): fc.Arbitrary => + fc.record({ + eventId: arbitraryEventId(), + transactionHash: arbitraryTransactionHash(), + eventIndex: arbitraryEventIndex(), + ledgerNumber: arbitraryLedgerNumber(), + processedAt: arbitraryPastDate(), + createdAt: arbitraryPastDate() + }) + +// Generate a FailedEvent +export const arbitraryFailedEvent = (): fc.Arbitrary => + fc.record({ + id: fc.integer({ min: 1, max: 100000 }), + eventId: arbitraryEventId(), + eventPayload: arbitraryParsedEvent(), + errorMessage: fc.string({ minLength: 1, maxLength: 500 }), + retryCount: fc.integer({ min: 0, max: 5 }), + failedAt: arbitraryPastDate(), + createdAt: arbitraryPastDate() + }) + +// Generate a ListenerState +export const arbitraryListenerState = (): fc.Arbitrary => + fc.record({ + id: fc.integer({ min: 1, max: 1000 }), + serviceName: fc.string({ minLength: 1, maxLength: 100 }), + lastProcessedLedger: arbitraryLedgerNumber(), + lastProcessedAt: arbitraryPastDate(), + createdAt: arbitraryPastDate(), + updatedAt: arbitraryPastDate() + }) + +// Generate a Milestone (database entity) +export const arbitraryMilestone = (): fc.Arbitrary => + fc.record({ + id: arbitraryMilestoneId(), + vaultId: arbitraryVaultId(), + title: fc.string({ minLength: 1, maxLength: 255 }), + description: fc.option(fc.string({ minLength: 0, maxLength: 1000 })), + targetAmount: arbitraryAmount(), + currentAmount: arbitraryAmount(), + deadline: arbitraryFutureDate(), + status: fc.constantFrom('pending', 'in_progress', 'completed', 'failed'), + createdAt: arbitraryPastDate(), + updatedAt: arbitraryPastDate() + }) + +// Generate a Validation (database entity) +export const arbitraryValidation = (): fc.Arbitrary => + fc.record({ + id: arbitraryValidationId(), + milestoneId: arbitraryMilestoneId(), + validatorAddress: arbitraryStellarAddress(), + validationResult: arbitraryValidationResult(), + evidenceHash: fc.option(arbitraryEvidenceHash()), + validatedAt: arbitraryPastDate(), + createdAt: arbitraryPastDate() + }) + +// Generate an organization ID +export const arbitraryOrganizationId = (): fc.Arbitrary => + fc.string({ minLength: 10, maxLength: 64 }).map(s => `org-${s}`) + +// Generate a team ID +export const arbitraryTeamId = (): fc.Arbitrary => + fc.string({ minLength: 10, maxLength: 64 }).map(s => `team-${s}`) + +// Generate a user ID +export const arbitraryUserId = (): fc.Arbitrary => + fc.string({ minLength: 10, maxLength: 64 }).map(s => `user-${s}`) + +// Generate a contract address (Contract IDs are 56 chars starting with C on Stellar) +export const arbitraryContractAddress = (): fc.Arbitrary => + fc.string({ minLength: 54, maxLength: 54 }).map(s => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + let result = 'C' + for (let i = 0; i < 54; i++) { + const idx = Math.abs((s.charCodeAt(i % s.length) + i * 17) % chars.length) + result += chars[idx] + } + return result + }) + +// Generate a valid Horizon URL +export const arbitraryHorizonUrl = (): fc.Arbitrary => + fc.oneof( + fc.constant('https://horizon.stellar.org'), + fc.constant('https://horizon-testnet.stellar.org'), + fc.constant('https://horizon-futurenet.stellar.org'), + fc.string({ minLength: 20, maxLength: 100 }).map(s => `https://${s.split('').map(c => 'abcdefghijklmnopqrstuvwxyz.'[Math.abs(c.charCodeAt(0)) % 28]).join('')}`) + ) + +// Generate a milestone status +export const arbitraryMilestoneStatus = (): fc.Arbitrary<'pending' | 'in_progress' | 'completed' | 'failed'> => + fc.constantFrom('pending', 'in_progress', 'completed', 'failed') + +// Generate a unique vault ID (for testing uniqueness constraints) +export const arbitraryUniqueVaultId = (): fc.Arbitrary => + fc.uniqueArray(arbitraryVaultId(), { minLength: 1, maxLength: 10 }).map(arr => arr[0]) + +// Generate multiple events as a sequence +export const arbitraryEventSequence = (minEvents: number = 2, maxEvents: number = 10): fc.Arbitrary => + fc.array(arbitraryParsedEvent(), { minLength: minEvents, maxLength: maxEvents }) + +// Generate events for a specific vault (all related events) +export const arbitraryVaultEventSequence = (): fc.Arbitrary<{ + created: ParsedEvent + milestones: ParsedEvent[] + completed: ParsedEvent +}> => + fc.record({ + created: arbitraryVaultCreatedEvent(), + milestones: fc.array(arbitraryMilestoneCreatedEvent(), { minLength: 1, maxLength: 5 }), + completed: arbitraryVaultCompletedEvent() + }) + +// Generate edge case: empty strings, extreme values +export const arbitraryEdgeCaseAmount = (): fc.Arbitrary => + fc.oneof( + fc.constant('0.0000000'), + fc.constant('0.0000001'), + fc.constant('999999.9999999'), + fc.constant('1000000.0000000'), + arbitraryAmount() + ) + +// Generate edge case: very long strings +export const arbitraryEdgeCaseString = (): fc.Arbitrary => + fc.oneof( + fc.constant(''), + fc.string({ minLength: 1000, maxLength: 2000 }), + fc.string({ minLength: 1, maxLength: 255 }) + ) + +// Filtered arbitrary: valid Stellar address without special chars in payload +export const arbitrarySafeStellarAddress = (): fc.Arbitrary => + arbitraryStellarAddress().map(addr => addr.replace(/[^A-Z0-9]/g, '').slice(0, 56)) + +// Generate a vault with invalid state transitions for negative testing +export const arbitraryInvalidStatusTransition = (): fc.Arbitrary<{ + fromStatus: 'active' | 'completed' | 'failed' | 'cancelled' + toStatus: 'active' | 'completed' | 'failed' | 'cancelled' + isValid: boolean +}> => + fc.record({ + fromStatus: arbitraryVaultStatus(), + toStatus: arbitraryVaultStatus(), + isValid: fc.boolean() + }).map(record => { + const invalidTransitions = [ + { from: 'completed', to: 'active' }, + { from: 'failed', to: 'active' }, + { from: 'cancelled', to: 'active' }, + { from: 'completed', to: 'failed' }, + { from: 'failed', to: 'completed' } + ] + const isInvalid = invalidTransitions.some(t => t.from === record.fromStatus && t.to === record.toStatus) + return { ...record, isValid: !isInvalid } + })