From d6ee96e0805d4e3ccf33915a5c56b2833fce6913 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 15:52:07 +0000 Subject: [PATCH] Add unit tests for User, UserPeer, UserHistory, and UserHistoryPeer Covers the previously untested model layer for user data and study history: - UserTest: init defaults, save-insert, save-update, delete protection for default user, cascade delete of history, and UserPeer retrieval methods - UserHistoryTest: NSCoding round-trip, saveToUserId persistence, userHistoriesForUserId retrieval, _nextAfterLevel level-progression logic for all edge cases, and DB-backed recordCorrect/recordWrong/buryCard https://claude.ai/code/session_01A6LuMixdzhqX3TdVUqZaxX --- jFlash/Tests/UserHistoryTest.h | 13 ++ jFlash/Tests/UserHistoryTest.m | 256 +++++++++++++++++++++++++++++++++ jFlash/Tests/UserTest.h | 12 ++ jFlash/Tests/UserTest.m | 172 ++++++++++++++++++++++ 4 files changed, 453 insertions(+) create mode 100644 jFlash/Tests/UserHistoryTest.h create mode 100644 jFlash/Tests/UserHistoryTest.m create mode 100644 jFlash/Tests/UserTest.h create mode 100644 jFlash/Tests/UserTest.m diff --git a/jFlash/Tests/UserHistoryTest.h b/jFlash/Tests/UserHistoryTest.h new file mode 100644 index 00000000..966b63d0 --- /dev/null +++ b/jFlash/Tests/UserHistoryTest.h @@ -0,0 +1,13 @@ +// +// UserHistoryTest.h +// jFlash +// +// Tests for UserHistory model (including NSCoding) and UserHistoryPeer +// card-recording / history-retrieval methods. +// + +#import +#import + +@interface UserHistoryTest : SenTestCase +@end diff --git a/jFlash/Tests/UserHistoryTest.m b/jFlash/Tests/UserHistoryTest.m new file mode 100644 index 00000000..7409b50f --- /dev/null +++ b/jFlash/Tests/UserHistoryTest.m @@ -0,0 +1,256 @@ +// +// UserHistoryTest.m +// jFlash +// +// Tests for UserHistory model (including NSCoding) and UserHistoryPeer +// card-recording / history-retrieval methods. +// + +#import "UserHistoryTest.h" +#import "SetupDatabaseHelper.h" +#import "UserHistory.h" +#import "UserHistoryPeer.h" +#import "TagPeer.h" + +// Expose private factory and level-progression methods for testing. +@interface UserHistory (Testing) +- (id)_init; +@end + +@interface UserHistoryPeer (Testing) ++ (NSInteger)_nextAfterLevel:(NSInteger)level gotItRight:(BOOL)gotItRight; +@end + +static NSString * const kLWEFavoriteTagName = @"Long Weekend Favorites"; +static const NSInteger kTestCardId = 88888; +static const NSInteger kTestCardId2 = 88889; +static const NSInteger kTestCardId3 = 88890; + +@implementation UserHistoryTest + +#pragma mark - UserHistory model tests + +- (void)testDirectInitRaisesException +{ + STAssertThrows([[UserHistory alloc] init], + @"Calling -init directly on UserHistory should raise NSInternalInconsistencyException"); +} + +- (void)testNSCodingRoundTrip +{ + UserHistory *original = [[[UserHistory alloc] _init] autorelease]; + original.cardId = 42; + original.rightCount = 7; + original.wrongCount = 3; + original.cardLevel = 4; + NSDate *now = [NSDate date]; + original.lastUpdated = now; + original.createdAt = now; + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:original]; + STAssertNotNil(data, @"Archiving should produce non-nil NSData"); + + UserHistory *decoded = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + STAssertNotNil(decoded, @"Unarchiving should produce a non-nil UserHistory"); + STAssertEquals(decoded.cardId, original.cardId, @"cardId should survive NSCoding round-trip"); + STAssertEquals(decoded.rightCount, original.rightCount, @"rightCount should survive NSCoding round-trip"); + STAssertEquals(decoded.wrongCount, original.wrongCount, @"wrongCount should survive NSCoding round-trip"); + STAssertEquals(decoded.cardLevel, original.cardLevel, @"cardLevel should survive NSCoding round-trip"); + STAssertEqualObjects(decoded.lastUpdated, original.lastUpdated, @"lastUpdated should survive NSCoding round-trip"); + STAssertEqualObjects(decoded.createdAt, original.createdAt, @"createdAt should survive NSCoding round-trip"); +} + +- (void)testSaveToUserIdPersists +{ + UserHistory *history = [[[UserHistory alloc] _init] autorelease]; + history.cardId = kTestCardId; + history.rightCount = 3; + history.wrongCount = 1; + history.cardLevel = 2; + history.lastUpdated = [NSDate date]; + history.createdAt = [NSDate date]; + + BOOL saved = [history saveToUserId:DEFAULT_USER_ID]; + STAssertTrue(saved, @"saveToUserId should return YES on success"); + + NSArray *histories = [UserHistoryPeer userHistoriesForUserId:DEFAULT_USER_ID]; + BOOL found = NO; + for (UserHistory *h in histories) + { + if (h.cardId == kTestCardId) + { + STAssertEquals(h.rightCount, 3, @"Saved rightCount should be retrievable"); + STAssertEquals(h.wrongCount, 1, @"Saved wrongCount should be retrievable"); + STAssertEquals(h.cardLevel, 2, @"Saved cardLevel should be retrievable"); + found = YES; + break; + } + } + STAssertTrue(found, @"Saved UserHistory should appear in userHistoriesForUserId results"); +} + +#pragma mark - UserHistoryPeer retrieval tests + +- (void)testUserHistoriesForUserIdReturnsKnownEntries +{ + UserHistory *h1 = [[[UserHistory alloc] _init] autorelease]; + h1.cardId = kTestCardId; h1.rightCount = 5; h1.wrongCount = 0; h1.cardLevel = 3; + h1.lastUpdated = [NSDate date]; h1.createdAt = [NSDate date]; + [h1 saveToUserId:DEFAULT_USER_ID]; + + UserHistory *h2 = [[[UserHistory alloc] _init] autorelease]; + h2.cardId = kTestCardId2; h2.rightCount = 1; h2.wrongCount = 4; h2.cardLevel = 1; + h2.lastUpdated = [NSDate date]; h2.createdAt = [NSDate date]; + [h2 saveToUserId:DEFAULT_USER_ID]; + + NSArray *results = [UserHistoryPeer userHistoriesForUserId:DEFAULT_USER_ID]; + STAssertNotNil(results, @"userHistoriesForUserId should never return nil"); + STAssertTrue([results isKindOfClass:[NSArray class]], @"Should return an NSArray"); + + NSInteger foundCount = 0; + for (UserHistory *h in results) + { + if (h.cardId == kTestCardId || h.cardId == kTestCardId2) + { + foundCount++; + } + } + STAssertEquals(foundCount, 2, @"Both saved UserHistory entries should appear in the results"); +} + +- (void)testUserHistoriesForNonExistentUserReturnsEmptyArray +{ + NSArray *results = [UserHistoryPeer userHistoriesForUserId:99999]; + STAssertNotNil(results, @"Should return an empty array, not nil, for an unknown user"); + STAssertEquals([results count], (NSUInteger)0, @"Unknown user should have zero history entries"); +} + +#pragma mark - Level-progression logic tests + +- (void)testNextAfterLevelWrongAlwaysReturnsOne +{ + for (NSInteger level = 0; level <= 5; level++) + { + NSInteger next = [UserHistoryPeer _nextAfterLevel:level gotItRight:NO]; + STAssertEquals(next, (NSInteger)1, @"Getting a card wrong at level %d should always return level 1", level); + } +} + +- (void)testNextAfterLevel0RightReturnsTwo +{ + NSInteger next = [UserHistoryPeer _nextAfterLevel:0 gotItRight:YES]; + STAssertEquals(next, (NSInteger)2, + @"An unseen card (level 0) answered correctly should go to level 2, not 1"); +} + +- (void)testNextAfterLevels1to4RightIncrementsLevel +{ + for (NSInteger level = 1; level <= 4; level++) + { + NSInteger next = [UserHistoryPeer _nextAfterLevel:level gotItRight:YES]; + STAssertEquals(next, level + 1, + @"Getting level %d right should advance to level %d", level, level + 1); + } +} + +- (void)testNextAfterLevel5RightClampsAtFive +{ + NSInteger next = [UserHistoryPeer _nextAfterLevel:5 gotItRight:YES]; + STAssertEquals(next, (NSInteger)5, @"Level 5 is the maximum; getting it right should stay at 5"); +} + +#pragma mark - DB-backed recording tests + +- (void)testRecordCorrectIncrementsRightCountInDB +{ + [[NSUserDefaults standardUserDefaults] setInteger:DEFAULT_USER_ID forKey:@"user_id"]; + + Tag *tag = [TagPeer retrieveTagByName:kLWEFavoriteTagName]; + [tag populateCardIds]; + NSArray *faultedCards = [CardPeer retrieveFaultedCardsForTag:tag]; + STAssertTrue([faultedCards count] > 0, @"Favorites tag must contain at least one card"); + + Card *faultedCard = [faultedCards objectAtIndex:0]; + Card *card = [CardPeer retrieveCardByPK:faultedCard.cardId]; + STAssertNotNil(card, @"retrieveCardByPK should return a valid card"); + + NSInteger originalRightCount = card.rightCount; + [UserHistoryPeer recordCorrectForCard:card inTag:tag]; + + Card *updated = [CardPeer retrieveCardByPK:card.cardId]; + STAssertEquals(updated.rightCount, originalRightCount + 1, + @"recordCorrectForCard should increment right_count by 1 in user_history"); +} + +- (void)testRecordWrongIncrementsWrongCountInDB +{ + [[NSUserDefaults standardUserDefaults] setInteger:DEFAULT_USER_ID forKey:@"user_id"]; + + Tag *tag = [TagPeer retrieveTagByName:kLWEFavoriteTagName]; + [tag populateCardIds]; + NSArray *faultedCards = [CardPeer retrieveFaultedCardsForTag:tag]; + STAssertTrue([faultedCards count] > 0, @"Favorites tag must contain at least one card"); + + Card *faultedCard = [faultedCards objectAtIndex:0]; + Card *card = [CardPeer retrieveCardByPK:faultedCard.cardId]; + STAssertNotNil(card, @"retrieveCardByPK should return a valid card"); + + NSInteger originalWrongCount = card.wrongCount; + [UserHistoryPeer recordWrongForCard:card inTag:tag]; + + Card *updated = [CardPeer retrieveCardByPK:card.cardId]; + STAssertEquals(updated.wrongCount, originalWrongCount + 1, + @"recordWrongForCard should increment wrong_count by 1 in user_history"); +} + +- (void)testBuryCardSetsLevelToFiveInDB +{ + [[NSUserDefaults standardUserDefaults] setInteger:DEFAULT_USER_ID forKey:@"user_id"]; + + Tag *tag = [TagPeer retrieveTagByName:kLWEFavoriteTagName]; + [tag populateCardIds]; + NSArray *faultedCards = [CardPeer retrieveFaultedCardsForTag:tag]; + STAssertTrue([faultedCards count] > 0, @"Favorites tag must contain at least one card"); + + Card *faultedCard = [faultedCards objectAtIndex:0]; + Card *card = [CardPeer retrieveCardByPK:faultedCard.cardId]; + STAssertNotNil(card, @"retrieveCardByPK should return a valid card"); + + [UserHistoryPeer buryCard:card inTag:tag]; + + LWEDatabase *db = [LWEDatabase sharedLWEDatabase]; + FMResultSet *rs = [db.dao executeQuery: + @"SELECT card_level FROM user_history WHERE card_id = ? AND user_id = ?", + [NSNumber numberWithInteger:card.cardId], + [NSNumber numberWithInteger:DEFAULT_USER_ID]]; + NSInteger cardLevel = -1; + if ([rs next]) + { + cardLevel = [rs intForColumn:@"card_level"]; + } + [rs close]; + STAssertEquals(cardLevel, (NSInteger)5, @"buryCard should set card_level to 5 in user_history"); +} + +#pragma mark - Setup / teardown + +- (void)setUp +{ + NSError *error = nil; + JFlashDatabase *db = [JFlashDatabase sharedJFlashDatabase]; + BOOL result = [db setupTestDatabaseAndOpenConnectionWithError:&error]; + STAssertTrue(result, @"Failed to set up test database: %@", [error localizedDescription]); + + result = [db setupAttachedDatabase:CURRENT_CARD_TEST_DATABASE asName:@"cards"]; + STAssertTrue(result, @"Failed to attach cards database"); +} + +- (void)tearDown +{ + JFlashDatabase *db = [JFlashDatabase sharedJFlashDatabase]; + NSError *error = nil; + BOOL result = [db removeTestDatabaseWithError:&error]; + STAssertTrue(result, @"Failed to remove test database: %@", [error localizedDescription]); +} + +@end diff --git a/jFlash/Tests/UserTest.h b/jFlash/Tests/UserTest.h new file mode 100644 index 00000000..b73ab5c2 --- /dev/null +++ b/jFlash/Tests/UserTest.h @@ -0,0 +1,12 @@ +// +// UserTest.h +// jFlash +// +// Tests for User model and UserPeer retrieval/storage methods. +// + +#import +#import + +@interface UserTest : SenTestCase +@end diff --git a/jFlash/Tests/UserTest.m b/jFlash/Tests/UserTest.m new file mode 100644 index 00000000..8a01e88e --- /dev/null +++ b/jFlash/Tests/UserTest.m @@ -0,0 +1,172 @@ +// +// UserTest.m +// jFlash +// +// Tests for User model and UserPeer retrieval/storage methods. +// + +#import "UserTest.h" +#import "SetupDatabaseHelper.h" +#import "UserPeer.h" +#import "User.h" + +static NSString * const kUserTestNickname = @"TestUser"; +static NSString * const kUserTestUpdatedNickname = @"UpdatedNickname"; + +@implementation UserTest + +#pragma mark - User model tests + +- (void)testInitDefaultsToUninitializedUserId +{ + User *user = [[[User alloc] init] autorelease]; + STAssertEquals(user.userId, kLWEUninitializedUserId, @"New User should start with kLWEUninitializedUserId"); +} + +- (void)testHistoryArchiveKeyFormat +{ + User *user = [UserPeer userWithUserId:DEFAULT_USER_ID]; + NSString *expectedKey = [NSString stringWithFormat:@"history_for_user_id_%d", DEFAULT_USER_ID]; + STAssertEqualObjects([user historyArchiveKey], expectedKey, @"historyArchiveKey should embed the user ID"); +} + +- (void)testSaveInsertsNewUser +{ + NSArray *before = [UserPeer allUsers]; + User *newUser = [[[User alloc] init] autorelease]; + newUser.userNickname = kUserTestNickname; + [newUser save]; + + NSArray *after = [UserPeer allUsers]; + STAssertEquals([after count], [before count] + 1, @"Saving a new User should insert one row"); +} + +- (void)testSaveUpdatesExistingUserNickname +{ + User *user = [UserPeer userWithUserId:DEFAULT_USER_ID]; + user.userNickname = kUserTestUpdatedNickname; + [user save]; + + User *reloaded = [UserPeer userWithUserId:DEFAULT_USER_ID]; + STAssertEqualObjects(reloaded.userNickname, kUserTestUpdatedNickname, @"Saved nickname should persist to DB"); +} + +- (void)testDeleteDefaultUserFails +{ + User *defaultUser = [UserPeer userWithUserId:DEFAULT_USER_ID]; + NSError *error = nil; + BOOL result = [defaultUser deleteUser:&error]; + STAssertFalse(result, @"Deleting the default user should return NO"); + STAssertNotNil(error, @"Deleting the default user should populate the error parameter"); +} + +- (void)testDeleteNonDefaultUserSucceeds +{ + User *newUser = [[[User alloc] init] autorelease]; + newUser.userNickname = kUserTestNickname; + [newUser save]; + + NSArray *allUsers = [UserPeer allUsers]; + User *insertedUser = nil; + for (User *u in allUsers) + { + if (u.userId != DEFAULT_USER_ID) + { + insertedUser = u; + break; + } + } + STAssertNotNil(insertedUser, @"Should find the newly inserted non-default user"); + + NSError *error = nil; + BOOL result = [insertedUser deleteUser:&error]; + STAssertTrue(result, @"Deleting a non-default user should return YES"); + STAssertNil(error, @"Deleting a non-default user should not produce an error"); + + NSArray *afterDelete = [UserPeer allUsers]; + for (User *u in afterDelete) + { + STAssertFalse(u.userId == insertedUser.userId, @"Deleted user should no longer appear in allUsers"); + } +} + +- (void)testDeleteNonDefaultUserAlsoRemovesHistory +{ + User *newUser = [[[User alloc] init] autorelease]; + newUser.userNickname = kUserTestNickname; + [newUser save]; + + NSArray *allUsers = [UserPeer allUsers]; + User *insertedUser = nil; + for (User *u in allUsers) + { + if (u.userId != DEFAULT_USER_ID) { insertedUser = u; break; } + } + STAssertNotNil(insertedUser, @"Should find inserted user"); + + NSError *error = nil; + [insertedUser deleteUser:&error]; + + LWEDatabase *db = [LWEDatabase sharedLWEDatabase]; + FMResultSet *rs = [db.dao executeQuery:@"SELECT * FROM user_history WHERE user_id = ?", + [NSNumber numberWithInteger:insertedUser.userId]]; + NSInteger rowCount = 0; + while ([rs next]) { rowCount++; } + [rs close]; + STAssertEquals(rowCount, 0, @"Deleting a user should also delete their user_history rows"); +} + +- (void)testStudyHistoriesReturnsArray +{ + User *user = [UserPeer userWithUserId:DEFAULT_USER_ID]; + NSArray *histories = [user studyHistories]; + STAssertNotNil(histories, @"studyHistories should never return nil"); + STAssertTrue([histories isKindOfClass:[NSArray class]], @"studyHistories should return an NSArray"); +} + +#pragma mark - UserPeer tests + +- (void)testAllUsersIncludesDefaultUser +{ + NSArray *users = [UserPeer allUsers]; + STAssertTrue([users count] >= 1, @"allUsers should return at least the default user"); + BOOL foundDefault = NO; + for (User *u in users) + { + if (u.userId == DEFAULT_USER_ID) { foundDefault = YES; break; } + } + STAssertTrue(foundDefault, @"allUsers should include the default user (id %d)", DEFAULT_USER_ID); +} + +- (void)testUserWithUserIdReturnsCorrectUserId +{ + User *user = [UserPeer userWithUserId:DEFAULT_USER_ID]; + STAssertEquals(user.userId, (NSInteger)DEFAULT_USER_ID, @"userWithUserId should return a user whose userId matches the argument"); +} + +- (void)testUserWithNonExistentIdHasUninitializedUserId +{ + User *user = [UserPeer userWithUserId:99999]; + STAssertEquals(user.userId, kLWEUninitializedUserId, + @"userWithUserId with an unknown ID should return a user with kLWEUninitializedUserId"); +} + +#pragma mark - Setup / teardown + +- (void)setUp +{ + NSError *error = nil; + JFlashDatabase *db = [JFlashDatabase sharedJFlashDatabase]; + BOOL result = [db setupTestDatabaseAndOpenConnectionWithError:&error]; + STAssertTrue(result, @"Failed to set up test database: %@", [error localizedDescription]); +} + +- (void)tearDown +{ + JFlashDatabase *db = [JFlashDatabase sharedJFlashDatabase]; + NSError *error = nil; + BOOL result = [db removeTestDatabaseWithError:&error]; + STAssertTrue(result, @"Failed to remove test database: %@", [error localizedDescription]); +} + +@end