Add debug bypass subscription toggle, tests, and data layer improvements

- Add runtime toggle in Settings (DEBUG only) to bypass subscription/hide trial banner
- IAPManager.bypassSubscription is now a @Published var persisted via UserDefaults
- Hide upgrade banner in SettingsTabView and trial warnings when bypass is enabled
- Add FeelsTests directory with integration tests
- Update DataController, DataControllerGET, DataControllerUPDATE
- Update Xcode project and scheme configuration
- Update localization strings and App Store screen docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-15 17:12:56 -06:00
parent 7c142568be
commit 7639f881da
14 changed files with 1064 additions and 34 deletions

View File

@@ -0,0 +1,141 @@
//
// IntegrationTests.swift
// FeelsTests
//
// Integration tests verifying full lifecycle pipelines.
//
import XCTest
import SwiftData
@testable import Feels
@MainActor
final class IntegrationTests: XCTestCase {
var sut: DataController!
override func setUp() {
super.setUp()
let schema = Schema([MoodEntryModel.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Phase 8: Integration Pipelines
func test_fullCRUD_lifecycle() {
let date = makeDate(2024, 6, 15)
// Create
sut.add(mood: .good, forDate: date, entryType: .listView)
XCTAssertNotNil(sut.getEntry(byDate: date))
// Read
let entry = sut.getEntry(byDate: date)
XCTAssertEqual(entry?.mood, .good)
// Update
let updated = sut.update(entryDate: date, withMood: .great)
XCTAssertTrue(updated)
XCTAssertEqual(sut.getEntry(byDate: date)?.mood, .great)
// Update notes
sut.updateNotes(forDate: date, notes: "Lifecycle test")
XCTAssertEqual(sut.getEntry(byDate: date)?.notes, "Lifecycle test")
// Delete
sut.deleteAllEntries(forDate: date)
XCTAssertNil(sut.getEntry(byDate: date))
}
func test_streak_throughLifecycle() {
let today = Calendar.current.startOfDay(for: Date())
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)!
// Add today streak=1
sut.add(mood: .good, forDate: today, entryType: .listView)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 1)
// Add yesterday streak=2
sut.add(mood: .great, forDate: yesterday, entryType: .listView)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 2)
// Update today's mood streak still 2
sut.update(entryDate: today, withMood: .average)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 2)
// Delete today streak=1 (counting from yesterday)
sut.deleteAllEntries(forDate: today)
XCTAssertEqual(sut.calculateStreak(from: today).streak, 1)
}
func test_voteStatus_throughLifecycle() {
let onboarding = OnboardingData()
onboarding.inputDay = .Today
// Set unlock time to 00:01 so we're always "after" unlock
var components = Calendar.current.dateComponents([.year, .month, .day], from: Date())
components.hour = 0
components.minute = 1
onboarding.date = Calendar.current.date(from: components)!
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboarding)
// Helper: check vote status using testable DataController
func isVoteNeeded() -> Bool {
let entry = sut.getEntry(byDate: votingDate.startOfDay)
return entry == nil || entry?.mood == .missing
}
// No entry vote needed
XCTAssertTrue(isVoteNeeded())
// Add entry no vote needed
sut.add(mood: .great, forDate: votingDate, entryType: .listView)
XCTAssertFalse(isVoteNeeded())
// Delete entry vote needed again
sut.deleteAllEntries(forDate: votingDate)
XCTAssertTrue(isVoteNeeded())
}
func test_duplicateRemoval() {
let date = makeDate(2024, 6, 15)
// Manually insert 3 entries for same date (simulating CloudKit conflict)
let entry1 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView)
entry1.timestamp = makeDate(2024, 6, 15, hour: 10)
sut.modelContext.insert(entry1)
let entry2 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView)
entry2.timestamp = makeDate(2024, 6, 15, hour: 8)
sut.modelContext.insert(entry2)
let entry3 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing)
entry3.timestamp = makeDate(2024, 6, 15, hour: 12)
sut.modelContext.insert(entry3)
sut.save()
let removed = sut.removeDuplicates()
XCTAssertEqual(removed, 2)
XCTAssertEqual(sut.getAllEntries(byDate: date).count, 1)
}
func test_dataListeners_fireOnMutation() {
var listenerCallCount = 0
sut.addNewDataListener {
listenerCallCount += 1
}
// add() calls saveAndRunDataListeners internally
sut.add(mood: .good, forDate: makeDate(2024, 6, 15), entryType: .listView)
// add() does: delete-save (if existing) + insert + saveAndRunDataListeners
// For a fresh add with no existing entry, listener fires once from saveAndRunDataListeners
XCTAssertGreaterThanOrEqual(listenerCallCount, 1, "Listener should fire at least once on mutation")
}
}