mirror of
https://github.com/pawelorzech/MacTorn.git
synced 2026-01-30 04:04:27 +00:00
feat: Implement comprehensive unit and UI tests for models and view models, including a new network session, mocks, fixtures, and a CI workflow.
This commit is contained in:
parent
ca7d0dfe84
commit
1ce929bffa
18 changed files with 2693 additions and 11 deletions
103
.github/workflows/tests.yml
vendored
Normal file
103
.github/workflows/tests.yml
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-tests:
|
||||||
|
name: Unit Tests
|
||||||
|
runs-on: macos-14
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||||
|
|
||||||
|
- name: Show Xcode version
|
||||||
|
run: xcodebuild -version
|
||||||
|
|
||||||
|
- name: Build and Test
|
||||||
|
run: |
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-resultBundlePath TestResults \
|
||||||
|
-enableCodeCoverage YES \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
| xcpretty --color --report junit
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: TestResults
|
||||||
|
|
||||||
|
- name: Upload Coverage Report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: success()
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: TestResults
|
||||||
|
|
||||||
|
ui-tests:
|
||||||
|
name: UI Tests
|
||||||
|
runs-on: macos-14
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||||
|
|
||||||
|
- name: Build and Run UI Tests
|
||||||
|
run: |
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-only-testing:MacTornUITests \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
| xcpretty --color
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build Release
|
||||||
|
runs-on: macos-14
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||||
|
|
||||||
|
- name: Build Release
|
||||||
|
run: |
|
||||||
|
xcodebuild build \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-configuration Release \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
- name: Archive Build
|
||||||
|
run: |
|
||||||
|
xcodebuild archive \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-archivePath build/MacTorn.xcarchive \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
|| echo "Archive step optional"
|
||||||
|
|
@ -27,8 +27,41 @@
|
||||||
AAA00018 /* FactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10019 /* FactionView.swift */; };
|
AAA00018 /* FactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10019 /* FactionView.swift */; };
|
||||||
AAA00019 /* WatchlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10020 /* WatchlistView.swift */; };
|
AAA00019 /* WatchlistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10020 /* WatchlistView.swift */; };
|
||||||
AAA00020 /* PropertiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10021 /* PropertiesView.swift */; };
|
AAA00020 /* PropertiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10021 /* PropertiesView.swift */; };
|
||||||
|
AAA00021 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10022 /* NetworkSession.swift */; };
|
||||||
|
/* Unit Tests */
|
||||||
|
BBB00001 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10001 /* MockNetworkSession.swift */; };
|
||||||
|
BBB00002 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10002 /* TestHelpers.swift */; };
|
||||||
|
BBB00003 /* TornAPIFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10003 /* TornAPIFixtures.swift */; };
|
||||||
|
BBB00004 /* BarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10004 /* BarTests.swift */; };
|
||||||
|
BBB00005 /* TravelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10005 /* TravelTests.swift */; };
|
||||||
|
BBB00006 /* StatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10006 /* StatusTests.swift */; };
|
||||||
|
BBB00007 /* ChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10007 /* ChainTests.swift */; };
|
||||||
|
BBB00008 /* TornResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10008 /* TornResponseTests.swift */; };
|
||||||
|
BBB00009 /* WatchlistItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10009 /* WatchlistItemTests.swift */; };
|
||||||
|
BBB00010 /* MoneyDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10010 /* MoneyDataTests.swift */; };
|
||||||
|
BBB00011 /* AppStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10011 /* AppStateTests.swift */; };
|
||||||
|
BBB00012 /* AppStateWatchlistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10012 /* AppStateWatchlistTests.swift */; };
|
||||||
|
/* UI Tests */
|
||||||
|
CCC00001 /* MacTornUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC10001 /* MacTornUITests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
BBB20001 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = AAA00000 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = AAA40000;
|
||||||
|
remoteInfo = MacTorn;
|
||||||
|
};
|
||||||
|
CCC20001 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = AAA00000 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = AAA40000;
|
||||||
|
remoteInfo = MacTorn;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
AAA10001 /* MacTornApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTornApp.swift; sourceTree = "<group>"; };
|
AAA10001 /* MacTornApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTornApp.swift; sourceTree = "<group>"; };
|
||||||
AAA10002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
AAA10002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -51,7 +84,25 @@
|
||||||
AAA10019 /* FactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactionView.swift; sourceTree = "<group>"; };
|
AAA10019 /* FactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactionView.swift; sourceTree = "<group>"; };
|
||||||
AAA10020 /* WatchlistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistView.swift; sourceTree = "<group>"; };
|
AAA10020 /* WatchlistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistView.swift; sourceTree = "<group>"; };
|
||||||
AAA10021 /* PropertiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertiesView.swift; sourceTree = "<group>"; };
|
AAA10021 /* PropertiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertiesView.swift; sourceTree = "<group>"; };
|
||||||
|
AAA10022 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; };
|
||||||
AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* Unit Test Files */
|
||||||
|
BBB10001 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10002 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10003 /* TornAPIFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TornAPIFixtures.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10004 /* BarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10005 /* TravelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TravelTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10006 /* StatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10007 /* ChainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10008 /* TornResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TornResponseTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10009 /* WatchlistItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistItemTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10010 /* MoneyDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyDataTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10011 /* AppStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10012 /* AppStateWatchlistTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateWatchlistTests.swift; sourceTree = "<group>"; };
|
||||||
|
BBB10000 /* MacTornTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacTornTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* UI Test Files */
|
||||||
|
CCC10001 /* MacTornUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTornUITests.swift; sourceTree = "<group>"; };
|
||||||
|
CCC10000 /* MacTornUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacTornUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -62,6 +113,20 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
BBB20000 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CCC20000 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
|
@ -69,6 +134,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
AAA30001 /* MacTorn */,
|
AAA30001 /* MacTorn */,
|
||||||
|
BBB30000 /* MacTornTests */,
|
||||||
|
CCC30000 /* MacTornUITests */,
|
||||||
AAA30002 /* Products */,
|
AAA30002 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -83,6 +150,7 @@
|
||||||
AAA30004 /* ViewModels */,
|
AAA30004 /* ViewModels */,
|
||||||
AAA30005 /* Views */,
|
AAA30005 /* Views */,
|
||||||
AAA30007 /* Utilities */,
|
AAA30007 /* Utilities */,
|
||||||
|
AAA30008 /* Networking */,
|
||||||
);
|
);
|
||||||
path = MacTorn;
|
path = MacTorn;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -91,6 +159,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
AAA10000 /* MacTorn.app */,
|
AAA10000 /* MacTorn.app */,
|
||||||
|
BBB10000 /* MacTornTests.xctest */,
|
||||||
|
CCC10000 /* MacTornUITests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -149,6 +219,75 @@
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
AAA30008 /* Networking */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AAA10022 /* NetworkSession.swift */,
|
||||||
|
);
|
||||||
|
path = Networking;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* Unit Tests Groups */
|
||||||
|
BBB30000 /* MacTornTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BBB30001 /* Mocks */,
|
||||||
|
BBB30002 /* Fixtures */,
|
||||||
|
BBB30003 /* Models */,
|
||||||
|
BBB30004 /* ViewModels */,
|
||||||
|
);
|
||||||
|
path = MacTornTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
BBB30001 /* Mocks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BBB10001 /* MockNetworkSession.swift */,
|
||||||
|
BBB10002 /* TestHelpers.swift */,
|
||||||
|
);
|
||||||
|
path = Mocks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
BBB30002 /* Fixtures */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BBB10003 /* TornAPIFixtures.swift */,
|
||||||
|
);
|
||||||
|
path = Fixtures;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
BBB30003 /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BBB10004 /* BarTests.swift */,
|
||||||
|
BBB10005 /* TravelTests.swift */,
|
||||||
|
BBB10006 /* StatusTests.swift */,
|
||||||
|
BBB10007 /* ChainTests.swift */,
|
||||||
|
BBB10008 /* TornResponseTests.swift */,
|
||||||
|
BBB10009 /* WatchlistItemTests.swift */,
|
||||||
|
BBB10010 /* MoneyDataTests.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
BBB30004 /* ViewModels */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BBB10011 /* AppStateTests.swift */,
|
||||||
|
BBB10012 /* AppStateWatchlistTests.swift */,
|
||||||
|
);
|
||||||
|
path = ViewModels;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* UI Tests Group */
|
||||||
|
CCC30000 /* MacTornUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CCC10001 /* MacTornUITests.swift */,
|
||||||
|
);
|
||||||
|
path = MacTornUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -169,6 +308,42 @@
|
||||||
productReference = AAA10000 /* MacTorn.app */;
|
productReference = AAA10000 /* MacTorn.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
BBB40000 /* MacTornTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = BBB60000 /* Build configuration list for PBXNativeTarget "MacTornTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
BBB50000 /* Sources */,
|
||||||
|
BBB20000 /* Frameworks */,
|
||||||
|
BBB50001 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
BBB40001 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = MacTornTests;
|
||||||
|
productName = MacTornTests;
|
||||||
|
productReference = BBB10000 /* MacTornTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
CCC40000 /* MacTornUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = CCC60000 /* Build configuration list for PBXNativeTarget "MacTornUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
CCC50000 /* Sources */,
|
||||||
|
CCC20000 /* Frameworks */,
|
||||||
|
CCC50001 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
CCC40001 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = MacTornUITests;
|
||||||
|
productName = MacTornUITests;
|
||||||
|
productReference = CCC10000 /* MacTornUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
|
|
@ -182,6 +357,14 @@
|
||||||
AAA40000 = {
|
AAA40000 = {
|
||||||
CreatedOnToolsVersion = 15.0;
|
CreatedOnToolsVersion = 15.0;
|
||||||
};
|
};
|
||||||
|
BBB40000 = {
|
||||||
|
CreatedOnToolsVersion = 15.0;
|
||||||
|
TestTargetID = AAA40000;
|
||||||
|
};
|
||||||
|
CCC40000 = {
|
||||||
|
CreatedOnToolsVersion = 15.0;
|
||||||
|
TestTargetID = AAA40000;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = AAA60001 /* Build configuration list for PBXProject "MacTorn" */;
|
buildConfigurationList = AAA60001 /* Build configuration list for PBXProject "MacTorn" */;
|
||||||
|
|
@ -198,6 +381,8 @@
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
AAA40000 /* MacTorn */,
|
AAA40000 /* MacTorn */,
|
||||||
|
BBB40000 /* MacTornTests */,
|
||||||
|
CCC40000 /* MacTornUITests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
@ -211,6 +396,20 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
BBB50001 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CCC50001 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|
@ -237,11 +436,52 @@
|
||||||
AAA00018 /* FactionView.swift in Sources */,
|
AAA00018 /* FactionView.swift in Sources */,
|
||||||
AAA00019 /* WatchlistView.swift in Sources */,
|
AAA00019 /* WatchlistView.swift in Sources */,
|
||||||
AAA00020 /* PropertiesView.swift in Sources */,
|
AAA00020 /* PropertiesView.swift in Sources */,
|
||||||
|
AAA00021 /* NetworkSession.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
BBB50000 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
BBB00001 /* MockNetworkSession.swift in Sources */,
|
||||||
|
BBB00002 /* TestHelpers.swift in Sources */,
|
||||||
|
BBB00003 /* TornAPIFixtures.swift in Sources */,
|
||||||
|
BBB00004 /* BarTests.swift in Sources */,
|
||||||
|
BBB00005 /* TravelTests.swift in Sources */,
|
||||||
|
BBB00006 /* StatusTests.swift in Sources */,
|
||||||
|
BBB00007 /* ChainTests.swift in Sources */,
|
||||||
|
BBB00008 /* TornResponseTests.swift in Sources */,
|
||||||
|
BBB00009 /* WatchlistItemTests.swift in Sources */,
|
||||||
|
BBB00010 /* MoneyDataTests.swift in Sources */,
|
||||||
|
BBB00011 /* AppStateTests.swift in Sources */,
|
||||||
|
BBB00012 /* AppStateWatchlistTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
CCC50000 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CCC00001 /* MacTornUITests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
BBB40001 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = AAA40000 /* MacTorn */;
|
||||||
|
targetProxy = BBB20001 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
CCC40001 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = AAA40000 /* MacTorn */;
|
||||||
|
targetProxy = CCC20001 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
AAA70000 /* Debug */ = {
|
AAA70000 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
|
@ -413,6 +653,78 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
/* Unit Test Configurations */
|
||||||
|
BBB70000 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacTorn.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MacTorn";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
BBB70001 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacTorn.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MacTorn";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* UI Test Configurations */
|
||||||
|
CCC70000 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_TARGET_NAME = MacTorn;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
CCC70001 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_TARGET_NAME = MacTorn;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
|
@ -434,6 +746,24 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
BBB60000 /* Build configuration list for PBXNativeTarget "MacTornTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
BBB70000 /* Debug */,
|
||||||
|
BBB70001 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
CCC60000 /* Build configuration list for PBXNativeTarget "MacTornUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
CCC70000 /* Debug */,
|
||||||
|
CCC70001 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = AAA00000 /* Project object */;
|
rootObject = AAA00000 /* Project object */;
|
||||||
|
|
|
||||||
9
MacTorn/MacTorn/Networking/NetworkSession.swift
Normal file
9
MacTorn/MacTorn/Networking/NetworkSession.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol for network session abstraction to enable dependency injection and testing
|
||||||
|
protocol NetworkSession: Sendable {
|
||||||
|
func data(for request: URLRequest) async throws -> (Data, URLResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to make URLSession conform to NetworkSession
|
||||||
|
extension URLSession: NetworkSession {}
|
||||||
|
|
@ -34,6 +34,9 @@ class AppState: ObservableObject {
|
||||||
let shortcutsManager = ShortcutsManager()
|
let shortcutsManager = ShortcutsManager()
|
||||||
let updateManager = UpdateManager.shared
|
let updateManager = UpdateManager.shared
|
||||||
|
|
||||||
|
// MARK: - Networking (Dependency Injection for Testing)
|
||||||
|
private let session: NetworkSession
|
||||||
|
|
||||||
// MARK: - State Comparison
|
// MARK: - State Comparison
|
||||||
private var previousBars: Bars?
|
private var previousBars: Bars?
|
||||||
private var previousCooldowns: Cooldowns?
|
private var previousCooldowns: Cooldowns?
|
||||||
|
|
@ -44,7 +47,8 @@ class AppState: ObservableObject {
|
||||||
// MARK: - Timer
|
// MARK: - Timer
|
||||||
private var timerCancellable: AnyCancellable?
|
private var timerCancellable: AnyCancellable?
|
||||||
|
|
||||||
init() {
|
init(session: NetworkSession = URLSession.shared) {
|
||||||
|
self.session = session
|
||||||
loadNotificationRules()
|
loadNotificationRules()
|
||||||
loadWatchlist()
|
loadWatchlist()
|
||||||
// Polling and permissions moved to onAppear in UI
|
// Polling and permissions moved to onAppear in UI
|
||||||
|
|
@ -126,7 +130,7 @@ class AppState: ObservableObject {
|
||||||
do {
|
do {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
logger.error("Item \(itemId) HTTP Error: \(httpResponse.statusCode)")
|
logger.error("Item \(itemId) HTTP Error: \(httpResponse.statusCode)")
|
||||||
|
|
@ -268,7 +272,7 @@ class AppState: ObservableObject {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
throw APIError.invalidResponse
|
throw APIError.invalidResponse
|
||||||
|
|
@ -452,7 +456,7 @@ class AppState: ObservableObject {
|
||||||
do {
|
do {
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||||||
let (data, _) = try await URLSession.shared.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
// Check for Torn API error
|
// Check for Torn API error
|
||||||
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
|
|
||||||
241
MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift
Normal file
241
MacTorn/MacTornTests/Fixtures/TornAPIFixtures.swift
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Sample JSON responses for testing
|
||||||
|
enum TornAPIFixtures {
|
||||||
|
|
||||||
|
// MARK: - Full Response
|
||||||
|
|
||||||
|
static let validFullResponse: [String: Any] = [
|
||||||
|
"name": "TestPlayer",
|
||||||
|
"player_id": 123456,
|
||||||
|
"energy": [
|
||||||
|
"current": 100,
|
||||||
|
"maximum": 150,
|
||||||
|
"increment": 5,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 60,
|
||||||
|
"fulltime": 600
|
||||||
|
],
|
||||||
|
"nerve": [
|
||||||
|
"current": 50,
|
||||||
|
"maximum": 60,
|
||||||
|
"increment": 1,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 120,
|
||||||
|
"fulltime": 1800
|
||||||
|
],
|
||||||
|
"life": [
|
||||||
|
"current": 7500,
|
||||||
|
"maximum": 7500,
|
||||||
|
"increment": 100,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 0,
|
||||||
|
"fulltime": 0
|
||||||
|
],
|
||||||
|
"happy": [
|
||||||
|
"current": 5000,
|
||||||
|
"maximum": 10000,
|
||||||
|
"increment": 50,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 100,
|
||||||
|
"fulltime": 30000
|
||||||
|
],
|
||||||
|
"cooldowns": [
|
||||||
|
"drug": 0,
|
||||||
|
"medical": 0,
|
||||||
|
"booster": 0
|
||||||
|
],
|
||||||
|
"travel": [
|
||||||
|
"destination": "Torn",
|
||||||
|
"timestamp": 0,
|
||||||
|
"departed": 0,
|
||||||
|
"time_left": 0
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
"description": "Okay",
|
||||||
|
"details": "",
|
||||||
|
"state": "Okay",
|
||||||
|
"until": 0
|
||||||
|
],
|
||||||
|
"chain": [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": 0,
|
||||||
|
"cooldown": 0
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
"1": [
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"event": "You received a message from <a href='...'>Someone</a>",
|
||||||
|
"seen": 0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"messages": [
|
||||||
|
"1": [
|
||||||
|
"name": "TestSender",
|
||||||
|
"type": "Private",
|
||||||
|
"title": "Test Message",
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"read": 0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Bars
|
||||||
|
|
||||||
|
static let energyFull: [String: Any] = [
|
||||||
|
"current": 150,
|
||||||
|
"maximum": 150,
|
||||||
|
"increment": 5,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 0,
|
||||||
|
"fulltime": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let energyHalf: [String: Any] = [
|
||||||
|
"current": 75,
|
||||||
|
"maximum": 150,
|
||||||
|
"increment": 5,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 150,
|
||||||
|
"fulltime": 4500
|
||||||
|
]
|
||||||
|
|
||||||
|
static let energyEmpty: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 150,
|
||||||
|
"increment": 5,
|
||||||
|
"interval": 300,
|
||||||
|
"ticktime": 300,
|
||||||
|
"fulltime": 9000
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Travel
|
||||||
|
|
||||||
|
static let travelInTorn: [String: Any] = [
|
||||||
|
"destination": "Torn",
|
||||||
|
"timestamp": 0,
|
||||||
|
"departed": 0,
|
||||||
|
"time_left": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let travelAbroad: [String: Any] = [
|
||||||
|
"destination": "Mexico",
|
||||||
|
"timestamp": 0,
|
||||||
|
"departed": 0,
|
||||||
|
"time_left": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let travelTraveling: [String: Any] = [
|
||||||
|
"destination": "Japan",
|
||||||
|
"timestamp": Int(Date().timeIntervalSince1970) + 600,
|
||||||
|
"departed": Int(Date().timeIntervalSince1970) - 300,
|
||||||
|
"time_left": 600
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Status
|
||||||
|
|
||||||
|
static let statusOkay: [String: Any] = [
|
||||||
|
"description": "Okay",
|
||||||
|
"details": "",
|
||||||
|
"state": "Okay",
|
||||||
|
"until": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let statusHospital: [String: Any] = [
|
||||||
|
"description": "In hospital for 30 minutes",
|
||||||
|
"details": "Hospitalized by TestAttacker",
|
||||||
|
"state": "Hospital",
|
||||||
|
"until": Int(Date().timeIntervalSince1970) + 1800
|
||||||
|
]
|
||||||
|
|
||||||
|
static let statusJail: [String: Any] = [
|
||||||
|
"description": "In jail for 15 minutes",
|
||||||
|
"details": "Jailed for assault",
|
||||||
|
"state": "Jail",
|
||||||
|
"until": Int(Date().timeIntervalSince1970) + 900
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Chain
|
||||||
|
|
||||||
|
static let chainInactive: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": 0,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let chainActive: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": Int(Date().timeIntervalSince1970) + 300,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
|
||||||
|
static let chainOnCooldown: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": 0,
|
||||||
|
"cooldown": 3600
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
|
||||||
|
static let tornErrorInvalidKey: [String: Any] = [
|
||||||
|
"error": [
|
||||||
|
"code": 2,
|
||||||
|
"error": "Incorrect Key"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
static let tornErrorRateLimit: [String: Any] = [
|
||||||
|
"error": [
|
||||||
|
"code": 5,
|
||||||
|
"error": "Too many requests"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Money
|
||||||
|
|
||||||
|
static let moneyData: [String: Any] = [
|
||||||
|
"money_onhand": 1000000,
|
||||||
|
"vault_amount": 50000000,
|
||||||
|
"points": 5000,
|
||||||
|
"company_funds": 100,
|
||||||
|
"cayman_bank": 100000000
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Market
|
||||||
|
|
||||||
|
static let marketItemSuccess: [String: Any] = [
|
||||||
|
"itemmarket": [
|
||||||
|
"listings": [
|
||||||
|
["price": 1000, "amount": 5],
|
||||||
|
["price": 1100, "amount": 3],
|
||||||
|
["price": 1200, "amount": 10]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"bazaar": [
|
||||||
|
["cost": 950, "quantity": 2],
|
||||||
|
["cost": 1050, "quantity": 7]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
static let marketItemNoListings: [String: Any] = [
|
||||||
|
"itemmarket": [
|
||||||
|
"listings": []
|
||||||
|
],
|
||||||
|
"bazaar": []
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
static func toData(_ json: [String: Any]) throws -> Data {
|
||||||
|
return try JSONSerialization.data(withJSONObject: json)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func toString(_ json: [String: Any]) throws -> String {
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
return String(data: data, encoding: .utf8)!
|
||||||
|
}
|
||||||
|
}
|
||||||
102
MacTorn/MacTornTests/Mocks/MockNetworkSession.swift
Normal file
102
MacTorn/MacTornTests/Mocks/MockNetworkSession.swift
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import Foundation
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
/// Mock network session for testing API calls
|
||||||
|
final class MockNetworkSession: NetworkSession, @unchecked Sendable {
|
||||||
|
var mockData: Data?
|
||||||
|
var mockResponse: URLResponse?
|
||||||
|
var mockError: Error?
|
||||||
|
var requestedURLs: [URL] = []
|
||||||
|
|
||||||
|
init(mockData: Data? = nil, mockResponse: URLResponse? = nil, mockError: Error? = nil) {
|
||||||
|
self.mockData = mockData
|
||||||
|
self.mockResponse = mockResponse
|
||||||
|
self.mockError = mockError
|
||||||
|
}
|
||||||
|
|
||||||
|
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||||
|
if let url = request.url {
|
||||||
|
requestedURLs.append(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = mockError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = mockResponse ?? HTTPURLResponse(
|
||||||
|
url: request.url ?? URL(string: "https://api.torn.com")!,
|
||||||
|
statusCode: 200,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)!
|
||||||
|
|
||||||
|
return (mockData ?? Data(), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
/// Set up successful response with JSON data
|
||||||
|
func setSuccessResponse(json: [String: Any]) throws {
|
||||||
|
mockData = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
mockResponse = HTTPURLResponse(
|
||||||
|
url: URL(string: "https://api.torn.com")!,
|
||||||
|
statusCode: 200,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)
|
||||||
|
mockError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up HTTP error response
|
||||||
|
func setHTTPError(statusCode: Int) {
|
||||||
|
mockData = Data()
|
||||||
|
mockResponse = HTTPURLResponse(
|
||||||
|
url: URL(string: "https://api.torn.com")!,
|
||||||
|
statusCode: statusCode,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)
|
||||||
|
mockError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up Torn API error response
|
||||||
|
func setTornAPIError(code: Int, message: String) throws {
|
||||||
|
let errorJSON: [String: Any] = [
|
||||||
|
"error": [
|
||||||
|
"code": code,
|
||||||
|
"error": message
|
||||||
|
]
|
||||||
|
]
|
||||||
|
mockData = try JSONSerialization.data(withJSONObject: errorJSON)
|
||||||
|
mockResponse = HTTPURLResponse(
|
||||||
|
url: URL(string: "https://api.torn.com")!,
|
||||||
|
statusCode: 200,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)
|
||||||
|
mockError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up network error
|
||||||
|
func setNetworkError(_ error: Error) {
|
||||||
|
mockData = nil
|
||||||
|
mockResponse = nil
|
||||||
|
mockError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset all mock data
|
||||||
|
func reset() {
|
||||||
|
mockData = nil
|
||||||
|
mockResponse = nil
|
||||||
|
mockError = nil
|
||||||
|
requestedURLs.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Errors
|
||||||
|
|
||||||
|
enum MockNetworkError: Error {
|
||||||
|
case connectionFailed
|
||||||
|
case timeout
|
||||||
|
case noInternet
|
||||||
|
}
|
||||||
101
MacTorn/MacTornTests/Mocks/TestHelpers.swift
Normal file
101
MacTorn/MacTornTests/Mocks/TestHelpers.swift
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
// MARK: - Test Helpers
|
||||||
|
|
||||||
|
/// Creates a Bar instance for testing
|
||||||
|
func makeBar(current: Int = 100, maximum: Int = 150, increment: Double? = 5.0, interval: Int? = 300, ticktime: Int? = 60, fulltime: Int? = 600) -> Bar {
|
||||||
|
return Bar(current: current, maximum: maximum, increment: increment, interval: interval, ticktime: ticktime, fulltime: fulltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Travel instance for testing
|
||||||
|
func makeTravel(destination: String? = "Torn", timestamp: Int? = nil, departed: Int? = nil, timeLeft: Int? = 0) -> Travel {
|
||||||
|
return Travel(destination: destination, timestamp: timestamp, departed: departed, timeLeft: timeLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Status instance for testing
|
||||||
|
func makeStatus(description: String? = "Okay", details: String? = nil, state: String? = "Okay", until: Int? = nil) -> Status {
|
||||||
|
return Status(description: description, details: details, state: state, until: until)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Chain instance for testing
|
||||||
|
func makeChain(current: Int? = 0, maximum: Int? = 10, timeout: Int? = 0, cooldown: Int? = 0) -> Chain {
|
||||||
|
return Chain(current: current, maximum: maximum, timeout: timeout, cooldown: cooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a WatchlistItem instance for testing
|
||||||
|
func makeWatchlistItem(
|
||||||
|
id: Int = 1,
|
||||||
|
name: String = "Test Item",
|
||||||
|
lowestPrice: Int = 1000,
|
||||||
|
lowestPriceQuantity: Int = 5,
|
||||||
|
secondLowestPrice: Int = 1100,
|
||||||
|
lastUpdated: Date? = Date(),
|
||||||
|
error: String? = nil
|
||||||
|
) -> WatchlistItem {
|
||||||
|
return WatchlistItem(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
lowestPrice: lowestPrice,
|
||||||
|
lowestPriceQuantity: lowestPriceQuantity,
|
||||||
|
secondLowestPrice: secondLowestPrice,
|
||||||
|
lastUpdated: lastUpdated,
|
||||||
|
error: error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - JSON Decoding Helpers
|
||||||
|
|
||||||
|
extension XCTestCase {
|
||||||
|
/// Decode JSON string to a Decodable type
|
||||||
|
func decode<T: Decodable>(_ type: T.Type, from jsonString: String) throws -> T {
|
||||||
|
let data = jsonString.data(using: .utf8)!
|
||||||
|
return try JSONDecoder().decode(type, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode JSON dictionary to a Decodable type
|
||||||
|
func decode<T: Decodable>(_ type: T.Type, from json: [String: Any]) throws -> T {
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
return try JSONDecoder().decode(type, from: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Async Test Helpers
|
||||||
|
|
||||||
|
extension XCTestCase {
|
||||||
|
/// Run async test with timeout
|
||||||
|
func runAsyncTest(timeout: TimeInterval = 5.0, testBlock: @escaping () async throws -> Void) {
|
||||||
|
let expectation = XCTestExpectation(description: "Async test")
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await testBlock()
|
||||||
|
expectation.fulfill()
|
||||||
|
} catch {
|
||||||
|
XCTFail("Async test failed with error: \(error)")
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wait(for: [expectation], timeout: timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UserDefaults Test Helpers
|
||||||
|
|
||||||
|
extension UserDefaults {
|
||||||
|
/// Create a mock UserDefaults for testing
|
||||||
|
static func createMockDefaults() -> UserDefaults {
|
||||||
|
let suiteName = "com.mactorn.tests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all data in UserDefaults
|
||||||
|
func clearAll() {
|
||||||
|
dictionaryRepresentation().keys.forEach { key in
|
||||||
|
removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
MacTorn/MacTornTests/Models/BarTests.swift
Normal file
94
MacTorn/MacTornTests/Models/BarTests.swift
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class BarTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - Percentage Calculation Tests
|
||||||
|
|
||||||
|
func testPercentageCalculation_fullBar() {
|
||||||
|
let bar = Bar(current: 150, maximum: 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 100.0, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPercentageCalculation_halfBar() {
|
||||||
|
let bar = Bar(current: 75, maximum: 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 50.0, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPercentageCalculation_emptyBar() {
|
||||||
|
let bar = Bar(current: 0, maximum: 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 0.0, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPercentageCalculation_overFull() {
|
||||||
|
// Edge case: current > maximum (can happen with boosters)
|
||||||
|
let bar = Bar(current: 200, maximum: 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 133.33, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPercentageCalculation_zeroMaximum() {
|
||||||
|
// Edge case: division by zero protection
|
||||||
|
let bar = Bar(current: 0, maximum: 0)
|
||||||
|
XCTAssertEqual(bar.percentage, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_fullBar() throws {
|
||||||
|
let json = TornAPIFixtures.energyFull
|
||||||
|
let bar = try decode(Bar.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(bar.current, 150)
|
||||||
|
XCTAssertEqual(bar.maximum, 150)
|
||||||
|
XCTAssertEqual(bar.increment, 5)
|
||||||
|
XCTAssertEqual(bar.interval, 300)
|
||||||
|
XCTAssertEqual(bar.ticktime, 0)
|
||||||
|
XCTAssertEqual(bar.fulltime, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_halfBar() throws {
|
||||||
|
let json = TornAPIFixtures.energyHalf
|
||||||
|
let bar = try decode(Bar.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(bar.current, 75)
|
||||||
|
XCTAssertEqual(bar.maximum, 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 50.0, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_emptyBar() throws {
|
||||||
|
let json = TornAPIFixtures.energyEmpty
|
||||||
|
let bar = try decode(Bar.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(bar.current, 0)
|
||||||
|
XCTAssertEqual(bar.maximum, 150)
|
||||||
|
XCTAssertEqual(bar.percentage, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Equatable Tests
|
||||||
|
|
||||||
|
func testEquatable_sameBars() {
|
||||||
|
let bar1 = Bar(current: 100, maximum: 150)
|
||||||
|
let bar2 = Bar(current: 100, maximum: 150)
|
||||||
|
XCTAssertEqual(bar1, bar2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquatable_differentBars() {
|
||||||
|
let bar1 = Bar(current: 100, maximum: 150)
|
||||||
|
let bar2 = Bar(current: 50, maximum: 150)
|
||||||
|
XCTAssertNotEqual(bar1, bar2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Edge Cases
|
||||||
|
|
||||||
|
func testNegativeCurrent() {
|
||||||
|
// Edge case: negative current (shouldn't happen but handle gracefully)
|
||||||
|
let bar = Bar(current: -10, maximum: 150)
|
||||||
|
XCTAssertEqual(bar.percentage, -6.67, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLargeNumbers() {
|
||||||
|
// Large numbers shouldn't cause overflow
|
||||||
|
let bar = Bar(current: 1000000, maximum: 10000000)
|
||||||
|
XCTAssertEqual(bar.percentage, 10.0, accuracy: 0.01)
|
||||||
|
}
|
||||||
|
}
|
||||||
183
MacTorn/MacTornTests/Models/ChainTests.swift
Normal file
183
MacTorn/MacTornTests/Models/ChainTests.swift
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class ChainTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - isActive Tests
|
||||||
|
|
||||||
|
func testIsActive_inactive() throws {
|
||||||
|
let json = TornAPIFixtures.chainInactive
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(chain.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsActive_active() throws {
|
||||||
|
let json = TornAPIFixtures.chainActive
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(chain.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsActive_zeroCurrent() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": Int(Date().timeIntervalSince1970) + 300,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertFalse(chain.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsActive_zeroTimeout() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": 0,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertFalse(chain.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsActive_nilValues() throws {
|
||||||
|
let json: [String: Any] = [:]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertFalse(chain.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - isOnCooldown Tests
|
||||||
|
|
||||||
|
func testIsOnCooldown_no() throws {
|
||||||
|
let json = TornAPIFixtures.chainInactive
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(chain.isOnCooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOnCooldown_yes() throws {
|
||||||
|
let json = TornAPIFixtures.chainOnCooldown
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(chain.isOnCooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOnCooldown_zeroCooldown() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": 0,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertFalse(chain.isOnCooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOnCooldown_nilCooldown() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"timeout": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertFalse(chain.isOnCooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - timeoutRemaining Tests
|
||||||
|
|
||||||
|
func testTimeoutRemaining_noTimeout() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 0,
|
||||||
|
"maximum": 10,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertEqual(chain.timeoutRemaining, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimeoutRemaining_timeoutInPast() throws {
|
||||||
|
let pastTime = Int(Date().timeIntervalSince1970) - 1000
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": pastTime,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertEqual(chain.timeoutRemaining, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimeoutRemaining_timeoutInFuture() throws {
|
||||||
|
let futureTime = Int(Date().timeIntervalSince1970) + 1000
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": futureTime,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
// Should be approximately 1000, allow some tolerance
|
||||||
|
XCTAssertGreaterThan(chain.timeoutRemaining, 900)
|
||||||
|
XCTAssertLessThanOrEqual(chain.timeoutRemaining, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_activeChain() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 50,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": 1700000300,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(chain.current, 50)
|
||||||
|
XCTAssertEqual(chain.maximum, 100)
|
||||||
|
XCTAssertEqual(chain.timeout, 1700000300)
|
||||||
|
XCTAssertEqual(chain.cooldown, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_cooldownChain() throws {
|
||||||
|
let json = TornAPIFixtures.chainOnCooldown
|
||||||
|
let chain = try decode(Chain.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(chain.current, 0)
|
||||||
|
XCTAssertEqual(chain.cooldown, 3600)
|
||||||
|
XCTAssertTrue(chain.isOnCooldown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Equatable Tests
|
||||||
|
|
||||||
|
func testEquatable() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": 1000,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain1 = try decode(Chain.self, from: json)
|
||||||
|
let chain2 = try decode(Chain.self, from: json)
|
||||||
|
XCTAssertEqual(chain1, chain2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquatable_different() throws {
|
||||||
|
let json1: [String: Any] = [
|
||||||
|
"current": 25,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": 1000,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let json2: [String: Any] = [
|
||||||
|
"current": 50,
|
||||||
|
"maximum": 100,
|
||||||
|
"timeout": 1000,
|
||||||
|
"cooldown": 0
|
||||||
|
]
|
||||||
|
let chain1 = try decode(Chain.self, from: json1)
|
||||||
|
let chain2 = try decode(Chain.self, from: json2)
|
||||||
|
XCTAssertNotEqual(chain1, chain2)
|
||||||
|
}
|
||||||
|
}
|
||||||
103
MacTorn/MacTornTests/Models/MoneyDataTests.swift
Normal file
103
MacTorn/MacTornTests/Models/MoneyDataTests.swift
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class MoneyDataTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_fullData() throws {
|
||||||
|
let data = try TornAPIFixtures.toData(TornAPIFixtures.moneyData)
|
||||||
|
let money = try JSONDecoder().decode(MoneyData.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 1000000)
|
||||||
|
XCTAssertEqual(money.vault, 50000000)
|
||||||
|
XCTAssertEqual(money.points, 5000)
|
||||||
|
XCTAssertEqual(money.tokens, 100)
|
||||||
|
XCTAssertEqual(money.cayman, 100000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_withDefaults() throws {
|
||||||
|
// When fields are missing, should use defaults (0)
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"money_onhand": 500000
|
||||||
|
// Other fields missing
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let money = try JSONDecoder().decode(MoneyData.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 500000)
|
||||||
|
XCTAssertEqual(money.vault, 0) // Default
|
||||||
|
XCTAssertEqual(money.points, 0) // Default
|
||||||
|
XCTAssertEqual(money.tokens, 0) // Default
|
||||||
|
XCTAssertEqual(money.cayman, 0) // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_allMissing() throws {
|
||||||
|
let json: [String: Any] = [:]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let money = try JSONDecoder().decode(MoneyData.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 0)
|
||||||
|
XCTAssertEqual(money.vault, 0)
|
||||||
|
XCTAssertEqual(money.points, 0)
|
||||||
|
XCTAssertEqual(money.tokens, 0)
|
||||||
|
XCTAssertEqual(money.cayman, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Memberwise Initializer Tests
|
||||||
|
|
||||||
|
func testMemberwiseInit() {
|
||||||
|
let money = MoneyData(cash: 100, vault: 200, points: 300, tokens: 400, cayman: 500)
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 100)
|
||||||
|
XCTAssertEqual(money.vault, 200)
|
||||||
|
XCTAssertEqual(money.points, 300)
|
||||||
|
XCTAssertEqual(money.tokens, 400)
|
||||||
|
XCTAssertEqual(money.cayman, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultInit() {
|
||||||
|
let money = MoneyData()
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 0)
|
||||||
|
XCTAssertEqual(money.vault, 0)
|
||||||
|
XCTAssertEqual(money.points, 0)
|
||||||
|
XCTAssertEqual(money.tokens, 0)
|
||||||
|
XCTAssertEqual(money.cayman, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Large Numbers Tests
|
||||||
|
|
||||||
|
func testLargeNumbers() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"money_onhand": 999999999999,
|
||||||
|
"vault_amount": 9999999999999,
|
||||||
|
"points": 100000,
|
||||||
|
"company_funds": 50000,
|
||||||
|
"cayman_bank": 99999999999999
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let money = try JSONDecoder().decode(MoneyData.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(money.cash, 999999999999)
|
||||||
|
XCTAssertEqual(money.vault, 9999999999999)
|
||||||
|
XCTAssertEqual(money.points, 100000)
|
||||||
|
XCTAssertEqual(money.tokens, 50000)
|
||||||
|
XCTAssertEqual(money.cayman, 99999999999999)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Encoding Tests
|
||||||
|
|
||||||
|
func testEncodingRoundTrip() throws {
|
||||||
|
let original = MoneyData(cash: 1000, vault: 2000, points: 100, tokens: 50, cayman: 5000)
|
||||||
|
|
||||||
|
let data = try JSONEncoder().encode(original)
|
||||||
|
let decoded = try JSONDecoder().decode(MoneyData.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(original.cash, decoded.cash)
|
||||||
|
XCTAssertEqual(original.vault, decoded.vault)
|
||||||
|
XCTAssertEqual(original.points, decoded.points)
|
||||||
|
XCTAssertEqual(original.tokens, decoded.tokens)
|
||||||
|
XCTAssertEqual(original.cayman, decoded.cayman)
|
||||||
|
}
|
||||||
|
}
|
||||||
148
MacTorn/MacTornTests/Models/StatusTests.swift
Normal file
148
MacTorn/MacTornTests/Models/StatusTests.swift
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class StatusTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - isOkay Tests
|
||||||
|
|
||||||
|
func testIsOkay_stateOkay() throws {
|
||||||
|
let json = TornAPIFixtures.statusOkay
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(status.isOkay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOkay_stateNil() throws {
|
||||||
|
let json: [String: Any] = [:]
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
XCTAssertTrue(status.isOkay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOkay_inHospital() throws {
|
||||||
|
let json = TornAPIFixtures.statusHospital
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(status.isOkay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsOkay_inJail() throws {
|
||||||
|
let json = TornAPIFixtures.statusJail
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(status.isOkay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - isInHospital Tests
|
||||||
|
|
||||||
|
func testIsInHospital_yes() throws {
|
||||||
|
let json = TornAPIFixtures.statusHospital
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(status.isInHospital)
|
||||||
|
XCTAssertFalse(status.isInJail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsInHospital_no() throws {
|
||||||
|
let json = TornAPIFixtures.statusOkay
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(status.isInHospital)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - isInJail Tests
|
||||||
|
|
||||||
|
func testIsInJail_yes() throws {
|
||||||
|
let json = TornAPIFixtures.statusJail
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(status.isInJail)
|
||||||
|
XCTAssertFalse(status.isInHospital)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsInJail_no() throws {
|
||||||
|
let json = TornAPIFixtures.statusOkay
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(status.isInJail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - timeRemaining Tests
|
||||||
|
|
||||||
|
func testTimeRemaining_noUntil() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"description": "Okay",
|
||||||
|
"state": "Okay"
|
||||||
|
]
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
XCTAssertEqual(status.timeRemaining, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimeRemaining_untilInPast() throws {
|
||||||
|
let pastTime = Int(Date().timeIntervalSince1970) - 1000
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"description": "In hospital",
|
||||||
|
"state": "Hospital",
|
||||||
|
"until": pastTime
|
||||||
|
]
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
XCTAssertEqual(status.timeRemaining, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimeRemaining_untilInFuture() throws {
|
||||||
|
let futureTime = Int(Date().timeIntervalSince1970) + 1000
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"description": "In hospital",
|
||||||
|
"state": "Hospital",
|
||||||
|
"until": futureTime
|
||||||
|
]
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
// Should be approximately 1000, allow some tolerance for test execution time
|
||||||
|
XCTAssertGreaterThan(status.timeRemaining, 900)
|
||||||
|
XCTAssertLessThanOrEqual(status.timeRemaining, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_fullStatus() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"description": "In hospital for 30 minutes",
|
||||||
|
"details": "Hospitalized by SomePlayer",
|
||||||
|
"state": "Hospital",
|
||||||
|
"until": 1700000000
|
||||||
|
]
|
||||||
|
let status = try decode(Status.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(status.description, "In hospital for 30 minutes")
|
||||||
|
XCTAssertEqual(status.details, "Hospitalized by SomePlayer")
|
||||||
|
XCTAssertEqual(status.state, "Hospital")
|
||||||
|
XCTAssertEqual(status.until, 1700000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Equatable Tests
|
||||||
|
|
||||||
|
func testEquatable() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"description": "Okay",
|
||||||
|
"state": "Okay"
|
||||||
|
]
|
||||||
|
let status1 = try decode(Status.self, from: json)
|
||||||
|
let status2 = try decode(Status.self, from: json)
|
||||||
|
XCTAssertEqual(status1, status2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquatable_different() throws {
|
||||||
|
let json1: [String: Any] = [
|
||||||
|
"description": "Okay",
|
||||||
|
"state": "Okay"
|
||||||
|
]
|
||||||
|
let json2: [String: Any] = [
|
||||||
|
"description": "Hospital",
|
||||||
|
"state": "Hospital",
|
||||||
|
"until": 1000
|
||||||
|
]
|
||||||
|
let status1 = try decode(Status.self, from: json1)
|
||||||
|
let status2 = try decode(Status.self, from: json2)
|
||||||
|
XCTAssertNotEqual(status1, status2)
|
||||||
|
}
|
||||||
|
}
|
||||||
184
MacTorn/MacTornTests/Models/TornResponseTests.swift
Normal file
184
MacTorn/MacTornTests/Models/TornResponseTests.swift
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class TornResponseTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - Full Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_validFullResponse() throws {
|
||||||
|
let data = try TornAPIFixtures.toData(TornAPIFixtures.validFullResponse)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.name, "TestPlayer")
|
||||||
|
XCTAssertEqual(response.playerId, 123456)
|
||||||
|
XCTAssertNotNil(response.energy)
|
||||||
|
XCTAssertNotNil(response.nerve)
|
||||||
|
XCTAssertNotNil(response.life)
|
||||||
|
XCTAssertNotNil(response.happy)
|
||||||
|
XCTAssertNotNil(response.cooldowns)
|
||||||
|
XCTAssertNotNil(response.travel)
|
||||||
|
XCTAssertNotNil(response.status)
|
||||||
|
XCTAssertNotNil(response.chain)
|
||||||
|
XCTAssertNotNil(response.events)
|
||||||
|
XCTAssertNotNil(response.messages)
|
||||||
|
XCTAssertNil(response.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Bars Computed Property Tests
|
||||||
|
|
||||||
|
func testBars_allBarsPresent() throws {
|
||||||
|
let data = try TornAPIFixtures.toData(TornAPIFixtures.validFullResponse)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
let bars = response.bars
|
||||||
|
XCTAssertNotNil(bars)
|
||||||
|
XCTAssertEqual(bars?.energy.current, 100)
|
||||||
|
XCTAssertEqual(bars?.nerve.current, 50)
|
||||||
|
XCTAssertEqual(bars?.life.current, 7500)
|
||||||
|
XCTAssertEqual(bars?.happy.current, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBars_missingBar() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer",
|
||||||
|
"player_id": 123456,
|
||||||
|
"energy": TornAPIFixtures.energyFull,
|
||||||
|
"nerve": TornAPIFixtures.energyHalf
|
||||||
|
// Missing life and happy
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertNil(response.bars) // Should be nil because not all bars present
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Unread Messages Count Tests
|
||||||
|
|
||||||
|
func testUnreadMessagesCount_noMessages() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer"
|
||||||
|
// No messages
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.unreadMessagesCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnreadMessagesCount_allRead() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer",
|
||||||
|
"messages": [
|
||||||
|
"1": ["name": "Sender1", "read": 1],
|
||||||
|
"2": ["name": "Sender2", "read": 1]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.unreadMessagesCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnreadMessagesCount_someUnread() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer",
|
||||||
|
"messages": [
|
||||||
|
"1": ["name": "Sender1", "read": 0],
|
||||||
|
"2": ["name": "Sender2", "read": 1],
|
||||||
|
"3": ["name": "Sender3", "read": 0]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.unreadMessagesCount, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Recent Events Tests
|
||||||
|
|
||||||
|
func testRecentEvents_noEvents() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer"
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertTrue(response.recentEvents.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRecentEvents_sortedByTimestamp() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestPlayer",
|
||||||
|
"events": [
|
||||||
|
"1": ["timestamp": 1000, "event": "Old event", "seen": 1],
|
||||||
|
"2": ["timestamp": 3000, "event": "Newest event", "seen": 0],
|
||||||
|
"3": ["timestamp": 2000, "event": "Middle event", "seen": 0]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
let data = try TornAPIFixtures.toData(json)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
let events = response.recentEvents
|
||||||
|
XCTAssertEqual(events.count, 3)
|
||||||
|
XCTAssertEqual(events[0].timestamp, 3000) // Newest first
|
||||||
|
XCTAssertEqual(events[1].timestamp, 2000)
|
||||||
|
XCTAssertEqual(events[2].timestamp, 1000) // Oldest last
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Error Response Tests
|
||||||
|
|
||||||
|
func testDecoding_errorResponse() throws {
|
||||||
|
let data = try TornAPIFixtures.toData(TornAPIFixtures.tornErrorInvalidKey)
|
||||||
|
let response = try JSONDecoder().decode(TornResponse.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertNotNil(response.error)
|
||||||
|
XCTAssertEqual(response.error?.code, 2)
|
||||||
|
XCTAssertEqual(response.error?.error, "Incorrect Key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TornEvent Tests
|
||||||
|
|
||||||
|
func testTornEvent_cleanEvent() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"event": "You received a message from <a href='profile.php?XID=12345'>SomePlayer</a>.",
|
||||||
|
"seen": 0
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let event = try JSONDecoder().decode(TornEvent.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(event.cleanEvent, "You received a message from SomePlayer.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTornEvent_date() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"event": "Test event",
|
||||||
|
"seen": 0
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let event = try JSONDecoder().decode(TornEvent.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(event.date, Date(timeIntervalSince1970: 1700000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TornMessage Tests
|
||||||
|
|
||||||
|
func testTornMessage_decoding() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"name": "TestSender",
|
||||||
|
"type": "Private",
|
||||||
|
"title": "Hello World",
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"read": 0
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let message = try JSONDecoder().decode(TornMessage.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(message.name, "TestSender")
|
||||||
|
XCTAssertEqual(message.type, "Private")
|
||||||
|
XCTAssertEqual(message.title, "Hello World")
|
||||||
|
XCTAssertEqual(message.timestamp, 1700000000)
|
||||||
|
XCTAssertEqual(message.read, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
MacTorn/MacTornTests/Models/TravelTests.swift
Normal file
135
MacTorn/MacTornTests/Models/TravelTests.swift
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class TravelTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - isAbroad Tests
|
||||||
|
|
||||||
|
func testIsAbroad_inTorn() throws {
|
||||||
|
let json = TornAPIFixtures.travelInTorn
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(travel.isAbroad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsAbroad_abroadInMexico() throws {
|
||||||
|
let json = TornAPIFixtures.travelAbroad
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(travel.isAbroad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsAbroad_travelingToDestination() throws {
|
||||||
|
let json = TornAPIFixtures.travelTraveling
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
// Still traveling, not yet abroad
|
||||||
|
XCTAssertFalse(travel.isAbroad)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - isTraveling Tests
|
||||||
|
|
||||||
|
func testIsTraveling_notTraveling() throws {
|
||||||
|
let json = TornAPIFixtures.travelInTorn
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(travel.isTraveling)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsTraveling_traveling() throws {
|
||||||
|
let json = TornAPIFixtures.travelTraveling
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertTrue(travel.isTraveling)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsTraveling_abroadNotTraveling() throws {
|
||||||
|
let json = TornAPIFixtures.travelAbroad
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertFalse(travel.isTraveling)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - arrivalDate Tests
|
||||||
|
|
||||||
|
func testArrivalDate_notTraveling() throws {
|
||||||
|
let json = TornAPIFixtures.travelInTorn
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertNil(travel.arrivalDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testArrivalDate_traveling() throws {
|
||||||
|
let json = TornAPIFixtures.travelTraveling
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertNotNil(travel.arrivalDate)
|
||||||
|
// Arrival should be in the future
|
||||||
|
XCTAssertGreaterThan(travel.arrivalDate!, Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_allFields() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"destination": "Japan",
|
||||||
|
"timestamp": 1700000000,
|
||||||
|
"departed": 1699999000,
|
||||||
|
"time_left": 1000
|
||||||
|
]
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(travel.destination, "Japan")
|
||||||
|
XCTAssertEqual(travel.timestamp, 1700000000)
|
||||||
|
XCTAssertEqual(travel.departed, 1699999000)
|
||||||
|
XCTAssertEqual(travel.timeLeft, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_nullFields() throws {
|
||||||
|
let json: [String: Any?] = [
|
||||||
|
"destination": nil,
|
||||||
|
"timestamp": nil,
|
||||||
|
"departed": nil,
|
||||||
|
"time_left": nil
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let travel = try JSONDecoder().decode(Travel.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertNil(travel.destination)
|
||||||
|
XCTAssertNil(travel.timestamp)
|
||||||
|
XCTAssertNil(travel.departed)
|
||||||
|
XCTAssertNil(travel.timeLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Equatable Tests
|
||||||
|
|
||||||
|
func testEquatable() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"destination": "Mexico",
|
||||||
|
"timestamp": 1000,
|
||||||
|
"departed": 500,
|
||||||
|
"time_left": 0
|
||||||
|
]
|
||||||
|
let travel1 = try decode(Travel.self, from: json)
|
||||||
|
let travel2 = try decode(Travel.self, from: json)
|
||||||
|
XCTAssertEqual(travel1, travel2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Edge Cases
|
||||||
|
|
||||||
|
func testIsAbroad_nilDestination() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"time_left": 0
|
||||||
|
]
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
XCTAssertFalse(travel.isAbroad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsTraveling_nilTimeLeft() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"destination": "Mexico"
|
||||||
|
]
|
||||||
|
let travel = try decode(Travel.self, from: json)
|
||||||
|
XCTAssertFalse(travel.isTraveling)
|
||||||
|
}
|
||||||
|
}
|
||||||
201
MacTorn/MacTornTests/Models/WatchlistItemTests.swift
Normal file
201
MacTorn/MacTornTests/Models/WatchlistItemTests.swift
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
final class WatchlistItemTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - priceDifference Tests
|
||||||
|
|
||||||
|
func testPriceDifference_normalCase() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 1000,
|
||||||
|
lowestPriceQuantity: 5,
|
||||||
|
secondLowestPrice: 1100,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.priceDifference, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceDifference_samePrices() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 1000,
|
||||||
|
lowestPriceQuantity: 5,
|
||||||
|
secondLowestPrice: 1000,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.priceDifference, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceDifference_noSecondPrice() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 1000,
|
||||||
|
lowestPriceQuantity: 5,
|
||||||
|
secondLowestPrice: 0,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.priceDifference, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceDifference_noLowestPrice() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 0,
|
||||||
|
lowestPriceQuantity: 0,
|
||||||
|
secondLowestPrice: 1100,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.priceDifference, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - isLoading Tests
|
||||||
|
|
||||||
|
func testIsLoading_loading() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 0,
|
||||||
|
lowestPriceQuantity: 0,
|
||||||
|
secondLowestPrice: 0,
|
||||||
|
lastUpdated: nil,
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(item.isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsLoading_loaded() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 1000,
|
||||||
|
lowestPriceQuantity: 5,
|
||||||
|
secondLowestPrice: 1100,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(item.isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsLoading_hasError() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 1,
|
||||||
|
name: "Test Item",
|
||||||
|
lowestPrice: 0,
|
||||||
|
lowestPriceQuantity: 0,
|
||||||
|
secondLowestPrice: 0,
|
||||||
|
lastUpdated: nil,
|
||||||
|
error: "No listings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Has error, so not loading
|
||||||
|
XCTAssertFalse(item.isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decoding Tests
|
||||||
|
|
||||||
|
func testDecoding_fullItem() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"id": 123,
|
||||||
|
"name": "Xanax",
|
||||||
|
"lowestPrice": 850000,
|
||||||
|
"lowestPriceQuantity": 10,
|
||||||
|
"secondLowestPrice": 860000,
|
||||||
|
"lastUpdated": Date().timeIntervalSinceReferenceDate
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let item = try JSONDecoder().decode(WatchlistItem.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.id, 123)
|
||||||
|
XCTAssertEqual(item.name, "Xanax")
|
||||||
|
XCTAssertEqual(item.lowestPrice, 850000)
|
||||||
|
XCTAssertEqual(item.lowestPriceQuantity, 10)
|
||||||
|
XCTAssertEqual(item.secondLowestPrice, 860000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_legacyItemMissingFields() throws {
|
||||||
|
// Legacy items might not have all fields
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"id": 123,
|
||||||
|
"name": "Xanax"
|
||||||
|
// Missing price fields
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let item = try JSONDecoder().decode(WatchlistItem.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.id, 123)
|
||||||
|
XCTAssertEqual(item.name, "Xanax")
|
||||||
|
XCTAssertEqual(item.lowestPrice, 0) // Default
|
||||||
|
XCTAssertEqual(item.lowestPriceQuantity, 0) // Default
|
||||||
|
XCTAssertEqual(item.secondLowestPrice, 0) // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding_withError() throws {
|
||||||
|
let json: [String: Any] = [
|
||||||
|
"id": 123,
|
||||||
|
"name": "Invalid Item",
|
||||||
|
"lowestPrice": 0,
|
||||||
|
"lowestPriceQuantity": 0,
|
||||||
|
"secondLowestPrice": 0,
|
||||||
|
"error": "Item not found"
|
||||||
|
]
|
||||||
|
let data = try JSONSerialization.data(withJSONObject: json)
|
||||||
|
let item = try JSONDecoder().decode(WatchlistItem.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.error, "Item not found")
|
||||||
|
XCTAssertFalse(item.isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Encoding Tests
|
||||||
|
|
||||||
|
func testEncoding_roundTrip() throws {
|
||||||
|
let original = WatchlistItem(
|
||||||
|
id: 456,
|
||||||
|
name: "Donator Pack",
|
||||||
|
lowestPrice: 9500000,
|
||||||
|
lowestPriceQuantity: 3,
|
||||||
|
secondLowestPrice: 9600000,
|
||||||
|
lastUpdated: Date(),
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let data = try JSONEncoder().encode(original)
|
||||||
|
let decoded = try JSONDecoder().decode(WatchlistItem.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(original.id, decoded.id)
|
||||||
|
XCTAssertEqual(original.name, decoded.name)
|
||||||
|
XCTAssertEqual(original.lowestPrice, decoded.lowestPrice)
|
||||||
|
XCTAssertEqual(original.lowestPriceQuantity, decoded.lowestPriceQuantity)
|
||||||
|
XCTAssertEqual(original.secondLowestPrice, decoded.secondLowestPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Identifiable Tests
|
||||||
|
|
||||||
|
func testIdentifiable() {
|
||||||
|
let item = WatchlistItem(
|
||||||
|
id: 789,
|
||||||
|
name: "Test",
|
||||||
|
lowestPrice: 100,
|
||||||
|
lowestPriceQuantity: 1,
|
||||||
|
secondLowestPrice: 200,
|
||||||
|
lastUpdated: nil,
|
||||||
|
error: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(item.id, 789)
|
||||||
|
}
|
||||||
|
}
|
||||||
260
MacTorn/MacTornTests/ViewModels/AppStateTests.swift
Normal file
260
MacTorn/MacTornTests/ViewModels/AppStateTests.swift
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AppStateTests: XCTestCase {
|
||||||
|
|
||||||
|
var mockSession: MockNetworkSession!
|
||||||
|
var appState: AppState!
|
||||||
|
|
||||||
|
override func setUp() async throws {
|
||||||
|
try await super.setUp()
|
||||||
|
mockSession = MockNetworkSession()
|
||||||
|
appState = AppState(session: mockSession)
|
||||||
|
// Clear any persisted data
|
||||||
|
UserDefaults.standard.removeObject(forKey: "apiKey")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "watchlist")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "notificationRules")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() async throws {
|
||||||
|
appState.stopPolling()
|
||||||
|
appState = nil
|
||||||
|
mockSession = nil
|
||||||
|
try await super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API Key Validation Tests
|
||||||
|
|
||||||
|
func testFetchData_emptyAPIKey() async {
|
||||||
|
appState.apiKey = ""
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
// Wait for async completion
|
||||||
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "API Key required")
|
||||||
|
XCTAssertNil(appState.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchData_invalidAPIKey_HTTP403() async throws {
|
||||||
|
appState.apiKey = "invalid_key"
|
||||||
|
mockSession.setHTTPError(statusCode: 403)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
// Wait for async completion
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "Invalid API Key")
|
||||||
|
XCTAssertNil(appState.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchData_invalidAPIKey_HTTP404() async throws {
|
||||||
|
appState.apiKey = "invalid_key"
|
||||||
|
mockSession.setHTTPError(statusCode: 404)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
// Wait for async completion
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "Invalid API Key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Fetch Success Tests
|
||||||
|
|
||||||
|
func testFetchData_success() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.validFullResponse)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
// Wait for async completion
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertNotNil(appState.data)
|
||||||
|
XCTAssertEqual(appState.data?.name, "TestPlayer")
|
||||||
|
XCTAssertEqual(appState.data?.playerId, 123456)
|
||||||
|
XCTAssertNil(appState.errorMsg)
|
||||||
|
XCTAssertNotNil(appState.lastUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchData_parsesAllBars() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.validFullResponse)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertNotNil(appState.data?.bars)
|
||||||
|
XCTAssertEqual(appState.data?.energy?.current, 100)
|
||||||
|
XCTAssertEqual(appState.data?.nerve?.current, 50)
|
||||||
|
XCTAssertEqual(appState.data?.life?.current, 7500)
|
||||||
|
XCTAssertEqual(appState.data?.happy?.current, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Torn API Error Tests
|
||||||
|
|
||||||
|
func testFetchData_tornAPIError() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setTornAPIError(code: 2, message: "Incorrect Key")
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "API Error: Incorrect Key")
|
||||||
|
XCTAssertNil(appState.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchData_tornAPIRateLimit() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setTornAPIError(code: 5, message: "Too many requests")
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "API Error: Too many requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Network Error Tests
|
||||||
|
|
||||||
|
func testFetchData_networkError() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
mockSession.setNetworkError(MockNetworkError.connectionFailed)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertNotNil(appState.errorMsg)
|
||||||
|
XCTAssertTrue(appState.errorMsg?.contains("Network error") ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - HTTP Error Tests
|
||||||
|
|
||||||
|
func testFetchData_HTTP500() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
mockSession.setHTTPError(statusCode: 500)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "HTTP Error: 500")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFetchData_HTTP502() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
mockSession.setHTTPError(statusCode: 502)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.errorMsg, "HTTP Error: 502")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Polling Tests
|
||||||
|
|
||||||
|
func testStartPolling_fetchesData() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.validFullResponse)
|
||||||
|
|
||||||
|
appState.startPolling()
|
||||||
|
|
||||||
|
// Initial fetch should happen immediately
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
XCTAssertTrue(mockSession.requestedURLs.count >= 1)
|
||||||
|
XCTAssertNotNil(appState.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStopPolling_stopsTimer() {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
appState.startPolling()
|
||||||
|
|
||||||
|
appState.stopPolling()
|
||||||
|
|
||||||
|
// Timer should be cancelled
|
||||||
|
// No way to directly verify timer is nil, but we can verify no more requests happen
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Loading State Tests
|
||||||
|
|
||||||
|
func testFetchData_setsLoadingState() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.validFullResponse)
|
||||||
|
|
||||||
|
appState.fetchData()
|
||||||
|
|
||||||
|
// Wait for completion - fetchData is async so loading transitions happen inside the Task
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
// After completion, loading should be false
|
||||||
|
XCTAssertFalse(appState.isLoading)
|
||||||
|
// And we should have data
|
||||||
|
XCTAssertNotNil(appState.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification Rules Tests
|
||||||
|
|
||||||
|
func testLoadNotificationRules_defaults() {
|
||||||
|
// Clear existing rules
|
||||||
|
UserDefaults.standard.removeObject(forKey: "notificationRules")
|
||||||
|
|
||||||
|
let newAppState = AppState(session: mockSession)
|
||||||
|
|
||||||
|
XCTAssertFalse(newAppState.notificationRules.isEmpty)
|
||||||
|
// Should have default rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSaveNotificationRules() {
|
||||||
|
let rule = NotificationRule(
|
||||||
|
id: "test_rule",
|
||||||
|
barType: .energy,
|
||||||
|
threshold: 80,
|
||||||
|
enabled: true,
|
||||||
|
soundName: "default"
|
||||||
|
)
|
||||||
|
appState.notificationRules = [rule]
|
||||||
|
appState.saveNotificationRules()
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
appState.loadNotificationRules()
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.notificationRules.count, 1)
|
||||||
|
XCTAssertEqual(appState.notificationRules.first?.id, "test_rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateRule() {
|
||||||
|
appState.notificationRules = NotificationRule.defaults
|
||||||
|
|
||||||
|
var rule = appState.notificationRules.first!
|
||||||
|
rule.enabled = false
|
||||||
|
appState.updateRule(rule)
|
||||||
|
|
||||||
|
XCTAssertFalse(appState.notificationRules.first!.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Refresh Now Tests
|
||||||
|
|
||||||
|
func testRefreshNow_triggersFetch() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.validFullResponse)
|
||||||
|
|
||||||
|
// refreshNow calls fetchData which is async
|
||||||
|
appState.refreshNow()
|
||||||
|
|
||||||
|
// Wait for async completion
|
||||||
|
try await Task.sleep(nanoseconds: 1_500_000_000)
|
||||||
|
|
||||||
|
// Verify request was made
|
||||||
|
XCTAssertGreaterThanOrEqual(mockSession.requestedURLs.count, 1)
|
||||||
|
XCTAssertNotNil(appState.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
215
MacTorn/MacTornTests/ViewModels/AppStateWatchlistTests.swift
Normal file
215
MacTorn/MacTornTests/ViewModels/AppStateWatchlistTests.swift
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import MacTorn
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AppStateWatchlistTests: XCTestCase {
|
||||||
|
|
||||||
|
var mockSession: MockNetworkSession!
|
||||||
|
var appState: AppState!
|
||||||
|
|
||||||
|
override func setUp() async throws {
|
||||||
|
try await super.setUp()
|
||||||
|
mockSession = MockNetworkSession()
|
||||||
|
appState = AppState(session: mockSession)
|
||||||
|
// Clear watchlist
|
||||||
|
UserDefaults.standard.removeObject(forKey: "watchlist")
|
||||||
|
appState.watchlistItems = []
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() async throws {
|
||||||
|
appState.stopPolling()
|
||||||
|
appState = nil
|
||||||
|
mockSession = nil
|
||||||
|
UserDefaults.standard.removeObject(forKey: "watchlist")
|
||||||
|
try await super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Add Item Tests
|
||||||
|
|
||||||
|
func testAddToWatchlist_addsItem() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Xanax")
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.watchlistItems.count, 1)
|
||||||
|
XCTAssertEqual(appState.watchlistItems.first?.id, 123)
|
||||||
|
XCTAssertEqual(appState.watchlistItems.first?.name, "Xanax")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddToWatchlist_preventsDuplicate() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Xanax")
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Xanax") // Duplicate
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.watchlistItems.count, 1) // Should still be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddToWatchlist_fetchesPriceImmediately() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Xanax")
|
||||||
|
|
||||||
|
// Wait for price fetch
|
||||||
|
try await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
|
||||||
|
// Should have made a request to fetch price
|
||||||
|
XCTAssertTrue(mockSession.requestedURLs.contains { $0.absoluteString.contains("123") })
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddToWatchlist_multipleItems() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Xanax")
|
||||||
|
appState.addToWatchlist(itemId: 456, name: "Donator Pack")
|
||||||
|
appState.addToWatchlist(itemId: 789, name: "Vicodin")
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.watchlistItems.count, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Remove Item Tests
|
||||||
|
|
||||||
|
func testRemoveFromWatchlist_removesItem() {
|
||||||
|
appState.watchlistItems = [
|
||||||
|
WatchlistItem(id: 123, name: "Xanax", lowestPrice: 1000, lowestPriceQuantity: 5, secondLowestPrice: 1100, lastUpdated: Date(), error: nil),
|
||||||
|
WatchlistItem(id: 456, name: "Donator Pack", lowestPrice: 9000000, lowestPriceQuantity: 3, secondLowestPrice: 9500000, lastUpdated: Date(), error: nil)
|
||||||
|
]
|
||||||
|
|
||||||
|
appState.removeFromWatchlist(123)
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.watchlistItems.count, 1)
|
||||||
|
XCTAssertNil(appState.watchlistItems.first(where: { $0.id == 123 }))
|
||||||
|
XCTAssertNotNil(appState.watchlistItems.first(where: { $0.id == 456 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoveFromWatchlist_nonExistentItem() {
|
||||||
|
appState.watchlistItems = [
|
||||||
|
WatchlistItem(id: 123, name: "Xanax", lowestPrice: 1000, lowestPriceQuantity: 5, secondLowestPrice: 1100, lastUpdated: Date(), error: nil)
|
||||||
|
]
|
||||||
|
|
||||||
|
appState.removeFromWatchlist(999) // Non-existent
|
||||||
|
|
||||||
|
XCTAssertEqual(appState.watchlistItems.count, 1) // Should still have 1 item
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Persistence Tests
|
||||||
|
|
||||||
|
func testSaveWatchlist_persists() {
|
||||||
|
appState.watchlistItems = [
|
||||||
|
WatchlistItem(id: 123, name: "Xanax", lowestPrice: 1000, lowestPriceQuantity: 5, secondLowestPrice: 1100, lastUpdated: Date(), error: nil)
|
||||||
|
]
|
||||||
|
|
||||||
|
appState.saveWatchlist()
|
||||||
|
|
||||||
|
// Create new instance and load
|
||||||
|
let newAppState = AppState(session: mockSession)
|
||||||
|
newAppState.loadWatchlist()
|
||||||
|
|
||||||
|
XCTAssertEqual(newAppState.watchlistItems.count, 1)
|
||||||
|
XCTAssertEqual(newAppState.watchlistItems.first?.id, 123)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadWatchlist_emptyWhenNothingSaved() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "watchlist")
|
||||||
|
|
||||||
|
appState.loadWatchlist()
|
||||||
|
|
||||||
|
XCTAssertTrue(appState.watchlistItems.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Price Refresh Tests
|
||||||
|
|
||||||
|
func testRefreshWatchlistPrices_refreshesAllItems() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.watchlistItems = [
|
||||||
|
WatchlistItem(id: 123, name: "Xanax", lowestPrice: 0, lowestPriceQuantity: 0, secondLowestPrice: 0, lastUpdated: nil, error: nil),
|
||||||
|
WatchlistItem(id: 456, name: "Donator Pack", lowestPrice: 0, lowestPriceQuantity: 0, secondLowestPrice: 0, lastUpdated: nil, error: nil)
|
||||||
|
]
|
||||||
|
|
||||||
|
appState.refreshWatchlistPrices()
|
||||||
|
|
||||||
|
// Wait for price fetches
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
// Should have made requests for both items
|
||||||
|
let requestedURLStrings = mockSession.requestedURLs.map { $0.absoluteString }
|
||||||
|
XCTAssertTrue(requestedURLStrings.contains { $0.contains("123") })
|
||||||
|
XCTAssertTrue(requestedURLStrings.contains { $0.contains("456") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Price Update Tests
|
||||||
|
|
||||||
|
func testPriceFetch_updatesPrices() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemSuccess)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Test Item")
|
||||||
|
|
||||||
|
// Wait for price fetch
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
let item = appState.watchlistItems.first
|
||||||
|
XCTAssertNotNil(item)
|
||||||
|
// Prices should be updated from fixtures (950 is lowest from bazaar)
|
||||||
|
XCTAssertGreaterThan(item?.lowestPrice ?? 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceFetch_noListings() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
try mockSession.setSuccessResponse(json: TornAPIFixtures.marketItemNoListings)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Rare Item")
|
||||||
|
|
||||||
|
// Wait for price fetch
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
let item = appState.watchlistItems.first
|
||||||
|
XCTAssertEqual(item?.error, "No listings")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceFetch_networkError() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
mockSession.setNetworkError(MockNetworkError.connectionFailed)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Test Item")
|
||||||
|
|
||||||
|
// Wait for price fetch
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
let item = appState.watchlistItems.first
|
||||||
|
XCTAssertEqual(item?.error, "Network Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPriceFetch_httpError() async throws {
|
||||||
|
appState.apiKey = "valid_key"
|
||||||
|
mockSession.setHTTPError(statusCode: 500)
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Test Item")
|
||||||
|
|
||||||
|
// Wait for price fetch
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
|
||||||
|
let item = appState.watchlistItems.first
|
||||||
|
XCTAssertEqual(item?.error, "HTTP 500")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Empty API Key Tests
|
||||||
|
|
||||||
|
func testPriceFetch_emptyAPIKey() async throws {
|
||||||
|
appState.apiKey = ""
|
||||||
|
|
||||||
|
appState.addToWatchlist(itemId: 123, name: "Test Item")
|
||||||
|
|
||||||
|
// Wait
|
||||||
|
try await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
|
||||||
|
// No requests should be made
|
||||||
|
XCTAssertTrue(mockSession.requestedURLs.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
150
MacTorn/MacTornUITests/MacTornUITests.swift
Normal file
150
MacTorn/MacTornUITests/MacTornUITests.swift
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class MacTornUITests: XCTestCase {
|
||||||
|
|
||||||
|
var app: XCUIApplication!
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
app = XCUIApplication()
|
||||||
|
// Reset state for each test
|
||||||
|
app.launchArguments = ["--uitesting"]
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
app = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Settings View Tests
|
||||||
|
|
||||||
|
func testSettingsView_appearsWhenNoAPIKey() throws {
|
||||||
|
// When no API key is set, settings should appear
|
||||||
|
// Note: This test assumes a clean state or --uitesting launch argument clears the key
|
||||||
|
|
||||||
|
// Look for settings-related UI elements
|
||||||
|
let settingsElements = app.windows.firstMatch
|
||||||
|
|
||||||
|
// The app should show some form of settings or API key input
|
||||||
|
// Adjust selectors based on actual UI implementation
|
||||||
|
XCTAssertTrue(settingsElements.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tab Navigation Tests
|
||||||
|
|
||||||
|
func testTabNavigation_allTabsExist() throws {
|
||||||
|
// Skip if settings view is blocking
|
||||||
|
// This test verifies the main tab structure
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
|
||||||
|
// Look for tab bar or navigation elements
|
||||||
|
// Adjust based on actual UI implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTabNavigation_switchBetweenTabs() throws {
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
|
||||||
|
// Tab navigation test
|
||||||
|
// Adjust selectors based on actual tab implementation
|
||||||
|
// e.g., app.buttons["Status"].tap()
|
||||||
|
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Refresh Button Tests
|
||||||
|
|
||||||
|
func testRefreshButton_exists() throws {
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
|
||||||
|
// Look for refresh button
|
||||||
|
// Adjust selector based on actual implementation
|
||||||
|
// let refreshButton = window.buttons["Refresh"]
|
||||||
|
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRefreshButton_triggersRefresh() throws {
|
||||||
|
// This test would verify that tapping refresh triggers a data fetch
|
||||||
|
// Would need to mock network or observe state changes
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Watchlist UI Tests
|
||||||
|
|
||||||
|
func testWatchlist_addItem() throws {
|
||||||
|
// Navigate to watchlist tab
|
||||||
|
// Add an item
|
||||||
|
// Verify it appears in the list
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWatchlist_removeItem() throws {
|
||||||
|
// Navigate to watchlist tab
|
||||||
|
// Remove an item
|
||||||
|
// Verify it's no longer in the list
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Error State Tests
|
||||||
|
|
||||||
|
func testErrorState_displaysErrorMessage() throws {
|
||||||
|
// Trigger an error state
|
||||||
|
// Verify error message is displayed
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Loading State Tests
|
||||||
|
|
||||||
|
func testLoadingState_showsLoadingIndicator() throws {
|
||||||
|
// During data fetch, loading indicator should be visible
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Status View Tests
|
||||||
|
|
||||||
|
func testStatusView_displaysBars() throws {
|
||||||
|
// Verify energy, nerve, life, happy bars are displayed
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Performance Tests
|
||||||
|
|
||||||
|
func testLaunchPerformance() throws {
|
||||||
|
if #available(macOS 10.15, *) {
|
||||||
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
|
XCUIApplication().launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UI Test Helpers
|
||||||
|
|
||||||
|
extension XCUIElement {
|
||||||
|
/// Wait for element to exist with timeout
|
||||||
|
func waitForExistence(timeout: TimeInterval = 5) -> Bool {
|
||||||
|
return self.waitForExistence(timeout: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tap if element exists
|
||||||
|
func tapIfExists() {
|
||||||
|
if self.exists {
|
||||||
|
self.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Makefile
Normal file
119
Makefile
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# MacTorn Makefile
|
||||||
|
# Run tests and build commands for local development
|
||||||
|
|
||||||
|
.PHONY: test test-unit test-ui build clean coverage help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help:
|
||||||
|
@echo "MacTorn Build Commands:"
|
||||||
|
@echo ""
|
||||||
|
@echo " make test - Run all unit tests"
|
||||||
|
@echo " make test-ui - Run UI tests"
|
||||||
|
@echo " make build - Build the app in Debug mode"
|
||||||
|
@echo " make release - Build the app in Release mode"
|
||||||
|
@echo " make clean - Clean build artifacts"
|
||||||
|
@echo " make coverage - Run tests with code coverage"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
test:
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-only-testing:MacTornTests \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
# Run unit tests (alias)
|
||||||
|
test-unit: test
|
||||||
|
|
||||||
|
# Run UI tests
|
||||||
|
test-ui:
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-only-testing:MacTornUITests \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
# Run all tests (unit + UI)
|
||||||
|
test-all:
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
# Build Debug
|
||||||
|
build:
|
||||||
|
xcodebuild build \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-configuration Debug \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
# Build Release
|
||||||
|
release:
|
||||||
|
xcodebuild build \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-configuration Release \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
xcodebuild clean \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn
|
||||||
|
rm -rf build/
|
||||||
|
rm -rf DerivedData/
|
||||||
|
rm -rf TestResults/
|
||||||
|
|
||||||
|
# Run tests with code coverage
|
||||||
|
coverage:
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-enableCodeCoverage YES \
|
||||||
|
-resultBundlePath TestResults \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO
|
||||||
|
@echo ""
|
||||||
|
@echo "Coverage report generated in TestResults/"
|
||||||
|
@echo "Open TestResults/action.xccovreport to view in Xcode"
|
||||||
|
|
||||||
|
# Quick test - faster iteration
|
||||||
|
quick-test:
|
||||||
|
xcodebuild test \
|
||||||
|
-project MacTorn/MacTorn.xcodeproj \
|
||||||
|
-scheme MacTorn \
|
||||||
|
-destination 'platform=macOS' \
|
||||||
|
-only-testing:MacTornTests \
|
||||||
|
-parallel-testing-enabled YES \
|
||||||
|
CODE_SIGN_IDENTITY="-" \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
2>&1 | xcpretty --color
|
||||||
|
|
||||||
|
# Watch for changes and run tests (requires fswatch)
|
||||||
|
watch:
|
||||||
|
@echo "Watching for changes... (requires fswatch)"
|
||||||
|
fswatch -o MacTorn/MacTorn MacTorn/MacTornTests | xargs -n1 -I{} make quick-test
|
||||||
|
|
||||||
|
# Open project in Xcode
|
||||||
|
open:
|
||||||
|
open MacTorn/MacTorn.xcodeproj
|
||||||
|
|
||||||
|
# Show test summary
|
||||||
|
test-summary:
|
||||||
|
@echo "Test Summary:"
|
||||||
|
@echo "============="
|
||||||
|
@find . -name "*.swift" -path "*/MacTornTests/*" | xargs grep -l "func test" | wc -l | xargs echo "Test files:"
|
||||||
|
@find . -name "*.swift" -path "*/MacTornTests/*" | xargs grep "func test" | wc -l | xargs echo "Test cases:"
|
||||||
Loading…
Reference in a new issue